Skip to content

Commit e18424a

Browse files
add lint rule to catch for-each-in (#691)
1 parent bd0b81c commit e18424a

3 files changed

Lines changed: 72 additions & 0 deletions

File tree

src/lint/collect-algorithm-diagnostics.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import lintAlgorithmStepNumbering from './rules/algorithm-step-numbering';
1111
import lintAlgorithmStepLabels from './rules/algorithm-step-labels';
1212
import lintEnumCasing from './rules/enum-casing';
1313
import lintForEachElement from './rules/for-each-element';
14+
import lintForEachOf from './rules/for-each-of';
1415
import lintStepAttributes from './rules/step-attributes';
1516
import lintIfElseConsistency from './rules/if-else-consistency';
1617
import lintAlgorithmSpelling from './rules/algorithm-spelling';
@@ -31,6 +32,7 @@ const stepRules: LineRule[] = [
3132
lintAlgorithmStepLabels,
3233
lintEnumCasing,
3334
lintForEachElement,
35+
lintForEachOf,
3436
lintStepAttributes,
3537
lintIfElseConsistency,
3638
lintAlgorithmSpelling,

src/lint/rules/for-each-of.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Reporter } from '../algorithm-error-reporter-type';
2+
import type { Seq } from '../../expr-parser';
3+
import type { OrderedListItemNode } from 'ecmarkdown';
4+
import { offsetToLineAndColumn } from '../../utils';
5+
6+
const ruleId = 'for-each-of';
7+
8+
/*
9+
Checks that "For each" loops use "of", not "in".
10+
*/
11+
export default function (
12+
report: Reporter,
13+
step: OrderedListItemNode,
14+
algorithmSource: string,
15+
parsedSteps: Map<OrderedListItemNode, Seq>,
16+
) {
17+
const stepSeq = parsedSteps.get(step);
18+
if (stepSeq == null || stepSeq.items.length < 3) {
19+
return;
20+
}
21+
const first = stepSeq.items[0];
22+
if (!(first.name === 'text' && first.contents.startsWith('For each '))) {
23+
return;
24+
}
25+
// Find the loop variable (underscore), then check the text after it
26+
for (let i = 1; i < stepSeq.items.length - 1; i++) {
27+
const item = stepSeq.items[i];
28+
if (item.name === 'underscore') {
29+
const next = stepSeq.items[i + 1];
30+
if (next.name === 'text' && /^ in\b/.test(next.contents)) {
31+
report({
32+
ruleId,
33+
...offsetToLineAndColumn(algorithmSource, next.location.start.offset + 1),
34+
message: 'expected "of" instead of "in" in "for each"',
35+
});
36+
}
37+
break;
38+
}
39+
}
40+
}

test/lint-algorithms.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,36 @@ describe('linting algorithms', () => {
524524
});
525525
});
526526

527+
describe('for each of', () => {
528+
const ruleId = 'for-each-of';
529+
it('rejects "in" instead of "of"', async () => {
530+
await assertLint(
531+
positioned`
532+
<emu-alg>
533+
1. Let _y_ be a List.
534+
1. For each String _x_ ${M}in _y_, do foo.
535+
</emu-alg>`,
536+
{
537+
ruleId,
538+
nodeType,
539+
message: 'expected "of" instead of "in" in "for each"',
540+
},
541+
);
542+
});
543+
544+
it('negative', async () => {
545+
await assertLintFree(`
546+
<emu-alg>
547+
1. Let _y_ be a List.
548+
1. Let _S_ be a Set.
549+
1. For each String _x_ of _y_, do foo.
550+
1. For each element _x_ of _y_, do foo.
551+
1. For each integer _x_ such that _x_ &in; _S_, do foo.
552+
</emu-alg>
553+
`);
554+
});
555+
});
556+
527557
describe('if/else consistency', () => {
528558
const ruleId = 'if-else-consistency';
529559
it('rejects single-line if with multiline else', async () => {

0 commit comments

Comments
 (0)