Skip to content

⬅️ Back to Table of Contents

📄 class-literal-property-style

📊 Analysis Summary

Metric Count
🔧 Functions 6
📦 Imports 10
📐 Interfaces 2
📑 Type Aliases 2

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin/src/rules/class-literal-property-style.ts

📤 Default Export

export default createRule<Options, MessageIds>({ ... })
Property Value
name 'class-literal-property-style'
meta.type 'problem'
meta.docs.description 'Enforce that literals on classes are exposed in a consistent style'
meta.docs.recommended 'stylistic'
meta.hasSuggestions true
meta.messages.preferFieldStyle 'Literals should be exposed using readonly fields.'
meta.messages.preferFieldStyleSuggestion 'Replace the literals with readonly fields.'
meta.messages.preferGetterStyle 'Literals should be exposed using getters.'
meta.messages.preferGetterStyleSuggestion 'Replace the literals with getters.'
meta.schema [ { type: 'string', description: 'Which literal class member syntax to prefer.', enum: ['fields', 'getters'], }, ]
defaultOptions ['fields']

Entry point: create — documented under Functions.


📦 Imports

Name Source
TSESLint @typescript-eslint/utils
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
createRule ../util
getFixOrSuggest ../util
getStaticMemberAccessValue ../util
isAssignee ../util
isFunction ../util
isStaticMemberAccessOfValue ../util
nullThrows ../util

Functions

create(context: any, [style]: any): { ClassBody: () => void; 'ClassBody:exit': () => void; 'Met…

Parameters:

  • context any
  • [style] any

Returns: { ClassBody: () => void; 'ClassBody:exit': () => void; 'MethodDefinition[kind="constructor"] ThisExpression'(node: TSESTree.ThisExpression): void; PropertyDefinition(node: any): void; MethodDefinition(node: any): void; }

Calls:

  • propertiesInfoStack.push
  • nullThrows (from ../util)
  • propertiesInfoStack.pop
  • properties.forEach
  • isSupportedLiteral
  • getStaticMemberAccessValue (from ../util)
  • excludeSet.has
  • context.report
  • context.sourceCode.getText
  • printNodeModifiers
  • fixer.replaceText
  • isAssignee (from ../util)
  • excludeSet.add
  • node.parent.body.some
  • isStaticMemberAccessOfValue (from ../util)
  • getFixOrSuggest (from ../util)
  • context.sourceCode.getTokenBefore
  • context.sourceCode .getText() .slice
  • isFunction (from ../util)
  • excludeAssignedProperty
  • properties.push
Code
create(context, [style]) {
    const propertiesInfoStack: PropertiesInfo[] = [];

    function enterClassBody(): void {
      propertiesInfoStack.push({
        excludeSet: new Set(),
        properties: [],
      });
    }

    function exitClassBody(): void {
      const { excludeSet, properties } = nullThrows(
        propertiesInfoStack.pop(),
        'Stack should exist on class exit',
      );

      properties.forEach(node => {
        const { value } = node;
        if (!value || !isSupportedLiteral(value)) {
          return;
        }

        const name = getStaticMemberAccessValue(node, context);
        if (name && excludeSet.has(name)) {
          return;
        }

        context.report({
          node: node.key,
          messageId: 'preferGetterStyle',
          suggest: [
            {
              messageId: 'preferGetterStyleSuggestion',
              fix(fixer): TSESLint.RuleFix {
                const name = context.sourceCode.getText(node.key);

                let text = '';
                text += printNodeModifiers(node, 'get');
                text += node.computed ? `[${name}]` : name;
                text += `() { return ${context.sourceCode.getText(value)}; }`;

                return fixer.replaceText(node, text);
              },
            },
          ],
        });
      });
    }

    function excludeAssignedProperty(node: TSESTree.MemberExpression): void {
      if (isAssignee(node)) {
        const { excludeSet } =
          propertiesInfoStack[propertiesInfoStack.length - 1];

        const name = getStaticMemberAccessValue(node, context);

        if (name) {
          excludeSet.add(name);
        }
      }
    }

    return {
      ...(style === 'fields' && {
        MethodDefinition(node): void {
          if (
            node.kind !== 'get' ||
            node.override ||
            !node.value.body ||
            node.value.body.body.length === 0
          ) {
            return;
          }

          const [statement] = node.value.body.body;

          if (statement.type !== AST_NODE_TYPES.ReturnStatement) {
            return;
          }

          const { argument } = statement;

          if (!argument || !isSupportedLiteral(argument)) {
            return;
          }

          const name = getStaticMemberAccessValue(node, context);

          const hasDuplicateKeySetter =
            name &&
            node.parent.body.some(element => {
              return (
                element.type === AST_NODE_TYPES.MethodDefinition &&
                element.kind === 'set' &&
                isStaticMemberAccessOfValue(element, context, name)
              );
            });
          if (hasDuplicateKeySetter) {
            return;
          }

          const getterBody = node.value.body;

          context.report({
            node: node.key,
            messageId: 'preferFieldStyle',
            ...getFixOrSuggest({
              fixOrSuggest: node.decorators.length === 0 ? 'suggest' : 'none',
              suggestion: {
                messageId: 'preferFieldStyleSuggestion',
                fix(fixer): TSESLint.RuleFix {
                  const name = context.sourceCode.getText(node.key);

                  const closingParen = nullThrows(
                    context.sourceCode.getTokenBefore(
                      node.value.returnType ?? getterBody,
                    ),
                    'Getter should have a closing parenthesis.',
                  );
                  const betweenParensAndBody = context.sourceCode
                    .getText()
                    .slice(closingParen.range[1], getterBody.range[0]);

                  let text = '';

                  text += printNodeModifiers(node, 'readonly');
                  text += node.computed ? `[${name}]` : name;
                  text += betweenParensAndBody;
                  text += `= ${context.sourceCode.getText(argument)};`;

                  return fixer.replaceText(node, text);
                },
              },
            }),
          });
        },
      }),
      ...(style === 'getters' && {
        ClassBody: enterClassBody,
        'ClassBody:exit': exitClassBody,
        'MethodDefinition[kind="constructor"] ThisExpression'(
          node: TSESTree.ThisExpression,
        ): void {
          if (node.parent.type === AST_NODE_TYPES.MemberExpression) {
            let parent: TSESTree.Node | undefined = node.parent;

            while (!isFunction(parent)) {
              parent = parent.parent;
            }

            if (
              parent.parent.type === AST_NODE_TYPES.MethodDefinition &&
              parent.parent.kind === 'constructor'
            ) {
              excludeAssignedProperty(node.parent);
            }
          }
        },
        PropertyDefinition(node): void {
          if (!node.readonly || node.declare || node.override) {
            return;
          }
          const { properties } =
            propertiesInfoStack[propertiesInfoStack.length - 1];
          properties.push(node);
        },
      }),
    };
  }

printNodeModifiers(node: NodeWithModifiers, final: 'get' | 'readonly'): string

Parameters:

  • node NodeWithModifiers
  • final 'get' | 'readonly'

Returns: string

Calls:

  • `${node.accessibility ?? ''}${ node.static ? ' static' : '' } ${final}.trimStart`
Code
(
  node: NodeWithModifiers,
  final: 'get' | 'readonly',
): string =>
  `${node.accessibility ?? ''}${
    node.static ? ' static' : ''
  } ${final} `.trimStart()

isSupportedLiteral(node: TSESTree.Node): node is TSESTree.LiteralExpression

Parameters:

  • node TSESTree.Node

Returns: node is TSESTree.LiteralExpression

Code
(
  node: TSESTree.Node,
): node is TSESTree.LiteralExpression => {
  switch (node.type) {
    case AST_NODE_TYPES.Literal:
      return true;

    case AST_NODE_TYPES.TaggedTemplateExpression:
      return node.quasi.quasis.length === 1;

    case AST_NODE_TYPES.TemplateLiteral:
      return node.quasis.length === 1;

    default:
      return false;
  }
}

Internal helpers

Declared inside another function in this file.

enterClassBody(): void

Returns: void

Calls:

  • propertiesInfoStack.push
Code
function enterClassBody(): void {
      propertiesInfoStack.push({
        excludeSet: new Set(),
        properties: [],
      });
    }

exitClassBody(): void

Returns: void

Calls:

  • nullThrows (from ../util)
  • propertiesInfoStack.pop
  • properties.forEach
  • isSupportedLiteral
  • getStaticMemberAccessValue (from ../util)
  • excludeSet.has
  • context.report
  • context.sourceCode.getText
  • printNodeModifiers
  • fixer.replaceText
Code
function exitClassBody(): void {
      const { excludeSet, properties } = nullThrows(
        propertiesInfoStack.pop(),
        'Stack should exist on class exit',
      );

      properties.forEach(node => {
        const { value } = node;
        if (!value || !isSupportedLiteral(value)) {
          return;
        }

        const name = getStaticMemberAccessValue(node, context);
        if (name && excludeSet.has(name)) {
          return;
        }

        context.report({
          node: node.key,
          messageId: 'preferGetterStyle',
          suggest: [
            {
              messageId: 'preferGetterStyleSuggestion',
              fix(fixer): TSESLint.RuleFix {
                const name = context.sourceCode.getText(node.key);

                let text = '';
                text += printNodeModifiers(node, 'get');
                text += node.computed ? `[${name}]` : name;
                text += `() { return ${context.sourceCode.getText(value)}; }`;

                return fixer.replaceText(node, text);
              },
            },
          ],
        });
      });
    }

excludeAssignedProperty(node: TSESTree.MemberExpression): void

Parameters:

  • node TSESTree.MemberExpression

Returns: void

Calls:

  • isAssignee (from ../util)
  • getStaticMemberAccessValue (from ../util)
  • excludeSet.add
Code
function excludeAssignedProperty(node: TSESTree.MemberExpression): void {
      if (isAssignee(node)) {
        const { excludeSet } =
          propertiesInfoStack[propertiesInfoStack.length - 1];

        const name = getStaticMemberAccessValue(node, context);

        if (name) {
          excludeSet.add(name);
        }
      }
    }

Interfaces

NodeWithModifiers

Interface Code
interface NodeWithModifiers {
  accessibility?: TSESTree.Accessibility;
  static: boolean;
}

Properties

Name Type Optional Description
accessibility TSESTree.Accessibility not shown
static boolean not shown

PropertiesInfo

Interface Code
interface PropertiesInfo {
  excludeSet: Set<string | symbol>;
  properties: TSESTree.PropertyDefinition[];
}

Properties

Name Type Optional Description
excludeSet Set<string \| symbol> not shown
properties TSESTree.PropertyDefinition[] not shown

Type Aliases

Options

type Options = ['fields' | 'getters'];

MessageIds

type MessageIds = | 'preferFieldStyle'
  | 'preferFieldStyleSuggestion'
  | 'preferGetterStyle'
  | 'preferGetterStyleSuggestion';

Generated by Syntax Scribe