Skip to content

Commit 8295493

Browse files
add a new lint for preferring throw over return ThrowCompletion(...) (#692)
1 parent e18424a commit 8295493

4 files changed

Lines changed: 127 additions & 1 deletion

File tree

src/lint/collect-algorithm-diagnostics.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import lintForEachElement from './rules/for-each-element';
1414
import lintForEachOf from './rules/for-each-of';
1515
import lintStepAttributes from './rules/step-attributes';
1616
import lintIfElseConsistency from './rules/if-else-consistency';
17+
import lintPreferThrowShorthand from './rules/prefer-throw-shorthand';
1718
import lintAlgorithmSpelling from './rules/algorithm-spelling';
1819
import { checkVariableUsage } from './rules/variable-use-def';
1920
import type { Seq } from '../expr-parser';
@@ -35,6 +36,7 @@ const stepRules: LineRule[] = [
3536
lintForEachOf,
3637
lintStepAttributes,
3738
lintIfElseConsistency,
39+
lintPreferThrowShorthand,
3840
lintAlgorithmSpelling,
3941
];
4042

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { Reporter } from '../algorithm-error-reporter-type';
2+
import type { OrderedListItemNode } from 'ecmarkdown';
3+
import type { Seq } from '../../expr-parser';
4+
import { offsetToLineAndColumn } from '../../utils';
5+
6+
const ruleId = 'prefer-throw-shorthand';
7+
8+
/*
9+
Checks that "return ThrowCompletion(...)" is written using the "throw" shorthand.
10+
Does not flag ThrowCompletion when its result is assigned to an alias.
11+
*/
12+
export default function (
13+
report: Reporter,
14+
step: OrderedListItemNode,
15+
algorithmSource: string,
16+
parsedSteps: Map<OrderedListItemNode, Seq>,
17+
) {
18+
const stepSeq = parsedSteps.get(step);
19+
if (stepSeq == null) {
20+
return;
21+
}
22+
const items = stepSeq.items;
23+
for (let i = 1; i < items.length; ++i) {
24+
const item = items[i];
25+
if (
26+
item.name === 'call' &&
27+
item.callee.length === 1 &&
28+
item.callee[0].name === 'text' &&
29+
item.callee[0].contents === 'ThrowCompletion'
30+
) {
31+
const prev = items[i - 1];
32+
if (prev.name === 'text' && /\breturn\s+$/i.test(prev.contents)) {
33+
report({
34+
ruleId,
35+
message: 'prefer "throw _x_" over "return ThrowCompletion(_x_)"',
36+
...offsetToLineAndColumn(algorithmSource, item.callee[0].location.start.offset),
37+
});
38+
}
39+
}
40+
}
41+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { describe, it, before } from 'node:test';
2+
import {
3+
assertLint,
4+
assertLintFree,
5+
lintLocationMarker as M,
6+
positioned,
7+
getBiblio,
8+
} from './utils.ts';
9+
import type { ExportedBiblio } from '../lib/Biblio.js';
10+
11+
const nodeType = 'emu-alg';
12+
13+
describe('prefer-throw-shorthand', () => {
14+
let throwBiblio: ExportedBiblio;
15+
before(async () => {
16+
throwBiblio = await getBiblio(`
17+
<emu-clause id="sec-throwcompletion" type="abstract operation">
18+
<h1>
19+
ThrowCompletion (
20+
_value_: an ECMAScript language value,
21+
): a throw completion
22+
</h1>
23+
<dl class="header">
24+
</dl>
25+
<emu-alg>
26+
1. Return Completion Record { [[Type]]: ~throw~, [[Value]]: _value_, [[Target]]: ~empty~ }.
27+
</emu-alg>
28+
</emu-clause>
29+
`);
30+
});
31+
32+
it('return ThrowCompletion', async () => {
33+
await assertLint(
34+
positioned`<emu-alg>
35+
1. Let _x_ be *"foo"*.
36+
1. Return ${M}ThrowCompletion(_x_).
37+
</emu-alg>`,
38+
{
39+
ruleId: 'prefer-throw-shorthand',
40+
nodeType,
41+
message: 'prefer "throw _x_" over "return ThrowCompletion(_x_)"',
42+
},
43+
{ extraBiblios: [throwBiblio] },
44+
);
45+
});
46+
47+
it('return ThrowCompletion in single-line If', async () => {
48+
await assertLint(
49+
positioned`<emu-alg>
50+
1. Let _x_ be *"foo"*.
51+
1. If _x_ is *"bar"*, return ${M}ThrowCompletion(_x_).
52+
</emu-alg>`,
53+
{
54+
ruleId: 'prefer-throw-shorthand',
55+
nodeType,
56+
message: 'prefer "throw _x_" over "return ThrowCompletion(_x_)"',
57+
},
58+
{ extraBiblios: [throwBiblio] },
59+
);
60+
});
61+
62+
it('throw shorthand is allowed', async () => {
63+
await assertLintFree(`
64+
<emu-alg>
65+
1. Let _x_ be *"foo"*.
66+
1. Throw _x_.
67+
</emu-alg>
68+
`);
69+
});
70+
71+
it('ThrowCompletion assigned to alias is allowed', async () => {
72+
await assertLintFree(
73+
`
74+
<emu-alg>
75+
1. Let _x_ be *"foo"*.
76+
1. Let _comp_ be ThrowCompletion(_x_).
77+
1. Return _comp_.
78+
</emu-alg>
79+
`,
80+
{ extraBiblios: [throwBiblio] },
81+
);
82+
});
83+
});

test/typecheck.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ describe('typechecking completions', () => {
191191
<dl class="header">
192192
</dl>
193193
<emu-alg>
194-
1. If some condition holds, return ThrowCompletion(*null*).
194+
1. If some condition holds, throw *null*.
195195
1. Else if some other condition holds, return ReturnCompletion(*null*).
196196
1. Return NormalCompletion(*null*).
197197
</emu-alg>

0 commit comments

Comments
 (0)