UNPKG

@quick-game/cli

Version:

Command line interface for rapid qg development

448 lines 18.3 kB
// Copyright 2022 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Acorn from '../../third_party/acorn/acorn.js'; import { ECMA_VERSION } from './AcornTokenizer.js'; export function parseScopes(expression, sourceType = 'script') { // Parse the expression and find variables and scopes. let root = null; try { root = Acorn.parse(expression, { ecmaVersion: ECMA_VERSION, allowAwaitOutsideFunction: true, ranges: false, sourceType }); } catch { return null; } return new ScopeVariableAnalysis(root).run(); } export class Scope { variables = new Map(); parent; start; end; children = []; constructor(start, end, parent) { this.start = start; this.end = end; this.parent = parent; if (parent) { parent.children.push(this); } } export() { const variables = []; for (const variable of this.variables) { const offsets = []; for (const use of variable[1].uses) { offsets.push(use.offset); } variables.push({ name: variable[0], kind: variable[1].definitionKind, offsets }); } const children = this.children.map(c => c.export()); return { start: this.start, end: this.end, variables, children, }; } addVariable(name, offset, definitionKind, isShorthandAssignmentProperty) { const variable = this.variables.get(name); const use = { offset, scope: this, isShorthandAssignmentProperty }; if (!variable) { this.variables.set(name, { definitionKind, uses: [use] }); return; } if (variable.definitionKind === 0 /* DefinitionKind.None */) { variable.definitionKind = definitionKind; } variable.uses.push(use); } findBinders(name) { const result = []; let scope = this; while (scope !== null) { const defUse = scope.variables.get(name); if (defUse && defUse.definitionKind !== 0 /* DefinitionKind.None */) { result.push(defUse); } scope = scope.parent; } return result; } #mergeChildDefUses(name, defUses) { const variable = this.variables.get(name); if (!variable) { this.variables.set(name, defUses); return; } variable.uses.push(...defUses.uses); if (defUses.definitionKind === 2 /* DefinitionKind.Var */) { console.assert(variable.definitionKind !== 1 /* DefinitionKind.Let */); if (variable.definitionKind === 0 /* DefinitionKind.None */) { variable.definitionKind = defUses.definitionKind; } } else { console.assert(defUses.definitionKind === 0 /* DefinitionKind.None */); } } finalizeToParent(isFunctionScope) { if (!this.parent) { console.error('Internal error: wrong nesting in scope analysis.'); throw new Error('Internal error'); } // Move all unbound variables to the parent (also move var-bound variables // if the parent is not a function). const keysToRemove = []; for (const [name, defUse] of this.variables.entries()) { if (defUse.definitionKind === 0 /* DefinitionKind.None */ || (defUse.definitionKind === 2 /* DefinitionKind.Var */ && !isFunctionScope)) { this.parent.#mergeChildDefUses(name, defUse); keysToRemove.push(name); } } keysToRemove.forEach(k => this.variables.delete(k)); } } export class ScopeVariableAnalysis { #rootScope; #allNames = new Set(); #currentScope; #rootNode; constructor(node) { this.#rootNode = node; this.#rootScope = new Scope(node.start, node.end, null); this.#currentScope = this.#rootScope; } run() { this.#processNode(this.#rootNode); return this.#rootScope; } #processNode(node) { if (node === null) { return; } switch (node.type) { case 'AwaitExpression': this.#processNode(node.argument); break; case 'ArrayExpression': node.elements.forEach(item => this.#processNode(item)); break; case 'ExpressionStatement': this.#processNode(node.expression); break; case 'Program': console.assert(this.#currentScope === this.#rootScope); node.body.forEach(item => this.#processNode(item)); console.assert(this.#currentScope === this.#rootScope); break; case 'ArrayPattern': node.elements.forEach(item => this.#processNode(item)); break; case 'ArrowFunctionExpression': { this.#pushScope(node.start, node.end); node.params.forEach(this.#processNodeAsDefinition.bind(this, 2 /* DefinitionKind.Var */, false)); if (node.body.type === 'BlockStatement') { // Include the body of the arrow function in the same scope as the arguments. node.body.body.forEach(this.#processNode.bind(this)); } else { this.#processNode(node.body); } this.#popScope(true); break; } case 'AssignmentExpression': case 'AssignmentPattern': case 'BinaryExpression': case 'LogicalExpression': this.#processNode(node.left); this.#processNode(node.right); break; case 'BlockStatement': this.#pushScope(node.start, node.end); node.body.forEach(this.#processNode.bind(this)); this.#popScope(false); break; case 'CallExpression': this.#processNode(node.callee); node.arguments.forEach(this.#processNode.bind(this)); break; case 'VariableDeclaration': { const definitionKind = node.kind === 'var' ? 2 /* DefinitionKind.Var */ : 1 /* DefinitionKind.Let */; node.declarations.forEach(this.#processVariableDeclarator.bind(this, definitionKind)); break; } case 'CatchClause': this.#pushScope(node.start, node.end); this.#processNodeAsDefinition(1 /* DefinitionKind.Let */, false, node.param); this.#processNode(node.body); this.#popScope(false); break; case 'ClassBody': node.body.forEach(this.#processNode.bind(this)); break; case 'ClassDeclaration': this.#processNodeAsDefinition(1 /* DefinitionKind.Let */, false, node.id); this.#processNode(node.superClass ?? null); this.#processNode(node.body); break; case 'ClassExpression': // Intentionally ignore the id. this.#processNode(node.superClass ?? null); this.#processNode(node.body); break; case 'ChainExpression': this.#processNode(node.expression); break; case 'ConditionalExpression': this.#processNode(node.test); this.#processNode(node.consequent); this.#processNode(node.alternate); break; case 'DoWhileStatement': this.#processNode(node.body); this.#processNode(node.test); break; case 'ForInStatement': case 'ForOfStatement': this.#pushScope(node.start, node.end); this.#processNode(node.left); this.#processNode(node.right); this.#processNode(node.body); this.#popScope(false); break; case 'ForStatement': this.#pushScope(node.start, node.end); this.#processNode(node.init ?? null); this.#processNode(node.test ?? null); this.#processNode(node.update ?? null); this.#processNode(node.body); this.#popScope(false); break; case 'FunctionDeclaration': this.#processNodeAsDefinition(2 /* DefinitionKind.Var */, false, node.id); this.#pushScope(node.id?.end ?? node.start, node.end); this.#addVariable('this', node.start, 3 /* DefinitionKind.Fixed */); this.#addVariable('arguments', node.start, 3 /* DefinitionKind.Fixed */); node.params.forEach(this.#processNodeAsDefinition.bind(this, 1 /* DefinitionKind.Let */, false)); // Process the body of the block statement directly to avoid creating new scope. node.body.body.forEach(this.#processNode.bind(this)); this.#popScope(true); break; case 'FunctionExpression': this.#pushScope(node.id?.end ?? node.start, node.end); this.#addVariable('this', node.start, 3 /* DefinitionKind.Fixed */); this.#addVariable('arguments', node.start, 3 /* DefinitionKind.Fixed */); node.params.forEach(this.#processNodeAsDefinition.bind(this, 1 /* DefinitionKind.Let */, false)); // Process the body of the block statement directly to avoid creating new scope. node.body.body.forEach(this.#processNode.bind(this)); this.#popScope(true); break; case 'Identifier': this.#addVariable(node.name, node.start); break; case 'IfStatement': this.#processNode(node.test); this.#processNode(node.consequent); this.#processNode(node.alternate ?? null); break; case 'LabeledStatement': this.#processNode(node.body); break; case 'MetaProperty': break; case 'MethodDefinition': if (node.computed) { this.#processNode(node.key); } this.#processNode(node.value); break; case 'NewExpression': this.#processNode(node.callee); node.arguments.forEach(this.#processNode.bind(this)); break; case 'MemberExpression': this.#processNode(node.object); if (node.computed) { this.#processNode(node.property); } break; case 'ObjectExpression': node.properties.forEach(this.#processNode.bind(this)); break; case 'ObjectPattern': node.properties.forEach(this.#processNode.bind(this)); break; case 'PrivateIdentifier': break; case 'PropertyDefinition': if (node.computed) { this.#processNode(node.key); } this.#processNode(node.value ?? null); break; case 'Property': if (node.shorthand) { console.assert(node.value.type === 'Identifier'); console.assert(node.key.type === 'Identifier'); console.assert(node.value.name === node.key.name); this.#addVariable(node.value.name, node.value.start, 0 /* DefinitionKind.None */, true); } else { if (node.computed) { this.#processNode(node.key); } this.#processNode(node.value); } break; case 'RestElement': this.#processNodeAsDefinition(1 /* DefinitionKind.Let */, false, node.argument); break; case 'ReturnStatement': this.#processNode(node.argument ?? null); break; case 'SequenceExpression': node.expressions.forEach(this.#processNode.bind(this)); break; case 'SpreadElement': this.#processNode(node.argument); break; case 'SwitchCase': this.#processNode(node.test ?? null); node.consequent.forEach(this.#processNode.bind(this)); break; case 'SwitchStatement': this.#processNode(node.discriminant); node.cases.forEach(this.#processNode.bind(this)); break; case 'TaggedTemplateExpression': this.#processNode(node.tag); this.#processNode(node.quasi); break; case 'TemplateLiteral': node.expressions.forEach(this.#processNode.bind(this)); break; case 'ThisExpression': this.#addVariable('this', node.start); break; case 'ThrowStatement': this.#processNode(node.argument); break; case 'TryStatement': this.#processNode(node.block); this.#processNode(node.handler ?? null); this.#processNode(node.finalizer ?? null); break; case 'WithStatement': this.#processNode(node.object); // TODO jarin figure how to treat the with body. this.#processNode(node.body); break; case 'YieldExpression': this.#processNode(node.argument ?? null); break; case 'UnaryExpression': case 'UpdateExpression': this.#processNode(node.argument); break; case 'WhileStatement': this.#processNode(node.test); this.#processNode(node.body); break; // Ignore, no expressions involved. case 'BreakStatement': case 'ContinueStatement': case 'DebuggerStatement': case 'EmptyStatement': case 'Literal': case 'Super': case 'TemplateElement': break; // Ignore, cannot be used outside of a module. case 'ImportDeclaration': case 'ImportDefaultSpecifier': case 'ImportNamespaceSpecifier': case 'ImportSpecifier': case 'ImportExpression': case 'ExportAllDeclaration': case 'ExportDefaultDeclaration': case 'ExportNamedDeclaration': case 'ExportSpecifier': break; case 'VariableDeclarator': console.error('Should not encounter VariableDeclarator in general traversal.'); break; } } getFreeVariables() { const result = new Map(); for (const [name, defUse] of this.#rootScope.variables) { if (defUse.definitionKind !== 0 /* DefinitionKind.None */) { // Skip bound variables. continue; } result.set(name, defUse.uses); } return result; } getAllNames() { return this.#allNames; } #pushScope(start, end) { this.#currentScope = new Scope(start, end, this.#currentScope); } #popScope(isFunctionContext) { if (this.#currentScope.parent === null) { console.error('Internal error: wrong nesting in scope analysis.'); throw new Error('Internal error'); } this.#currentScope.finalizeToParent(isFunctionContext); this.#currentScope = this.#currentScope.parent; } #addVariable(name, offset, definitionKind = 0 /* DefinitionKind.None */, isShorthandAssignmentProperty = false) { this.#allNames.add(name); this.#currentScope.addVariable(name, offset, definitionKind, isShorthandAssignmentProperty); } #processNodeAsDefinition(definitionKind, isShorthandAssignmentProperty, node) { if (node === null) { return; } switch (node.type) { case 'ArrayPattern': node.elements.forEach(this.#processNodeAsDefinition.bind(this, definitionKind, false)); break; case 'AssignmentPattern': this.#processNodeAsDefinition(definitionKind, isShorthandAssignmentProperty, node.left); this.#processNode(node.right); break; case 'Identifier': this.#addVariable(node.name, node.start, definitionKind, isShorthandAssignmentProperty); break; case 'MemberExpression': this.#processNode(node.object); if (node.computed) { this.#processNode(node.property); } break; case 'ObjectPattern': node.properties.forEach(this.#processNodeAsDefinition.bind(this, definitionKind, false)); break; case 'Property': if (node.computed) { this.#processNode(node.key); } this.#processNodeAsDefinition(definitionKind, node.shorthand, node.value); break; case 'RestElement': this.#processNodeAsDefinition(definitionKind, false, node.argument); break; } } #processVariableDeclarator(definitionKind, decl) { this.#processNodeAsDefinition(definitionKind, false, decl.id); this.#processNode(decl.init ?? null); } } //# sourceMappingURL=ScopeParser.js.map