Skip to content

⬅️ Back to Table of Contents

📄 parameter-properties

📊 Analysis Summary

Metric Count
🔧 Functions 4
📦 Imports 4
📐 Interfaces 1
📑 Type Aliases 4

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin/src/rules/parameter-properties.ts

📤 Default Export

export default createRule<Options, MessageIds>({ ... })
Property Value
name 'parameter-properties'
meta.type 'problem'
meta.docs.description 'Require or disallow parameter properties in class constructors'
meta.messages.preferClassProperty 'Property {{parameter}} should be declared as a class property.'
meta.messages.preferParameterProperty 'Property {{parameter}} should be declared as a parameter property.'
meta.schema [ { type: 'object', $defs: { modifier: { type: 'string', enum: [ 'readonly', 'private', 'protected', 'public', 'priva...
defaultOptions [ { allow: [], prefer: 'class-property', }, ]

Entry point: create — documented under Functions.


📦 Imports

Name Source
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
createRule ../util
nullThrows ../util

Functions

create(context: any, [{ allow = [], prefer = 'class-…: any): { TSParameterProperty(node: any): void; ':matches(ClassDecl…

Parameters:

  • context any
  • [{ allow = [], prefer = 'class-property' }] any

Returns: { TSParameterProperty(node: any): void; ':matches(ClassDeclaration, ClassExpression):exit'?: undefined; ClassBody?: undefined; 'ClassDeclaration, ClassExpression'?: undefined; 'MethodDefinition[kind="constructor"]'?: undefined; } | { ':matches(ClassDeclaration, ClassExpression):exit'(): void; ClassBody(node: any): void; 'ClassDeclaration, ClassExpression'(): void; 'MethodDefinition[kind="constructor"]'(node: TSESTree.MethodDefinition): void; TSParameterProperty?: undefined; }

Calls:

  • modifiers.push
  • modifiers.filter(Boolean).join
  • getModifiers
  • allow.includes
  • context.report
  • propertyNodesByName.get
  • propertyNodesByName.set
  • context.sourceCode.getText
  • nullThrows (from ../util)
  • propertyNodesByNameStack.pop
  • typeAnnotationsMatch
  • getNodesByName
  • propertyNodesByNameStack.push

Internal Comments:

/**
     * Gets the modifiers of `node`.
     * @param node the node to be inspected.
     */

Code
create(context, [{ allow = [], prefer = 'class-property' }]) {
    /**
     * Gets the modifiers of `node`.
     * @param node the node to be inspected.
     */
    function getModifiers(
      node: TSESTree.PropertyDefinition | TSESTree.TSParameterProperty,
    ): Modifier {
      const modifiers: Modifier[] = [];

      if (node.accessibility) {
        modifiers.push(node.accessibility);
      }
      if (node.readonly) {
        modifiers.push('readonly');
      }

      return modifiers.filter(Boolean).join(' ') as Modifier;
    }

    if (prefer === 'class-property') {
      return {
        TSParameterProperty(node): void {
          const modifiers = getModifiers(node);

          if (!allow.includes(modifiers)) {
            const name =
              node.parameter.type === AST_NODE_TYPES.Identifier
                ? node.parameter.name
                : node.parameter.left.name;

            context.report({
              node,
              messageId: 'preferClassProperty',
              data: {
                parameter: name,
              },
            });
          }
        },
      };
    }

    interface PropertyNodes {
      classProperty?: TSESTree.PropertyDefinition;
      constructorAssignment?: TSESTree.AssignmentExpression;
      constructorParameter?: TSESTree.Identifier;
    }

    const propertyNodesByNameStack: Map<string, PropertyNodes>[] = [];

    function getNodesByName(name: string): PropertyNodes {
      const propertyNodesByName =
        propertyNodesByNameStack[propertyNodesByNameStack.length - 1];
      const existing = propertyNodesByName.get(name);
      if (existing) {
        return existing;
      }

      const created: PropertyNodes = {};
      propertyNodesByName.set(name, created);
      return created;
    }

    function typeAnnotationsMatch(
      classProperty: TSESTree.PropertyDefinition,
      constructorParameter: TSESTree.Identifier,
    ): boolean {
      if (
        !classProperty.typeAnnotation ||
        !constructorParameter.typeAnnotation
      ) {
        return (
          classProperty.typeAnnotation === constructorParameter.typeAnnotation
        );
      }

      return (
        context.sourceCode.getText(classProperty.typeAnnotation) ===
        context.sourceCode.getText(constructorParameter.typeAnnotation)
      );
    }

    return {
      ':matches(ClassDeclaration, ClassExpression):exit'(): void {
        const propertyNodesByName = nullThrows(
          propertyNodesByNameStack.pop(),
          'Stack should exist on class exit',
        );

        for (const [name, nodes] of propertyNodesByName) {
          if (
            nodes.classProperty &&
            nodes.constructorAssignment &&
            nodes.constructorParameter &&
            typeAnnotationsMatch(
              nodes.classProperty,
              nodes.constructorParameter,
            )
          ) {
            context.report({
              node: nodes.classProperty,
              messageId: 'preferParameterProperty',
              data: {
                parameter: name,
              },
            });
          }
        }
      },

      ClassBody(node): void {
        for (const element of node.body) {
          if (
            element.type === AST_NODE_TYPES.PropertyDefinition &&
            element.key.type === AST_NODE_TYPES.Identifier &&
            !element.value &&
            !allow.includes(getModifiers(element))
          ) {
            getNodesByName(element.key.name).classProperty = element;
          }
        }
      },

      'ClassDeclaration, ClassExpression'(): void {
        propertyNodesByNameStack.push(new Map());
      },

      'MethodDefinition[kind="constructor"]'(
        node: TSESTree.MethodDefinition,
      ): void {
        for (const parameter of node.value.params) {
          if (parameter.type === AST_NODE_TYPES.Identifier) {
            getNodesByName(parameter.name).constructorParameter = parameter;
          }
        }

        for (const statement of node.value.body?.body ?? []) {
          if (
            statement.type !== AST_NODE_TYPES.ExpressionStatement ||
            statement.expression.type !== AST_NODE_TYPES.AssignmentExpression ||
            statement.expression.left.type !==
              AST_NODE_TYPES.MemberExpression ||
            statement.expression.left.object.type !==
              AST_NODE_TYPES.ThisExpression ||
            statement.expression.left.property.type !==
              AST_NODE_TYPES.Identifier ||
            statement.expression.right.type !== AST_NODE_TYPES.Identifier
          ) {
            break;
          }

          getNodesByName(
            statement.expression.right.name,
          ).constructorAssignment = statement.expression;
        }
      },
    };
  }

Internal helpers

Declared inside another function in this file.

getModifiers(node: TSESTree.PropertyDefinition | TSESTree.…): Modifier

Gets the modifiers of node.

Parameters:

  • node any: the node to be inspected.
Raw JSDoc
/**
     * Gets the modifiers of `node`.
     * @param node the node to be inspected.
     */

Calls:

  • modifiers.push
  • modifiers.filter(Boolean).join
Code
function getModifiers(
      node: TSESTree.PropertyDefinition | TSESTree.TSParameterProperty,
    ): Modifier {
      const modifiers: Modifier[] = [];

      if (node.accessibility) {
        modifiers.push(node.accessibility);
      }
      if (node.readonly) {
        modifiers.push('readonly');
      }

      return modifiers.filter(Boolean).join(' ') as Modifier;
    }

getNodesByName(name: string): PropertyNodes

Parameters:

  • name string

Returns: PropertyNodes

Calls:

  • propertyNodesByName.get
  • propertyNodesByName.set
Code
function getNodesByName(name: string): PropertyNodes {
      const propertyNodesByName =
        propertyNodesByNameStack[propertyNodesByNameStack.length - 1];
      const existing = propertyNodesByName.get(name);
      if (existing) {
        return existing;
      }

      const created: PropertyNodes = {};
      propertyNodesByName.set(name, created);
      return created;
    }

typeAnnotationsMatch(classProperty: TSESTree.PropertyDefinition, constructorParameter: TSESTree.Identifier): boolean

Parameters:

  • classProperty TSESTree.PropertyDefinition
  • constructorParameter TSESTree.Identifier

Returns: boolean

Calls:

  • context.sourceCode.getText
Code
function typeAnnotationsMatch(
      classProperty: TSESTree.PropertyDefinition,
      constructorParameter: TSESTree.Identifier,
    ): boolean {
      if (
        !classProperty.typeAnnotation ||
        !constructorParameter.typeAnnotation
      ) {
        return (
          classProperty.typeAnnotation === constructorParameter.typeAnnotation
        );
      }

      return (
        context.sourceCode.getText(classProperty.typeAnnotation) ===
        context.sourceCode.getText(constructorParameter.typeAnnotation)
      );
    }

Interfaces

PropertyNodes

Interface Code
interface PropertyNodes {
      classProperty?: TSESTree.PropertyDefinition;
      constructorAssignment?: TSESTree.AssignmentExpression;
      constructorParameter?: TSESTree.Identifier;
    }

Properties

Name Type Optional Description
classProperty TSESTree.PropertyDefinition not shown
constructorAssignment TSESTree.AssignmentExpression not shown
constructorParameter TSESTree.Identifier not shown

Type Aliases

Modifier

type Modifier = | 'private'
  | 'private readonly'
  | 'protected'
  | 'protected readonly'
  | 'public'
  | 'public readonly'
  | 'readonly';

Prefer

type Prefer = 'class-property' | 'parameter-property';

Options

type Options = [
  {
    allow?: Modifier[];
    prefer?: Prefer;
  },
];

MessageIds

type MessageIds = 'preferClassProperty' | 'preferParameterProperty';

Generated by Syntax Scribe