UNPKG

@typescript-eslint/eslint-plugin

Version:
520 lines (519 loc) • 20.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Member = void 0; exports.analyzeClassMemberUsage = analyzeClassMemberUsage; const scope_manager_1 = require("@typescript-eslint/scope-manager"); const utils_1 = require("@typescript-eslint/utils"); const __1 = require(".."); const extractComputedName_1 = require("./extractComputedName"); const types_1 = require("./types"); class Member { /** * The node that declares this member */ node; /** * The resolved, unique key for this member. */ key; /** * The member name, as given in the source code. */ name; /** * The node that represents the member name in the source code. * Used for reporting errors. */ nameNode; /** * The number of writes to this member. */ writeCount = 0; /** * The number of reads from this member. */ readCount = 0; constructor(node, key, name, nameNode) { this.node = node; this.key = key; this.name = name; this.nameNode = nameNode; } static create(node) { const name = (0, extractComputedName_1.extractNameForMember)(node); if (name == null) { return null; } return new Member(node, name.key, name.codeName, name.nameNode); } isAccessor() { if (this.node.type === utils_1.AST_NODE_TYPES.MethodDefinition || this.node.type === utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition) { return this.node.kind === 'set' || this.node.kind === 'get'; } return (this.node.type === utils_1.AST_NODE_TYPES.AccessorProperty || this.node.type === utils_1.AST_NODE_TYPES.TSAbstractAccessorProperty); } isHashPrivate() { return ('key' in this.node && this.node.key.type === utils_1.AST_NODE_TYPES.PrivateIdentifier); } isPrivate() { return this.node.accessibility === 'private'; } isStatic() { return this.node.static; } isUsed() { return (this.readCount > 0 || // any usage of an accessor is considered a usage as accessor can have side effects (this.writeCount > 0 && this.isAccessor())); } } exports.Member = Member; function isWriteOnlyUsage(node, parent) { if (parent.type !== utils_1.AST_NODE_TYPES.AssignmentExpression && parent.type !== utils_1.AST_NODE_TYPES.ForInStatement && parent.type !== utils_1.AST_NODE_TYPES.ForOfStatement && parent.type !== utils_1.AST_NODE_TYPES.AssignmentPattern) { return false; } // If it's on the right then it's a read not a write if (parent.left !== node) { return false; } if (parent.type === utils_1.AST_NODE_TYPES.AssignmentExpression && // For any other operator (such as '+=') we still consider it a read operation parent.operator !== '=') { // if the read operation is "discarded" in an empty statement, then it is write only. return parent.parent.type === utils_1.AST_NODE_TYPES.ExpressionStatement; } return true; } function countReference(identifierParent, member) { const identifierGrandparent = (0, __1.nullThrows)(identifierParent.parent, __1.NullThrowsReasons.MissingParent); if (isWriteOnlyUsage(identifierParent, identifierGrandparent)) { member.writeCount += 1; return; } const identifierGreatGrandparent = identifierGrandparent.parent; // A statement which only increments (`this.#x++;`) if (identifierGrandparent.type === utils_1.AST_NODE_TYPES.UpdateExpression && identifierGreatGrandparent?.type === utils_1.AST_NODE_TYPES.ExpressionStatement) { member.writeCount += 1; return; } /* * ({ x: this.#usedInDestructuring } = bar); * * But should treat the following as a read: * ({ [this.#x]: a } = foo); */ if (identifierGrandparent.type === utils_1.AST_NODE_TYPES.Property && identifierGreatGrandparent?.type === utils_1.AST_NODE_TYPES.ObjectPattern && identifierGrandparent.value === identifierParent) { member.writeCount += 1; return; } // [...this.#unusedInRestPattern] = bar; if (identifierGrandparent.type === utils_1.AST_NODE_TYPES.RestElement) { member.writeCount += 1; return; } // [this.#unusedInAssignmentPattern] = bar; if (identifierGrandparent.type === utils_1.AST_NODE_TYPES.ArrayPattern) { member.writeCount += 1; return; } member.readCount += 1; } class ThisScope extends scope_manager_1.Visitor { /** * True if the context is considered a static context and so `this` refers to * the class and not an instance (eg a static method or a static block). */ isStaticThisContext; /** * The classes directly declared within this class -- for example a class declared within a method. * This does not include grandchild classes. */ childScopes = []; /** * The scope manager instance used to resolve variables to improve discovery of usages. */ scopeManager; /** * The parent class scope if one exists. */ upper; /** * The context of the `this` reference in the current scope. */ thisContext; constructor(scopeManager, upper, thisContext, isStaticThisContext) { super({}); this.scopeManager = scopeManager; this.upper = upper; this.isStaticThisContext = isStaticThisContext; if (thisContext === 'self') { if (!(this instanceof ClassScope)) { throw new Error('Cannot use `self` unless it is in a ClassScope'); } this.thisContext = this; } else if (thisContext === 'none') { this.thisContext = null; } else { this.thisContext = thisContext; } } findNearestScope(node) { let currentScope; let currentNode = node; do { currentScope = this.scopeManager.acquire(currentNode); if (currentNode.parent == null) { break; } currentNode = currentNode.parent; } while (currentScope == null); return currentScope; } findVariableInScope(node, name) { let currentScope = this.findNearestScope(node); let variable = null; while (currentScope != null) { variable = currentScope.set.get(name) ?? null; if (variable != null) { break; } currentScope = currentScope.upper; } return variable; } getObjectClass(node) { switch (node.object.type) { case utils_1.AST_NODE_TYPES.ThisExpression: { if (this.thisContext == null) { return null; } return { thisContext: this.thisContext, type: this.isStaticThisContext ? 'static' : 'instance', }; } case utils_1.AST_NODE_TYPES.Identifier: { const thisContext = this.findClassScopeWithName(node.object.name); if (thisContext != null) { return { thisContext, type: 'static' }; } // the following code does some very rudimentary scope analysis to handle some trivial cases const variable = this.findVariableInScope(node, node.object.name); if (variable == null || variable.defs.length === 0) { return null; } const firstDef = variable.defs[0]; switch (firstDef.node.type) { // detect simple reassignment of `this` // ``` // class Foo { // private prop: number; // method(thing: Foo) { // const self = this; // return self.prop; // } // } // ``` case utils_1.AST_NODE_TYPES.VariableDeclarator: { const value = firstDef.node.init; if (value == null || value.type !== utils_1.AST_NODE_TYPES.ThisExpression) { return null; } if (variable.references.some(ref => ref.isWrite() && ref.init !== true)) { // variable is assigned to multiple times so we can't be sure that it's still the same class return null; } // we have a case like `const self = this` or `let self = this` that is not reassigned // so we can safely assume that it's still the same class! return { thisContext: this.thisContext, type: this.isStaticThisContext ? 'static' : 'instance', }; } // Look for variables typed as the current class: // ``` // class Foo { // private prop: number; // method(thing: Foo) { // // this references the private instance member but not via `this` so we can't see it // thing.prop = 1; // } // } // ``` default: { const typeAnnotation = (() => { if ('typeAnnotation' in firstDef.name && firstDef.name.typeAnnotation != null) { return firstDef.name.typeAnnotation.typeAnnotation; } return null; })(); if (typeAnnotation == null) { return null; } // Cases like `method(thing: Foo) { ... }` if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference && typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier) { const typeName = typeAnnotation.typeName.name; const typeScope = this.findClassScopeWithName(typeName); if (typeScope != null) { return { thisContext: typeScope, type: 'instance' }; } } // Cases like `method(thing: typeof Foo) { ... }` if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeQuery && typeAnnotation.exprName.type === utils_1.AST_NODE_TYPES.Identifier) { const exprName = typeAnnotation.exprName.name; const exprScope = this.findClassScopeWithName(exprName); if (exprScope != null) { return { thisContext: exprScope, type: 'static' }; } } } } return null; } case utils_1.AST_NODE_TYPES.MemberExpression: // TODO - we could probably recurse here to do some more complex analysis and support like `foo.bar.baz` nested references return null; default: return null; } } visitClass(node) { const classScope = new ClassScope(node, this, this.scopeManager); this.childScopes.push(classScope); classScope.visitChildren(node); } visitIntermediate(node) { const intermediateScope = new IntermediateScope(this.scopeManager, this, node); this.childScopes.push(intermediateScope); intermediateScope.visitChildren(node); } /** * Gets the nearest class scope with the given name. */ findClassScopeWithName(name) { let currentScope = this; while (currentScope != null) { if (currentScope instanceof ClassScope && currentScope.className === name) { return currentScope; } currentScope = currentScope.upper; } return null; } ///////////////////// // Visit selectors // ///////////////////// ClassDeclaration(node) { this.visitClass(node); } ClassExpression(node) { this.visitClass(node); } FunctionDeclaration(node) { this.visitIntermediate(node); } FunctionExpression(node) { this.visitIntermediate(node); } MemberExpression(node) { this.visitChildren(node); if (node.property.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) { // will be handled by the PrivateIdentifier visitor return; } const propertyName = (0, extractComputedName_1.extractNameForMemberExpression)(node); if (propertyName == null) { return; } const objectClassName = this.getObjectClass(node); if (objectClassName == null) { return; } if (objectClassName.thisContext == null) { return; } const members = objectClassName.type === 'instance' ? objectClassName.thisContext.members.instance : objectClassName.thisContext.members.static; const member = members.get(propertyName.key); if (member == null) { return; } countReference(node, member); } PrivateIdentifier(node) { this.visitChildren(node); if ((node.parent.type === utils_1.AST_NODE_TYPES.MethodDefinition || node.parent.type === utils_1.AST_NODE_TYPES.PropertyDefinition) && node.parent.key === node) { // ignore the member definition return; } // We can actually be pretty loose with our code here thanks to how private // members are designed. // // 1) classes CANNOT have a static and instance private member with the // same name, so we don't need to match up static access. // 2) nested classes CANNOT access a private member of their parent class if // the member has the same name as a private member of the nested class. // // together this means that we can just look for the member upwards until we // find a match and we know that will be the correct match! let currentScope = this; const key = (0, types_1.privateKey)(node); while (currentScope != null) { if (currentScope.thisContext != null) { const member = currentScope.thisContext.members.instance.get(key) ?? currentScope.thisContext.members.static.get(key); if (member != null) { countReference(node.parent, member); return; } } currentScope = currentScope.upper; } } StaticBlock(node) { this.visitIntermediate(node); } } /** * Any other scope that is not a class scope * * When we visit a function declaration/expression the `this` reference is * rebound so it no longer refers to the class. * * This also supports a function's `this` parameter. */ class IntermediateScope extends ThisScope { constructor(scopeManager, upper, node) { if (node.type === utils_1.AST_NODE_TYPES.Program) { super(scopeManager, upper, 'none', false); return; } if (node.type === utils_1.AST_NODE_TYPES.StaticBlock) { if (upper == null || !(upper instanceof ClassScope)) { throw new Error('Cannot have a static block without an upper ClassScope'); } super(scopeManager, upper, upper, true); return; } // method definition if ((node.parent.type === utils_1.AST_NODE_TYPES.MethodDefinition || node.parent.type === utils_1.AST_NODE_TYPES.PropertyDefinition) && node.parent.value === node) { if (upper == null || !(upper instanceof ClassScope)) { throw new Error('Cannot have a class method/property without an upper ClassScope'); } super(scopeManager, upper, upper, node.parent.static); return; } // function with a `this` parameter if (upper != null && node.params.length > 0 && node.params[0].type === utils_1.AST_NODE_TYPES.Identifier && node.params[0].name === 'this') { const thisType = node.params[0].typeAnnotation?.typeAnnotation; if (thisType?.type === utils_1.AST_NODE_TYPES.TSTypeReference && thisType.typeName.type === utils_1.AST_NODE_TYPES.Identifier) { const thisContext = upper.findClassScopeWithName(thisType.typeName.name); if (thisContext != null) { super(scopeManager, upper, thisContext, false); return; } } } super(scopeManager, upper, 'none', false); } } class ClassScope extends ThisScope { className; /** * The class's members, keyed by their name */ members = { instance: new Map(), static: new Map(), }; /** * The node that declares this class. */ theClass; constructor(theClass, upper, scopeManager) { super(scopeManager, upper, 'self', false); this.theClass = theClass; this.className = theClass.id?.name ?? null; for (const memberNode of theClass.body.body) { switch (memberNode.type) { case utils_1.AST_NODE_TYPES.MethodDefinition: if (memberNode.kind === 'constructor') { for (const parameter of memberNode.value.params) { if (parameter.type !== utils_1.AST_NODE_TYPES.TSParameterProperty) { continue; } const member = Member.create(parameter); if (member == null) { continue; } this.members.instance.set(member.key, member); } // break instead of falling through because the constructor is not a "member" we track break; } // intentional fallthrough case utils_1.AST_NODE_TYPES.AccessorProperty: case utils_1.AST_NODE_TYPES.PropertyDefinition: case utils_1.AST_NODE_TYPES.TSAbstractAccessorProperty: case utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition: case utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition: { const member = Member.create(memberNode); if (member == null) { continue; } if (member.isStatic()) { this.members.static.set(member.key, member); } else { this.members.instance.set(member.key, member); } break; } case utils_1.AST_NODE_TYPES.StaticBlock: // static blocks declare no members continue; case utils_1.AST_NODE_TYPES.TSIndexSignature: // index signatures are type signatures only and are fully computed continue; } } } } function analyzeClassMemberUsage(program, scopeManager) { const rootScope = new IntermediateScope(scopeManager, null, program); rootScope.visit(program); return traverseScopes(rootScope); } function traverseScopes(currentScope, analysisResults = new Map()) { if (currentScope instanceof ClassScope) { analysisResults.set(currentScope.theClass, currentScope); } for (const childScope of currentScope.childScopes) { traverseScopes(childScope, analysisResults); } return analysisResults; }