Skip to content

Commit 0d17479

Browse files
committed
add lint rule for multiline if/else consistency
1 parent d0ea1c9 commit 0d17479

File tree

3 files changed

+194
-6
lines changed

3 files changed

+194
-6
lines changed

src/lint/collect-algorithm-diagnostics.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Node as EcmarkdownNode, OrderedListItemNode } from 'ecmarkdown';
1+
import type { Node as EcmarkdownNode, OrderedListItemNode, OrderedListNode } from 'ecmarkdown';
22

33
import type { LintingError, Reporter } from './algorithm-error-reporter-type';
44
import type { default as Spec, Warning } from '../Spec';
@@ -11,21 +11,24 @@ import lintAlgorithmStepNumbering from './rules/algorithm-step-numbering';
1111
import lintAlgorithmStepLabels from './rules/algorithm-step-labels';
1212
import lintForEachElement from './rules/for-each-element';
1313
import lintStepAttributes from './rules/step-attributes';
14+
import lintIfElseConsistency from './rules/if-else-consistency';
1415
import { checkVariableUsage } from './rules/variable-use-def';
1516
import { parse, Seq } from '../expr-parser';
1617

1718
type LineRule = (
1819
report: Reporter,
1920
step: OrderedListItemNode,
2021
algorithmSource: string,
21-
parsedSteps: Map<OrderedListItemNode, Seq>
22+
parsedSteps: Map<OrderedListItemNode, Seq>,
23+
parent: OrderedListNode
2224
) => void;
2325
const stepRules: LineRule[] = [
2426
lintAlgorithmLineStyle,
2527
lintAlgorithmStepNumbering,
2628
lintAlgorithmStepLabels,
2729
lintForEachElement,
2830
lintStepAttributes,
31+
lintIfElseConsistency,
2932
];
3033

3134
export function collectAlgorithmDiagnostics(
@@ -84,13 +87,13 @@ export function collectAlgorithmDiagnostics(
8487
}
8588
}
8689

87-
function applyRule(visit: LineRule, step: OrderedListItemNode) {
90+
function applyRule(visit: LineRule, step: OrderedListItemNode, parent: OrderedListNode) {
8891
// we don't know the names of ops at this point
8992
// TODO maybe run later in the process? but not worth worrying about for now
90-
visit(reporter, step, algorithmSource, parsedSteps);
93+
visit(reporter, step, algorithmSource, parsedSteps, parent);
9194
if (step.sublist?.name === 'ol') {
9295
for (const substep of step.sublist.contents) {
93-
applyRule(visit, substep);
96+
applyRule(visit, substep, step.sublist);
9497
}
9598
}
9699
}
@@ -101,7 +104,7 @@ export function collectAlgorithmDiagnostics(
101104

102105
for (const rule of stepRules) {
103106
for (const step of tree.contents.contents) {
104-
applyRule(rule, step);
107+
applyRule(rule, step, tree.contents);
105108
}
106109
}
107110
if (allNodesParsedSuccessfully) {

src/lint/rules/if-else-consistency.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { Reporter } from '../algorithm-error-reporter-type';
2+
import type { Seq } from '../../expr-parser';
3+
import type { OrderedListItemNode, OrderedListNode } from 'ecmarkdown';
4+
import { offsetToLineAndColumn } from '../../utils';
5+
6+
const ruleId = 'if-else-consistency';
7+
8+
/*
9+
Checks that `if`/`else` statements are both single-line or both multi-line.
10+
*/
11+
export default function (
12+
report: Reporter,
13+
step: OrderedListItemNode,
14+
algorithmSource: string,
15+
parsedSteps: Map<OrderedListItemNode, Seq>,
16+
parent: OrderedListNode
17+
) {
18+
const stepSeq = parsedSteps.get(step);
19+
if (stepSeq == null) {
20+
return;
21+
}
22+
const firstSeqItem = stepSeq.items[0];
23+
if (firstSeqItem?.name !== 'text' || !/^(?:If|Else if)\b/.test(firstSeqItem.contents)) {
24+
return;
25+
}
26+
const idx = parent.contents.indexOf(step);
27+
if (idx >= parent.contents.length - 1) {
28+
return;
29+
}
30+
const nextStep = parent.contents[idx + 1];
31+
const nextSeq = parsedSteps.get(nextStep);
32+
if (nextSeq == null) {
33+
return;
34+
}
35+
const nextFirstSeqitem = nextSeq.items[0];
36+
if (
37+
nextFirstSeqitem?.name !== 'text' ||
38+
!/^(?:Else|Otherwise)\b/.test(nextFirstSeqitem.contents)
39+
) {
40+
return;
41+
}
42+
if (step.sublist != null && nextStep.sublist == null) {
43+
const location = offsetToLineAndColumn(algorithmSource, nextFirstSeqitem.location.start.offset);
44+
report({
45+
ruleId,
46+
...location,
47+
message: '"Else" steps should be multiline whenever their corresponding "If" is',
48+
});
49+
} else if (step.sublist == null && nextStep.sublist != null) {
50+
const location = offsetToLineAndColumn(algorithmSource, firstSeqItem.location.start.offset);
51+
report({
52+
ruleId,
53+
...location,
54+
message: '"If" steps should be multiline whenever their corresponding "Else" is',
55+
});
56+
}
57+
}

test/lint-algorithms.js

+128
Original file line numberDiff line numberDiff line change
@@ -467,4 +467,132 @@ describe('linting algorithms', () => {
467467
`);
468468
});
469469
});
470+
471+
describe('if/else consistency', () => {
472+
const ruleId = 'if-else-consistency';
473+
it('rejects single-line if with multiline else', async () => {
474+
await assertLint(
475+
positioned`
476+
<emu-alg>
477+
1. ${M}If some condition holds, do something.
478+
1. Else,
479+
1. Do something else.
480+
</emu-alg>`,
481+
{
482+
ruleId,
483+
nodeType,
484+
message: '"If" steps should be multiline whenever their corresponding "Else" is',
485+
}
486+
);
487+
});
488+
489+
it('rejects single-line if with multiline else-if', async () => {
490+
await assertLint(
491+
positioned`
492+
<emu-alg>
493+
1. ${M}If some condition holds, do something.
494+
1. Else if another condition holds, then
495+
1. Do something else.
496+
1. Else,
497+
1. Do something yet otherwise.
498+
</emu-alg>`,
499+
{
500+
ruleId,
501+
nodeType,
502+
message: '"If" steps should be multiline whenever their corresponding "Else" is',
503+
}
504+
);
505+
});
506+
507+
it('rejects single-line else-if with multiline else', async () => {
508+
await assertLint(
509+
positioned`
510+
<emu-alg>
511+
1. If some condition holds, do something.
512+
1. ${M}Else if another condition holds, do something else.
513+
1. Else,
514+
1. Do something yet otherwise.
515+
</emu-alg>`,
516+
{
517+
ruleId,
518+
nodeType,
519+
message: '"If" steps should be multiline whenever their corresponding "Else" is',
520+
}
521+
);
522+
});
523+
524+
it('rejects multi-line if with single-line else', async () => {
525+
await assertLint(
526+
positioned`
527+
<emu-alg>
528+
1. If some condition holds, then
529+
1. Do something.
530+
1. ${M}Else do something else.
531+
</emu-alg>`,
532+
{
533+
ruleId,
534+
nodeType,
535+
message: '"Else" steps should be multiline whenever their corresponding "If" is',
536+
}
537+
);
538+
});
539+
540+
it('rejects multi-line if with single-line else-f', async () => {
541+
await assertLint(
542+
positioned`
543+
<emu-alg>
544+
1. If some condition holds, then
545+
1. Do something.
546+
1. ${M}Else if another condition holds do something else.
547+
1. Else, do something yet otherwise.
548+
</emu-alg>`,
549+
{
550+
ruleId,
551+
nodeType,
552+
message: '"Else" steps should be multiline whenever their corresponding "If" is',
553+
}
554+
);
555+
});
556+
557+
it('rejects multi-line else-if with single-line else', async () => {
558+
await assertLint(
559+
positioned`
560+
<emu-alg>
561+
1. If some condition holds, then
562+
1. Do something.
563+
1. Else if another condition holds, then
564+
1. Do something else.
565+
1. ${M}Else, do something yet otherwise.
566+
</emu-alg>`,
567+
{
568+
ruleId,
569+
nodeType,
570+
message: '"Else" steps should be multiline whenever their corresponding "If" is',
571+
}
572+
);
573+
});
574+
575+
it('negative', async () => {
576+
await assertLintFree(`
577+
<emu-alg>
578+
1. If some condition holds, do something simple.
579+
1. Else, do something yet otherwise simple.
580+
1. If some condition holds, do something simple.
581+
1. Else if another condition holds, do something else simple.
582+
1. Else, do something yet otherwise simple.
583+
1. NOTE: Also works for multiline.
584+
1. If some condition holds, then
585+
1. Do something simple.
586+
1. Else,
587+
1. Do something yet otherwise simple.
588+
1. If some condition holds, then
589+
1. Do something simple.
590+
1. Else if another condition holds, then
591+
1. Do something else simple.
592+
1. Else,
593+
1. Do something yet otherwise simple.
594+
</emu-alg>
595+
`);
596+
});
597+
});
470598
});

0 commit comments

Comments
 (0)