Skip to content

⬅️ Back to Table of Contents

πŸ“„ prefer-readonly

πŸ“Š Analysis Summary

Metric Count
πŸ”§ Functions 19
🧱 Classes 1
πŸ“¦ Imports 10
πŸ“Š Variables & Constants 2
πŸ“‘ Type Aliases 3
🎯 Enums 1

πŸ“š Table of Contents

πŸ› οΈ File Location:

πŸ“‚ packages/eslint-plugin/src/rules/prefer-readonly.ts

πŸ“€ Default Export

export default createRule<Options, MessageIds>({ ... })
Property Value
name 'prefer-readonly'
meta.type 'suggestion'
meta.docs.description "Require private members to be marked as readonly if they're never modified outside of the constructor"
meta.docs.requiresTypeChecking true
meta.fixable 'code'
meta.messages.preferReadonly "Member '{{name}}' is never reassigned; mark it as readonly."
meta.schema [ { type: 'object', additionalProperties: false, properties: { onlyInlineLambdas: { type: 'boolean', description: 'Wh...
defaultOptions [{ onlyInlineLambdas: false }]

Entry point: create β€” documented under Functions.


πŸ“¦ Imports

Name Source
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
ASTUtils @typescript-eslint/utils
createRule ../util
getStaticMemberAccessValue ../util
getParserServices ../util
nullThrows ../util
typeIsOrHasBaseType ../util
getMemberHeadLoc ../util/getMemberHeadLoc
getParameterPropertyHeadLoc ../util/getMemberHeadLoc

Variables & Constants

Name Type Kind Value Exported
OUTSIDE_CONSTRUCTOR -1 const -1 βœ—
DIRECTLY_INSIDE_CONSTRUCTOR 0 const 0 βœ—

Functions

create(context: any, [{ onlyInlineLambdas }]: any): { [x: string]: (node: TSESTree.ArrowFunctionExpression | TS…

Parameters:

  • context any
  • [{ onlyInlineLambdas }] any

Returns: { [x: string]: (node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration | TSESTree.FunctionExpression | TSESTree.MethodDefinition) => void; 'ClassDeclaration, ClassExpression'(node: TSESTree.ClassDeclaration | TSESTree.ClassExpression): void; 'ClassDeclaration, ClassExpression:exit'(): void; MemberExpression(node: any): void; }

Calls:

  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • ts.isBinaryExpression
  • handleParentBinaryExpression
  • ts.isDeleteExpression
  • isDestructuringAssignment
  • classScope.addVariableModification
  • ts.isPostfixUnaryExpression
  • ts.isPrefixUnaryExpression
  • handleParentPostfixOrPrefixUnaryExpression
  • tsutils.isAssignmentKind
  • ts.isObjectLiteralExpression
  • ts.isArrayLiteralExpression
  • ts.isSpreadAssignment
  • ts.isSpreadElement
  • ts.isPropertyAccessExpression
  • services.esTreeNodeToTSNodeMap.get
  • ts.isConstructorDeclaration
  • tsutils.isFunctionScopeBoundary
  • services.tsNodeToESTreeNodeMap.get
  • checker.typeToString
  • tsutils.isTypeFlagSet
  • context.sourceCode.getScope
  • ASTUtils.findVariable
  • variable.defs.find
  • services.getTypeAtLocation
  • ASTUtils.isConstructor
  • classScopeStack[classScopeStack.length - 1].exitConstructor
  • isFunctionScopeBoundaryInStack
  • classScopeStack[classScopeStack.length - 1].exitNonConstructor
  • classScopeStack.push
  • nullThrows (from ../util)
  • classScopeStack.pop
  • finalizedClassScope.finalizeUnmodifiedPrivateNonReadonlys
  • getEsNodesFromViolatingNode
  • complex_call_7065
  • getMemberHeadLoc (from ../util/getMemberHeadLoc)
  • getParameterPropertyHeadLoc (from ../util/getMemberHeadLoc)
  • complex_call_7817
  • finalizedClassScope.memberHasConstructorModifications
  • tsutils.isLiteralType
  • getTypeAnnotationForViolatingNode
  • context.report
  • context.sourceCode.getText
  • context.sourceCode.getTokenBefore
  • fixer.insertTextBefore
  • fixer.insertTextAfter
  • classScopeStack[classScopeStack.length - 1].enterConstructor
  • classScopeStack[classScopeStack.length - 1].enterNonConstructor
  • handlePropertyAccessExpression
  • ts.isElementAccessExpression
  • getStaticMemberAccessValue (from ../util)
  • classScope.addVariableModificationByName

Internal Comments:

// verify the about-to-be-added type annotation is in-scope
// if the RHS is a literal, its type would be narrowed, while the
// type of the initializer (which isn't `readonly`) would be the
// widened type

Code
create(context, [{ onlyInlineLambdas }]) {
    const services = getParserServices(context);
    const checker = services.program.getTypeChecker();
    const classScopeStack: ClassScope[] = [];

    function handlePropertyAccessExpression(
      node: ts.PropertyAccessExpression,
      parent: ts.Node,
      classScope: ClassScope,
    ): void {
      if (ts.isBinaryExpression(parent)) {
        handleParentBinaryExpression(node, parent, classScope);
        return;
      }

      if (ts.isDeleteExpression(parent) || isDestructuringAssignment(node)) {
        classScope.addVariableModification(node);
        return;
      }

      if (
        ts.isPostfixUnaryExpression(parent) ||
        ts.isPrefixUnaryExpression(parent)
      ) {
        handleParentPostfixOrPrefixUnaryExpression(parent, classScope);
      }
    }

    function handleParentBinaryExpression(
      node: ts.PropertyAccessExpression,
      parent: ts.BinaryExpression,
      classScope: ClassScope,
    ): void {
      if (
        parent.left === node &&
        tsutils.isAssignmentKind(parent.operatorToken.kind)
      ) {
        classScope.addVariableModification(node);
      }
    }

    function handleParentPostfixOrPrefixUnaryExpression(
      node: ts.PostfixUnaryExpression | ts.PrefixUnaryExpression,
      classScope: ClassScope,
    ): void {
      if (
        node.operator === ts.SyntaxKind.PlusPlusToken ||
        node.operator === ts.SyntaxKind.MinusMinusToken
      ) {
        classScope.addVariableModification(
          node.operand as ts.PropertyAccessExpression,
        );
      }
    }

    function isDestructuringAssignment(
      node: ts.PropertyAccessExpression,
    ): boolean {
      let current = node.parent as ts.Node | undefined;

      while (current) {
        const parent = current.parent;

        if (
          ts.isObjectLiteralExpression(parent) ||
          ts.isArrayLiteralExpression(parent) ||
          ts.isSpreadAssignment(parent) ||
          (ts.isSpreadElement(parent) &&
            ts.isArrayLiteralExpression(parent.parent))
        ) {
          current = parent;
        } else if (
          ts.isBinaryExpression(parent) &&
          !ts.isPropertyAccessExpression(current)
        ) {
          return (
            parent.left === current &&
            parent.operatorToken.kind === ts.SyntaxKind.EqualsToken
          );
        } else {
          break;
        }
      }

      return false;
    }

    function isFunctionScopeBoundaryInStack(
      node:
        | TSESTree.ArrowFunctionExpression
        | TSESTree.FunctionDeclaration
        | TSESTree.FunctionExpression
        | TSESTree.MethodDefinition,
    ): boolean {
      if (classScopeStack.length === 0) {
        return false;
      }

      const tsNode = services.esTreeNodeToTSNodeMap.get(node);
      if (ts.isConstructorDeclaration(tsNode)) {
        return false;
      }

      return tsutils.isFunctionScopeBoundary(tsNode);
    }

    function getEsNodesFromViolatingNode(
      violatingNode: ParameterOrPropertyDeclaration,
    ): { esNode: TSESTree.Node; nameNode: TSESTree.Node } {
      return {
        esNode: services.tsNodeToESTreeNodeMap.get(violatingNode),
        nameNode: services.tsNodeToESTreeNodeMap.get(violatingNode.name),
      };
    }

    function getTypeAnnotationForViolatingNode(
      node: TSESTree.Node,
      type: ts.Type,
      initializerType: ts.Type,
    ) {
      const annotation = checker.typeToString(type);

      // verify the about-to-be-added type annotation is in-scope
      if (tsutils.isTypeFlagSet(initializerType, ts.TypeFlags.EnumLiteral)) {
        const scope = context.sourceCode.getScope(node);
        const variable = ASTUtils.findVariable(scope, annotation);

        if (variable == null) {
          return null;
        }

        const definition = variable.defs.find(def => def.isTypeDefinition);

        if (definition == null) {
          return null;
        }

        const definitionType = services.getTypeAtLocation(definition.node);

        if (definitionType !== type) {
          return null;
        }
      }

      return annotation;
    }

    return {
      [`${functionScopeBoundaries}:exit`](
        node:
          | TSESTree.ArrowFunctionExpression
          | TSESTree.FunctionDeclaration
          | TSESTree.FunctionExpression
          | TSESTree.MethodDefinition,
      ): void {
        if (ASTUtils.isConstructor(node)) {
          classScopeStack[classScopeStack.length - 1].exitConstructor();
        } else if (isFunctionScopeBoundaryInStack(node)) {
          classScopeStack[classScopeStack.length - 1].exitNonConstructor();
        }
      },
      'ClassDeclaration, ClassExpression'(
        node: TSESTree.ClassDeclaration | TSESTree.ClassExpression,
      ): void {
        classScopeStack.push(
          new ClassScope(
            checker,
            services.esTreeNodeToTSNodeMap.get(node),
            onlyInlineLambdas,
          ),
        );
      },
      'ClassDeclaration, ClassExpression:exit'(): void {
        const finalizedClassScope = nullThrows(
          classScopeStack.pop(),
          'Stack should exist on class exit',
        );

        for (const violatingNode of finalizedClassScope.finalizeUnmodifiedPrivateNonReadonlys()) {
          const { esNode, nameNode } =
            getEsNodesFromViolatingNode(violatingNode);

          const reportNodeOrLoc:
            { loc: TSESTree.SourceLocation } | { node: TSESTree.Node } =
            (() => {
              switch (esNode.type) {
                case AST_NODE_TYPES.MethodDefinition:
                case AST_NODE_TYPES.PropertyDefinition:
                case AST_NODE_TYPES.TSAbstractMethodDefinition:
                  return { loc: getMemberHeadLoc(context.sourceCode, esNode) };
                case AST_NODE_TYPES.TSParameterProperty:
                  return {
                    loc: getParameterPropertyHeadLoc(
                      context.sourceCode,
                      esNode,
                      (nameNode as TSESTree.Identifier).name,
                    ),
                  };
                default:
                  return { node: esNode };
              }
            })();

          const typeAnnotation = (() => {
            if (esNode.type !== AST_NODE_TYPES.PropertyDefinition) {
              return null;
            }

            if (esNode.typeAnnotation || !esNode.value) {
              return null;
            }

            if (nameNode.type !== AST_NODE_TYPES.Identifier) {
              return null;
            }

            const hasConstructorModifications =
              finalizedClassScope.memberHasConstructorModifications(
                nameNode.name,
              );

            if (!hasConstructorModifications) {
              return null;
            }

            const violatingType = services.getTypeAtLocation(esNode);
            const initializerType = services.getTypeAtLocation(esNode.value);

            // if the RHS is a literal, its type would be narrowed, while the
            // type of the initializer (which isn't `readonly`) would be the
            // widened type
            if (initializerType === violatingType) {
              return null;
            }

            if (!tsutils.isLiteralType(initializerType)) {
              return null;
            }

            return getTypeAnnotationForViolatingNode(
              esNode,
              violatingType,
              initializerType,
            );
          })();

          context.report({
            ...reportNodeOrLoc,
            messageId: 'preferReadonly',
            data: {
              name: context.sourceCode.getText(nameNode),
            },
            *fix(fixer) {
              const readonlyInsertionTarget =
                esNode.type === AST_NODE_TYPES.PropertyDefinition &&
                esNode.computed
                  ? nullThrows(
                      context.sourceCode.getTokenBefore(nameNode),
                      'Expected to find a token before computed property name',
                    )
                  : nameNode;

              yield fixer.insertTextBefore(
                readonlyInsertionTarget,
                'readonly ',
              );

              if (typeAnnotation) {
                yield fixer.insertTextAfter(nameNode, `: ${typeAnnotation}`);
              }
            },
          });
        }
      },
      [functionScopeBoundaries](
        node:
          | TSESTree.ArrowFunctionExpression
          | TSESTree.FunctionDeclaration
          | TSESTree.FunctionExpression
          | TSESTree.MethodDefinition,
      ): void {
        if (ASTUtils.isConstructor(node)) {
          classScopeStack[classScopeStack.length - 1].enterConstructor(
            services.esTreeNodeToTSNodeMap.get(node),
          );
        } else if (isFunctionScopeBoundaryInStack(node)) {
          classScopeStack[classScopeStack.length - 1].enterNonConstructor();
        }
      },
      MemberExpression(node): void {
        if (classScopeStack.length === 0) {
          return;
        }

        const classScope = classScopeStack[classScopeStack.length - 1];

        if (!node.computed) {
          const tsNode = services.esTreeNodeToTSNodeMap.get(
            node,
          ) as ts.PropertyAccessExpression;
          handlePropertyAccessExpression(tsNode, tsNode.parent, classScope);
        } else {
          const tsNode = services.esTreeNodeToTSNodeMap.get(node);
          if (
            ts.isElementAccessExpression(tsNode) &&
            ts.isBinaryExpression(tsNode.parent) &&
            tsNode.parent.left === tsNode &&
            tsutils.isAssignmentKind(tsNode.parent.operatorToken.kind)
          ) {
            const memberName = getStaticMemberAccessValue(node, context);
            if (typeof memberName === 'string') {
              classScope.addVariableModificationByName(
                tsNode.expression,
                memberName,
              );
            }
          }
        }
      },
    };
  }

ClassScope.addDeclaredVariable(node: ParameterOrPropertyDeclaration): void

Parameters:

  • node ParameterOrPropertyDeclaration

Returns: void

Calls:

  • tsutils.isModifierFlagSet
  • ts.isArrowFunction
  • getMemberName
  • (tsutils.isModifierFlagSet(node, ts.ModifierFlags.Static) ? this.privateModifiableStatics : this.privateModifiableMembers ).set
Code
public addDeclaredVariable(node: ParameterOrPropertyDeclaration): void {
    if (
      !(
        tsutils.isModifierFlagSet(node, ts.ModifierFlags.Private) ||
        node.name.kind === ts.SyntaxKind.PrivateIdentifier
      ) ||
      tsutils.isModifierFlagSet(
        node,
        ts.ModifierFlags.Accessor | ts.ModifierFlags.Readonly,
      )
    ) {
      return;
    }

    if (
      this.onlyInlineLambdas &&
      node.initializer != null &&
      !ts.isArrowFunction(node.initializer)
    ) {
      return;
    }

    const memberName = getMemberName(node.name);
    if (memberName == null) {
      return;
    }

    (tsutils.isModifierFlagSet(node, ts.ModifierFlags.Static)
      ? this.privateModifiableStatics
      : this.privateModifiableMembers
    ).set(memberName, node);
  }

ClassScope.addVariableModification(node: ts.PropertyAccessExpression): void

Parameters:

  • node ts.PropertyAccessExpression

Returns: void

Calls:

  • this.addVariableModificationByName
Code
public addVariableModification(node: ts.PropertyAccessExpression): void {
    this.addVariableModificationByName(node.expression, node.name.text);
  }

ClassScope.addVariableModificationByName(expression: ts.Expression, memberName: string): void

Parameters:

  • expression ts.Expression
  • memberName string

Returns: void

Calls:

  • this.checker.getTypeAtLocation
  • this.getTypeToClassRelation
  • this.memberVariableWithConstructorModifications.add
  • this.memberVariableModifications.add
  • this.staticVariableModifications.add
Code
public addVariableModificationByName(
    expression: ts.Expression,
    memberName: string,
  ): void {
    const modifierType = this.checker.getTypeAtLocation(expression);

    const relationOfModifierTypeToClass =
      this.getTypeToClassRelation(modifierType);

    if (
      relationOfModifierTypeToClass === TypeToClassRelation.Instance &&
      this.constructorScopeDepth === DIRECTLY_INSIDE_CONSTRUCTOR
    ) {
      this.memberVariableWithConstructorModifications.add(memberName);
      return;
    }

    if (
      relationOfModifierTypeToClass === TypeToClassRelation.Instance ||
      relationOfModifierTypeToClass === TypeToClassRelation.ClassAndInstance
    ) {
      this.memberVariableModifications.add(memberName);
    }
    if (
      relationOfModifierTypeToClass === TypeToClassRelation.Class ||
      relationOfModifierTypeToClass === TypeToClassRelation.ClassAndInstance
    ) {
      this.staticVariableModifications.add(memberName);
    }
  }

ClassScope.enterConstructor(node: | ts.ConstructorDeclaration | ts.GetAcc…): void

Parameters:

  • node | ts.ConstructorDeclaration | ts.GetAccessorDeclaration | ts.MethodDeclaration | ts.SetAccessorDeclaration

Returns: void

Calls:

  • tsutils.isModifierFlagSet
  • this.addDeclaredVariable
Code
public enterConstructor(
    node:
      | ts.ConstructorDeclaration
      | ts.GetAccessorDeclaration
      | ts.MethodDeclaration
      | ts.SetAccessorDeclaration,
  ): void {
    this.constructorScopeDepth = DIRECTLY_INSIDE_CONSTRUCTOR;

    for (const parameter of node.parameters) {
      if (tsutils.isModifierFlagSet(parameter, ts.ModifierFlags.Private)) {
        this.addDeclaredVariable(parameter);
      }
    }
  }

ClassScope.enterNonConstructor(): void

Returns: void

Code
public enterNonConstructor(): void {
    if (this.constructorScopeDepth !== OUTSIDE_CONSTRUCTOR) {
      this.constructorScopeDepth += 1;
    }
  }

ClassScope.exitConstructor(): void

Returns: void

Code
public exitConstructor(): void {
    this.constructorScopeDepth = OUTSIDE_CONSTRUCTOR;
  }

ClassScope.exitNonConstructor(): void

Returns: void

Code
public exitNonConstructor(): void {
    if (this.constructorScopeDepth !== OUTSIDE_CONSTRUCTOR) {
      this.constructorScopeDepth -= 1;
    }
  }

ClassScope.finalizeUnmodifiedPrivateNonReadonlys(): ParameterOrPropertyDeclaration[]

Returns: ParameterOrPropertyDeclaration[]

Calls:

  • this.memberVariableModifications.forEach
  • this.privateModifiableMembers.delete
  • this.staticVariableModifications.forEach
  • this.privateModifiableStatics.delete
  • this.privateModifiableMembers.values
  • this.privateModifiableStatics.values
Code
public finalizeUnmodifiedPrivateNonReadonlys(): ParameterOrPropertyDeclaration[] {
    this.memberVariableModifications.forEach(variableName => {
      this.privateModifiableMembers.delete(variableName);
    });

    this.staticVariableModifications.forEach(variableName => {
      this.privateModifiableStatics.delete(variableName);
    });

    return [
      ...this.privateModifiableMembers.values(),
      ...this.privateModifiableStatics.values(),
    ];
  }

ClassScope.getTypeToClassRelation(type: ts.Type): TypeToClassRelation

Parameters:

  • type ts.Type

Returns: TypeToClassRelation

Calls:

  • type.isIntersection
  • this.getTypeToClassRelation
  • type.isUnion
  • type.getSymbol
  • typeIsOrHasBaseType (from ../util)
  • tsutils.isObjectType
  • tsutils.isObjectFlagSet

Internal Comments:

// any union of class/instance and something else will prevent access to
// private members, so we assume that union consists only of classes
// or class instances, because otherwise tsc will report an error

Code
public getTypeToClassRelation(type: ts.Type): TypeToClassRelation {
    if (type.isIntersection()) {
      let result: TypeToClassRelation = TypeToClassRelation.None;
      for (const subType of type.types) {
        const subTypeResult = this.getTypeToClassRelation(subType);
        switch (subTypeResult) {
          case TypeToClassRelation.Class:
            if (result === TypeToClassRelation.Instance) {
              return TypeToClassRelation.ClassAndInstance;
            }
            result = TypeToClassRelation.Class;
            break;
          case TypeToClassRelation.Instance:
            if (result === TypeToClassRelation.Class) {
              return TypeToClassRelation.ClassAndInstance;
            }
            result = TypeToClassRelation.Instance;
            break;
        }
      }
      return result;
    }
    if (type.isUnion()) {
      // any union of class/instance and something else will prevent access to
      // private members, so we assume that union consists only of classes
      // or class instances, because otherwise tsc will report an error
      return this.getTypeToClassRelation(type.types[0]);
    }

    if (!type.getSymbol() || !typeIsOrHasBaseType(type, this.classType)) {
      return TypeToClassRelation.None;
    }

    const typeIsClass =
      tsutils.isObjectType(type) &&
      tsutils.isObjectFlagSet(type, ts.ObjectFlags.Anonymous);

    if (typeIsClass) {
      return TypeToClassRelation.Class;
    }

    return TypeToClassRelation.Instance;
  }

ClassScope.memberHasConstructorModifications(name: string): boolean

Parameters:

  • name string

Returns: boolean

Calls:

  • this.memberVariableWithConstructorModifications.has
Code
public memberHasConstructorModifications(name: string) {
    return this.memberVariableWithConstructorModifications.has(name);
  }

getMemberName(name: ts.DeclarationName): string | undefined

Parameters:

  • name ts.DeclarationName

Returns: string | undefined

Calls:

  • ts.isIdentifier
  • ts.isPrivateIdentifier
  • ts.isStringLiteral
  • ts.isNoSubstitutionTemplateLiteral
  • ts.isNumericLiteral
  • ts.isComputedPropertyName
  • ts.isPropertyAccessExpression
  • expression.getText
Code
function getMemberName(name: ts.DeclarationName): string | undefined {
  if (
    ts.isIdentifier(name) ||
    ts.isPrivateIdentifier(name) ||
    ts.isStringLiteral(name) ||
    ts.isNoSubstitutionTemplateLiteral(name) ||
    ts.isNumericLiteral(name)
  ) {
    return name.text;
  }

  if (ts.isComputedPropertyName(name)) {
    const expression = name.expression;

    if (ts.isNumericLiteral(expression)) {
      return expression.text;
    }

    if (
      ts.isPropertyAccessExpression(expression) &&
      ts.isIdentifier(expression.expression) &&
      expression.expression.text === 'Symbol'
    ) {
      return expression.getText();
    }
  }

  return undefined;
}

Internal helpers

Declared inside another function in this file.

handlePropertyAccessExpression(node: ts.PropertyAccessExpression, parent: ts.Node, classScope: ClassScope): void

Parameters:

  • node ts.PropertyAccessExpression
  • parent ts.Node
  • classScope ClassScope

Returns: void

Calls:

  • ts.isBinaryExpression
  • handleParentBinaryExpression
  • ts.isDeleteExpression
  • isDestructuringAssignment
  • classScope.addVariableModification
  • ts.isPostfixUnaryExpression
  • ts.isPrefixUnaryExpression
  • handleParentPostfixOrPrefixUnaryExpression
Code
function handlePropertyAccessExpression(
      node: ts.PropertyAccessExpression,
      parent: ts.Node,
      classScope: ClassScope,
    ): void {
      if (ts.isBinaryExpression(parent)) {
        handleParentBinaryExpression(node, parent, classScope);
        return;
      }

      if (ts.isDeleteExpression(parent) || isDestructuringAssignment(node)) {
        classScope.addVariableModification(node);
        return;
      }

      if (
        ts.isPostfixUnaryExpression(parent) ||
        ts.isPrefixUnaryExpression(parent)
      ) {
        handleParentPostfixOrPrefixUnaryExpression(parent, classScope);
      }
    }

handleParentBinaryExpression(node: ts.PropertyAccessExpression, parent: ts.BinaryExpression, classScope: ClassScope): void

Parameters:

  • node ts.PropertyAccessExpression
  • parent ts.BinaryExpression
  • classScope ClassScope

Returns: void

Calls:

  • tsutils.isAssignmentKind
  • classScope.addVariableModification
Code
function handleParentBinaryExpression(
      node: ts.PropertyAccessExpression,
      parent: ts.BinaryExpression,
      classScope: ClassScope,
    ): void {
      if (
        parent.left === node &&
        tsutils.isAssignmentKind(parent.operatorToken.kind)
      ) {
        classScope.addVariableModification(node);
      }
    }

handleParentPostfixOrPrefixUnaryExpression(node: ts.PostfixUnaryExpression | ts.PrefixUn…, classScope: ClassScope): void

Parameters:

  • node ts.PostfixUnaryExpression | ts.PrefixUnaryExpression
  • classScope ClassScope

Returns: void

Calls:

  • classScope.addVariableModification
Code
function handleParentPostfixOrPrefixUnaryExpression(
      node: ts.PostfixUnaryExpression | ts.PrefixUnaryExpression,
      classScope: ClassScope,
    ): void {
      if (
        node.operator === ts.SyntaxKind.PlusPlusToken ||
        node.operator === ts.SyntaxKind.MinusMinusToken
      ) {
        classScope.addVariableModification(
          node.operand as ts.PropertyAccessExpression,
        );
      }
    }

isDestructuringAssignment(node: ts.PropertyAccessExpression): boolean

Parameters:

  • node ts.PropertyAccessExpression

Returns: boolean

Calls:

  • ts.isObjectLiteralExpression
  • ts.isArrayLiteralExpression
  • ts.isSpreadAssignment
  • ts.isSpreadElement
  • ts.isBinaryExpression
  • ts.isPropertyAccessExpression
Code
function isDestructuringAssignment(
      node: ts.PropertyAccessExpression,
    ): boolean {
      let current = node.parent as ts.Node | undefined;

      while (current) {
        const parent = current.parent;

        if (
          ts.isObjectLiteralExpression(parent) ||
          ts.isArrayLiteralExpression(parent) ||
          ts.isSpreadAssignment(parent) ||
          (ts.isSpreadElement(parent) &&
            ts.isArrayLiteralExpression(parent.parent))
        ) {
          current = parent;
        } else if (
          ts.isBinaryExpression(parent) &&
          !ts.isPropertyAccessExpression(current)
        ) {
          return (
            parent.left === current &&
            parent.operatorToken.kind === ts.SyntaxKind.EqualsToken
          );
        } else {
          break;
        }
      }

      return false;
    }

isFunctionScopeBoundaryInStack(node: | TSESTree.ArrowFunctionExpression | TS…): boolean

Parameters:

  • node | TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration | TSESTree.FunctionExpression | TSESTree.MethodDefinition

Returns: boolean

Calls:

  • services.esTreeNodeToTSNodeMap.get
  • ts.isConstructorDeclaration
  • tsutils.isFunctionScopeBoundary
Code
function isFunctionScopeBoundaryInStack(
      node:
        | TSESTree.ArrowFunctionExpression
        | TSESTree.FunctionDeclaration
        | TSESTree.FunctionExpression
        | TSESTree.MethodDefinition,
    ): boolean {
      if (classScopeStack.length === 0) {
        return false;
      }

      const tsNode = services.esTreeNodeToTSNodeMap.get(node);
      if (ts.isConstructorDeclaration(tsNode)) {
        return false;
      }

      return tsutils.isFunctionScopeBoundary(tsNode);
    }

getEsNodesFromViolatingNode(violatingNode: ParameterOrPropertyDeclaration): { esNode: TSESTree.Node; nameNode: TSESTree.Node }

Parameters:

  • violatingNode ParameterOrPropertyDeclaration

Returns: { esNode: TSESTree.Node; nameNode: TSESTree.Node }

Calls:

  • services.tsNodeToESTreeNodeMap.get
Code
function getEsNodesFromViolatingNode(
      violatingNode: ParameterOrPropertyDeclaration,
    ): { esNode: TSESTree.Node; nameNode: TSESTree.Node } {
      return {
        esNode: services.tsNodeToESTreeNodeMap.get(violatingNode),
        nameNode: services.tsNodeToESTreeNodeMap.get(violatingNode.name),
      };
    }

getTypeAnnotationForViolatingNode(node: TSESTree.Node, type: ts.Type, initializerType: ts.Type): any

Parameters:

  • node TSESTree.Node
  • type ts.Type
  • initializerType ts.Type

Returns: any

Calls:

  • checker.typeToString
  • tsutils.isTypeFlagSet
  • context.sourceCode.getScope
  • ASTUtils.findVariable
  • variable.defs.find
  • services.getTypeAtLocation

Internal Comments:

// verify the about-to-be-added type annotation is in-scope

Code
function getTypeAnnotationForViolatingNode(
      node: TSESTree.Node,
      type: ts.Type,
      initializerType: ts.Type,
    ) {
      const annotation = checker.typeToString(type);

      // verify the about-to-be-added type annotation is in-scope
      if (tsutils.isTypeFlagSet(initializerType, ts.TypeFlags.EnumLiteral)) {
        const scope = context.sourceCode.getScope(node);
        const variable = ASTUtils.findVariable(scope, annotation);

        if (variable == null) {
          return null;
        }

        const definition = variable.defs.find(def => def.isTypeDefinition);

        if (definition == null) {
          return null;
        }

        const definitionType = services.getTypeAtLocation(definition.node);

        if (definitionType !== type) {
          return null;
        }
      }

      return annotation;
    }

Classes

ClassScope

Methods (10) β€” full entries under Functions

Method Signature
addDeclaredVariable (node: ParameterOrPropertyDeclaration): void
addVariableModification (node: ts.PropertyAccessExpression): void
addVariableModificationByName (expression: ts.Expression, memberName: string): void
enterConstructor (node: \| ts.ConstructorDeclaration \| ts.GetAccessorDeclaration \| ts.MethodDeclaration \| ts.Se...
enterNonConstructor (): void
exitConstructor (): void
exitNonConstructor (): void
finalizeUnmodifiedPrivateNonReadonlys (): ParameterOrPropertyDeclaration[]
getTypeToClassRelation (type: ts.Type): TypeToClassRelation
memberHasConstructorModifications (name: string): boolean

Type Aliases

MessageIds

type MessageIds = 'preferReadonly';

Options

type Options = [
  {
    onlyInlineLambdas?: boolean;
  },
];

ParameterOrPropertyDeclaration

type ParameterOrPropertyDeclaration = ts.ParameterDeclaration | ts.PropertyDeclaration;

Enums

enum TypeToClassRelation

Enum Code
enum TypeToClassRelation {
  ClassAndInstance,
  Class,
  Instance,
  None,
}

Members

Name Value Description
ClassAndInstance auto not shown
Class auto not shown
Instance auto not shown
None auto not shown

Generated by Syntax Scribe