Skip to content

Commit b9ac0e1

Browse files
committed
allow local masking of global variables
1 parent 16be836 commit b9ac0e1

File tree

5 files changed

+72
-32
lines changed

5 files changed

+72
-32
lines changed

src/javascript/assignments.test.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {assert, test} from "vitest";
2-
import {checkAssignments} from "./assignments.js";
32
import {parseJavaScript} from "./parse.js";
3+
import {findReferences} from "./references.js";
44

55
function check(input: string) {
66
const cell = parseJavaScript(input);
7-
checkAssignments(cell.body, cell.references, input);
7+
findReferences(cell.body, {input});
88
}
99

1010
test("allows non-external assignments", () => {
@@ -35,5 +35,20 @@ test("does not allow external assignments via for…of or for…in", () => {
3535
});
3636

3737
test("does not allow global assignments", () => {
38-
assert.throws(() => check("window = 1;"), /global 'window'/);
38+
assert.throws(() => check("window = 1;"), /Assignment to global 'window'/);
39+
assert.throws(() => check("const foo = (window = 1);"), /Assignment to global 'window'/);
40+
});
41+
42+
test("does not allow conflicting top-level variables", () => {
43+
assert.throws(() => check("const window = 1;"), /Global 'window' cannot be redefined/);
44+
assert.throws(() => check("const foo = 1, window = 2;"), /Global 'window' cannot be redefined/);
45+
assert.throws(() => check("const {window} = {};"), /Global 'window' cannot be redefined/);
46+
assert.throws(() => check("const {window = 1} = {};"), /Global 'window' cannot be redefined/);
47+
});
48+
49+
test("allows conflicting non-top-level variables", () => {
50+
assert.doesNotThrow(() => check("{ const window = 1; }"));
51+
assert.doesNotThrow(() => check("{ let window; window = 2; }"));
52+
assert.doesNotThrow(() => check("{ const {window} = {}; }"));
53+
assert.doesNotThrow(() => check("{ const {window = 1} = {}; }"));
3954
});

src/javascript/assignments.ts

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,59 @@
11
import type {Expression, Identifier, Node, Pattern, VariableDeclaration} from "acorn";
22
import {defaultGlobals} from "./globals.js";
33
import {syntaxError} from "./syntaxError.js";
4-
import {simple} from "./walk.js";
4+
import {ancestor} from "./walk.js";
55

66
type Assignable = Expression | Pattern | VariableDeclaration;
77

8-
export function checkAssignments(node: Node, references: Identifier[], input: string): void {
9-
function checkConst(node: Assignable) {
8+
export function checkAssignments(
9+
node: Node,
10+
{
11+
input,
12+
locals,
13+
references,
14+
globals = defaultGlobals
15+
}: {
16+
input: string;
17+
locals: Map<Node, Set<string>>;
18+
globals?: Set<string>;
19+
references: Identifier[];
20+
}
21+
): void {
22+
function isLocal({name}: Identifier, parents: Node[]): boolean {
23+
for (const p of parents) if (locals.get(p)?.has(name)) return true;
24+
return false;
25+
}
26+
27+
function checkConst(node: Assignable, parents: Node[]) {
1028
switch (node.type) {
1129
case "Identifier":
30+
if (isLocal(node, parents)) break;
1231
if (references.includes(node))
1332
throw syntaxError(`Assignment to external variable '${node.name}'`, node, input);
14-
if (defaultGlobals.has(node.name))
33+
if (globals.has(node.name))
1534
throw syntaxError(`Assignment to global '${node.name}'`, node, input);
1635
break;
17-
case "ObjectPattern":
18-
node.properties.forEach((node) => checkConst(node.type === "Property" ? node.value : node));
19-
break;
2036
case "ArrayPattern":
21-
node.elements.forEach((node) => node && checkConst(node));
37+
for (const e of node.elements) if (e) checkConst(e, parents);
38+
break;
39+
case "ObjectPattern":
40+
for (const p of node.properties) checkConst(p.type === "Property" ? p.value : p, parents);
2241
break;
2342
case "RestElement":
24-
checkConst(node.argument);
43+
checkConst(node.argument, parents);
2544
break;
2645
}
2746
}
28-
function checkConstLeft({left}: {left: Assignable}) {
29-
checkConst(left);
47+
48+
function checkConstArgument({argument}: {argument: Assignable}, parents: Node[]) {
49+
checkConst(argument, parents);
3050
}
31-
function checkConstArgument({argument}: {argument: Assignable}) {
32-
checkConst(argument);
51+
52+
function checkConstLeft({left}: {left: Assignable}, parents: Node[]) {
53+
checkConst(left, parents);
3354
}
34-
simple(node, {
55+
56+
ancestor(node, {
3557
AssignmentExpression: checkConstLeft,
3658
AssignmentPattern: checkConstLeft,
3759
UpdateExpression: checkConstArgument,

src/javascript/imports.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type NamedImportSpecifier = ImportSpecifier | ImportDefaultSpecifier;
1414
type AnyImportSpecifier = ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier;
1515

1616
/** Throws a syntax error if any export declarations are found. */
17-
export function checkExports(body: Node, input: string): void {
17+
export function checkExports(body: Node, {input}: {input: string}): void {
1818
function checkExport(child: Node) {
1919
throw syntaxError("Unexpected token 'export'", child, input);
2020
}
@@ -164,10 +164,12 @@ function renderImport(source: string, node: ImportDeclaration, input: string): s
164164
: ""
165165
})${
166166
names.length > 0
167-
? `.then((module) => {${names.map(
168-
(name) => `
167+
? `.then((module) => {${names
168+
.map(
169+
(name) => `
169170
if (!("${name}" in module)) throw new SyntaxError(\`export '${name}' not found\`);`
170-
).join("")}
171+
)
172+
.join("")}
171173
return module;
172174
})`
173175
: ""

src/javascript/parse.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import {Parser, tokTypes} from "acorn";
22
import type {Expression, Identifier, Options, Program} from "acorn";
3-
import {checkExports} from "./imports.js";
43
import {findReferences} from "./references.js";
5-
import {checkAssignments} from "./assignments.js";
64
import {findDeclarations} from "./declarations.js";
75
import {findAwaits} from "./awaits.js";
86

@@ -20,9 +18,7 @@ export interface JavaScriptCell {
2018
async: boolean; // does this use top-level await?
2119
}
2220

23-
export function maybeParseJavaScript(
24-
input: string
25-
): JavaScriptCell | undefined {
21+
export function maybeParseJavaScript(input: string): JavaScriptCell | undefined {
2622
try {
2723
return parseJavaScript(input);
2824
} catch (error) {
@@ -36,13 +32,10 @@ export function parseJavaScript(input: string): JavaScriptCell {
3632
if (expression?.type === "ClassExpression" && expression.id) expression = null; // treat named class as program
3733
if (expression?.type === "FunctionExpression" && expression.id) expression = null; // treat named function as program
3834
const body = expression ?? parseProgram(input); // otherwise parse as a program
39-
checkExports(body, input);
40-
const references = findReferences(body);
41-
checkAssignments(body, references, input);
4235
return {
4336
body,
4437
declarations: expression ? null : findDeclarations(body as Program, input),
45-
references,
38+
references: findReferences(body, {input}),
4639
expression: !!expression,
4740
async: findAwaits(body).length > 0
4841
};

src/javascript/references.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import type {BlockStatement, CatchClause, Class} from "acorn";
33
import type {ForInStatement, ForOfStatement, ForStatement} from "acorn";
44
import type {FunctionDeclaration, FunctionExpression} from "acorn";
55
import type {Identifier, Node, Pattern, Program} from "acorn";
6+
import {checkAssignments} from "./assignments.js";
67
import {defaultGlobals} from "./globals.js";
8+
import {checkExports} from "./imports.js";
79
import {ancestor} from "./walk.js";
810

911
// Based on https://github.com/ForbesLindesay/acorn-globals
@@ -40,10 +42,12 @@ function isBlockScope(node: Node): node is FunctionNode | Program | BlockStateme
4042
export function findReferences(
4143
node: Node,
4244
{
45+
input,
4346
globals = defaultGlobals,
4447
filterReference = (identifier: Identifier) => !globals.has(identifier.name),
4548
filterDeclaration = () => true
4649
}: {
50+
input?: string;
4751
globals?: Set<string>;
4852
filterReference?: (identifier: Identifier) => boolean;
4953
filterDeclaration?: (identifier: {name: string}) => boolean;
@@ -53,8 +57,7 @@ export function findReferences(
5357
const references: Identifier[] = [];
5458

5559
function hasLocal(node: Node, name: string): boolean {
56-
const l = locals.get(node);
57-
return l ? l.has(name) : false;
60+
return locals.get(node)?.has(name) ?? false;
5861
}
5962

6063
function declareLocal(node: Node, id: {name: string}): void {
@@ -160,5 +163,10 @@ export function findReferences(
160163
Identifier: identifier
161164
});
162165

166+
if (input !== undefined) {
167+
checkAssignments(node, {locals, references, globals, input});
168+
checkExports(node, {input});
169+
}
170+
163171
return references;
164172
}

0 commit comments

Comments
 (0)