"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;
}
