-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat(eslint-plugin): add class-literal-property-style
rule
#1582
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
feat(eslint-plugin): add class-literal-property-style
rule
#1582
Conversation
I haven't reviewed the code yet, but
IMO Instead of creating a special any type, I would suggest just making it an optional tuple element. type Options = [('fields' | 'getters')?]; |
@c32hedge cheers, those were good catches 😅
Thinking about it some more I think I'd be happy to make
Very clever - that's much better than having a special type 😄 I'll use the optional tuple, but given the above I'm happy to set a default if you'd like 🙂 Also, the build failed due to missing
|
packages/eslint-plugin/tests/rules/class-literals-styles.test.ts
Outdated
Show resolved
Hide resolved
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.
a few comments, otherwise LGTM.
Thanks for working on this!
): node is TSESTree.LiteralExpression => | ||
node.type === AST_NODE_TYPES.Literal || | ||
node.type === AST_NODE_TYPES.BigIntLiteral || | ||
(node.type === AST_NODE_TYPES.TemplateLiteral && node.quasis.length === 1); |
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.
no support for TaggedTemplateLiteral
?
Would probably good to support them for things like graphql tagged strings:
readonly query = graphql`
query {
# ...
}
`;
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.
Very good point! I've added support, with the same bailout on if they've got any expressions since we'd have to scope-check them which is probably overkill (unless I've missed the magic method that checks exactly that?).
I've also not had the fixer bother trying to make them look nicer by having them multi-line, as that feels like more overkill as we'd have to try to calculate indentation.
Let me know what you think :)
packages/eslint-plugin/tests/rules/class-literals-style.test.ts
Outdated
Show resolved
Hide resolved
fixer.replaceTextRange( | ||
[node.key.range[1] + keyOffset, value.range[0]], | ||
'(){return ', | ||
), | ||
// replace the end bits with the end of the getter | ||
fixer.replaceTextRange([value.range[1], node.range[1]], '}'), |
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.
stylistically, do you think more people enforce spaces inside function body?
get foo() {return 1}
// or
get foo() { return 1; }
I think more people do the latter, so it might be good to do it to save the majority having to edit it afterward?
I use prettier in everything, so it's no skin off my nose. I'm happy with whatever you want to do.
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 don't know how interactions between rules work, but how would this interact with the block-spacing eslint rule? Same question about the semi rule for the return statement.
Sorry @bradzacher, I just wasn't clear from your comment if these other rules would fix this if they are set based on your remark about "having to edit it afterward".
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.
The docs explain this better than I can:
https://eslint.org/docs/developer-guide/working-with-rules#applying-fixes
After applying fixes, ESLint will run all of the enabled rules again on the fixed code, potentially applying more fixes. This process will repeat up to 10 times, or until no more fixable problems are found. Afterwards, any remaining problems will be reported as usual.
Best practices for fixes:
\4. Since all rules are run again after the initial round of fixes is applied, it's not necessary for a rule to check whether the code style of a fix will cause errors to be reported by another rule.
One exception to this is fixers applied via an IDE - fixing a single error via the IDE will run the fixer exactly once.
Which is why when applying larger fixes like this, I still like to try and go with what I believe to be the "most common" styling, so there's less chance of the fix causing a lint error, and so people are less likely to want to fix it up manually afterward.
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.
One exception to this is fixers applied via an IDE - fixing a single error via the IDE will run the fixer exactly once.
That's a good point - I'm not particularly fussed, and do prefer the nicer output in the tests anyway, so +1
fixer.replaceTextRange( | ||
[node.range[0], node.key.range[0] - keyOffset], | ||
printNodeModifiers(node, 'readonly'), | ||
), |
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.
Considering you're pretty much replacing the entire node, I would just simplify this and actually replace the entire node:
let text = printNodeModifiers(node, 'readonly');
const name = sourceCode.getText(node.key);
if (node.computed) {
text += `[${name}]`;
} else {
text += name;
}
text += ` = ${sourceCode.getText(argument)};`;
return fixer.replace(node, text);
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.
Happy to do so, but note that replacing the entire node means that any comments would be lost:
readonly /* public */ p1 = 'hello world'
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.
tbf, you're nuking the majority of the comments anyways 😄
/* */ public /* */ static /* */ readonly /* */ p1 /* */ = /* */ 'hello world' /* */;
I think your current code only keeps the outer two comments.
Replacing the node would also only keep the outer two comments.
If someone comes later on to complain about missing comments, we can reassess the fixer - I'm not fussed about killing comments in really weird places.
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'm not fussed about killing comments in really weird places.
That's my feelings too :)
return [ | ||
// replace the start bits with the getter modifiers | ||
fixer.replaceTextRange( | ||
[node.range[0], node.key.range[0] - keyOffset], | ||
printNodeModifiers(node, 'get'), | ||
), | ||
// replace the middle bits with the start of the getter | ||
fixer.replaceTextRange( | ||
[node.key.range[1] + keyOffset, value.range[0]], | ||
'(){return ', | ||
), | ||
// replace the end bits with the end of the getter | ||
fixer.replaceTextRange([value.range[1], node.range[1]], '}'), | ||
]; |
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.
same comment as the above fixer - might be better to just replace the entire node.
packages/eslint-plugin/tests/rules/class-literals-style.test.ts
Outdated
Show resolved
Hide resolved
Co-Authored-By: Brad Zacher <brad.zacher@gmail.com>
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.
LGTM - thanks for doing this.
One question before merging:
Do you think this rule would be better named as one of:
class-literal-property-style
class-property-literal-style
literal-class-property-style
class-readonly-property-style
readonly-class-property-style
I'm wondering if just "literals" by itself is too vague to convey intention?
Yeah I was wondering that too. I don't think that the rule name should include I think On an aside: do you have any opinions on plural vs singular rule names? it's another one of those things I've hit a couple of times where neither seems to strongly win out vs the other to me :/ |
I think that as long as the name makes sense it doesn't really matter. Examples from eyeballing our rule list:
|
class-literals-style
ruleclass-literal-property-style
rule
Closes #1556
While I see the value in this rule, I think it'll be misleading to have a default value as I think neither style is Strictly Better.
For now I've typed the rule as having a secret
any
style that just does nothing, which I've omitted from the schema.I originally had it as an error to force you to provide a style, but that's not a convention I've seen around - Very much open to ideas on how to handle the configuration of this rule.
Also open to docs suggestions - I aimed for passable, but the wording is a little hamfisted (in particularly at the start) 😬
I've decided to leave arrays, objects, and functions off the docket as theres some interesting implications & subtle differences in the behaviours of ways you could refactor them to follow this rule; For example:
While the property is readonly, the array isn't, and so could be mutated;
getters
style would have the class written like so:Which means a new array will be created every time, effectively making the array itself
readonly
, which might not be desired.An off-the-cuff fix that could be suggested might be to use a variable:
The gotcha with this is that in the original example a new array was initialised along with the class - now the array is initialised upon definition, and the field is assigned a reference to the array.
This means that all instances will share - and mutate - the same array.
This was a rabbit hole I decided not to go down for now, given that I think the primary reasons behind why this rule was requested is met, and that this can be easily added to down the line if it comes up.
Including them is just a matter of removing the
isSupportedLiteral
check; I believe thatreadonly
types should be fine (i.ereadonly string[]
) but that would require type checking.