Skip to content

Commit ad51fad

Browse files
committed
Allow functions to redeclare vars and functions in function scopes
1 parent 53d6360 commit ad51fad

12 files changed

Lines changed: 139 additions & 13 deletions

File tree

src/ast/scopes/CatchBodyScope.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,6 @@ export default class CatchBodyScope extends ChildScope {
7070
this.addHoistedVariable(name, declaredVariable);
7171
return declaredVariable;
7272
}
73-
// Functions can never re-declare catch parameters
74-
if (kind === VariableKind.function) {
75-
const name = identifier.name;
76-
if (this.hoistedVariables?.get(name)) {
77-
context.error(logRedeclarationError(name), identifier.start);
78-
}
79-
}
8073
return super.addDeclaration(identifier, context, init, kind);
8174
}
8275
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { AstContext } from '../../Module';
2+
import { logRedeclarationError } from '../../utils/logs';
3+
import type Identifier from '../nodes/Identifier';
4+
import type { ExpressionEntity } from '../nodes/shared/Expression';
5+
import { VariableKind } from '../nodes/shared/VariableKinds';
6+
import LocalVariable from '../variables/LocalVariable';
7+
import ChildScope from './ChildScope';
8+
import type ParameterScope from './ParameterScope';
9+
10+
export default class FunctionBodyScope extends ChildScope {
11+
constructor(
12+
readonly parent: ParameterScope,
13+
readonly context: AstContext
14+
) {
15+
super(parent, context);
16+
}
17+
18+
// There is stuff that is only allowed in function scopes, i.e. functions can
19+
// be redeclared, functions and var can redeclare each other
20+
addDeclaration(
21+
identifier: Identifier,
22+
context: AstContext,
23+
init: ExpressionEntity,
24+
kind: VariableKind
25+
): LocalVariable {
26+
const name = identifier.name;
27+
const existingVariable =
28+
this.hoistedVariables?.get(name) || (this.variables.get(name) as LocalVariable);
29+
if (existingVariable) {
30+
const existingKind = existingVariable.kind;
31+
if (
32+
(kind === VariableKind.var || kind === VariableKind.function) &&
33+
(existingKind === VariableKind.var ||
34+
existingKind === VariableKind.function ||
35+
existingKind === VariableKind.parameter)
36+
) {
37+
existingVariable.addDeclaration(identifier, init);
38+
return existingVariable;
39+
}
40+
context.error(logRedeclarationError(name), identifier.start);
41+
}
42+
const newVariable = new LocalVariable(identifier.name, identifier, init, context, kind);
43+
this.variables.set(name, newVariable);
44+
return newVariable;
45+
}
46+
}

src/ast/scopes/ParameterScope.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { ExpressionEntity } from '../nodes/shared/Expression';
77
import ParameterVariable from '../variables/ParameterVariable';
88
import CatchBodyScope from './CatchBodyScope';
99
import ChildScope from './ChildScope';
10+
import FunctionBodyScope from './FunctionBodyScope';
1011
import type Scope from './Scope';
1112

1213
export default class ParameterScope extends ChildScope {
@@ -19,7 +20,7 @@ export default class ParameterScope extends ChildScope {
1920
super(parent, context);
2021
this.bodyScope = isCatchScope
2122
? new CatchBodyScope(this, context)
22-
: new ChildScope(this, context);
23+
: new FunctionBodyScope(this, context);
2324
}
2425

2526
/**

src/ast/scopes/Scope.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default class Scope {
1515
/*
1616
Redeclaration rules:
1717
- var can redeclare var
18+
- in function scopes, function and var can redeclare function and var
1819
- var is hoisted across scopes, function remains in the scope it is declared
1920
- var and function can redeclare function parameters, but parameters cannot redeclare parameters
2021
- function cannot redeclare catch scope parameters
@@ -34,11 +35,7 @@ export default class Scope {
3435
this.hoistedVariables?.get(name) || (this.variables.get(name) as LocalVariable);
3536
if (existingVariable) {
3637
const existingKind = existingVariable.kind;
37-
if (
38-
(kind === VariableKind.var &&
39-
(existingKind === VariableKind.var || existingKind === VariableKind.parameter)) ||
40-
(kind === VariableKind.function && existingKind === VariableKind.parameter)
41-
) {
38+
if (kind === VariableKind.var && existingKind === VariableKind.var) {
4239
existingVariable.addDeclaration(identifier, init);
4340
return existingVariable;
4441
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const path = require('node:path');
2+
const ID_MAIN = path.join(__dirname, 'main.js');
3+
4+
module.exports = defineTest({
5+
description: 'throws when redeclaring a function binding as a function in a block scope',
6+
error: {
7+
code: 'REDECLARATION_ERROR',
8+
frame: `
9+
1: {
10+
2: function foo() {}
11+
3: function foo() {}
12+
^
13+
4: }`,
14+
id: ID_MAIN,
15+
loc: {
16+
column: 10,
17+
file: ID_MAIN,
18+
line: 3
19+
},
20+
message: 'Identifier "foo" has already been declared',
21+
pos: 31,
22+
watchFiles: [ID_MAIN]
23+
}
24+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
function foo() {}
3+
function foo() {}
4+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const path = require('node:path');
2+
const ID_MAIN = path.join(__dirname, 'main.js');
3+
4+
module.exports = defineTest({
5+
description: 'throws when redeclaring a top-level function binding as a function',
6+
error: {
7+
code: 'REDECLARATION_ERROR',
8+
frame: `
9+
1: function foo() {}
10+
2: function foo() {}
11+
^`,
12+
id: ID_MAIN,
13+
loc: {
14+
column: 9,
15+
file: ID_MAIN,
16+
line: 2
17+
},
18+
message: 'Identifier "foo" has already been declared',
19+
pos: 27,
20+
watchFiles: [ID_MAIN]
21+
}
22+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
function foo() {}
2+
function foo() {}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const path = require('node:path');
2+
const ID_MAIN = path.join(__dirname, 'main.js');
3+
4+
module.exports = defineTest({
5+
description: 'throws when redeclaring a top-level var binding as a function',
6+
error: {
7+
code: 'REDECLARATION_ERROR',
8+
frame: `
9+
1: var foo;
10+
2: function foo() {}
11+
^`,
12+
id: ID_MAIN,
13+
loc: {
14+
column: 9,
15+
file: ID_MAIN,
16+
line: 2
17+
},
18+
message: 'Identifier "foo" has already been declared',
19+
pos: 18,
20+
watchFiles: [ID_MAIN]
21+
}
22+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
var foo;
2+
function foo() {}

0 commit comments

Comments
 (0)