From cfaa6424655cf21637a0b0566c11341593dfb8ce Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Tue, 9 Apr 2024 13:06:01 -0500 Subject: [PATCH 1/4] [prefer-readonly] Refine report locations --- .../src/rules/prefer-readonly.ts | 114 +++++++- .../prefer-readonly.shot | 12 +- .../tests/rules/prefer-readonly.test.ts | 266 +++++++++++++++--- 3 files changed, 330 insertions(+), 62 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-readonly.ts b/packages/eslint-plugin/src/rules/prefer-readonly.ts index 80f398394965..3b68b7c3e1e2 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly.ts @@ -7,6 +7,7 @@ import { createRule, getParserServices, nullThrows, + NullThrowsReasons, typeIsOrHasBaseType, } from '../util'; @@ -160,21 +161,93 @@ export default createRule({ function getEsNodesFromViolatingNode( violatingNode: ParameterOrPropertyDeclaration, ): { esNode: TSESTree.Node; nameNode: TSESTree.Node } { - if ( - ts.isParameterPropertyDeclaration(violatingNode, violatingNode.parent) - ) { - return { - esNode: services.tsNodeToESTreeNodeMap.get(violatingNode.name), - nameNode: services.tsNodeToESTreeNodeMap.get(violatingNode.name), - }; - } - return { esNode: services.tsNodeToESTreeNodeMap.get(violatingNode), nameNode: services.tsNodeToESTreeNodeMap.get(violatingNode.name), }; } + /** + * For missing readonly modifiers, we want to report any keywords + * out in front of the key, and the key itself, but not anything afterwards, + * i.e. parens, type annotations, method bodies, or `?`. + */ + function getReportLoc( + node: + | TSESTree.MethodDefinition + | TSESTree.TSAbstractMethodDefinition + | TSESTree.PropertyDefinition + | TSESTree.TSAbstractPropertyDefinition, + ): TSESTree.SourceLocation { + let start: TSESTree.Position; + + if (node.decorators.length === 0) { + start = node.loc.start; + } else { + const lastDecorator = node.decorators[node.decorators.length - 1]; + const nextToken = nullThrows( + context.sourceCode.getTokenAfter(lastDecorator), + NullThrowsReasons.MissingToken('token', 'last decorator'), + ); + start = nextToken.loc.start; + } + + let end: TSESTree.Position; + + if (!node.computed) { + end = node.key.loc.end; + } else { + const closingBracket = nullThrows( + context.sourceCode.getTokenAfter( + node.key, + token => token.value === ']', + ), + NullThrowsReasons.MissingToken(']', node.type), + ); + end = closingBracket.loc.end; + } + + return { + start: structuredClone(start), + end: structuredClone(end), + }; + } + + /** + * For missing readonly modifiers, we want to report any keywords + * out in front of the key, and the key itself, but not anything afterwards, + * i.e. parens, type annotations, method bodies, or `?`. + */ + function getReportLocForParameterProperty( + node: TSESTree.TSParameterProperty, + nodeName: string, + ): TSESTree.SourceLocation { + // Parameter properties have a weirdly different AST structure + // than other class members. + + let start: TSESTree.Position; + + if (node.decorators.length === 0) { + start = structuredClone(node.loc.start); + } else { + const lastDecorator = node.decorators[node.decorators.length - 1]; + const nextToken = nullThrows( + context.sourceCode.getTokenAfter(lastDecorator), + NullThrowsReasons.MissingToken('token', 'last decorator'), + ); + start = structuredClone(nextToken.loc.start); + } + + const end = context.sourceCode.getLocFromIndex( + node.parameter.range[0] + nodeName.length, + ); + + return { + start, + end, + }; + } + return { 'ClassDeclaration, ClassExpression'( node: TSESTree.ClassDeclaration | TSESTree.ClassExpression, @@ -196,13 +269,34 @@ export default createRule({ for (const violatingNode of finalizedClassScope.finalizeUnmodifiedPrivateNonReadonlys()) { const { esNode, nameNode } = getEsNodesFromViolatingNode(violatingNode); + + const reportNodeOrLoc: + | { node: TSESTree.Node } + | { loc: TSESTree.SourceLocation } = (() => { + switch (esNode.type) { + case AST_NODE_TYPES.MethodDefinition: + case AST_NODE_TYPES.PropertyDefinition: + case AST_NODE_TYPES.TSAbstractMethodDefinition: + return { loc: getReportLoc(esNode) }; + case AST_NODE_TYPES.TSParameterProperty: + return { + loc: getReportLocForParameterProperty( + esNode, + (nameNode as TSESTree.Identifier).name, + ), + }; + default: + return { node: esNode }; + } + })(); + context.report({ + ...reportNodeOrLoc, data: { name: context.sourceCode.getText(nameNode), }, fix: fixer => fixer.insertTextBefore(nameNode, 'readonly '), messageId: 'preferReadonly', - node: esNode, }); } }, diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-readonly.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-readonly.shot index 375126979792..e45cf78a327c 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-readonly.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-readonly.shot @@ -6,17 +6,17 @@ exports[`Validating rule docs prefer-readonly.mdx code examples ESLint output 1` class Container { // These member variables could be marked as readonly private neverModifiedMember = true; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Member 'neverModifiedMember' is never reassigned; mark it as \`readonly\`. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Member 'neverModifiedMember' is never reassigned; mark it as \`readonly\`. private onlyModifiedInConstructor: number; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Member 'onlyModifiedInConstructor' is never reassigned; mark it as \`readonly\`. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Member 'onlyModifiedInConstructor' is never reassigned; mark it as \`readonly\`. #neverModifiedPrivateField = 3; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Member '#neverModifiedPrivateField' is never reassigned; mark it as \`readonly\`. + ~~~~~~~~~~~~~~~~~~~~~~~~~~ Member '#neverModifiedPrivateField' is never reassigned; mark it as \`readonly\`. public constructor( onlyModifiedInConstructor: number, // Private parameter properties can also be marked as readonly private neverModifiedParameter: string, - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Member 'neverModifiedParameter: string' is never reassigned; mark it as \`readonly\`. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Member 'neverModifiedParameter: string' is never reassigned; mark it as \`readonly\`. ) { this.onlyModifiedInConstructor = onlyModifiedInConstructor; } @@ -57,11 +57,9 @@ Options: { "onlyInlineLambdas": true } class Container { private onClick = () => { - ~~~~~~~~~~~~~~~~~~~~~~~~~ Member 'onClick' is never reassigned; mark it as \`readonly\`. + ~~~~~~~~~~~~~~~ Member 'onClick' is never reassigned; mark it as \`readonly\`. /* ... */ -~~~~~~~~~~~~~ }; -~~~~ } " `; diff --git a/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts b/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts index 3b1c25e75f11..31e134788601 100644 --- a/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts @@ -473,6 +473,11 @@ ruleTester.run('prefer-readonly', rule, { } } `, + ` + class TestComputedParameter { + private ['computed-ignored-by-rule'] = 1; + } + `, { code: ` class Foo { @@ -747,6 +752,10 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 53, data: { name: 'incorrectlyModifiableStatic', }, @@ -767,6 +776,10 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 46, data: { name: '#incorrectlyModifiableStatic', }, @@ -787,6 +800,10 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 58, data: { name: 'incorrectlyModifiableStaticArrow', }, @@ -807,6 +824,10 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 51, data: { name: '#incorrectlyModifiableStaticArrow', }, @@ -833,17 +854,23 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 46, data: { name: 'incorrectlyModifiableInline', }, - line: 3, messageId: 'preferReadonly', }, { + line: 7, + column: 15, + endLine: 7, + endColumn: 50, data: { name: 'incorrectlyModifiableInline', }, - line: 7, messageId: 'preferReadonly', }, ], @@ -873,17 +900,23 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 39, data: { name: '#incorrectlyModifiableInline', }, - line: 3, messageId: 'preferReadonly', }, { + line: 7, + column: 15, + endLine: 7, + endColumn: 43, data: { name: '#incorrectlyModifiableInline', }, - line: 7, messageId: 'preferReadonly', }, ], @@ -911,6 +944,10 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 47, data: { name: 'incorrectlyModifiableDelayed', }, @@ -939,6 +976,10 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 40, data: { name: '#incorrectlyModifiableDelayed', }, @@ -973,10 +1014,13 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 49, data: { name: 'childClassExpressionModifiable', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1014,10 +1058,13 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 42, data: { name: '#childClassExpressionModifiable', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1049,10 +1096,14 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 49, + data: { name: 'incorrectlyModifiablePostMinus', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1078,10 +1129,13 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 42, data: { name: '#incorrectlyModifiablePostMinus', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1107,10 +1161,14 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 48, + data: { name: 'incorrectlyModifiablePostPlus', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1136,10 +1194,14 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 41, + data: { name: '#incorrectlyModifiablePostPlus', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1165,10 +1227,13 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 48, data: { name: 'incorrectlyModifiablePreMinus', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1194,10 +1259,14 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 41, + data: { name: '#incorrectlyModifiablePreMinus', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1223,10 +1292,14 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 47, + data: { name: 'incorrectlyModifiablePrePlus', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1252,10 +1325,14 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 40, + data: { name: '#incorrectlyModifiablePrePlus', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1285,10 +1362,14 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 43, + data: { name: 'overlappingClassVariable', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1314,10 +1395,13 @@ class Foo { `, errors: [ { + line: 3, + column: 30, + endLine: 3, + endColumn: 68, data: { name: 'incorrectlyModifiableParameter', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1338,10 +1422,14 @@ class Foo { `, errors: [ { + line: 5, + column: 13, + endLine: 5, + endColumn: 51, + data: { name: 'incorrectlyModifiableParameter', }, - line: 5, messageId: 'preferReadonly', }, ], @@ -1362,10 +1450,13 @@ class Foo { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 42, data: { name: 'incorrectlyInlineLambda', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1397,10 +1488,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 4, + column: 5, + endLine: 4, + endColumn: 18, data: { name: '_name', }, - line: 4, messageId: 'preferReadonly', }, ], @@ -1422,10 +1516,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 4, + column: 5, + endLine: 4, + endColumn: 10, data: { name: '#name', }, - line: 4, messageId: 'preferReadonly', }, ], @@ -1455,10 +1552,14 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 26, + data: { name: 'testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1488,10 +1589,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 19, data: { name: '#testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1525,10 +1629,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 7, + column: 11, + endLine: 7, + endColumn: 26, data: { name: 'testObj', }, - line: 7, messageId: 'preferReadonly', }, ], @@ -1562,10 +1669,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 7, + column: 11, + endLine: 7, + endColumn: 19, data: { name: '#testObj', }, - line: 7, messageId: 'preferReadonly', }, ], @@ -1593,10 +1703,14 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 26, + data: { name: 'testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1624,10 +1738,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 19, data: { name: '#testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1651,10 +1768,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 26, data: { name: 'testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1678,10 +1798,14 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 19, + data: { name: '#testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1705,10 +1829,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 26, data: { name: 'testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1732,10 +1859,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 19, data: { name: '#testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1759,10 +1889,14 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 26, + data: { name: 'testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1786,10 +1920,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 19, data: { name: '#testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1813,10 +1950,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 26, data: { name: 'testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1840,10 +1980,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 19, data: { name: '#testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1867,10 +2010,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 26, data: { name: 'testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1894,10 +2040,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 19, data: { name: '#testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1921,10 +2070,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 26, data: { name: 'testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1948,10 +2100,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 19, data: { name: '#testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -1975,10 +2130,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 26, data: { name: 'testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -2002,10 +2160,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 19, data: { name: '#testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -2029,10 +2190,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 26, data: { name: 'testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -2056,10 +2220,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 19, data: { name: '#testObj', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -2087,10 +2254,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 23, data: { name: 'prop', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -2118,10 +2288,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 23, data: { name: 'prop', }, - line: 3, messageId: 'preferReadonly', }, ], @@ -2149,10 +2322,13 @@ function ClassWithName {}>(Base: TBase) { `, errors: [ { + line: 3, + column: 11, + endLine: 3, + endColumn: 23, data: { name: 'prop', }, - line: 3, messageId: 'preferReadonly', }, ], From df3759de1ab83a0e584af939f9bd04fcea8d552c Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Thu, 30 May 2024 16:39:17 -0600 Subject: [PATCH 2/4] wip --- .../rules/explicit-member-accessibility.ts | 51 +--------- .../src/rules/prefer-readonly.ts | 49 +--------- .../src/util/getMemberHeadLoc.ts | 96 +++++++++++++++++++ 3 files changed, 101 insertions(+), 95 deletions(-) create mode 100644 packages/eslint-plugin/src/util/getMemberHeadLoc.ts diff --git a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts index a0e11f5cb69e..94f85865f235 100644 --- a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts +++ b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts @@ -7,6 +7,7 @@ import { nullThrows, NullThrowsReasons, } from '../util'; +import { getMemberHeadLoc } from '../util/getMemberHeadLoc'; type AccessibilityLevel = | 'explicit' // require an accessor (including public) @@ -166,7 +167,7 @@ export default createRule({ }); } else if (check === 'explicit' && !methodDefinition.accessibility) { context.report({ - loc: getMissingAccessibilityReportLoc(methodDefinition), + loc: getMemberHeadLoc(context.sourceCode, methodDefinition), messageId: 'missingAccessibility', data: { type: nodeType, @@ -221,52 +222,6 @@ export default createRule({ return { range: keywordRange, rangeToRemove }; } - /** - * For missing accessibility modifiers, we want to report any keywords - * out in front of the key, and the key itself, but not anything afterwards, - * i.e. parens, type annotations, method bodies, or `?`. - */ - function getMissingAccessibilityReportLoc( - node: - | TSESTree.MethodDefinition - | TSESTree.TSAbstractMethodDefinition - | TSESTree.PropertyDefinition - | TSESTree.TSAbstractPropertyDefinition, - ): TSESTree.SourceLocation { - let start: TSESTree.Position; - - if (node.decorators.length === 0) { - start = node.loc.start; - } else { - const lastDecorator = node.decorators[node.decorators.length - 1]; - const nextToken = nullThrows( - context.sourceCode.getTokenAfter(lastDecorator), - NullThrowsReasons.MissingToken('token', 'last decorator'), - ); - start = nextToken.loc.start; - } - - let end: TSESTree.Position; - - if (!node.computed) { - end = node.key.loc.end; - } else { - const closingBracket = nullThrows( - context.sourceCode.getTokenAfter( - node.key, - token => token.value === ']', - ), - NullThrowsReasons.MissingToken(']', node.type), - ); - end = closingBracket.loc.end; - } - - return { - start: structuredClone(start), - end: structuredClone(end), - }; - } - /** * For missing accessibility modifiers, we want to report any keywords * out in front of the key, and the key itself, but not anything afterwards, @@ -385,7 +340,7 @@ export default createRule({ !propertyDefinition.accessibility ) { context.report({ - loc: getMissingAccessibilityReportLoc(propertyDefinition), + loc: getMemberHeadLoc(context.sourceCode, propertyDefinition), messageId: 'missingAccessibility', data: { type: nodeType, diff --git a/packages/eslint-plugin/src/rules/prefer-readonly.ts b/packages/eslint-plugin/src/rules/prefer-readonly.ts index 3b68b7c3e1e2..b82c152482bd 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly.ts @@ -10,6 +10,7 @@ import { NullThrowsReasons, typeIsOrHasBaseType, } from '../util'; +import { getMemberHeadLoc } from '../util/getMemberHeadLoc'; type MessageIds = 'preferReadonly'; type Options = [ @@ -167,52 +168,6 @@ export default createRule({ }; } - /** - * For missing readonly modifiers, we want to report any keywords - * out in front of the key, and the key itself, but not anything afterwards, - * i.e. parens, type annotations, method bodies, or `?`. - */ - function getReportLoc( - node: - | TSESTree.MethodDefinition - | TSESTree.TSAbstractMethodDefinition - | TSESTree.PropertyDefinition - | TSESTree.TSAbstractPropertyDefinition, - ): TSESTree.SourceLocation { - let start: TSESTree.Position; - - if (node.decorators.length === 0) { - start = node.loc.start; - } else { - const lastDecorator = node.decorators[node.decorators.length - 1]; - const nextToken = nullThrows( - context.sourceCode.getTokenAfter(lastDecorator), - NullThrowsReasons.MissingToken('token', 'last decorator'), - ); - start = nextToken.loc.start; - } - - let end: TSESTree.Position; - - if (!node.computed) { - end = node.key.loc.end; - } else { - const closingBracket = nullThrows( - context.sourceCode.getTokenAfter( - node.key, - token => token.value === ']', - ), - NullThrowsReasons.MissingToken(']', node.type), - ); - end = closingBracket.loc.end; - } - - return { - start: structuredClone(start), - end: structuredClone(end), - }; - } - /** * For missing readonly modifiers, we want to report any keywords * out in front of the key, and the key itself, but not anything afterwards, @@ -277,7 +232,7 @@ export default createRule({ case AST_NODE_TYPES.MethodDefinition: case AST_NODE_TYPES.PropertyDefinition: case AST_NODE_TYPES.TSAbstractMethodDefinition: - return { loc: getReportLoc(esNode) }; + return { loc: getMemberHeadLoc(context.sourceCode, esNode) }; case AST_NODE_TYPES.TSParameterProperty: return { loc: getReportLocForParameterProperty( diff --git a/packages/eslint-plugin/src/util/getMemberHeadLoc.ts b/packages/eslint-plugin/src/util/getMemberHeadLoc.ts new file mode 100644 index 000000000000..f7a220009b7f --- /dev/null +++ b/packages/eslint-plugin/src/util/getMemberHeadLoc.ts @@ -0,0 +1,96 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { + nullThrows, + NullThrowsReasons, +} from '@typescript-eslint/utils/eslint-utils'; + +/** + * Generates report loc suitable for reporting on how a class member is + * "declared", rather than how it's implemented. + * + * ```ts + * class A { + * abstract method(): void; + * ~~~~~~~~~~~~~~~ + * + * concreteMethod(): void { + * ~~~~~~~~~~~~~~ + * // code + * } + * + * abstract private property?: string; + * ~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * @decorator override concreteProperty = 'value'; + * ~~~~~~~~~~~~~~~~~~~~~~~~~ + * } + * ``` + */ +export function getMemberHeadLoc( + sourceCode: Readonly, + node: + | TSESTree.MethodDefinition + | TSESTree.TSAbstractMethodDefinition + | TSESTree.PropertyDefinition + | TSESTree.TSAbstractPropertyDefinition, +): TSESTree.SourceLocation { + let start: TSESTree.Position; + + if (node.decorators.length === 0) { + start = node.loc.start; + } else { + const lastDecorator = node.decorators[node.decorators.length - 1]; + const nextToken = nullThrows( + sourceCode.getTokenAfter(lastDecorator), + NullThrowsReasons.MissingToken('token', 'last decorator'), + ); + start = nextToken.loc.start; + } + + let end: TSESTree.Position; + + if (!node.computed) { + end = node.key.loc.end; + } else { + const closingBracket = nullThrows( + sourceCode.getTokenAfter(node.key, token => token.value === ']'), + NullThrowsReasons.MissingToken(']', node.type), + ); + end = closingBracket.loc.end; + } + + return { + start: structuredClone(start), + end: structuredClone(end), + }; +} + +export function getParameterPropertyHeadLoc( + sourceCode: Readonly, + node: TSESTree.TSParameterProperty, +): TSESTree.SourceLocation { + // Parameter properties have a weirdly different AST structure + // than other class members. + + let start: TSESTree.Position; + + if (node.decorators.length === 0) { + start = structuredClone(node.loc.start); + } else { + const lastDecorator = node.decorators[node.decorators.length - 1]; + const nextToken = nullThrows( + sourceCode.getTokenAfter(lastDecorator), + NullThrowsReasons.MissingToken('token', 'last decorator'), + ); + start = structuredClone(nextToken.loc.start); + } + + const end = sourceCode.getLocFromIndex( + node.parameter.range[0] + nodeName.length, + ); + + return { + start, + end, + }; +} From c963914c555f09ac26ae2e3ddb09702795bd3ee7 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Tue, 4 Jun 2024 20:03:53 -0600 Subject: [PATCH 3/4] bring stuff to shared code --- .../rules/explicit-member-accessibility.ts | 43 +++---------------- .../src/rules/prefer-readonly.ts | 43 +++---------------- .../src/util/getMemberHeadLoc.ts | 14 +++++- 3 files changed, 25 insertions(+), 75 deletions(-) diff --git a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts index 94f85865f235..5947d292f8bd 100644 --- a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts +++ b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts @@ -7,7 +7,10 @@ import { nullThrows, NullThrowsReasons, } from '../util'; -import { getMemberHeadLoc } from '../util/getMemberHeadLoc'; +import { + getMemberHeadLoc, + getParameterPropertyHeadLoc, +} from '../util/getMemberHeadLoc'; type AccessibilityLevel = | 'explicit' // require an accessor (including public) @@ -222,41 +225,6 @@ export default createRule({ return { range: keywordRange, rangeToRemove }; } - /** - * For missing accessibility modifiers, we want to report any keywords - * out in front of the key, and the key itself, but not anything afterwards, - * i.e. parens, type annotations, method bodies, or `?`. - */ - function getMissingAccessibilityReportLocForParameterProperty( - node: TSESTree.TSParameterProperty, - nodeName: string, - ): TSESTree.SourceLocation { - // Parameter properties have a weirdly different AST structure - // than other class members. - - let start: TSESTree.Position; - - if (node.decorators.length === 0) { - start = structuredClone(node.loc.start); - } else { - const lastDecorator = node.decorators[node.decorators.length - 1]; - const nextToken = nullThrows( - context.sourceCode.getTokenAfter(lastDecorator), - NullThrowsReasons.MissingToken('token', 'last decorator'), - ); - start = structuredClone(nextToken.loc.start); - } - - const end = context.sourceCode.getLocFromIndex( - node.parameter.range[0] + nodeName.length, - ); - - return { - start, - end, - }; - } - /** * Creates a fixer that adds an accessibility modifier keyword */ @@ -377,7 +345,8 @@ export default createRule({ case 'explicit': { if (!node.accessibility) { context.report({ - loc: getMissingAccessibilityReportLocForParameterProperty( + loc: getParameterPropertyHeadLoc( + context.sourceCode, node, nodeName, ), diff --git a/packages/eslint-plugin/src/rules/prefer-readonly.ts b/packages/eslint-plugin/src/rules/prefer-readonly.ts index b82c152482bd..1e3d80d531a1 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly.ts @@ -10,7 +10,10 @@ import { NullThrowsReasons, typeIsOrHasBaseType, } from '../util'; -import { getMemberHeadLoc } from '../util/getMemberHeadLoc'; +import { + getMemberHeadLoc, + getParameterPropertyHeadLoc, +} from '../util/getMemberHeadLoc'; type MessageIds = 'preferReadonly'; type Options = [ @@ -168,41 +171,6 @@ export default createRule({ }; } - /** - * For missing readonly modifiers, we want to report any keywords - * out in front of the key, and the key itself, but not anything afterwards, - * i.e. parens, type annotations, method bodies, or `?`. - */ - function getReportLocForParameterProperty( - node: TSESTree.TSParameterProperty, - nodeName: string, - ): TSESTree.SourceLocation { - // Parameter properties have a weirdly different AST structure - // than other class members. - - let start: TSESTree.Position; - - if (node.decorators.length === 0) { - start = structuredClone(node.loc.start); - } else { - const lastDecorator = node.decorators[node.decorators.length - 1]; - const nextToken = nullThrows( - context.sourceCode.getTokenAfter(lastDecorator), - NullThrowsReasons.MissingToken('token', 'last decorator'), - ); - start = structuredClone(nextToken.loc.start); - } - - const end = context.sourceCode.getLocFromIndex( - node.parameter.range[0] + nodeName.length, - ); - - return { - start, - end, - }; - } - return { 'ClassDeclaration, ClassExpression'( node: TSESTree.ClassDeclaration | TSESTree.ClassExpression, @@ -235,7 +203,8 @@ export default createRule({ return { loc: getMemberHeadLoc(context.sourceCode, esNode) }; case AST_NODE_TYPES.TSParameterProperty: return { - loc: getReportLocForParameterProperty( + loc: getParameterPropertyHeadLoc( + context.sourceCode, esNode, (nameNode as TSESTree.Identifier).name, ), diff --git a/packages/eslint-plugin/src/util/getMemberHeadLoc.ts b/packages/eslint-plugin/src/util/getMemberHeadLoc.ts index f7a220009b7f..19401230e235 100644 --- a/packages/eslint-plugin/src/util/getMemberHeadLoc.ts +++ b/packages/eslint-plugin/src/util/getMemberHeadLoc.ts @@ -6,7 +6,7 @@ import { /** * Generates report loc suitable for reporting on how a class member is - * "declared", rather than how it's implemented. + * declared, rather than how it's implemented. * * ```ts * class A { @@ -65,9 +65,21 @@ export function getMemberHeadLoc( }; } +/** + * Generates report loc suitable for reporting on how a parameter property is + * declared. + * + * ```ts + * class A { + * constructor(private property: string = 'value') { + * ~~~~~~~~~~~~~~~~ + * } + * ``` + */ export function getParameterPropertyHeadLoc( sourceCode: Readonly, node: TSESTree.TSParameterProperty, + nodeName: string, ): TSESTree.SourceLocation { // Parameter properties have a weirdly different AST structure // than other class members. From 9f3e4031a841f7b22ead6a7edf064cb9ed6bac09 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Tue, 4 Jun 2024 21:08:59 -0600 Subject: [PATCH 4/4] lint --- packages/eslint-plugin/src/rules/prefer-readonly.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-readonly.ts b/packages/eslint-plugin/src/rules/prefer-readonly.ts index 1e3d80d531a1..f163b1aaab34 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly.ts @@ -7,7 +7,6 @@ import { createRule, getParserServices, nullThrows, - NullThrowsReasons, typeIsOrHasBaseType, } from '../util'; import {