Skip to content

Commit 15ea5b3

Browse files
authored
lint for redeclarations (#541)
1 parent 6f7aeab commit 15ea5b3

File tree

4 files changed

+161
-14
lines changed

4 files changed

+161
-14
lines changed

src/lint/rules/variable-use-def.ts

+63-8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,26 @@ import type { Reporter } from '../algorithm-error-reporter-type';
99
import { Seq, walk as walkExpr } from '../../expr-parser';
1010
import { offsetToLineAndColumn } from '../../utils';
1111

12+
/*
13+
Ecmaspeak scope rules are a bit weird.
14+
Variables can be declared across branches and used subsequently, as in
15+
16+
1. If condition, then
17+
1. Let _var_ be true.
18+
1. Else,
19+
1. Let _var_ be false.
20+
1. Use _var_.
21+
22+
So it mostly behaves like `var`-declared variables in JS.
23+
24+
(Exception: loop variables can't be used outside the loop.)
25+
26+
But for readability reasons, we don't want to allow redeclaration or shadowing.
27+
28+
The `Scope` class tracks names as if they are `var`-scoped, and the `strictScopes` property is
29+
used to track names as if they are `let`-scoped, so we can warn on redeclaration.
30+
*/
31+
1232
type HasLocation = { location: { start: { line: number; column: number } } };
1333
type VarKind =
1434
| 'parameter'
@@ -19,14 +39,17 @@ type VarKind =
1939
| 'attribute declaration';
2040
class Scope {
2141
declare vars: Map<string, { kind: VarKind; used: boolean; node: HasLocation | null }>;
42+
declare strictScopes: Set<string>[];
2243
declare report: Reporter;
2344
constructor(report: Reporter) {
2445
this.vars = new Map();
46+
this.strictScopes = [new Set()];
47+
this.report = report;
48+
2549
// TODO remove this when regex state objects become less dumb
2650
for (const name of ['captures', 'input', 'startIndex', 'endIndex']) {
27-
this.declare(name, null);
51+
this.declare(name, null, undefined, true);
2852
}
29-
this.report = report;
3053
}
3154

3255
declared(name: string): boolean {
@@ -38,11 +61,32 @@ class Scope {
3861
return this.vars.get(name)!.used;
3962
}
4063

41-
declare(name: string, nameNode: HasLocation | null, kind: VarKind = 'variable'): void {
64+
declare(
65+
name: string,
66+
nameNode: HasLocation | null,
67+
kind: VarKind = 'variable',
68+
mayBeShadowed: boolean = false
69+
): void {
4270
if (this.declared(name)) {
43-
return;
71+
if (!mayBeShadowed) {
72+
for (const scope of this.strictScopes) {
73+
if (scope.has(name)) {
74+
this.report({
75+
ruleId: 're-declaration',
76+
message: `${JSON.stringify(name)} is already declared`,
77+
line: nameNode!.location.start.line,
78+
column: nameNode!.location.start.column,
79+
});
80+
return;
81+
}
82+
}
83+
}
84+
} else {
85+
this.vars.set(name, { kind, used: false, node: nameNode });
86+
}
87+
if (!mayBeShadowed) {
88+
this.strictScopes[this.strictScopes.length - 1].add(name);
4489
}
45-
this.vars.set(name, { kind, used: false, node: nameNode });
4690
}
4791

4892
undeclare(name: string) {
@@ -100,7 +144,7 @@ export function checkVariableUsage(
100144
) {
101145
// `__` is for <del>_x_</del><ins>_y_</ins>, which has textContent `_x__y_`
102146
for (const name of preceding.textContent.matchAll(/(?<=\b|_)_([a-zA-Z0-9]+)_(?=\b|_)/g)) {
103-
scope.declare(name[1], null);
147+
scope.declare(name[1], null, undefined, true);
104148
}
105149
}
106150
preceding = previousOrParent(preceding, parentClause);
@@ -145,6 +189,7 @@ function walkAlgorithm(
145189
}
146190
return;
147191
}
192+
scope.strictScopes.push(new Set());
148193

149194
stepLoop: for (const step of steps.contents) {
150195
const loopVars: Set<UnderscoreNode> = new Set();
@@ -175,7 +220,12 @@ function walkAlgorithm(
175220
column,
176221
});
177222
} else {
178-
scope.declare(name, { location: { start: { line, column } } }, 'attribute declaration');
223+
scope.declare(
224+
name,
225+
{ location: { start: { line, column } } },
226+
'attribute declaration',
227+
true
228+
);
179229
}
180230
}
181231
}
@@ -327,8 +377,12 @@ function walkAlgorithm(
327377
(isSuchThat && cur.name === 'text' && !/(?: of | in )/.test(cur.contents)) ||
328378
(isBe && cur.name === 'text' && /\blet (?:each of )?$/i.test(cur.contents))
329379
) {
380+
const conditional =
381+
expr.items[0].name === 'text' &&
382+
/^(If|Else|Otherwise)\b/.test(expr.items[0].contents);
383+
330384
for (const v of varsDeclaredHere) {
331-
scope.declare(v.contents, v);
385+
scope.declare(v.contents, v, 'variable', conditional);
332386
declaredThisLine.add(v);
333387
}
334388
}
@@ -353,6 +407,7 @@ function walkAlgorithm(
353407
scope.undeclare(decl.contents);
354408
}
355409
}
410+
scope.strictScopes.pop();
356411
}
357412

358413
function isVariable(node: UnderscoreNode | { name: string } | null): node is UnderscoreNode {

test/expr-parser.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ describe('expression parsing', () => {
9999
await assertLintFree(`
100100
<emu-alg>
101101
1. Let _x_ be a new Record { [[Foo]]: 0, <!-- comment --> [[Bar]]: 1 }.
102-
1. Let _x_ be a new Record { [[Foo]]: 0, <ins>[[Bar]]: 1</ins> }.
103-
1. Let _x_ be a new Record { [[Foo]]: 0, <ins>[[Bar]]: 1, [[Baz]]: 2</ins> }.
104-
1. Let _x_ be a new Record { [[Foo]]: 0, <ins>[[Bar]]: 1,</ins> [[Baz]]: 2 }.
102+
1. Set _x_ to a new Record { [[Foo]]: 0, <ins>[[Bar]]: 1</ins> }.
103+
1. Set _x_ to a new Record { [[Foo]]: 0, <ins>[[Bar]]: 1, [[Baz]]: 2</ins> }.
104+
1. Set _x_ to a new Record { [[Foo]]: 0, <ins>[[Bar]]: 1,</ins> [[Baz]]: 2 }.
105105
1. Use _x_.
106106
</emu-alg>
107107
`);

test/lint-variable-use-def.js

+92
Original file line numberDiff line numberDiff line change
@@ -497,3 +497,95 @@ describe('variables are declared and used appropriately', () => {
497497
});
498498
});
499499
});
500+
501+
describe('variables cannot be redeclared', () => {
502+
it('redeclaration at the same level is an error', async () => {
503+
await assertLint(
504+
positioned`
505+
<emu-alg>
506+
1. Let _x_ be 0.
507+
1. Let ${M}_x_ be 1.
508+
1. Return _x_.
509+
</emu-alg>
510+
</emu-clause>
511+
`,
512+
{
513+
ruleId: 're-declaration',
514+
nodeType: 'emu-alg',
515+
message: '"x" is already declared',
516+
}
517+
);
518+
});
519+
520+
it('redeclaration at an inner level is an error', async () => {
521+
await assertLint(
522+
positioned`
523+
<emu-alg>
524+
1. Let _x_ be 0.
525+
1. Repeat,
526+
1. Let ${M}_x_ be 1.
527+
1. Return _x_.
528+
</emu-alg>
529+
</emu-clause>
530+
`,
531+
{
532+
ruleId: 're-declaration',
533+
nodeType: 'emu-alg',
534+
message: '"x" is already declared',
535+
}
536+
);
537+
});
538+
539+
it('multi-line if-else does not count as redeclaration', async () => {
540+
await assertLintFree(
541+
`
542+
<emu-alg>
543+
1. If condition, then
544+
1. Let _result_ be 0.
545+
1. Else,
546+
1. Let _result_ be 1.
547+
1. Return _result_.
548+
</emu-alg>
549+
`
550+
);
551+
});
552+
553+
it('single-line if-else does not count as redeclaration', async () => {
554+
await assertLintFree(
555+
`
556+
<emu-alg>
557+
1. If condition, let _result_ be 0.
558+
1. Else, let _result_ be 1.
559+
1. Return _result_.
560+
</emu-alg>
561+
`
562+
);
563+
});
564+
565+
it('[declared] annotation can be redeclared', async () => {
566+
await assertLintFree(
567+
`
568+
<emu-alg>
569+
1. [declared="var"] NOTE: Something about _var_.
570+
1. Let _var_ be 0.
571+
1. Return _var_.
572+
</emu-alg>
573+
`
574+
);
575+
});
576+
577+
it('variables mentioned in the premable can be redeclared', async () => {
578+
await assertLintFree(
579+
`
580+
<emu-clause id="example">
581+
<h1>Example</h1>
582+
<p>In the following algorithm, _var_ is a variable.</p>
583+
<emu-alg>
584+
1. Let _var_ be 0.
585+
1. Return _var_.
586+
</emu-alg>
587+
</emu-clause>
588+
`
589+
);
590+
});
591+
});

test/typecheck.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,12 @@ describe('typechecking completions', () => {
131131
</dl>
132132
<emu-alg>
133133
1. Let _a_ be Completion(ExampleAlg()).
134-
1. Let _a_ be Completion(<emu-meta suppress-effects="user-code">ExampleAlg()</emu-meta>).
134+
1. Set _a_ to Completion(<emu-meta suppress-effects="user-code">ExampleAlg()</emu-meta>).
135135
1. Set _a_ to ! ExampleAlg().
136136
1. Return ? ExampleAlg().
137137
1. Let _foo_ be 0.
138-
1. Let _a_ be Completion(ExampleSDO of _foo_).
139-
1. Let _a_ be Completion(ExampleSDO of _foo_ with argument 0).
138+
1. Set _a_ to Completion(ExampleSDO of _foo_).
139+
1. Set _a_ to Completion(ExampleSDO of _foo_ with argument 0).
140140
1. If ? ExampleSDO of _foo_ is *true*, then
141141
1. Something.
142142
</emu-alg>

0 commit comments

Comments
 (0)