diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-parameter-property-assignment.mdx b/packages/eslint-plugin/docs/rules/no-unnecessary-parameter-property-assignment.mdx new file mode 100644 index 000000000000..836ac8bd67b4 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-parameter-property-assignment.mdx @@ -0,0 +1,42 @@ +--- +description: 'Disallow unnecessary assignment of constructor property parameter.' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/no-unnecessary-parameter-property-assignment** for documentation. + +[TypeScript's parameter properties](https://www.typescriptlang.org/docs/handbook/2/classes.html#parameter-properties) allow creating and initializing a member in one place. +Therefore, in most cases, it is not necessary to assign parameter properties of the same name to members within a constructor. + +## Examples + + + + +```ts +class Foo { + constructor(public bar: string) { + this.bar = bar; + } +} +``` + + + + +```ts +class Foo { + constructor(public bar: string) {} +} +``` + + + + +## When Not To Use It + +If you don't use parameter properties, you can ignore this rule. diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index ee778e7e48cd..ec91896c51d3 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -92,6 +92,7 @@ export = { '@typescript-eslint/no-this-alias': 'error', '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error', '@typescript-eslint/no-unnecessary-condition': 'error', + '@typescript-eslint/no-unnecessary-parameter-property-assignment': 'error', '@typescript-eslint/no-unnecessary-qualifier': 'error', '@typescript-eslint/no-unnecessary-template-expression': 'error', '@typescript-eslint/no-unnecessary-type-arguments': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index fe0d36026bd1..d092ef237d2d 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -80,6 +80,7 @@ import noThrowLiteral from './no-throw-literal'; import noTypeAlias from './no-type-alias'; import noUnnecessaryBooleanLiteralCompare from './no-unnecessary-boolean-literal-compare'; import noUnnecessaryCondition from './no-unnecessary-condition'; +import noUnnecessaryParameterPropertyAssignment from './no-unnecessary-parameter-property-assignment'; import noUnnecessaryQualifier from './no-unnecessary-qualifier'; import noUnnecessaryTemplateExpression from './no-unnecessary-template-expression'; import noUnnecessaryTypeArguments from './no-unnecessary-type-arguments'; @@ -226,6 +227,8 @@ export default { 'no-type-alias': noTypeAlias, 'no-unnecessary-boolean-literal-compare': noUnnecessaryBooleanLiteralCompare, 'no-unnecessary-condition': noUnnecessaryCondition, + 'no-unnecessary-parameter-property-assignment': + noUnnecessaryParameterPropertyAssignment, 'no-unnecessary-qualifier': noUnnecessaryQualifier, 'no-unnecessary-template-expression': noUnnecessaryTemplateExpression, 'no-unnecessary-type-arguments': noUnnecessaryTypeArguments, diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-parameter-property-assignment.ts b/packages/eslint-plugin/src/rules/no-unnecessary-parameter-property-assignment.ts new file mode 100644 index 000000000000..43cc5ebbcf24 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unnecessary-parameter-property-assignment.ts @@ -0,0 +1,233 @@ +import { DefinitionType } from '@typescript-eslint/scope-manager'; +import type { TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; + +import { createRule, getStaticStringValue, nullThrows } from '../util'; + +const UNNECESSARY_OPERATORS = new Set(['=', '&&=', '||=', '??=']); + +export default createRule({ + name: 'no-unnecessary-parameter-property-assignment', + meta: { + docs: { + description: + 'Disallow unnecessary assignment of constructor property parameter', + }, + fixable: 'code', + messages: { + unnecessaryAssign: + 'This assignment is unnecessary since it is already assigned by a parameter property.', + }, + schema: [], + type: 'suggestion', + }, + defaultOptions: [], + create(context) { + const reportInfoStack: { + assignedBeforeUnnecessary: Set; + assignedBeforeConstructor: Set; + unnecessaryAssignments: { + name: string; + node: TSESTree.AssignmentExpression; + }[]; + }[] = []; + + function isThisMemberExpression( + node: TSESTree.Node, + ): node is TSESTree.MemberExpression { + return ( + node.type === AST_NODE_TYPES.MemberExpression && + node.object.type === AST_NODE_TYPES.ThisExpression + ); + } + + function getPropertyName(node: TSESTree.Node): string | null { + if (!isThisMemberExpression(node)) { + return null; + } + + if (node.property.type === AST_NODE_TYPES.Identifier) { + return node.property.name; + } + if (node.computed) { + return getStaticStringValue(node.property); + } + return null; + } + + function findParentFunction( + node: TSESTree.Node | undefined, + ): + | TSESTree.FunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.ArrowFunctionExpression + | undefined { + if ( + !node || + node.type === AST_NODE_TYPES.FunctionDeclaration || + node.type === AST_NODE_TYPES.FunctionExpression || + node.type === AST_NODE_TYPES.ArrowFunctionExpression + ) { + return node; + } + return findParentFunction(node.parent); + } + + function findParentPropertyDefinition( + node: TSESTree.Node | undefined, + ): TSESTree.PropertyDefinition | undefined { + if (!node || node.type === AST_NODE_TYPES.PropertyDefinition) { + return node; + } + return findParentPropertyDefinition(node.parent); + } + + function isConstructorFunctionExpression( + node: TSESTree.Node | undefined, + ): node is TSESTree.FunctionExpression { + return ( + node?.type === AST_NODE_TYPES.FunctionExpression && + ASTUtils.isConstructor(node.parent) + ); + } + + function isReferenceFromParameter(node: TSESTree.Identifier): boolean { + const scope = context.sourceCode.getScope(node); + + const rightRef = scope.references.find( + ref => ref.identifier.name === node.name, + ); + return rightRef?.resolved?.defs.at(0)?.type === DefinitionType.Parameter; + } + + function isParameterPropertyWithName( + node: TSESTree.Parameter, + name: string, + ): boolean { + return ( + node.type === AST_NODE_TYPES.TSParameterProperty && + ((node.parameter.type === AST_NODE_TYPES.Identifier && // constructor (public foo) {} + node.parameter.name === name) || + (node.parameter.type === AST_NODE_TYPES.AssignmentPattern && // constructor (public foo = 1) {} + node.parameter.left.type === AST_NODE_TYPES.Identifier && + node.parameter.left.name === name)) + ); + } + + function getIdentifier(node: TSESTree.Node): TSESTree.Identifier | null { + if (node.type === AST_NODE_TYPES.Identifier) { + return node; + } + if ( + node.type === AST_NODE_TYPES.TSAsExpression || + node.type === AST_NODE_TYPES.TSNonNullExpression + ) { + return getIdentifier(node.expression); + } + return null; + } + + function isArrowIIFE(node: TSESTree.Node): boolean { + return ( + node.type === AST_NODE_TYPES.ArrowFunctionExpression && + node.parent.type === AST_NODE_TYPES.CallExpression + ); + } + + return { + ClassBody(): void { + reportInfoStack.push({ + unnecessaryAssignments: [], + assignedBeforeUnnecessary: new Set(), + assignedBeforeConstructor: new Set(), + }); + }, + 'ClassBody:exit'(): void { + const { unnecessaryAssignments, assignedBeforeConstructor } = + nullThrows(reportInfoStack.pop(), 'The top stack should exist'); + unnecessaryAssignments.forEach(({ name, node }) => { + if (assignedBeforeConstructor.has(name)) { + return; + } + context.report({ + node, + messageId: 'unnecessaryAssign', + }); + }); + }, + 'PropertyDefinition AssignmentExpression'( + node: TSESTree.AssignmentExpression, + ): void { + const name = getPropertyName(node.left); + + if (!name) { + return; + } + + const functionNode = findParentFunction(node); + if (functionNode) { + if ( + !( + isArrowIIFE(functionNode) && + findParentPropertyDefinition(node)?.value === functionNode.parent + ) + ) { + return; + } + } + + const { assignedBeforeConstructor } = nullThrows( + reportInfoStack.at(-1), + 'The top stack should exist', + ); + assignedBeforeConstructor.add(name); + }, + "MethodDefinition[kind='constructor'] > FunctionExpression AssignmentExpression"( + node: TSESTree.AssignmentExpression, + ): void { + const leftName = getPropertyName(node.left); + + if (!leftName) { + return; + } + + let functionNode = findParentFunction(node); + if (functionNode && isArrowIIFE(functionNode)) { + functionNode = findParentFunction(functionNode.parent); + } + + if (!isConstructorFunctionExpression(functionNode)) { + return; + } + + const { assignedBeforeUnnecessary, unnecessaryAssignments } = + nullThrows( + reportInfoStack.at(reportInfoStack.length - 1), + 'The top of stack should exist', + ); + + if (!UNNECESSARY_OPERATORS.has(node.operator)) { + assignedBeforeUnnecessary.add(leftName); + return; + } + + const rightId = getIdentifier(node.right); + + if (leftName !== rightId?.name || !isReferenceFromParameter(rightId)) { + return; + } + + const hasParameterProperty = functionNode.params.some(param => + isParameterPropertyWithName(param, rightId.name), + ); + + if (hasParameterProperty && !assignedBeforeUnnecessary.has(leftName)) { + unnecessaryAssignments.push({ + name: leftName, + node, + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-parameter-property-assignment.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-parameter-property-assignment.shot new file mode 100644 index 000000000000..c3108fe12164 --- /dev/null +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-parameter-property-assignment.shot @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Validating rule docs no-unnecessary-parameter-property-assignment.mdx code examples ESLint output 1`] = ` +"Incorrect + +class Foo { + constructor(public bar: string) { + this.bar = bar; + ~~~~~~~~~~~~~~ This assignment is unnecessary since it is already assigned by a parameter property. + } +} +" +`; + +exports[`Validating rule docs no-unnecessary-parameter-property-assignment.mdx code examples ESLint output 2`] = ` +"Correct + +class Foo { + constructor(public bar: string) {} +} +" +`; diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-parameter-property-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-parameter-property-assignment.test.ts new file mode 100644 index 000000000000..68001a622e39 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-parameter-property-assignment.test.ts @@ -0,0 +1,486 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-unnecessary-parameter-property-assignment'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + }, +}); + +ruleTester.run('no-unnecessary-parameter-property-assignment', rule, { + valid: [ + ` +class Foo { + constructor(foo: string) {} +} + `, + ` +class Foo { + constructor(private foo: string) {} +} + `, + ` +class Foo { + constructor(private foo: string) { + this.foo = bar; + } +} + `, + ` +class Foo { + constructor(private foo: any) { + this.foo = foo.bar; + } +} + `, + ` +class Foo { + constructor(private foo: string) { + this.foo = this.bar; + } +} + `, + ` +class Foo { + foo: string; + constructor(foo: string) { + this.foo = foo; + } +} + `, + ` +class Foo { + bar: string; + constructor(private foo: string) { + this.bar = foo; + } +} + `, + ` +class Foo { + constructor(private foo: string) { + this.bar = () => { + this.foo = foo; + }; + } +} + `, + ` +class Foo { + constructor(private foo: string) { + this[\`\${foo}\`] = foo; + } +} + `, + ` +function Foo(foo) { + this.foo = foo; +} + `, + ` +const foo = 'foo'; +this.foo = foo; + `, + ` +class Foo { + constructor(public foo: number) { + this.foo += foo; + this.foo -= foo; + this.foo *= foo; + this.foo /= foo; + this.foo %= foo; + this.foo **= foo; + } +} + `, + ` +class Foo { + constructor(public foo: number) { + this.foo += 1; + this.foo = foo; + } +} + `, + ` +class Foo { + constructor( + public foo: number, + bar: boolean, + ) { + if (bar) { + this.foo += 1; + } else { + this.foo = foo; + } + } +} + `, + ` +class Foo { + constructor(public foo: number) { + this.foo = foo; + } + init = (this.foo += 1); +} + `, + ` +class Foo { + constructor(public foo: number) { + { + const foo = 1; + this.foo = foo; + } + } +} + `, + ` +declare const name: string; +class Foo { + constructor(public foo: number) { + this[name] = foo; + } +} + `, + ` +declare const name: string; +class Foo { + constructor(public foo: number) { + Foo.foo = foo; + } +} + `, + ` +class Foo { + constructor(public foo: number) { + this.foo = foo; + } + init = (() => { + this.foo += 1; + })(); +} + `, + ` +declare const name: string; +class Foo { + constructor(public foo: number) { + this[name] = foo; + } + init = (this[name] = 1); + init2 = (Foo.foo = 1); +} + `, + ], + invalid: [ + { + code: ` +class Foo { + constructor(public foo: string) { + this.foo = foo; + } +} + `, + errors: [ + { + line: 4, + column: 5, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(public foo?: string) { + this.foo = foo!; + } +} + `, + errors: [ + { + line: 4, + column: 5, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(public foo?: string) { + this.foo = foo as any; + } +} + `, + errors: [ + { + line: 4, + column: 5, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(public foo = '') { + this.foo = foo; + } +} + `, + errors: [ + { + line: 4, + column: 5, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(public foo = '') { + this.foo = foo; + this.foo += 'foo'; + } +} + `, + errors: [ + { + line: 4, + column: 5, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(public foo: string) { + this.foo ||= foo; + } +} + `, + errors: [ + { + line: 4, + column: 5, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(public foo: string) { + this.foo ??= foo; + } +} + `, + errors: [ + { + line: 4, + column: 5, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(public foo: string) { + this.foo &&= foo; + } +} + `, + errors: [ + { + line: 4, + column: 5, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(private foo: string) { + this['foo'] = foo; + } +} + `, + errors: [ + { + line: 4, + column: 5, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(private foo: string) { + function bar() { + this.foo = foo; + } + this.foo = foo; + } +} + `, + errors: [ + { + line: 7, + column: 5, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(private foo: string) { + this.bar = () => { + this.foo = foo; + }; + this.foo = foo; + } +} + `, + errors: [ + { + line: 7, + column: 5, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(private foo: string) { + class Bar { + constructor(private foo: string) { + this.foo = foo; + } + } + this.foo = foo; + } +} + `, + errors: [ + { + line: 6, + column: 9, + messageId: 'unnecessaryAssign', + }, + { + line: 9, + column: 5, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(private foo: string) { + this.foo = foo; + } + bar = () => { + this.foo = 'foo'; + }; +} + `, + errors: [ + { + line: 4, + column: 5, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(private foo: string) { + this.foo = foo; + } + init = foo => { + this.foo = foo; + }; +} + `, + errors: [ + { + line: 4, + column: 5, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(private foo: string) { + this.foo = foo; + } + init = class Bar { + constructor(private foo: string) { + this.foo = foo; + } + }; +} + `, + errors: [ + { + line: 4, + column: 5, + messageId: 'unnecessaryAssign', + }, + { + line: 8, + column: 7, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(private foo: string) { + { + this.foo = foo; + } + } +} + `, + errors: [ + { + line: 5, + column: 7, + messageId: 'unnecessaryAssign', + }, + ], + }, + { + code: ` +class Foo { + constructor(private foo: string) { + (() => { + this.foo = foo; + })(); + } +} + `, + errors: [ + { + line: 5, + column: 7, + messageId: 'unnecessaryAssign', + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-parameter-property-assignment.shot b/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-parameter-property-assignment.shot new file mode 100644 index 000000000000..eea1075666f7 --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-parameter-property-assignment.shot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes no-unnecessary-parameter-property-assignment 1`] = ` +" +# SCHEMA: + +[] + + +# TYPES: + +/** No options declared */ +type Options = [];" +`; diff --git a/packages/typescript-eslint/src/configs/all.ts b/packages/typescript-eslint/src/configs/all.ts index c92e80a1f275..1414a72e14ef 100644 --- a/packages/typescript-eslint/src/configs/all.ts +++ b/packages/typescript-eslint/src/configs/all.ts @@ -101,6 +101,8 @@ export default ( '@typescript-eslint/no-this-alias': 'error', '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error', '@typescript-eslint/no-unnecessary-condition': 'error', + '@typescript-eslint/no-unnecessary-parameter-property-assignment': + 'error', '@typescript-eslint/no-unnecessary-qualifier': 'error', '@typescript-eslint/no-unnecessary-template-expression': 'error', '@typescript-eslint/no-unnecessary-type-arguments': 'error', pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy