@@ -9,6 +9,26 @@ import type { Reporter } from '../algorithm-error-reporter-type';
9
9
import { Seq , walk as walkExpr } from '../../expr-parser' ;
10
10
import { offsetToLineAndColumn } from '../../utils' ;
11
11
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
+
12
32
type HasLocation = { location : { start : { line : number ; column : number } } } ;
13
33
type VarKind =
14
34
| 'parameter'
@@ -19,14 +39,17 @@ type VarKind =
19
39
| 'attribute declaration' ;
20
40
class Scope {
21
41
declare vars : Map < string , { kind : VarKind ; used : boolean ; node : HasLocation | null } > ;
42
+ declare strictScopes : Set < string > [ ] ;
22
43
declare report : Reporter ;
23
44
constructor ( report : Reporter ) {
24
45
this . vars = new Map ( ) ;
46
+ this . strictScopes = [ new Set ( ) ] ;
47
+ this . report = report ;
48
+
25
49
// TODO remove this when regex state objects become less dumb
26
50
for ( const name of [ 'captures' , 'input' , 'startIndex' , 'endIndex' ] ) {
27
- this . declare ( name , null ) ;
51
+ this . declare ( name , null , undefined , true ) ;
28
52
}
29
- this . report = report ;
30
53
}
31
54
32
55
declared ( name : string ) : boolean {
@@ -38,11 +61,32 @@ class Scope {
38
61
return this . vars . get ( name ) ! . used ;
39
62
}
40
63
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 {
42
70
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 ) ;
44
89
}
45
- this . vars . set ( name , { kind, used : false , node : nameNode } ) ;
46
90
}
47
91
48
92
undeclare ( name : string ) {
@@ -100,7 +144,7 @@ export function checkVariableUsage(
100
144
) {
101
145
// `__` is for <del>_x_</del><ins>_y_</ins>, which has textContent `_x__y_`
102
146
for ( const name of preceding . textContent . matchAll ( / (?< = \b | _ ) _ ( [ a - z A - Z 0 - 9 ] + ) _ (? = \b | _ ) / g) ) {
103
- scope . declare ( name [ 1 ] , null ) ;
147
+ scope . declare ( name [ 1 ] , null , undefined , true ) ;
104
148
}
105
149
}
106
150
preceding = previousOrParent ( preceding , parentClause ) ;
@@ -145,6 +189,7 @@ function walkAlgorithm(
145
189
}
146
190
return ;
147
191
}
192
+ scope . strictScopes . push ( new Set ( ) ) ;
148
193
149
194
stepLoop: for ( const step of steps . contents ) {
150
195
const loopVars : Set < UnderscoreNode > = new Set ( ) ;
@@ -175,7 +220,12 @@ function walkAlgorithm(
175
220
column,
176
221
} ) ;
177
222
} 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
+ ) ;
179
229
}
180
230
}
181
231
}
@@ -327,8 +377,12 @@ function walkAlgorithm(
327
377
( isSuchThat && cur . name === 'text' && ! / (?: o f | i n ) / . test ( cur . contents ) ) ||
328
378
( isBe && cur . name === 'text' && / \b l e t (?: e a c h o f ) ? $ / i. test ( cur . contents ) )
329
379
) {
380
+ const conditional =
381
+ expr . items [ 0 ] . name === 'text' &&
382
+ / ^ ( I f | E l s e | O t h e r w i s e ) \b / . test ( expr . items [ 0 ] . contents ) ;
383
+
330
384
for ( const v of varsDeclaredHere ) {
331
- scope . declare ( v . contents , v ) ;
385
+ scope . declare ( v . contents , v , 'variable' , conditional ) ;
332
386
declaredThisLine . add ( v ) ;
333
387
}
334
388
}
@@ -353,6 +407,7 @@ function walkAlgorithm(
353
407
scope . undeclare ( decl . contents ) ;
354
408
}
355
409
}
410
+ scope . strictScopes . pop ( ) ;
356
411
}
357
412
358
413
function isVariable ( node : UnderscoreNode | { name : string } | null ) : node is UnderscoreNode {
0 commit comments