-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
fix(eslint-plugin): [prefer-readonly] autofixer doesn't add type to property that is mutated in the constructor #10552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7993076
f044615
a63f335
125b56e
b77a289
02c3124
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -173,6 +173,38 @@ export default createRule<Options, MessageIds>({ | |
}; | ||
} | ||
|
||
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: | ||
|
@@ -229,13 +261,62 @@ export default createRule<Options, MessageIds>({ | |
} | ||
})(); | ||
|
||
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; | ||
} | ||
Comment on lines
+273
to
+275
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To my understanding, this can realistically only be an identifier. This means some cases that should have a type annotation added won't receive it, for example: class TestOverlappingClassVariable {
private 'overlappingClassVariable' = 7;
constructor() {
this.overlappingClassVariable = 10;
}
} This seems like an existing bug that's not necessarily related to this addition. I've opened #10655 as a separate issue to address this. |
||
|
||
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)) { | ||
JoshuaKGoldberg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return null; | ||
} | ||
|
||
return getTypeAnnotationForViolatingNode( | ||
esNode, | ||
violatingType, | ||
initializerType, | ||
); | ||
})(); | ||
|
||
context.report({ | ||
...reportNodeOrLoc, | ||
messageId: 'preferReadonly', | ||
data: { | ||
name: context.sourceCode.getText(nameNode), | ||
}, | ||
fix: fixer => fixer.insertTextBefore(nameNode, 'readonly '), | ||
*fix(fixer) { | ||
yield fixer.insertTextBefore(nameNode, 'readonly '); | ||
|
||
if (typeAnnotation) { | ||
yield fixer.insertTextAfter(nameNode, `: ${typeAnnotation}`); | ||
} | ||
}, | ||
}); | ||
} | ||
}, | ||
|
@@ -288,6 +369,8 @@ class ClassScope { | |
private readonly classType: ts.Type; | ||
private constructorScopeDepth = OUTSIDE_CONSTRUCTOR; | ||
private readonly memberVariableModifications = new Set<string>(); | ||
private readonly memberVariableWithConstructorModifications = | ||
new Set<string>(); | ||
private readonly privateModifiableMembers = new Map< | ||
string, | ||
ParameterOrPropertyDeclaration | ||
|
@@ -358,6 +441,7 @@ class ClassScope { | |
relationOfModifierTypeToClass === TypeToClassRelation.Instance && | ||
this.constructorScopeDepth === DIRECTLY_INSIDE_CONSTRUCTOR | ||
) { | ||
this.memberVariableWithConstructorModifications.add(node.name.text); | ||
return; | ||
} | ||
|
||
|
@@ -465,4 +549,8 @@ class ClassScope { | |
|
||
return TypeToClassRelation.Instance; | ||
} | ||
|
||
public memberHasConstructorModifications(name: string) { | ||
return this.memberVariableWithConstructorModifications.has(name); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wrote about this on #1056 (comment), but on some occasions (as far as I know only enums), the rule won't be able to add a type annotation:
I chose not to add a type annotation on these cases, but an alternative I think is also valid is to have this be a suggestion instead of an auto-fix.
Would be happy to to hear opinions on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a vague memory of this coming up once in a while... I think it's fine for now. We don't have any kind of shared/standard plumbing for importing constructs in fixes or suggestions.