Skip to content

⬅️ Back to Table of Contents

πŸ“„ class-methods-use-this

πŸ“Š Analysis Summary

Metric Count
πŸ”§ Functions 7
πŸ“¦ Imports 6
πŸ“‘ Type Aliases 3

πŸ“š Table of Contents

πŸ› οΈ File Location:

πŸ“‚ packages/eslint-plugin/src/rules/class-methods-use-this.ts

πŸ“€ Default Export

export default createRule<Options, MessageIds>({ ... })
Property Value
name 'class-methods-use-this'
meta.type 'suggestion'
meta.docs.description 'Enforce that class methods utilize this'
meta.docs.extendsBaseRule true
meta.docs.requiresTypeChecking false
meta.messages.missingThis "Expected 'this' to be used by class {{name}}."
meta.schema [ { type: 'object', additionalProperties: false, properties: { enforceForClassFields: { type: 'boolean', description:...
defaultOptions [ { enforceForClassFields: true, exceptMethods: [], ignoreClassesThatImplementAnInterface: false, ignoreOverrideMetho...

Entry point: create β€” documented under Functions.


πŸ“¦ Imports

Name Source
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
createRule ../util
getFunctionHeadLoc ../util
getFunctionNameWithKind ../util
getStaticMemberAccessValue ../util

Functions

create(context: any, [ { enforceForClassFields, exce…: any): { 'AccessorProperty:exit'(): void; 'AccessorProperty > *.ke…

Parameters:

  • context any
  • [ { enforceForClassFields, exceptMethods: exceptMethodsRaw, ignoreClassesThatImplementAnInterface, ignoreOverrideMethods, }, ] any

Returns: { 'AccessorProperty:exit'(): void; 'AccessorProperty > *.key:exit'(): void; 'PropertyDefinition:exit'(): void; 'PropertyDefinition > *.key:exit'(): void; StaticBlock(): void; 'StaticBlock:exit'(): void; 'ThisExpression, Super'(): void; 'AccessorProperty > ArrowFunctionExpression.value'?: (node: TSESTree.ArrowFunctionExpression) => void; 'AccessorProperty > ArrowFunctionExpression.value:exit'?: (node: TSESTree.ArrowFunctionExpression) => void; 'PropertyDefinition > ArrowFunctionExpression.value'?: (node: TSESTree.ArrowFunctionExpression) => void; 'PropertyDefinition > ArrowFunctionExpression.value:exit'?: (node: TSESTree.ArrowFunctionExpression) => void; FunctionDeclaration(): void; 'FunctionDeclaration:exit'(): void; FunctionExpression(node: any): void; 'FunctionExpression:exit'(node: any): void; }

Calls:

  • pushContext
  • getStaticMemberAccessValue (from ../util)
  • exceptMethods.has
  • popContext
  • isPublicField
  • isIncludedInstanceMethod
  • context.report
  • getFunctionHeadLoc (from ../util)
  • getFunctionNameWithKind (from ../util)
  • enterFunction
  • exitFunction

Internal Comments:

/**
     * Pop `this` used flag from the stack.
     */
/**
     * Check if the node is an instance method not excluded by config
     */
/**
     * Checks if we are leaving a function that is a method, and reports if 'this' has not been used.
     * Static methods and the constructor are exempt.
     * Then pops the context off the stack.
     */
// function declarations have their own `this` context (x2)
/*
       * Class field value are implicit functions.
       */ (x2)
/*
       * Class static blocks are implicit functions. They aren't required to use `this`,
       * but we have to push context so that it captures any use of `this` in the static block
       * separately from enclosing contexts, because static blocks have their own `this` and it
       * shouldn't count as used `this` in enclosing contexts.
       */ (x2)

Code
create(
    context,
    [
      {
        enforceForClassFields,
        exceptMethods: exceptMethodsRaw,
        ignoreClassesThatImplementAnInterface,
        ignoreOverrideMethods,
      },
    ],
  ) {
    const exceptMethods = new Set(exceptMethodsRaw);
    type Stack =
      | {
          class: null;
          member: null;
          parent: Stack | undefined;
          usesThis: boolean;
        }
      | {
          class: TSESTree.ClassDeclaration | TSESTree.ClassExpression;
          member:
            | TSESTree.AccessorProperty
            | TSESTree.MethodDefinition
            | TSESTree.PropertyDefinition;
          parent: Stack | undefined;
          usesThis: boolean;
        };
    let stack: Stack | undefined;

    function pushContext(
      member?:
        | TSESTree.AccessorProperty
        | TSESTree.MethodDefinition
        | TSESTree.PropertyDefinition,
    ): void {
      if (member?.parent.type === AST_NODE_TYPES.ClassBody) {
        stack = {
          class: member.parent.parent,
          member,
          parent: stack,
          usesThis: false,
        };
      } else {
        stack = {
          class: null,
          member: null,
          parent: stack,
          usesThis: false,
        };
      }
    }

    function enterFunction(
      node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
    ): void {
      if (
        node.parent.type === AST_NODE_TYPES.MethodDefinition ||
        node.parent.type === AST_NODE_TYPES.PropertyDefinition ||
        node.parent.type === AST_NODE_TYPES.AccessorProperty
      ) {
        pushContext(node.parent);
      } else {
        pushContext();
      }
    }

    /**
     * Pop `this` used flag from the stack.
     */
    function popContext(): Stack | undefined {
      const oldStack = stack;
      stack = stack?.parent;
      return oldStack;
    }

    function isPublicField(
      accessibility: TSESTree.Accessibility | undefined,
    ): boolean {
      if (!accessibility || accessibility === 'public') {
        return true;
      }

      return false;
    }

    /**
     * Check if the node is an instance method not excluded by config
     */
    function isIncludedInstanceMethod(
      node: NonNullable<Stack['member']>,
    ): boolean {
      if (
        node.static ||
        (node.type === AST_NODE_TYPES.MethodDefinition &&
          node.kind === 'constructor') ||
        ((node.type === AST_NODE_TYPES.PropertyDefinition ||
          node.type === AST_NODE_TYPES.AccessorProperty) &&
          !enforceForClassFields)
      ) {
        return false;
      }

      if (node.computed || exceptMethods.size === 0) {
        return true;
      }

      const hashIfNeeded =
        node.key.type === AST_NODE_TYPES.PrivateIdentifier ? '#' : '';
      const name = getStaticMemberAccessValue(node, context);

      return (
        typeof name !== 'string' || !exceptMethods.has(hashIfNeeded + name)
      );
    }

    /**
     * Checks if we are leaving a function that is a method, and reports if 'this' has not been used.
     * Static methods and the constructor are exempt.
     * Then pops the context off the stack.
     */
    function exitFunction(
      node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
    ): void {
      const stackContext = popContext();
      if (
        stackContext?.member == null ||
        stackContext.usesThis ||
        (ignoreOverrideMethods && stackContext.member.override) ||
        (ignoreClassesThatImplementAnInterface === true &&
          stackContext.class.implements.length > 0) ||
        (ignoreClassesThatImplementAnInterface === 'public-fields' &&
          stackContext.class.implements.length > 0 &&
          isPublicField(stackContext.member.accessibility))
      ) {
        return;
      }

      if (isIncludedInstanceMethod(stackContext.member)) {
        context.report({
          loc: getFunctionHeadLoc(node, context.sourceCode),
          node,
          messageId: 'missingThis',
          data: {
            name: getFunctionNameWithKind(node),
          },
        });
      }
    }

    return {
      // function declarations have their own `this` context
      FunctionDeclaration(): void {
        pushContext();
      },
      'FunctionDeclaration:exit'(): void {
        popContext();
      },

      FunctionExpression(node): void {
        enterFunction(node);
      },
      'FunctionExpression:exit'(node): void {
        exitFunction(node);
      },
      ...(enforceForClassFields
        ? {
            'AccessorProperty > ArrowFunctionExpression.value'(
              node: TSESTree.ArrowFunctionExpression,
            ): void {
              enterFunction(node);
            },
            'AccessorProperty > ArrowFunctionExpression.value:exit'(
              node: TSESTree.ArrowFunctionExpression,
            ): void {
              exitFunction(node);
            },
            'PropertyDefinition > ArrowFunctionExpression.value'(
              node: TSESTree.ArrowFunctionExpression,
            ): void {
              enterFunction(node);
            },
            'PropertyDefinition > ArrowFunctionExpression.value:exit'(
              node: TSESTree.ArrowFunctionExpression,
            ): void {
              exitFunction(node);
            },
          }
        : {}),

      /*
       * Class field value are implicit functions.
       */
      'AccessorProperty:exit'(): void {
        popContext();
      },
      'AccessorProperty > *.key:exit'(): void {
        pushContext();
      },
      'PropertyDefinition:exit'(): void {
        popContext();
      },
      'PropertyDefinition > *.key:exit'(): void {
        pushContext();
      },

      /*
       * Class static blocks are implicit functions. They aren't required to use `this`,
       * but we have to push context so that it captures any use of `this` in the static block
       * separately from enclosing contexts, because static blocks have their own `this` and it
       * shouldn't count as used `this` in enclosing contexts.
       */
      StaticBlock(): void {
        pushContext();
      },
      'StaticBlock:exit'(): void {
        popContext();
      },

      'ThisExpression, Super'(): void {
        if (stack) {
          stack.usesThis = true;
        }
      },
    };
  }

Internal helpers

Declared inside another function in this file.

pushContext(member: | TSESTree.AccessorProperty | TSESTree.…): void

Parameters:

  • member | TSESTree.AccessorProperty | TSESTree.MethodDefinition | TSESTree.PropertyDefinition

Returns: void

Code
function pushContext(
      member?:
        | TSESTree.AccessorProperty
        | TSESTree.MethodDefinition
        | TSESTree.PropertyDefinition,
    ): void {
      if (member?.parent.type === AST_NODE_TYPES.ClassBody) {
        stack = {
          class: member.parent.parent,
          member,
          parent: stack,
          usesThis: false,
        };
      } else {
        stack = {
          class: null,
          member: null,
          parent: stack,
          usesThis: false,
        };
      }
    }

enterFunction(node: TSESTree.ArrowFunctionExpression | TSES…): void

Parameters:

  • node TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression

Returns: void

Calls:

  • pushContext
Code
function enterFunction(
      node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
    ): void {
      if (
        node.parent.type === AST_NODE_TYPES.MethodDefinition ||
        node.parent.type === AST_NODE_TYPES.PropertyDefinition ||
        node.parent.type === AST_NODE_TYPES.AccessorProperty
      ) {
        pushContext(node.parent);
      } else {
        pushContext();
      }
    }

popContext(): Stack | undefined

Pop this used flag from the stack.

Raw JSDoc
/**
     * Pop `this` used flag from the stack.
     */
Code
function popContext(): Stack | undefined {
      const oldStack = stack;
      stack = stack?.parent;
      return oldStack;
    }

isPublicField(accessibility: TSESTree.Accessibility | undefined): boolean

Parameters:

  • accessibility TSESTree.Accessibility | undefined

Returns: boolean

Code
function isPublicField(
      accessibility: TSESTree.Accessibility | undefined,
    ): boolean {
      if (!accessibility || accessibility === 'public') {
        return true;
      }

      return false;
    }

isIncludedInstanceMethod(node: NonNullable<Stack['member']>): boolean

Check if the node is an instance method not excluded by config

Raw JSDoc
/**
     * Check if the node is an instance method not excluded by config
     */

Calls:

  • getStaticMemberAccessValue (from ../util)
  • exceptMethods.has
Code
function isIncludedInstanceMethod(
      node: NonNullable<Stack['member']>,
    ): boolean {
      if (
        node.static ||
        (node.type === AST_NODE_TYPES.MethodDefinition &&
          node.kind === 'constructor') ||
        ((node.type === AST_NODE_TYPES.PropertyDefinition ||
          node.type === AST_NODE_TYPES.AccessorProperty) &&
          !enforceForClassFields)
      ) {
        return false;
      }

      if (node.computed || exceptMethods.size === 0) {
        return true;
      }

      const hashIfNeeded =
        node.key.type === AST_NODE_TYPES.PrivateIdentifier ? '#' : '';
      const name = getStaticMemberAccessValue(node, context);

      return (
        typeof name !== 'string' || !exceptMethods.has(hashIfNeeded + name)
      );
    }

exitFunction(node: TSESTree.ArrowFunctionExpression | TSES…): void

Checks if we are leaving a function that is a method, and reports if 'this' has not been used. Static methods and the constructor are exempt. Then pops the context off the stack.

Raw JSDoc
/**
     * Checks if we are leaving a function that is a method, and reports if 'this' has not been used.
     * Static methods and the constructor are exempt.
     * Then pops the context off the stack.
     */

Calls:

  • popContext
  • isPublicField
  • isIncludedInstanceMethod
  • context.report
  • getFunctionHeadLoc (from ../util)
  • getFunctionNameWithKind (from ../util)
Code
function exitFunction(
      node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
    ): void {
      const stackContext = popContext();
      if (
        stackContext?.member == null ||
        stackContext.usesThis ||
        (ignoreOverrideMethods && stackContext.member.override) ||
        (ignoreClassesThatImplementAnInterface === true &&
          stackContext.class.implements.length > 0) ||
        (ignoreClassesThatImplementAnInterface === 'public-fields' &&
          stackContext.class.implements.length > 0 &&
          isPublicField(stackContext.member.accessibility))
      ) {
        return;
      }

      if (isIncludedInstanceMethod(stackContext.member)) {
        context.report({
          loc: getFunctionHeadLoc(node, context.sourceCode),
          node,
          messageId: 'missingThis',
          data: {
            name: getFunctionNameWithKind(node),
          },
        });
      }
    }

Type Aliases

Options

type Options = [
  {
    enforceForClassFields?: boolean;
    exceptMethods?: string[];
    ignoreClassesThatImplementAnInterface?: boolean | 'public-fields';
    ignoreOverrideMethods?: boolean;
  },
];

MessageIds

type MessageIds = 'missingThis';

Stack

type Stack = | {
          class: null;
          member: null;
          parent: Stack | undefined;
          usesThis: boolean;
        }
      | {
          class: TSESTree.ClassDeclaration | TSESTree.ClassExpression;
          member:
            | TSESTree.AccessorProperty
            | TSESTree.MethodDefinition
            | TSESTree.PropertyDefinition;
          parent: Stack | undefined;
          usesThis: boolean;
        };

Generated by Syntax Scribe