-
Notifications
You must be signed in to change notification settings - Fork 887
Conversation
src/rules/noUnnecessaryParensRule.ts
Outdated
return { | ||
message: `the ${whichChild} child of an expression of type ${parentKind}`, | ||
test(node: ts.ParenthesizedExpression | ts.ParenthesizedTypeNode) { | ||
if (node.parent == undefined) { |
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.
That condition will never be true
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.
True, but I thought the check would be preferable to the !
. Switched to the !
.
src/rules/noUnnecessaryParensRule.ts
Outdated
"VariableDeclaration.initializer", | ||
"ParenthesizedExpression.expression", | ||
"CallExpression.arguments", | ||
"ExpressionStatement.expression", |
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.
All of these options require knowledge of typescript's AST. I don't think that's practicle for most TSLint users.
Maybe the rule should just find all unnecessary parens by default and provide options to disable some of them. Something like "allow-typeof"
to allow (typeof foo)==="string"
for example. These can be added on demand.
Also with these options, there's currently no way to ban parens on "*.type"
, e.g. let foo: (string)
and foo(param: (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.
There's currently no way to ban (1 + 2) + 3
without banning (1 + 2) * 3
, too.
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.
Maybe the rule should just find all unnecessary parens
So, I didn't want to get into litigating what's "necessary". I don't want to tell people to not do (true && false) || true
or something, when I think that's probably preferable to leaving out the parentheses since it's hard to remember boolean prescience.
Instead, I want to ban them where they're clearly unnecessary, and leave the edge cases for human reviewers (or the judgement of the author).
I feel like having a list of allowances would get out of hand.
If you think this approach is fundamentally unsuitable, that's fine, just let me know. I'll just use it on my project.
Also with these options, there's currently no way to ban parens on "*.type", e.g. let foo: (string) and foo(param: (1)) {}
Why not? I added some examples and tests.
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.
Maybe the documentation could point to http://astexplorer.net. That's how I find these AST names.
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.
Adding a link to astexplorer.net sounds like a good idea.
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.
Code looks good to me. Need to think a bit more about the defaults since we cannot simply change them later.
src/rules/noUnnecessaryParensRule.ts
Outdated
|
||
function isNodeOfKind(node: ts.Node, kindName: string) { | ||
switch (kindName) { | ||
case "LiteralExpression": |
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.
consider adding case "*": return true;
to allow for *.type
src/rules/noUnnecessaryParensRule.ts
Outdated
// ex: (foo.bar()); | ||
"ExpressionStatement.expression", | ||
// ex: function foo((a: string), b: number) {} | ||
"SignatureDeclaration.parameters", |
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.
SignatureDeclaration
is not a SyntaxKind
. It's the base type for FunctionDeclaration
, FunctionExpression
, ConstructorDeclaration
, IndexSignature
, ...
This option will not work as intended.
Also note that a parameter cannot be wrapped in parens
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.
Ah, good call on both counts. I was being overenthusiastic. :-)
src/rules/noUnnecessaryParensRule.ts
Outdated
// ex: let x: (string|number) = 3; | ||
"VariableDeclaration.type", | ||
// ex: function(foo: (number|string)) {} | ||
"Parameter.type", |
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.
// foo[(bar + "baz")]
"ElementAccessExpression.argumentExpression",
// `${(foo)}`
"TemplateSpan.expression"
src/rules/noUnnecessaryParensRule.ts
Outdated
// ex: ((1 + 1)) + 2 | ||
"ParenthesizedExpression.expression", | ||
// ex: foo((a), b) | ||
"CallExpression.arguments", |
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.
also "NewExpression.arguments"
src/rules/noUnnecessaryParensRule.ts
Outdated
// ex: foo((a), b) | ||
"CallExpression.arguments", | ||
// ex: Foo<(string|number), string> | ||
"TypeReference.typeArguments", |
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.
"*.typeArguments"
?
src/rules/noUnnecessaryParensRule.ts
Outdated
"VariableDeclaration.initializer", | ||
"ParenthesizedExpression.expression", | ||
"CallExpression.arguments", | ||
"ExpressionStatement.expression", |
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.
Adding a link to astexplorer.net sounds like a good idea.
@calebegg please merge master to fix the build |
* Add link to astexplorer.net * Support *.foo in asChildOf (and add a test) * Add new default rules * Remove erroneous default rule
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.
CI is failing at
(castType.getProperties().some((symbol) => !isNaN(Number(symbol.name))))) { |
@@ -0,0 +1,33 @@ | |||
let x = (3); |
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.
There's one edge case that would result in invalid code:
let y, z;
let x = (y = 1, z = 2);
I hope nobody writes such code.
Nontheless there should probably be an exception for ts.SyntaxKind.BinaryExpression
where operatorToken.kind === ts.SyntaxKind.CommaToken
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.
Ugh, the comma operator is the worst. On my shortlist of new tslint rules is one to ban it entirely.
I made it so that it doesn't suggest a fix if the child uses a comma operator, but it still flags it as an issue. How does that sound?
src/rules/noUnnecessaryParensRule.ts
Outdated
// ex: let x: (string|number) = 3; | ||
"VariableDeclaration.type", | ||
// ex: function(foo: (number|string)) {} | ||
"Parameter.type", |
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 still in favor of using "*.type"
. I can't come up with a case where parens are desired or even required.
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.
Done.
* Don’t suggest a fix when the child is a comma operator expresssion * Use *.type instead of Parameter.type
Hey, that's my own commit! :-) Fixed. |
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
What do you think about renaming the rule to ban-parens
? I think that expresses the intention of the rule better: the user needs to specify where to ban parens, the rule does not automagically find all unnecessary parens.
src/rules/noUnnecessaryParensRule.ts
Outdated
// ex: let x = (1 + foo()); | ||
"VariableDeclaration.initializer", | ||
// ex: type x = (string|number); | ||
"TypeAliasDeclaration.type", |
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.
this is redundant now, but I'm fine with leaving it in for documentation
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.
Yeah, that's true. I guess it does give a better error message, kinda, to have the more specific ones.
I'd be fine either way. Up to you.
src/rules/noUnnecessaryParensRule.ts
Outdated
// ex: (foo.bar()); | ||
"ExpressionStatement.expression", | ||
// ex: let x: (string|number) = 3; | ||
"VariableDeclaration.type", |
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.
also redundant
Makes sense. Renamed. |
Also `throw(err)` to `throwerr`
FYI, updated to fix a minor issue that came up when running over our codebase -- |
Fixed more issues that cropped up in our codebase --
becomes
which also doesn't compile. |
src/rules/banParensRule.ts
Outdated
// Don't flag `(0).foo()`, because `0.foo()` doesn't work. | ||
(isParenthesizedExpression(node) && | ||
isNumericLiteral(node.expression) && | ||
node.parent != undefined && |
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.
node.parent
cannot be undefined
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.
Changed to !
For another case, this broke the type narrowing, so I had to add a cast.
src/rules/banParensRule.ts
Outdated
isPropertyAccessExpression(node.parent)) || | ||
// Don't flag `return (\nfoo)`, since the parens are necessary. | ||
(isParenthesizedExpression(node) && | ||
node.parent != undefined && |
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 here
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.
Done
src/rules/banParensRule.ts
Outdated
(isParenthesizedExpression(node) && | ||
node.parent != undefined && | ||
isReturnStatement(node.parent) && | ||
ctx.sourceFile.text[node.getStart() + 1] === "\n") |
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.
isSameLine(ctx.sourceFile, node.expression.pos, node.expression.getStart(ctx.sourceFile))
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.
Done (had to tweak it a bit; used node.expression.getStart()
and node.getStart()
)
src/rules/banParensRule.ts
Outdated
replacement = []; | ||
} | ||
|
||
if (!( |
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.
Consider extracting this condition to a function and invoke it in the condition in line 199
if (isParenthesizedExpression(node) && !parensAreNecessary(node, ctx.sourceFile) || isParenthesizedType(node))
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.
Done
} | ||
} | ||
|
||
function isParenthesizedType(node: ts.Node): node is ts.ParenthesizedTypeNode { |
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.
note to myself: remove this after the next release of tsutils
src/rules/banParensRule.ts
Outdated
isPropertyAccessExpression(node.parent!)) || | ||
// Don't flag `return (\nfoo)`, since the parens are necessary. | ||
(isReturnStatement(node.parent!) && | ||
!isSameLine(sourceFile, node.getStart(), node.expression.getStart())) || |
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.
please pass the sourceFile
parameter to .getStart()
to avoid unnecesary lookups.
I'm also very confident that you can replace node.getStart()
with node.expression.pos
. The former gives you the start of the open paren, the latter gives the end of the open paren. But that difference doesn't matter for isSameLine
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.
Done. Sorry, I wrestled with this a bit until I got something working and then didn't pay more attention. I didn't really know the difference between .getStart
and .pos
(though I think I get it now). Is there documentation for this stuff somewhere?
What's the benefit of passing sourceFile
in?
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.
.pos
is the position where the parser started to parse the current node (or token). node.pos
is the same value as the preceding token's .end
.
node.getStart()
is the position where the first token of a node starts. The difference between .pos
and .getStart()
is caused by trivia (whitespace and comments).
Also note that node.pos === node.getFullStart()
and node.end === node.getEnd()
Re: passing sourceFile
parameter:
I wrote this down in https://palantir.github.io/tslint/develop/custom-rules/performance-tips.html
There are serveral methods that have an optional parameter sourceFile. Don’t omit this parameter if you care for performance. If ommitted, typescript needs to walk up the node’s parent chain until it reaches the SourceFile. This can be quite costly when done frequently on deeply nested nodes.
You also find there why I prefer not to call .getStart()
:
node.getStart() scans the source to skip all the leading trivia. Although barely noticeable, this operation is not for free. If you need the start position of a node more than once per function, consider caching it.
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.
Got it, makes sense. Thanks for the tips!
src/rules/banParensRule.ts
Outdated
isExpressionStatement(node.parent!)) || | ||
// Don't flag parentheses in an arrow function's body | ||
(isArrowFunction(node.parent!) && | ||
(node.parent as ts.ArrowFunction).body === node)); |
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.
you can remove the second part of the condition. A ParenthesizedExpression
can only occur in the body of an ArrowFunction
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.
Done.
src/rules/banParensRule.ts
Outdated
// Don't flag parentheses in an arrow function's body | ||
(isArrowFunction(node.parent!) && | ||
(node.parent as ts.ArrowFunction).body === node)); | ||
} |
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.
We should stop adding more exceptions or it will get out of hand. Users should not ban parens around certain Expressions, for example BinaryExpression and ConditionalExpression. (maybe add this to the docs?)
Otherwise you would need to allow BinaryExpression
and ConditionalExpression
when it is (New|Call|PropertyAccess|ElementAccess|Void|Await|Delete)Expression.expression
, e.g. (foo.bar || foo.baz)()
, (foo ? bar : baz)()
, ...
... and probably many more
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.
Added documentation.
I agree it's getting complex. If you think there are any default syntax kinds that aren't worth the exceptions, let me know. It's running cleanly over our codebase now with defaults, so I am at least somewhat confident that we've found most of the strange cases.
Anything else you need from me for this? |
@calebegg nope, I just want @adidahiya to sign this off before landing, but it seems like he's busy lately. |
// No issues with | ||
let z = (x) => x + 1; | ||
let x = (1 + 1) * 2; | ||
(f()) + 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.
can you also add some tests for common JSX syntax?
const foo = (
<div>text</div>
);
<button ref={(el => this.buttonEl = el)} />
~ ~
<button ref={el => (this.buttonEl = el)} />
~ ~
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.
Added two tests. I don't really know what I'm doing with JSX :-) so let me know if they could be improved. (The 2nd and 3rd you gave aren't errors with my current test settings)
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.
Ok, the 3rd example I gave should not be an error, but I think the 2nd one should so that we can align more closely with prettier
. Would you be able to update your rule implementation so that my 2nd snippet does cause a lint failure?
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.
Done.
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.
sorry for the delay. I have some minor comments, see above
test/rules/ban-parens/test.tsx.lint
Outdated
~ | ||
<div>text</div> | ||
~~~~~~~~~~~~~~~~~~~ | ||
); |
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.
these should not be banned by default. parens around multiline jsx statements are enforced in the default tslint-react config.
this is why I wanted you to add these tests -- the default config should not conflict with tslint-react.
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 see -- I wish you'd just said that.
I don't know if it'll be easy to do this, if jsx expressions can occur anywhere. That eliminates most of the rules. I can just exclude jsx files entirely from this check, maybe?
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.
Sorry if I wasn't clear upfront. Excluding .tsx
files completely in this rule is not a good option because it will feel pretty inconsistent for TSX users -- there's lots of regular TS code in .tsx
files. I haven't really dug into the internals of this rule implementation here (deferring to @ajafff) but I am concerned about the overall UX of this rule for tslint-react users, so I'd like you to update it before merging.
@calebegg I would avoid any hard coded special handling for JsxElements or .jsx files. Consider this code snippet from the tests: const foo = (
<div>text</div>
); The blacklist disallows parens in @adidahiya I'd rather not distinguish between single-line and multi-line JsxElements. In the near future TypeScript will add support for |
Makes sense. Done. (I can easily add |
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.
Sorry for the delay. This should be good to go once my 2 comments are addressed
src/rules/banParensRule.ts
Outdated
return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void { | ||
if ((isParenthesizedExpression(node) && !parensAreNecessary(node, ctx.sourceFile)) || isParenthesizedType(node)) { | ||
const restriction = restrictions.find((r) => r.test(node)); | ||
if (restriction != undefined) { |
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.
consider moving the check for exceptions
to this condition. that avoid checking exceptions for every blacklist entry.
also: prefer !==
over !=
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.
Oh, that makes much more sense!
I actually moved it to above restrictions.find so that it can just skip the testing entirely for exception nodes.
...ctx.options.asChildOf, | ||
] : ctx.options.asChildOf; | ||
const exceptions = ctx.options.default ? [ | ||
"JsxElement", |
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 think it's best to also whitelist JsxSelfClosingElement
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.
Done
You need to merge latest master to fix the CI failures |
* Add JsxSelfClosingElement to default exceptions * Refactor exception logic to one place.
@adidahiya PTAL @calebegg merging master caused some new lint failures:
|
Ok, I have some more feedback but I apologize if this is poorly timed. I haven't been able to keep up with all the external PRs submitted to this repo recently. I don't think this rule in its current form belongs in the core rule set. The configuration options are esoteric and unapproachable for most users. This is mainly because they expose internals of the AST semantics to the end user -- I'm drawing a line there when it comes to developer experience. I think we need to limit the scope of possible configuration because otherwise users will get confused by the error messages it produces and will have a hard time figuring out how to configure the rule's options to make their linter pass. Alternatively, it would be fine to keep this in a separate ruleset package for power users of TSLint. |
Closing. It would have been great to get this feedback earlier on in the process. |
Since the definition of “unnecessary” is by its nature very opinionated, I made this rule very open-ended, with a nice set of default “unnecessary” parens that I hope should be relatively uncontroversial.
Let me know if there are defaults you think I should either add or remove.