Files
ask262/engine262/src/parser/Scope.mts
T

539 lines
15 KiB
TypeScript

import { Assert, Parser } from '../index.mts';
import { isArray, OutOfRange } from '../helpers.mts';
import type { TokenData } from './Lexer.mts';
import type { ParseNode } from './ParseNode.mts';
export enum Flag {
return = 1 << 0,
await = 1 << 1,
yield = 1 << 2,
parameters = 1 << 3,
newTarget = 1 << 4,
importMeta = 1 << 5,
superCall = 1 << 6,
superProperty = 1 << 7,
in = 1 << 8,
default = 1 << 9,
module = 1 << 10,
classStaticBlock = 1 << 11,
}
export interface DeclarationInfo {
readonly name: string;
readonly node: ParseNode;
}
export function getDeclarations(node: ParseNode | readonly ParseNode[]): DeclarationInfo[] {
if (isArray(node)) {
return node.flatMap((n) => getDeclarations(n));
}
switch (node.type) {
case 'LexicalBinding':
case 'VariableDeclaration':
case 'BindingRestElement':
case 'ForBinding':
if (node.BindingIdentifier) {
return getDeclarations(node.BindingIdentifier);
}
if (node.BindingPattern) {
return getDeclarations(node.BindingPattern);
}
return [];
case 'BindingRestProperty':
if (node.BindingIdentifier) {
return getDeclarations(node.BindingIdentifier);
}
return [];
case 'SingleNameBinding':
return getDeclarations(node.BindingIdentifier);
case 'ImportClause': {
const d = [];
if (node.ImportedDefaultBinding) {
d.push(...getDeclarations(node.ImportedDefaultBinding));
}
if (node.NameSpaceImport) {
d.push(...getDeclarations(node.NameSpaceImport));
}
if (node.NamedImports) {
d.push(...getDeclarations(node.NamedImports));
}
return d;
}
case 'ImportSpecifier':
return getDeclarations(node.ImportedBinding);
case 'ImportedDefaultBinding':
case 'NameSpaceImport':
return getDeclarations(node.ImportedBinding);
case 'NamedImports':
return getDeclarations(node.ImportsList);
case 'ObjectBindingPattern': {
const declarations = getDeclarations(node.BindingPropertyList);
if (node.BindingRestProperty) {
declarations.push(...getDeclarations(node.BindingRestProperty));
}
return declarations;
}
case 'ArrayBindingPattern': {
const declarations = getDeclarations(node.BindingElementList);
if (node.BindingRestElement) {
declarations.push(...getDeclarations(node.BindingRestElement));
}
return declarations;
}
case 'BindingElement':
return getDeclarations(node.BindingPattern);
case 'BindingProperty':
return getDeclarations(node.BindingElement);
case 'BindingIdentifier':
case 'IdentifierName':
case 'LabelIdentifier':
return [{ name: node.name, node }];
case 'PrivateIdentifier':
return [{ name: `#${node.name}`, node }];
case 'StringLiteral':
return [{ name: node.value, node }];
case 'Elision':
return [];
case 'ForDeclaration':
return getDeclarations(node.ForBinding);
case 'ExportSpecifier':
return getDeclarations(node.exportName);
case 'FunctionDeclaration':
case 'GeneratorDeclaration':
case 'AsyncFunctionDeclaration':
case 'AsyncGeneratorDeclaration':
Assert(!!node.BindingIdentifier);
return getDeclarations(node.BindingIdentifier);
case 'LexicalDeclaration':
return getDeclarations(node.BindingList);
case 'VariableStatement':
return getDeclarations(node.VariableDeclarationList);
case 'ClassDeclaration':
Assert(!!node.BindingIdentifier);
return getDeclarations(node.BindingIdentifier);
default:
throw new OutOfRange('getDeclarations', node);
}
}
export type ScopeFlagSetters =
& { readonly [P in (keyof typeof Flag) & string]?: boolean; }
& {
readonly lexical?: boolean;
readonly variable?: boolean;
readonly variableFunctions?: boolean;
readonly private?: boolean;
readonly label?: LabelType | 'boundary';
readonly strict?: boolean;
};
export interface ScopeInfo {
readonly flags: ScopeFlagSetters;
readonly lexicals: Set<string>;
readonly variables: Set<string>;
readonly functions: Set<string>;
readonly parameters: Set<string>;
}
export interface PrivateScopeInfo {
readonly outer: PrivateScopeInfo | undefined;
readonly names: Map<string, Set<'field' | 'method' | 'get' | 'set'>>;
}
export interface UndefinedPrivateAccessInfo {
readonly node: ParseNode;
readonly name: string;
readonly scope: PrivateScopeInfo | undefined;
}
export interface ArrowInfo {
readonly isAsync: boolean;
hasTrailingComma: boolean;
readonly yieldExpressions: ParseNode[];
readonly awaitExpressions: ParseNode[];
readonly awaitIdentifiers: ParseNode[];
merge(other: ArrowInfo): void;
}
export interface AssignmentInfo {
readonly type: 'assign' | 'arrow' | 'for';
readonly earlyErrors: SyntaxError[];
clear(): void;
}
export type LabelType = 'switch' | 'loop';
export interface Label {
type: LabelType | null;
readonly name?: string;
readonly nextToken?: TokenData | null;
}
export class Scope {
private readonly parser: Parser;
private readonly scopeStack: ScopeInfo[] = [];
labels: Label[] = [];
readonly arrowInfoStack: (ArrowInfo | null)[] = [];
readonly assignmentInfoStack: AssignmentInfo[] = [];
readonly exports = new Set<string>();
readonly undefinedExports = new Map<string, ParseNode.ModuleExportName>();
privateScope: PrivateScopeInfo | undefined;
private readonly undefinedPrivateAccesses: UndefinedPrivateAccessInfo[] = [];
private flags: Flag = 0 as Flag;
constructor(parser: Parser) {
this.parser = parser;
}
hasReturn() {
return (this.flags & Flag.return) !== 0;
}
hasAwait() {
return (this.flags & Flag.await) !== 0;
}
hasYield() {
return (this.flags & Flag.yield) !== 0;
}
hasNewTarget() {
return (this.flags & Flag.newTarget) !== 0;
}
hasSuperCall() {
return (this.flags & Flag.superCall) !== 0;
}
hasSuperProperty() {
return (this.flags & Flag.superProperty) !== 0;
}
hasImportMeta() {
return (this.flags & Flag.importMeta) !== 0;
}
hasIn() {
return (this.flags & Flag.in) !== 0;
}
inParameters() {
return (this.flags & Flag.parameters) !== 0;
}
inClassStaticBlock() {
return (this.flags & Flag.classStaticBlock) !== 0;
}
isDefault() {
return (this.flags & Flag.default) !== 0;
}
isModule() {
return (this.flags & Flag.module) !== 0;
}
with<R>(flags: ScopeFlagSetters, f: () => R) {
const oldFlags = this.flags;
Object.entries(flags)
.forEach(([k, v]) => {
if (k in Flag && typeof Flag[k as keyof typeof Flag] === 'number') {
if (v === true) {
this.flags |= Flag[k as keyof typeof Flag];
} else if (v === false) {
this.flags &= ~Flag[k as keyof typeof Flag];
}
}
});
if (flags.lexical || flags.variable) {
this.scopeStack.push({
flags,
lexicals: new Set(),
variables: new Set(),
functions: new Set(),
parameters: new Set(),
});
}
if (flags.private) {
this.privateScope = {
outer: this.privateScope,
names: new Map(),
};
}
const oldLabels = this.labels;
if (flags.label === 'boundary') {
this.labels = [];
} else if (flags.label) {
this.labels.push({ type: flags.label });
}
const oldStrict = this.parser.state.strict;
if (flags.strict === true) {
this.parser.state.strict = true;
} else if (flags.strict === false) {
this.parser.state.strict = false;
}
const r = f();
if (flags.label === 'boundary') {
this.labels = oldLabels;
} else if (flags.label) {
this.labels.pop();
}
if (flags.private) {
this.privateScope = this.privateScope!.outer;
if (this.privateScope === undefined) {
this.undefinedPrivateAccesses.forEach(({ node, name, scope }) => {
while (scope) {
if (scope.names.has(name)) {
return;
}
scope = scope.outer;
}
this.parser.raiseEarly('NotDefined', node, name);
});
}
}
if (flags.lexical || flags.variable) {
this.scopeStack.pop();
}
this.parser.state.strict = oldStrict;
this.flags = oldFlags;
return r;
}
pushArrowInfo(isAsync = false) {
this.arrowInfoStack.push({
isAsync,
hasTrailingComma: false,
yieldExpressions: [],
awaitExpressions: [],
awaitIdentifiers: [],
merge(other) {
this.yieldExpressions.push(...other.yieldExpressions);
this.awaitExpressions.push(...other.awaitExpressions);
this.awaitIdentifiers.push(...other.awaitIdentifiers);
},
});
}
popArrowInfo() {
const arrowInfo = this.arrowInfoStack.pop();
Assert(!!arrowInfo);
return arrowInfo;
}
get arrowInfo() {
if (this.arrowInfoStack.length > 0) {
return this.arrowInfoStack[this.arrowInfoStack.length - 1];
}
return undefined;
}
pushAssignmentInfo(type: 'assign' | 'arrow' | 'for') {
const parser = this.parser;
this.assignmentInfoStack.push({
type,
earlyErrors: [],
clear() {
this.earlyErrors.forEach((e) => {
parser.earlyErrors.delete(e);
});
},
});
}
popAssignmentInfo() {
const assignmentInfo = this.assignmentInfoStack.pop();
Assert(!!assignmentInfo);
return assignmentInfo;
}
registerObjectLiteralEarlyError(error: SyntaxError) {
for (let i = this.assignmentInfoStack.length - 1; i >= 0; i -= 1) {
const info = this.assignmentInfoStack[i];
info.earlyErrors.push(error);
if (info.type !== 'assign') {
break;
}
}
}
lexicalScope() {
for (let i = this.scopeStack.length - 1; i >= 0; i -= 1) {
const scope = this.scopeStack[i];
if (scope.flags.lexical) {
return scope;
}
}
/* node:coverage ignore next */
throw new RangeError();
}
variableScope() {
for (let i = this.scopeStack.length - 1; i >= 0; i -= 1) {
const scope = this.scopeStack[i];
if (scope.flags.variable) {
return scope;
}
}
/* node:coverage ignore next */
throw new RangeError();
}
declare(node: ParseNode | readonly ParseNode[], type: 'private', extraType?: 'field' | 'method' | 'get' | 'set'): void;
declare(node: ParseNode | readonly ParseNode[], type: 'lexical' | 'import' | 'function' | 'parameter' | 'variable' | 'export'): void;
declare(node: ParseNode | readonly ParseNode[], type: 'lexical' | 'import' | 'function' | 'parameter' | 'variable' | 'export' | 'private', extraType?: 'field' | 'method' | 'get' | 'set') {
const declarations = getDeclarations(node);
declarations.forEach((d) => {
switch (type) {
case 'lexical':
case 'import': {
if (type === 'lexical' && d.name === 'let') {
this.parser.raiseEarly('LetInLexicalBinding', d.node);
}
const scope = this.lexicalScope();
if (scope.lexicals.has(d.name)
|| scope.variables.has(d.name)
|| scope.functions.has(d.name)
|| scope.parameters.has(d.name)) {
this.parser.raiseEarly('AlreadyDeclared', d.node, d.name);
}
scope.lexicals.add(d.name);
if (scope === this.scopeStack[0] && this.undefinedExports.has(d.name)) {
this.undefinedExports.delete(d.name);
}
break;
}
case 'function': {
const scope = this.lexicalScope();
if (scope.lexicals.has(d.name)) {
this.parser.raiseEarly('AlreadyDeclared', d.node, d.name);
}
if (scope.flags.variableFunctions) {
scope.functions.add(d.name);
} else {
if (scope.variables.has(d.name)) {
this.parser.raiseEarly('AlreadyDeclared', d.node, d.name);
}
scope.lexicals.add(d.name);
}
if (scope === this.scopeStack[0] && this.undefinedExports.has(d.name)) {
this.undefinedExports.delete(d.name);
}
break;
}
case 'parameter':
this.variableScope().parameters.add(d.name);
break;
case 'variable':
for (let i = this.scopeStack.length - 1; i >= 0; i -= 1) {
const scope = this.scopeStack[i];
scope.variables.add(d.name);
if (scope.lexicals.has(d.name) || (!scope.flags.variableFunctions && scope.functions.has(d.name))) {
this.parser.raiseEarly('AlreadyDeclared', d.node, d.name);
}
if (i === 0 && this.undefinedExports.has(d.name)) {
this.undefinedExports.delete(d.name);
}
if (scope.flags.variable) {
break;
}
}
break;
case 'export':
if (this.exports.has(d.name)) {
this.parser.raiseEarly('AlreadyDeclared', d.node, d.name);
} else {
this.exports.add(d.name);
}
break;
case 'private': {
const types = this.privateScope!.names.get(d.name);
if (types) {
let duplicate = true;
switch (extraType) {
case 'field':
case 'method':
break;
case 'set':
case 'get':
duplicate = types.has(extraType) || types.has('field') || types.has('method');
types.add(extraType);
break;
default:
break;
}
if (duplicate) {
this.parser.raiseEarly('AlreadyDeclared', d.node, d.name);
}
} else if (extraType) {
this.privateScope!.names.set(d.name, new Set([extraType]));
}
break;
}
/* node:coverage ignore next 2 */
default:
throw new RangeError(type);
}
});
}
checkUndefinedExports(NamedExports: ParseNode.NamedExports) {
const scope = this.variableScope();
NamedExports.ExportsList.forEach((n) => {
const name = n.localName.type === 'IdentifierName' ? n.localName.name : n.localName.value;
if (!scope.lexicals.has(name) && !scope.variables.has(name)) {
this.undefinedExports.set(name, n.localName);
}
});
}
checkUndefinedPrivate(PrivateIdentifier: ParseNode.PrivateIdentifier) {
if (this.parser.state.allowAllPrivateNames) {
return;
}
const [{ node, name }] = getDeclarations(PrivateIdentifier);
if (!this.privateScope) {
this.parser.raiseEarly('NotDefined', node, name);
return;
}
let scope: PrivateScopeInfo | undefined = this.privateScope;
while (scope) {
if (scope.names.has(name)) {
return;
}
scope = scope.outer;
}
this.undefinedPrivateAccesses.push({
node,
name,
scope: this.privateScope,
});
}
}