Skip to content

⬅️ Back to Table of Contents

📄 classScopeAnalyzer

📊 Analysis Summary

Metric Count
🔧 Functions 27
🧱 Classes 4
📦 Imports 15
📐 Interfaces 1
📑 Type Aliases 1

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin/src/util/class-scope-analyzer/classScopeAnalyzer.ts

📦 Imports

Name Source
Scope @typescript-eslint/scope-manager
ScopeManager @typescript-eslint/scope-manager
Variable @typescript-eslint/scope-manager
TSESTree @typescript-eslint/utils
Visitor @typescript-eslint/scope-manager
AST_NODE_TYPES @typescript-eslint/utils
ClassNode ./types
Key ./types
MemberNode ./types
nullThrows ..
NullThrowsReasons ..
extractNameForMember ./extractComputedName
extractNameForMemberExpression ./extractComputedName
privateKey ./types
publicKey ./types

Functions

Member.create(node: MemberNode): Member | null

Parameters:

  • node MemberNode

Returns: Member | null

Calls:

  • extractNameForMember (from ./extractComputedName)
Code
public static create(node: MemberNode): Member | null {
    const name = extractNameForMember(node);
    if (name == null) {
      return null;
    }
    return new Member(node, name.key, name.codeName, name.nameNode);
  }

Member.isAccessor(): boolean

Returns: boolean

Code
public isAccessor(): boolean {
    if (
      this.node.type === AST_NODE_TYPES.MethodDefinition ||
      this.node.type === AST_NODE_TYPES.TSAbstractMethodDefinition
    ) {
      return this.node.kind === 'set' || this.node.kind === 'get';
    }

    return (
      this.node.type === AST_NODE_TYPES.AccessorProperty ||
      this.node.type === AST_NODE_TYPES.TSAbstractAccessorProperty
    );
  }

Member.isHashPrivate(): boolean

Returns: boolean

Code
public isHashPrivate(): boolean {
    return (
      'key' in this.node &&
      this.node.key.type === AST_NODE_TYPES.PrivateIdentifier
    );
  }

Member.isPrivate(): boolean

Returns: boolean

Code
public isPrivate(): boolean {
    return this.node.accessibility === 'private';
  }

Member.isStatic(): boolean

Returns: boolean

Code
public isStatic(): boolean {
    return this.node.static;
  }

Member.isUsed(): boolean

Returns: boolean

Calls:

  • this.isAccessor

Internal Comments:

// any usage of an accessor is considered a usage as accessor can have side effects

Code
public isUsed(): boolean {
    return (
      this.readCount > 0 ||
      // any usage of an accessor is considered a usage as accessor can have side effects
      (this.writeCount > 0 && this.isAccessor())
    );
  }

analyzeClassMemberUsage(program: TSESTree.Program, scopeManager: ScopeManager): ReadonlyMap<ClassNode, ClassScopeResult>

Parameters:

  • program TSESTree.Program
  • scopeManager ScopeManager

Returns: ReadonlyMap<ClassNode, ClassScopeResult>

Calls:

  • rootScope.visit
  • traverseScopes
Code
export function analyzeClassMemberUsage(
  program: TSESTree.Program,
  scopeManager: ScopeManager,
): ReadonlyMap<ClassNode, ClassScopeResult> {
  const rootScope = new IntermediateScope(scopeManager, null, program);
  rootScope.visit(program);
  return traverseScopes(rootScope);
}

isWriteOnlyUsage(node: TSESTree.Node, parent: TSESTree.Node): boolean

Parameters:

  • node TSESTree.Node
  • parent TSESTree.Node

Returns: boolean

Internal Comments:

// If it's on the right then it's a read not a write
// For any other operator (such as '+=') we still consider it a read operation (x3)
// if the read operation is "discarded" in an empty statement, then it is write only.

Code
function isWriteOnlyUsage(node: TSESTree.Node, parent: TSESTree.Node): boolean {
  if (
    parent.type !== AST_NODE_TYPES.AssignmentExpression &&
    parent.type !== AST_NODE_TYPES.ForInStatement &&
    parent.type !== AST_NODE_TYPES.ForOfStatement &&
    parent.type !== 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 === 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 === AST_NODE_TYPES.ExpressionStatement;
  }

  return true;
}

countReference(identifierParent: TSESTree.Node, member: Member): void

Parameters:

  • identifierParent TSESTree.Node
  • member Member

Returns: void

Calls:

  • nullThrows (from ..)
  • isWriteOnlyUsage

Internal Comments:

// A statement which only increments (`this.#x++;`)
/*
   * ({ x: this.#usedInDestructuring } = bar);
   *
   * But should treat the following as a read:
   * ({ [this.#x]: a } = foo);
   */
// [...this.#unusedInRestPattern] = bar;
// [this.#unusedInAssignmentPattern] = bar;

Code
function countReference(identifierParent: TSESTree.Node, member: Member) {
  const identifierGrandparent = nullThrows(
    identifierParent.parent,
    NullThrowsReasons.MissingParent,
  );

  if (isWriteOnlyUsage(identifierParent, identifierGrandparent)) {
    member.writeCount += 1;
    return;
  }

  const identifierGreatGrandparent = identifierGrandparent.parent;

  // A statement which only increments (`this.#x++;`)
  if (
    identifierGrandparent.type === AST_NODE_TYPES.UpdateExpression &&
    identifierGreatGrandparent?.type === 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 === AST_NODE_TYPES.Property &&
    identifierGreatGrandparent?.type === AST_NODE_TYPES.ObjectPattern &&
    identifierGrandparent.value === identifierParent
  ) {
    member.writeCount += 1;
    return;
  }

  // [...this.#unusedInRestPattern] = bar;
  if (identifierGrandparent.type === AST_NODE_TYPES.RestElement) {
    member.writeCount += 1;
    return;
  }

  // [this.#unusedInAssignmentPattern] = bar;
  if (identifierGrandparent.type === AST_NODE_TYPES.ArrayPattern) {
    member.writeCount += 1;
    return;
  }

  member.readCount += 1;
}

ThisScope.findNearestScope(node: TSESTree.Node): Scope | null

Parameters:

  • node TSESTree.Node

Returns: Scope | null

Calls:

  • this.scopeManager.acquire
Code
private findNearestScope(node: TSESTree.Node): Scope | null {
    let currentScope: Scope | null | undefined;
    let currentNode: TSESTree.Node | undefined = node;
    do {
      currentScope = this.scopeManager.acquire(currentNode);
      if (currentNode.parent == null) {
        break;
      }
      currentNode = currentNode.parent;
    } while (currentScope == null);
    return currentScope;
  }

ThisScope.findVariableInScope(node: TSESTree.Node, name: string): Variable | null

Parameters:

  • node TSESTree.Node
  • name string

Returns: Variable | null

Calls:

  • this.findNearestScope
  • currentScope.set.get
Code
private findVariableInScope(
    node: TSESTree.Node,
    name: string,
  ): Variable | null {
    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;
  }

ThisScope.getObjectClass(node: TSESTree.MemberExpression): { thisContext: ThisScope['thisContext']; type: 'instance' |…

Parameters:

  • node TSESTree.MemberExpression

Returns: { thisContext: ThisScope['thisContext']; type: 'instance' | 'static'; } | null

Calls:

  • this.findClassScopeWithName
  • this.findVariableInScope
  • variable.references.some
  • ref.isWrite
  • complex_call_9763

Internal Comments:

// the following code does some very rudimentary scope analysis to handle some trivial cases (x2)
// detect simple reassignment of `this`
// ``` (x4)
// class Foo { (x2)
//   private prop: number; (x2)
//   method(thing: Foo) { (x2)
//     const self = this;
//     return self.prop;
//   } (x2)
// } (x2)
// variable is assigned to multiple times so we can't be sure that it's still the same class
// 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!
// Look for variables typed as the current class:
//     // this references the private instance member but not via `this` so we can't see it
//     thing.prop = 1;
// Cases like `method(thing: Foo) { ... }`
// Cases like `method(thing: typeof Foo) { ... }`
// TODO - we could probably recurse here to do some more complex analysis and support like `foo.bar.baz` nested references

Code
private getObjectClass(node: TSESTree.MemberExpression): {
    thisContext: ThisScope['thisContext'];
    type: 'instance' | 'static';
  } | null {
    switch (node.object.type) {
      case AST_NODE_TYPES.ThisExpression: {
        if (this.thisContext == null) {
          return null;
        }
        return {
          thisContext: this.thisContext,
          type: this.isStaticThisContext ? 'static' : 'instance',
        };
      }

      case 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 AST_NODE_TYPES.VariableDeclarator: {
            const value = firstDef.node.init;
            if (value?.type !== 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 === AST_NODE_TYPES.TSTypeReference &&
              typeAnnotation.typeName.type === 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 === AST_NODE_TYPES.TSTypeQuery &&
              typeAnnotation.exprName.type === 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 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;
    }
  }

ThisScope.visitClass(node: ClassNode): void

Parameters:

  • node ClassNode

Returns: void

Calls:

  • this.childScopes.push
  • classScope.visitChildren
Code
private visitClass(node: ClassNode): void {
    const classScope = new ClassScope(node, this, this.scopeManager);
    this.childScopes.push(classScope);
    classScope.visitChildren(node);
  }

ThisScope.visitIntermediate(node: IntermediateNode): void

Parameters:

  • node IntermediateNode

Returns: void

Calls:

  • this.childScopes.push
  • intermediateScope.visitChildren
Code
private visitIntermediate(node: IntermediateNode): void {
    const intermediateScope = new IntermediateScope(
      this.scopeManager,
      this,
      node,
    );
    this.childScopes.push(intermediateScope);
    intermediateScope.visitChildren(node);
  }

ThisScope.findClassScopeWithName(name: string): ClassScope | null

Gets the nearest class scope with the given name.

Raw JSDoc
/**
   * Gets the nearest class scope with the given name.
   */
Code
public findClassScopeWithName(name: string): ClassScope | null {
    let currentScope: ThisScope | null = this;
    while (currentScope != null) {
      if (
        currentScope instanceof ClassScope &&
        currentScope.className === name
      ) {
        return currentScope;
      }
      currentScope = currentScope.upper;
    }
    return null;
  }

ThisScope.AssignmentExpression(node: TSESTree.AssignmentExpression): void

Parameters:

  • node TSESTree.AssignmentExpression

Returns: void

Calls:

  • this.visitChildren
  • this.handleThisDestructuring
Code
protected AssignmentExpression(node: TSESTree.AssignmentExpression): void {
    this.visitChildren(node);

    if (
      node.right.type === AST_NODE_TYPES.ThisExpression &&
      node.left.type === AST_NODE_TYPES.ObjectPattern
    ) {
      this.handleThisDestructuring(node.left);
    }
  }

ThisScope.AssignmentPattern(node: TSESTree.AssignmentPattern): void

Parameters:

  • node TSESTree.AssignmentPattern

Returns: void

Calls:

  • this.visitChildren
  • this.handleThisDestructuring
Code
protected AssignmentPattern(node: TSESTree.AssignmentPattern): void {
    this.visitChildren(node);

    if (
      node.right.type === AST_NODE_TYPES.ThisExpression &&
      node.left.type === AST_NODE_TYPES.ObjectPattern
    ) {
      this.handleThisDestructuring(node.left);
    }
  }

ThisScope.ClassDeclaration(node: TSESTree.ClassDeclaration): void

Parameters:

  • node TSESTree.ClassDeclaration

Returns: void

Calls:

  • this.visitClass
Code
protected ClassDeclaration(node: TSESTree.ClassDeclaration): void {
    this.visitClass(node);
  }

ThisScope.ClassExpression(node: TSESTree.ClassExpression): void

Parameters:

  • node TSESTree.ClassExpression

Returns: void

Calls:

  • this.visitClass
Code
protected ClassExpression(node: TSESTree.ClassExpression): void {
    this.visitClass(node);
  }

ThisScope.FunctionDeclaration(node: TSESTree.FunctionDeclaration): void

Parameters:

  • node TSESTree.FunctionDeclaration

Returns: void

Calls:

  • this.visitIntermediate
Code
protected FunctionDeclaration(node: TSESTree.FunctionDeclaration): void {
    this.visitIntermediate(node);
  }

ThisScope.FunctionExpression(node: TSESTree.FunctionExpression): void

Parameters:

  • node TSESTree.FunctionExpression

Returns: void

Calls:

  • this.visitIntermediate
Code
protected FunctionExpression(node: TSESTree.FunctionExpression): void {
    this.visitIntermediate(node);
  }

ThisScope.MemberExpression(node: TSESTree.MemberExpression): void

Parameters:

  • node TSESTree.MemberExpression

Returns: void

Calls:

  • this.visitChildren
  • extractNameForMemberExpression (from ./extractComputedName)
  • this.getObjectClass
  • members.get
  • countReference

Internal Comments:

// will be handled by the PrivateIdentifier visitor

Code
protected MemberExpression(node: TSESTree.MemberExpression): void {
    this.visitChildren(node);

    if (node.property.type === AST_NODE_TYPES.PrivateIdentifier) {
      // will be handled by the PrivateIdentifier visitor
      return;
    }

    const propertyName = 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);
  }

ThisScope.PrivateIdentifier(node: TSESTree.PrivateIdentifier): void

Parameters:

  • node TSESTree.PrivateIdentifier

Returns: void

Calls:

  • this.visitChildren
  • privateKey (from ./types)
  • currentScope.thisContext.members.instance.get
  • currentScope.thisContext.members.static.get
  • countReference

Internal Comments:

// ignore the member definition
// We can actually be pretty loose with our code here thanks to how private (x2)
// members are designed. (x2)
// (x4)
// 1) classes CANNOT have a static and instance private member with the (x2)
//    same name, so we don't need to match up static access. (x2)
// 2) nested classes CANNOT access a private member of their parent class if (x2)
//    the member has the same name as a private member of the nested class. (x2)
// together this means that we can just look for the member upwards until we (x2)
// find a match and we know that will be the correct match! (x2)

Code
protected PrivateIdentifier(node: TSESTree.PrivateIdentifier): void {
    this.visitChildren(node);

    if (
      (node.parent.type === AST_NODE_TYPES.MethodDefinition ||
        node.parent.type === 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: ThisScope | null = this;
    const key = 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;
    }
  }

ThisScope.StaticBlock(node: TSESTree.StaticBlock): void

Parameters:

  • node TSESTree.StaticBlock

Returns: void

Calls:

  • this.visitIntermediate
Code
protected StaticBlock(node: TSESTree.StaticBlock): void {
    this.visitIntermediate(node);
  }

ThisScope.VariableDeclarator(node: TSESTree.VariableDeclarator): void

Parameters:

  • node TSESTree.VariableDeclarator

Returns: void

Calls:

  • this.visitChildren
  • this.handleThisDestructuring
Code
protected VariableDeclarator(node: TSESTree.VariableDeclarator): void {
    this.visitChildren(node);

    if (
      node.init?.type === AST_NODE_TYPES.ThisExpression &&
      node.id.type === AST_NODE_TYPES.ObjectPattern
    ) {
      this.handleThisDestructuring(node.id);
    }
  }

ThisScope.handleThisDestructuring(pattern: TSESTree.ObjectPattern): void

Handles destructuring from this in ObjectPattern. Example: const { property } = this;

Raw JSDoc
/**
   * Handles destructuring from `this` in ObjectPattern.
   * Example: const { property } = this;
   */

Calls:

  • publicKey (from ./types)
  • members.get
  • countReference
Code
private handleThisDestructuring(pattern: TSESTree.ObjectPattern): void {
    if (this.thisContext == null) {
      return;
    }

    for (const prop of pattern.properties) {
      if (prop.type !== AST_NODE_TYPES.Property) {
        continue;
      }

      if (prop.key.type !== AST_NODE_TYPES.Identifier || prop.computed) {
        continue;
      }

      const memberKey = publicKey(prop.key.name);
      const members = this.isStaticThisContext
        ? this.thisContext.members.static
        : this.thisContext.members.instance;
      const member = members.get(memberKey);

      if (member == null) {
        continue;
      }

      countReference(prop.key, member);
    }
  }

traverseScopes(currentScope: ThisScope, analysisResults: Map<any, ClassScopeResult>): Map<any, ClassScopeResult>

Parameters:

  • currentScope ThisScope
  • analysisResults Map<any, ClassScopeResult>

Returns: Map<any, ClassScopeResult>

Calls:

  • analysisResults.set
  • traverseScopes
Code
function traverseScopes(
  currentScope: ThisScope,
  analysisResults = new Map<ClassNode, ClassScopeResult>(),
) {
  if (currentScope instanceof ClassScope) {
    analysisResults.set(currentScope.theClass, currentScope);
  }

  for (const childScope of currentScope.childScopes) {
    traverseScopes(childScope, analysisResults);
  }

  return analysisResults;
}

Classes

Member

Class Code
export class Member {
  /**
   * The node that declares this member
   */
  public readonly node: MemberNode;

  /**
   * The resolved, unique key for this member.
   */
  public readonly key: Key;

  /**
   * The member name, as given in the source code.
   */
  public readonly name: string;

  /**
   * The node that represents the member name in the source code.
   * Used for reporting errors.
   */
  public readonly nameNode: TSESTree.Node;

  /**
   * The number of writes to this member.
   */
  public writeCount = 0;

  /**
   * The number of reads from this member.
   */
  public readCount = 0;

  private constructor(
    node: MemberNode,
    key: Key,
    name: string,
    nameNode: TSESTree.Node,
  ) {
    this.node = node;
    this.key = key;
    this.name = name;
    this.nameNode = nameNode;
  }
  public static create(node: MemberNode): Member | null {
    const name = extractNameForMember(node);
    if (name == null) {
      return null;
    }
    return new Member(node, name.key, name.codeName, name.nameNode);
  }

  public isAccessor(): boolean {
    if (
      this.node.type === AST_NODE_TYPES.MethodDefinition ||
      this.node.type === AST_NODE_TYPES.TSAbstractMethodDefinition
    ) {
      return this.node.kind === 'set' || this.node.kind === 'get';
    }

    return (
      this.node.type === AST_NODE_TYPES.AccessorProperty ||
      this.node.type === AST_NODE_TYPES.TSAbstractAccessorProperty
    );
  }

  public isHashPrivate(): boolean {
    return (
      'key' in this.node &&
      this.node.key.type === AST_NODE_TYPES.PrivateIdentifier
    );
  }

  public isPrivate(): boolean {
    return this.node.accessibility === 'private';
  }

  public isStatic(): boolean {
    return this.node.static;
  }

  public isUsed(): boolean {
    return (
      this.readCount > 0 ||
      // any usage of an accessor is considered a usage as accessor can have side effects
      (this.writeCount > 0 && this.isAccessor())
    );
  }
}

Methods (6) — full entries under Functions

Method Signature
create (node: MemberNode): Member \| null
isAccessor (): boolean
isHashPrivate (): boolean
isPrivate (): boolean
isStatic (): boolean
isUsed (): boolean

ThisScope

Extends: Visitor

Methods (17) — full entries under Functions

Method Signature
findNearestScope (node: TSESTree.Node): Scope \| null
findVariableInScope (node: TSESTree.Node, name: string): Variable \| null
getObjectClass (node: TSESTree.MemberExpression): { thisContext: ThisScope['thisContext']; type: 'instance' \| '...
visitClass (node: ClassNode): void
visitIntermediate (node: IntermediateNode): void
findClassScopeWithName (name: string): ClassScope \| null
AssignmentExpression (node: TSESTree.AssignmentExpression): void
AssignmentPattern (node: TSESTree.AssignmentPattern): void
ClassDeclaration (node: TSESTree.ClassDeclaration): void
ClassExpression (node: TSESTree.ClassExpression): void
FunctionDeclaration (node: TSESTree.FunctionDeclaration): void
FunctionExpression (node: TSESTree.FunctionExpression): void
MemberExpression (node: TSESTree.MemberExpression): void
PrivateIdentifier (node: TSESTree.PrivateIdentifier): void
StaticBlock (node: TSESTree.StaticBlock): void
VariableDeclarator (node: TSESTree.VariableDeclarator): void
handleThisDestructuring (pattern: TSESTree.ObjectPattern): void

IntermediateScope

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.

Extends: ThisScope

Class Code
class IntermediateScope extends ThisScope {
  constructor(
    scopeManager: ScopeManager,
    upper: ThisScope | null,
    node: IntermediateNode,
  ) {
    if (node.type === AST_NODE_TYPES.Program) {
      super(scopeManager, upper, 'none', false);
      return;
    }

    if (node.type === 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 === AST_NODE_TYPES.MethodDefinition ||
        node.parent.type === 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 === AST_NODE_TYPES.Identifier &&
      node.params[0].name === 'this'
    ) {
      const thisType = node.params[0].typeAnnotation?.typeAnnotation;
      if (
        thisType?.type === AST_NODE_TYPES.TSTypeReference &&
        thisType.typeName.type === 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);
  }
}

ClassScope

Extends: ThisScope

Implements: ClassScopeResult

Class Code
class ClassScope extends ThisScope implements ClassScopeResult {
  public readonly className: string | null;

  /**
   * The class's members, keyed by their name
   */
  public readonly members: ClassScopeResult['members'] = {
    instance: new Map(),
    static: new Map(),
  };

  /**
   * The node that declares this class.
   */
  public readonly theClass: ClassNode;

  public constructor(
    theClass: ClassNode,
    upper: ClassScope | IntermediateScope | null,
    scopeManager: 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 AST_NODE_TYPES.MethodDefinition:
          if (memberNode.kind === 'constructor') {
            for (const parameter of memberNode.value.params) {
              if (parameter.type !== 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 AST_NODE_TYPES.AccessorProperty:
        case AST_NODE_TYPES.PropertyDefinition:
        case AST_NODE_TYPES.TSAbstractAccessorProperty:
        case AST_NODE_TYPES.TSAbstractMethodDefinition:
        case 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 AST_NODE_TYPES.StaticBlock:
          // static blocks declare no members
          continue;

        case AST_NODE_TYPES.TSIndexSignature:
          // index signatures are type signatures only and are fully computed
          continue;
      }
    }
  }
}

Interfaces

ClassScopeResult

Interface Code
export interface ClassScopeResult {
  /**
   * The classes name as given in the source code.
   * If this is `null` then the class is an anonymous class.
   */
  readonly className: string | null;
  /**
   * The class's members, keyed by their name
   */
  readonly members: {
    readonly instance: Map<Key, Member>;
    readonly static: Map<Key, Member>;
  };
}

Properties

Name Type Optional Description
className string \| null not shown
members { readonly instance: Map<Key, Member>; readonly static: Map<Key, Member>; } not shown

Type Aliases

IntermediateNode

type IntermediateNode = | TSESTree.FunctionDeclaration
  | TSESTree.FunctionExpression
  | TSESTree.Program
  | TSESTree.StaticBlock;

Generated by Syntax Scribe