tools/hermes-parser/js/hermes-eslint/src/scope-manager/referencer/Referencer.js (641 lines of code) (raw):

/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format */ 'use strict'; import type { ArrowFunctionExpression, AssignmentExpression, AssignmentPattern, BlockStatement, BreakStatement, CallExpression, CatchClause, ClassDeclaration, ClassExpression, ContinueStatement, DeclareClass, DeclareExportAllDeclaration, DeclareExportDeclaration, DeclareFunction, DeclareInterface, DeclareModule, DeclareOpaqueType, DeclareTypeAlias, DeclareVariable, EnumDeclaration, ESNode, ExportAllDeclaration, ExportDefaultDeclaration, ExportNamedDeclaration, ExportNamespaceSpecifier, ForInStatement, ForOfStatement, ForStatement, FunctionDeclaration, FunctionExpression, Identifier, ImportAttribute, ImportDeclaration, InterfaceDeclaration, JSXAttribute, JSXClosingElement, JSXFragment, JSXIdentifier, JSXMemberExpression, JSXOpeningElement, LabeledStatement, MemberExpression, MetaProperty, NewExpression, OpaqueType, OptionalCallExpression, OptionalMemberExpression, PrivateName, Program, Property, SwitchStatement, TaggedTemplateExpression, TypeAlias, TypeCastExpression, UpdateExpression, VariableDeclaration, WithStatement, DeclareModuleExports, } from 'hermes-estree'; import type {ReferenceImplicitGlobal} from './Reference'; import type {VisitorOptions} from './Visitor'; import type {Scope} from '../scope'; import {ClassVisitor} from './ClassVisitor'; import {ExportVisitor} from './ExportVisitor'; import {ImportVisitor} from './ImportVisitor'; import {PatternVisitor} from './PatternVisitor'; import {ReferenceFlag} from './Reference'; import {ScopeManager} from '../ScopeManager'; import {TypeVisitor} from './TypeVisitor'; import {Visitor} from './Visitor'; import { CatchClauseDefinition, EnumDefinition, FunctionNameDefinition, ParameterDefinition, VariableDefinition, } from '../definition'; type ReferencerOptions = $ReadOnly<{ ...VisitorOptions, jsxPragma: string | null, jsxFragmentName: string | null, }>; // Referencing variables and creating bindings. class Referencer extends Visitor { +_jsxPragma: string | null; +_jsxFragmentName: string | null; _hasReferencedJsxFactory = false; _hasReferencedJsxFragmentFactory = false; +scopeManager: ScopeManager; constructor( {childVisitorKeys, jsxFragmentName, jsxPragma}: ReferencerOptions, scopeManager: ScopeManager, ) { super({childVisitorKeys}); this.scopeManager = scopeManager; this._jsxPragma = jsxPragma; this._jsxFragmentName = jsxFragmentName; } currentScope: { (): Scope, (throwOnNull: true): Scope | null, } = (dontThrowOnNull?: boolean) => { if (dontThrowOnNull !== true) { if (this.scopeManager.currentScope == null) { throw new Error('Expected there to be a current scope.'); } } // $FlowExpectedError[incompatible-type] return this.scopeManager.currentScope; }; close(node: ESNode): void { while (this.currentScope(true) && node === this.currentScope().block) { this.scopeManager.currentScope = this.currentScope().close( this.scopeManager, ); } } referencingDefaultValue( pattern: Identifier, assignments: Array<AssignmentExpression | AssignmentPattern>, maybeImplicitGlobal: ReferenceImplicitGlobal | null, init: boolean, ): void { assignments.forEach(assignment => { this.currentScope().referenceValue( pattern, ReferenceFlag.Write, assignment.right, maybeImplicitGlobal, init, ); }); } /** * Searches for a variable named "name" in the upper scopes and adds a pseudo-reference from itself to itself */ _referenceInSomeUpperScope(name: string): boolean { let scope = this.scopeManager.currentScope; while (scope) { const variable = scope.set.get(name); if (!variable) { scope = scope.upper; continue; } scope.referenceValue(variable.identifiers[0]); return true; } return false; } _referenceJsxPragma(): void { if (this._jsxPragma == null || this._hasReferencedJsxFactory) { return; } this._hasReferencedJsxFactory = this._referenceInSomeUpperScope( this._jsxPragma, ); } _referenceJsxFragment(): void { if ( this._jsxFragmentName == null || this._hasReferencedJsxFragmentFactory ) { return; } this._hasReferencedJsxFragmentFactory = this._referenceInSomeUpperScope( this._jsxFragmentName, ); } /////////////////// // Visit helpers // /////////////////// visitCallExpression(node: CallExpression | OptionalCallExpression): void { this.visitChildren(node, ['typeArguments']); this.visitType(node.typeArguments); } visitClass(node: ClassDeclaration | ClassExpression): void { ClassVisitor.visit(this, node); } visitForIn(node: ForInStatement | ForOfStatement): void { if (node.left.type === 'VariableDeclaration' && node.left.kind !== 'var') { this.scopeManager.nestForScope(node); } if (node.left.type === 'VariableDeclaration') { const left = node.left; this.visit(left); this.visitPattern( left.declarations[0].id, pattern => { this.currentScope().referenceValue( pattern, ReferenceFlag.Write, node.right, null, true, ); }, typeAnnotation => { this.visitType(typeAnnotation); }, ); } else { this.visitPattern( node.left, (pattern, info) => { const maybeImplicitGlobal = !this.currentScope().isStrict ? { pattern, node, } : null; this.referencingDefaultValue( pattern, info.assignments, maybeImplicitGlobal, false, ); this.currentScope().referenceValue( pattern, ReferenceFlag.Write, node.right, maybeImplicitGlobal, false, ); }, typeAnnotation => { this.visitType(typeAnnotation); }, {processRightHandNodes: true}, ); } this.visit(node.right); this.visit(node.body); this.close(node); } visitFunction( node: ArrowFunctionExpression | FunctionDeclaration | FunctionExpression, ): void { // FunctionDeclaration name is defined in upper scope // NOTE: Not referring variableScope. It is intended. // Since // in ES5, FunctionDeclaration should be in FunctionBody. // in ES6, FunctionDeclaration should be block scoped. switch (node.type) { case 'FunctionExpression': { if (node.id) { // FunctionExpression with name creates its special scope; // FunctionExpressionNameScope. this.scopeManager.nestFunctionExpressionNameScope(node); } break; } case 'FunctionDeclaration': { if (node.id != null) { const id = node.id; // id is defined in upper scope this.currentScope().defineIdentifier( id, new FunctionNameDefinition(id, node), ); } break; } case 'ArrowFunctionExpression': { break; } } this.scopeManager.nestFunctionScope(node, false); // function type parameters can be referenced by function params, so have to be declared first this.visitType(node.typeParameters); // Return type may reference type parameters but not function parameters, so visit it before the parameters this.visitType(node.returnType); // Process parameter declarations. for (const param of node.params) { if (param.type === 'Identifier' && param.name === 'this') { // `this` parameters don't declare variables, nor can they have default values // but will have an annotation this.visitType(param.typeAnnotation); continue; } this.visitPattern( param, (pattern, info) => { this.currentScope().defineIdentifier( pattern, new ParameterDefinition(pattern, node, info.rest), ); this.referencingDefaultValue(pattern, info.assignments, null, true); }, typeAnnotation => { this.visitType(typeAnnotation); }, {processRightHandNodes: true}, ); } // In TypeScript there are a number of function-like constructs which have no body, // so check it exists before traversing if (node.body) { // Skip BlockStatement to prevent creating BlockStatement scope. if (node.body.type === 'BlockStatement') { this.visitChildren(node.body); } else { this.visit(node.body); } } this.close(node); } visitMemberExpression( node: MemberExpression | OptionalMemberExpression, ): void { this.visit(node.object); if (node.computed === true) { this.visit(node.property); } } visitProperty(node: Property): void { if (node.computed) { this.visit(node.key); } this.visit(node.value); } visitType: (?ESNode) => void = (node): void => { if (!node) { return; } TypeVisitor.visit(this, node); }; ///////////////////// // Visit selectors // ///////////////////// ArrowFunctionExpression(node: ArrowFunctionExpression): void { this.visitFunction(node); } AssignmentExpression(node: AssignmentExpression): void { const left = node.left; if (PatternVisitor.isPattern(left)) { if (node.operator === '=') { this.visitPattern( left, (pattern, info) => { const maybeImplicitGlobal = !this.currentScope().isStrict ? { pattern, node, } : null; this.referencingDefaultValue( pattern, info.assignments, maybeImplicitGlobal, false, ); this.currentScope().referenceValue( pattern, ReferenceFlag.Write, node.right, maybeImplicitGlobal, false, ); }, () => {}, {processRightHandNodes: true}, ); } else if (left.type === 'Identifier') { this.currentScope().referenceValue( left, ReferenceFlag.ReadWrite, node.right, ); } } else { this.visit(left); } this.visit(node.right); } BlockStatement(node: BlockStatement): void { if (this.scopeManager.isES6()) { this.scopeManager.nestBlockScope(node); } this.visitChildren(node); this.close(node); } BreakStatement(_: BreakStatement): void { // don't reference the break statement's label } CallExpression(node: CallExpression): void { this.visitCallExpression(node); } CatchClause(node: CatchClause): void { this.scopeManager.nestCatchScope(node); if (node.param) { const param = node.param; this.visitPattern( param, (pattern, info) => { this.currentScope().defineIdentifier( pattern, new CatchClauseDefinition(param, node), ); this.referencingDefaultValue(pattern, info.assignments, null, true); }, typeAnnotation => { this.visitType(typeAnnotation); }, {processRightHandNodes: true}, ); } this.visit(node.body); this.close(node); } ClassExpression(node: ClassExpression): void { this.visitClass(node); } ClassDeclaration(node: ClassDeclaration): void { this.visitClass(node); } ContinueStatement(_: ContinueStatement): void { // don't reference the continue statement's label } EnumDeclaration(node: EnumDeclaration): void { this.currentScope().defineIdentifier( node.id, new EnumDefinition(node.id, node), ); // Enum body cannot contain identifier references, so no need to visit body. } ExportAllDeclaration(_: ExportAllDeclaration): void { // this defines no local variables } ExportDefaultDeclaration(node: ExportDefaultDeclaration): void { if (node.declaration.type === 'Identifier') { ExportVisitor.visit(this, node); } else { this.visit(node.declaration); } } ExportNamedDeclaration(node: ExportNamedDeclaration): void { if (node.declaration) { this.visit(node.declaration); } else { ExportVisitor.visit(this, node); } } ExportNamespaceSpecifier(_: ExportNamespaceSpecifier): void { // this defines no local variables } ForInStatement(node: ForInStatement): void { this.visitForIn(node); } ForOfStatement(node: ForOfStatement): void { this.visitForIn(node); } ForStatement(node: ForStatement): void { // Create ForStatement declaration. // NOTE: In ES6, ForStatement dynamically generates per iteration environment. However, this is // a static analyzer, we only generate one scope for ForStatement. if ( node.init && node.init.type === 'VariableDeclaration' && node.init.kind !== 'var' ) { this.scopeManager.nestForScope(node); } this.visitChildren(node); this.close(node); } FunctionDeclaration(node: FunctionDeclaration): void { this.visitFunction(node); } FunctionExpression(node: FunctionExpression): void { this.visitFunction(node); } Identifier(node: Identifier): void { this.currentScope().referenceValue(node); this.visitType(node.typeAnnotation); } ImportAttribute(_: ImportAttribute): void { // import assertions are module metadata and thus have no variables to reference } ImportDeclaration(node: ImportDeclaration): void { if (!this.scopeManager.isES6() || !this.scopeManager.isModule()) { throw new Error( 'ImportDeclaration should appear when the mode is ES6 and in the module context.', ); } ImportVisitor.visit(this, node); } JSXAttribute(node: JSXAttribute): void { this.visit(node.value); } JSXClosingElement(_: JSXClosingElement): void { // should not be counted as a reference } JSXFragment(node: JSXFragment): void { this._referenceJsxPragma(); this._referenceJsxFragment(); this.visitChildren(node); } JSXIdentifier(node: JSXIdentifier): void { this.currentScope().referenceValue(node); } JSXMemberExpression(node: JSXMemberExpression): void { this.visit(node.object); // we don't ever reference the property as it's always going to be a property on the thing } JSXOpeningElement(node: JSXOpeningElement): void { this._referenceJsxPragma(); if (node.name.type === 'JSXIdentifier') { const name = node.name.name; if (name[0].toUpperCase() === name[0]) { // lower cased component names are always treated as "intrinsic" names, and are converted to a string, // not a variable by JSX transforms: // <div /> => React.createElement("div", null) this.visit(node.name); } } else { this.visit(node.name); } for (const attr of node.attributes) { this.visit(attr); } } LabeledStatement(node: LabeledStatement): void { this.visit(node.body); } MemberExpression(node: MemberExpression): void { this.visitMemberExpression(node); } MetaProperty(_: MetaProperty): void { // meta properties all builtin globals } NewExpression(node: NewExpression): void { this.visitChildren(node, ['typeArguments']); this.visitType(node.typeArguments); } OptionalCallExpression(node: OptionalCallExpression): void { this.visitCallExpression(node); } OptionalMemberExpression(node: MemberExpression): void { this.visitMemberExpression(node); } PrivateName(_: PrivateName): void { // private names can only reference class properties } Program(node: Program): void { this.scopeManager.nestGlobalScope(node); if (this.scopeManager.isGlobalReturn()) { // Force strictness of GlobalScope to false when using node.js scope. this.currentScope().isStrict = false; this.scopeManager.nestFunctionScope(node, false); } if (this.scopeManager.isES6() && this.scopeManager.isModule()) { this.scopeManager.nestModuleScope(node); } if ( this.scopeManager.isStrictModeSupported() && this.scopeManager.isImpliedStrict() ) { this.currentScope().isStrict = true; } this.visitChildren(node); this.close(node); } Property(node: Property): void { this.visitProperty(node); } SwitchStatement(node: SwitchStatement): void { this.visit(node.discriminant); if (this.scopeManager.isES6()) { this.scopeManager.nestSwitchScope(node); } for (const switchCase of node.cases) { this.visit(switchCase); } this.close(node); } TaggedTemplateExpression(node: TaggedTemplateExpression): void { this.visit(node.tag); this.visit(node.quasi); } UpdateExpression(node: UpdateExpression): void { if (PatternVisitor.isPattern(node.argument)) { this.visitPattern( node.argument, pattern => { this.currentScope().referenceValue( pattern, ReferenceFlag.ReadWrite, null, ); }, () => {}, ); } else { this.visitChildren(node); } } VariableDeclaration(node: VariableDeclaration): void { const variableTargetScope = node.kind === 'var' ? this.currentScope().variableScope : this.currentScope(); for (const decl of node.declarations) { const init = decl.init; this.visitPattern( decl.id, (pattern, info) => { variableTargetScope.defineIdentifier( pattern, new VariableDefinition(pattern, decl, node), ); this.referencingDefaultValue(pattern, info.assignments, null, true); if (init) { this.currentScope().referenceValue( pattern, ReferenceFlag.Write, init, null, true, ); } }, typeAnnotation => { this.visitType(typeAnnotation); }, {processRightHandNodes: true}, ); if (decl.init) { this.visit(decl.init); } } } WithStatement(node: WithStatement): void { this.visit(node.object); // Then nest scope for WithStatement. this.scopeManager.nestWithScope(node); this.visit(node.body); this.close(node); } // // Type node passthrough visitors // DeclareClass(node: DeclareClass): void { this.visitType(node); } DeclareVariable(node: DeclareVariable): void { this.visitType(node); } DeclareFunction(node: DeclareFunction): void { this.visitType(node); } DeclareModule(node: DeclareModule): void { this.visitType(node); } DeclareModuleExports(node: DeclareModuleExports): void { this.visitType(node); } DeclareInterface(node: DeclareInterface): void { this.visitType(node); } DeclareTypeAlias(node: DeclareTypeAlias): void { this.visitType(node); } DeclareOpaqueType(node: DeclareOpaqueType): void { this.visitType(node); } DeclareExportAllDeclaration(node: DeclareExportAllDeclaration): void { this.visitType(node); } DeclareExportDeclaration(node: DeclareExportDeclaration): void { this.visitType(node); } InterfaceDeclaration(node: InterfaceDeclaration): void { this.visitType(node); } OpaqueType(node: OpaqueType): void { this.visitType(node); } TypeAlias(node: TypeAlias): void { this.visitType(node); } TypeCastExpression(node: TypeCastExpression): void { this.visit(node.expression); this.visitType(node.typeAnnotation); } } export type {ReferencerOptions}; export {Referencer};