@typescript-eslint/scope-manager
Version:
TypeScript scope analyser for ESLint
553 lines (552 loc) • 21 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Referencer = void 0;
const types_1 = require("@typescript-eslint/types");
const assert_1 = require("../assert");
const definition_1 = require("../definition");
const lib_1 = require("../lib");
const ClassVisitor_1 = require("./ClassVisitor");
const ExportVisitor_1 = require("./ExportVisitor");
const ImportVisitor_1 = require("./ImportVisitor");
const PatternVisitor_1 = require("./PatternVisitor");
const Reference_1 = require("./Reference");
const TypeVisitor_1 = require("./TypeVisitor");
const Visitor_1 = require("./Visitor");
// Referencing variables and creating bindings.
class Referencer extends Visitor_1.Visitor {
#hasReferencedJsxFactory = false;
#hasReferencedJsxFragmentFactory = false;
#jsxFragmentName;
#jsxPragma;
#lib;
scopeManager;
constructor(options, scopeManager) {
super(options);
this.scopeManager = scopeManager;
this.#jsxPragma = options.jsxPragma;
this.#jsxFragmentName = options.jsxFragmentName;
this.#lib = options.lib;
}
populateGlobalsFromLib(globalScope) {
const flattenedLibs = new Set();
for (const lib of this.#lib) {
const definition = lib_1.lib.get(lib);
if (!definition) {
throw new Error(`Invalid value for lib provided: ${lib}`);
}
flattenedLibs.add(definition);
}
// Flatten and deduplicate the set of included libs
for (const lib of flattenedLibs) {
// By adding the dependencies to the set as we iterate it,
// they get iterated only if they are new
for (const referencedLib of lib.libs) {
flattenedLibs.add(referencedLib);
}
// This loop is guaranteed to see each included lib exactly once
for (const [name, variable] of lib.variables) {
globalScope.defineImplicitVariable(name, variable);
}
}
// for const assertions (`{} as const` / `<const>{}`)
globalScope.defineImplicitVariable('const', {
eslintImplicitGlobalSetting: 'readonly',
isTypeVariable: true,
isValueVariable: false,
});
}
close(node) {
while (this.currentScope(true) && node === this.currentScope().block) {
this.scopeManager.currentScope = this.currentScope().close(this.scopeManager);
}
}
currentScope(dontThrowOnNull) {
if (!dontThrowOnNull) {
(0, assert_1.assert)(this.scopeManager.currentScope, 'aaa');
}
return this.scopeManager.currentScope;
}
referencingDefaultValue(pattern, assignments, maybeImplicitGlobal, init) {
assignments.forEach(assignment => {
this.currentScope().referenceValue(pattern, Reference_1.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) {
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;
}
referenceJsxFragment() {
if (this.#jsxFragmentName == null ||
this.#hasReferencedJsxFragmentFactory) {
return;
}
this.#hasReferencedJsxFragmentFactory = this.referenceInSomeUpperScope(this.#jsxFragmentName);
}
referenceJsxPragma() {
if (this.#jsxPragma == null || this.#hasReferencedJsxFactory) {
return;
}
this.#hasReferencedJsxFactory = this.referenceInSomeUpperScope(this.#jsxPragma);
}
///////////////////
// Visit helpers //
///////////////////
visitClass(node) {
ClassVisitor_1.ClassVisitor.visit(this, node);
}
visitForIn(node) {
if (node.left.type === types_1.AST_NODE_TYPES.VariableDeclaration &&
node.left.kind !== 'var') {
this.scopeManager.nestForScope(node);
}
if (node.left.type === types_1.AST_NODE_TYPES.VariableDeclaration) {
this.visit(node.left);
this.visitPattern(node.left.declarations[0].id, pattern => {
this.currentScope().referenceValue(pattern, Reference_1.ReferenceFlag.Write, node.right, null, true);
});
}
else {
this.visitPattern(node.left, (pattern, info) => {
const maybeImplicitGlobal = !this.currentScope().isStrict
? {
node,
pattern,
}
: null;
this.referencingDefaultValue(pattern, info.assignments, maybeImplicitGlobal, false);
this.currentScope().referenceValue(pattern, Reference_1.ReferenceFlag.Write, node.right, maybeImplicitGlobal, false);
}, { processRightHandNodes: true });
}
this.visit(node.right);
this.visit(node.body);
this.close(node);
}
visitFunction(node) {
// 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.
if (node.type === types_1.AST_NODE_TYPES.FunctionExpression) {
if (node.id) {
// FunctionExpression with name creates its special scope;
// FunctionExpressionNameScope.
this.scopeManager.nestFunctionExpressionNameScope(node);
}
}
else if (node.id) {
// id is defined in upper scope
this.currentScope().defineIdentifier(node.id, new definition_1.FunctionNameDefinition(node.id, node));
}
// Consider this function is in the MethodDefinition.
this.scopeManager.nestFunctionScope(node, false);
// Process parameter declarations.
for (const param of node.params) {
this.visitPattern(param, (pattern, info) => {
this.currentScope().defineIdentifier(pattern, new definition_1.ParameterDefinition(pattern, node, info.rest));
this.referencingDefaultValue(pattern, info.assignments, null, true);
}, { processRightHandNodes: true });
this.visitFunctionParameterTypeAnnotation(param);
param.decorators.forEach(d => this.visit(d));
}
this.visitType(node.returnType);
this.visitType(node.typeParameters);
// 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 === types_1.AST_NODE_TYPES.BlockStatement) {
this.visitChildren(node.body);
}
else {
this.visit(node.body);
}
}
this.close(node);
}
visitFunctionParameterTypeAnnotation(node) {
switch (node.type) {
case types_1.AST_NODE_TYPES.AssignmentPattern:
this.visitType(node.left.typeAnnotation);
break;
case types_1.AST_NODE_TYPES.TSParameterProperty:
this.visitFunctionParameterTypeAnnotation(node.parameter);
break;
default:
this.visitType(node.typeAnnotation);
break;
}
}
visitJSXElement(node) {
if (node.name.type === types_1.AST_NODE_TYPES.JSXIdentifier) {
if (node.name.name[0].toUpperCase() === node.name.name[0] ||
node.name.name === 'this') {
// 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)
// the only case we want to visit a lower-cased component has its name as "this",
this.visit(node.name);
}
}
else {
this.visit(node.name);
}
}
visitProperty(node) {
if (node.computed) {
this.visit(node.key);
}
this.visit(node.value);
}
visitType(node) {
if (!node) {
return;
}
TypeVisitor_1.TypeVisitor.visit(this, node);
}
visitTypeAssertion(node) {
this.visit(node.expression);
this.visitType(node.typeAnnotation);
}
/////////////////////
// Visit selectors //
/////////////////////
ArrowFunctionExpression(node) {
this.visitFunction(node);
}
AssignmentExpression(node) {
const left = this.visitExpressionTarget(node.left);
if (PatternVisitor_1.PatternVisitor.isPattern(left)) {
if (node.operator === '=') {
this.visitPattern(left, (pattern, info) => {
const maybeImplicitGlobal = !this.currentScope().isStrict
? {
node,
pattern,
}
: null;
this.referencingDefaultValue(pattern, info.assignments, maybeImplicitGlobal, false);
this.currentScope().referenceValue(pattern, Reference_1.ReferenceFlag.Write, node.right, maybeImplicitGlobal, false);
}, { processRightHandNodes: true });
}
else if (left.type === types_1.AST_NODE_TYPES.Identifier) {
this.currentScope().referenceValue(left, Reference_1.ReferenceFlag.ReadWrite, node.right);
}
}
else {
this.visit(left);
}
this.visit(node.right);
}
BlockStatement(node) {
this.scopeManager.nestBlockScope(node);
this.visitChildren(node);
this.close(node);
}
BreakStatement() {
// don't reference the break statement's label
}
CallExpression(node) {
this.visitChildren(node, ['typeArguments']);
this.visitType(node.typeArguments);
}
CatchClause(node) {
this.scopeManager.nestCatchScope(node);
if (node.param) {
const param = node.param;
this.visitPattern(param, (pattern, info) => {
this.currentScope().defineIdentifier(pattern, new definition_1.CatchClauseDefinition(param, node));
this.referencingDefaultValue(pattern, info.assignments, null, true);
}, { processRightHandNodes: true });
}
this.visit(node.body);
this.close(node);
}
ClassDeclaration(node) {
this.visitClass(node);
}
ClassExpression(node) {
this.visitClass(node);
}
ContinueStatement() {
// don't reference the continue statement's label
}
ExportAllDeclaration() {
// this defines no local variables
}
ExportDefaultDeclaration(node) {
if (node.declaration.type === types_1.AST_NODE_TYPES.Identifier) {
ExportVisitor_1.ExportVisitor.visit(this, node);
}
else {
this.visit(node.declaration);
}
}
ExportNamedDeclaration(node) {
if (node.declaration) {
this.visit(node.declaration);
}
else {
ExportVisitor_1.ExportVisitor.visit(this, node);
}
}
ForInStatement(node) {
this.visitForIn(node);
}
ForOfStatement(node) {
this.visitForIn(node);
}
ForStatement(node) {
// 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 === types_1.AST_NODE_TYPES.VariableDeclaration &&
node.init.kind !== 'var') {
this.scopeManager.nestForScope(node);
}
this.visitChildren(node);
this.close(node);
}
FunctionDeclaration(node) {
this.visitFunction(node);
}
FunctionExpression(node) {
this.visitFunction(node);
}
Identifier(node) {
this.currentScope().referenceValue(node);
this.visitType(node.typeAnnotation);
}
ImportAttribute() {
// import assertions are module metadata and thus have no variables to reference
}
ImportDeclaration(node) {
(0, assert_1.assert)(this.scopeManager.isModule(), 'ImportDeclaration should appear when the mode is ES6 and in the module context.');
ImportVisitor_1.ImportVisitor.visit(this, node);
}
JSXAttribute(node) {
this.visit(node.value);
}
JSXClosingElement(node) {
this.visitJSXElement(node);
}
JSXFragment(node) {
this.referenceJsxPragma();
this.referenceJsxFragment();
this.visitChildren(node);
}
JSXIdentifier(node) {
this.currentScope().referenceValue(node);
}
JSXMemberExpression(node) {
if (node.object.type !== types_1.AST_NODE_TYPES.JSXIdentifier ||
node.object.name !== 'this') {
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) {
this.referenceJsxPragma();
this.visitJSXElement(node);
this.visitType(node.typeArguments);
for (const attr of node.attributes) {
this.visit(attr);
}
}
LabeledStatement(node) {
this.visit(node.body);
}
MemberExpression(node) {
this.visit(node.object);
if (node.computed) {
this.visit(node.property);
}
}
MetaProperty() {
// meta properties all builtin globals
}
NewExpression(node) {
this.visitChildren(node, ['typeArguments']);
this.visitType(node.typeArguments);
}
PrivateIdentifier() {
// private identifiers are members on classes and thus have no variables to reference
}
Program(node) {
const globalScope = this.scopeManager.nestGlobalScope(node);
this.populateGlobalsFromLib(globalScope);
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.isModule()) {
this.scopeManager.nestModuleScope(node);
}
if (this.scopeManager.isImpliedStrict()) {
this.currentScope().isStrict = true;
}
this.visitChildren(node);
this.close(node);
}
Property(node) {
this.visitProperty(node);
}
SwitchStatement(node) {
this.visit(node.discriminant);
this.scopeManager.nestSwitchScope(node);
for (const switchCase of node.cases) {
this.visit(switchCase);
}
this.close(node);
}
TaggedTemplateExpression(node) {
this.visit(node.tag);
this.visit(node.quasi);
this.visitType(node.typeArguments);
}
TSAsExpression(node) {
this.visitTypeAssertion(node);
}
TSDeclareFunction(node) {
this.visitFunction(node);
}
TSEmptyBodyFunctionExpression(node) {
this.visitFunction(node);
}
TSEnumDeclaration(node) {
this.currentScope().defineIdentifier(node.id, new definition_1.TSEnumNameDefinition(node.id, node));
// enum members can be referenced within the enum body
this.scopeManager.nestTSEnumScope(node);
for (const member of node.body.members) {
// TS resolves literal named members to be actual names
// enum Foo {
// 'a' = 1,
// b = a, // this references the 'a' member
// }
if (member.id.type === types_1.AST_NODE_TYPES.Literal &&
typeof member.id.value === 'string') {
const name = member.id;
this.currentScope().defineLiteralIdentifier(name, new definition_1.TSEnumMemberDefinition(name, member));
}
else if (!member.computed &&
member.id.type === types_1.AST_NODE_TYPES.Identifier) {
this.currentScope().defineIdentifier(member.id, new definition_1.TSEnumMemberDefinition(member.id, member));
}
this.visit(member.initializer);
}
this.close(node);
}
TSExportAssignment(node) {
if (node.expression.type === types_1.AST_NODE_TYPES.Identifier) {
// this is a special case - you can `export = T` where `T` is a type OR a
// value however `T[U]` is illegal when `T` is a type and `T.U` is illegal
// when `T.U` is a type
// i.e. if the expression is JUST an Identifier - it could be either ref
// kind; otherwise the standard rules apply
this.currentScope().referenceDualValueType(node.expression);
}
else {
this.visit(node.expression);
}
}
TSImportEqualsDeclaration(node) {
this.currentScope().defineIdentifier(node.id, new definition_1.ImportBindingDefinition(node.id, node, node));
if (node.moduleReference.type === types_1.AST_NODE_TYPES.TSQualifiedName) {
let moduleIdentifier = node.moduleReference.left;
while (moduleIdentifier.type === types_1.AST_NODE_TYPES.TSQualifiedName) {
moduleIdentifier = moduleIdentifier.left;
}
this.visit(moduleIdentifier);
}
else {
this.visit(node.moduleReference);
}
}
TSInstantiationExpression(node) {
this.visitChildren(node, ['typeArguments']);
this.visitType(node.typeArguments);
}
TSInterfaceDeclaration(node) {
this.visitType(node);
}
TSModuleDeclaration(node) {
if (node.id.type === types_1.AST_NODE_TYPES.Identifier && node.kind !== 'global') {
this.currentScope().defineIdentifier(node.id, new definition_1.TSModuleNameDefinition(node.id, node));
}
this.scopeManager.nestTSModuleScope(node);
this.visit(node.body);
this.close(node);
}
TSSatisfiesExpression(node) {
this.visitTypeAssertion(node);
}
TSTypeAliasDeclaration(node) {
this.visitType(node);
}
TSTypeAssertion(node) {
this.visitTypeAssertion(node);
}
UpdateExpression(node) {
const argument = this.visitExpressionTarget(node.argument);
if (PatternVisitor_1.PatternVisitor.isPattern(argument)) {
this.visitPattern(argument, pattern => {
this.currentScope().referenceValue(pattern, Reference_1.ReferenceFlag.ReadWrite, null);
});
}
else {
this.visitChildren(node);
}
}
VariableDeclaration(node) {
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 definition_1.VariableDefinition(pattern, decl, node));
this.referencingDefaultValue(pattern, info.assignments, null, true);
if (init) {
this.currentScope().referenceValue(pattern, Reference_1.ReferenceFlag.Write, init, null, true);
}
}, { processRightHandNodes: true });
this.visit(decl.init);
this.visitType(decl.id.typeAnnotation);
}
}
WithStatement(node) {
this.visit(node.object);
// Then nest scope for WithStatement.
this.scopeManager.nestWithScope(node);
this.visit(node.body);
this.close(node);
}
visitExpressionTarget(left) {
switch (left.type) {
case types_1.AST_NODE_TYPES.TSAsExpression:
case types_1.AST_NODE_TYPES.TSTypeAssertion:
// explicitly visit the type annotation
this.visitType(left.typeAnnotation);
// intentional fallthrough
case types_1.AST_NODE_TYPES.TSNonNullExpression:
// unwrap the expression
left = left.expression;
}
return left;
}
}
exports.Referencer = Referencer;