-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat(eslint-plugin): [no-misused-spread] add new rule no-misused-spread
#7822
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
feb9556
cc72332
bcf64c3
f0b5507
bd83499
28eaaaa
4377ed4
a8c6469
1f23aa7
b994177
33ef9f3
34a9dbc
d84cf04
4aed59b
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 |
---|---|---|
@@ -0,0 +1,45 @@ | ||
--- | ||
description: "Disallow spread operator that shouldn't be spread most of the time." | ||
--- | ||
|
||
> 🛑 This file is source code, not the primary documentation location! 🛑 | ||
> | ||
> See **https://typescript-eslint.io/rules/no-misused-spread** for documentation. | ||
|
||
Spreading a function is almost always a mistake. Most of the time you forgot to call the function. | ||
|
||
## Examples | ||
|
||
<!--tabs--> | ||
|
||
### ❌ Incorrect | ||
|
||
```ts | ||
const fn = () => ({ name: 'name' }); | ||
const obj = { | ||
...fn, | ||
value: 1, | ||
}; | ||
``` | ||
|
||
```ts | ||
const fn = () => ({ value: 33 }); | ||
const otherFn = ({ value: number }) => ({ value: value }); | ||
otherFn({ ...fn }); | ||
``` | ||
|
||
### ✅ Correct | ||
|
||
```ts | ||
const fn = () => ({ name: 'name' }); | ||
const obj = { | ||
...fn(), | ||
value: 1, | ||
}; | ||
``` | ||
|
||
```ts | ||
const fn = () => ({ value: 33 }); | ||
const otherFn = ({ value: number }) => ({ value: value }); | ||
otherFn({ ...fn() }); | ||
``` |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,46 @@ | ||||||||||||
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils'; | ||||||||||||
|
||||||||||||
import { createRule, getParserServices } from '../util'; | ||||||||||||
|
||||||||||||
type MessageIds = 'forbiddenFunctionSpread'; | ||||||||||||
|
||||||||||||
export default createRule<[], MessageIds>({ | ||||||||||||
defaultOptions: [], | ||||||||||||
name: 'no-misused-spread', | ||||||||||||
meta: { | ||||||||||||
docs: { | ||||||||||||
description: | ||||||||||||
"Disallow spread operator that shouldn't be spread most of the time", | ||||||||||||
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. [Proofreading] "most of the time" is kind of an imprecise, vague statement. Users may not understand it. Instead, let's speak to what this rule is specifically trying to avoid: spreads that don't do anything.
Suggested change
|
||||||||||||
recommended: 'stylistic', | ||||||||||||
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. hmm 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. What will be your recommendation? 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. TBH I'm not sure what the guidelines are, let's wait for a maintainer 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. https://typescript-eslint.io/linting/configs - this is a functional, bug-catching rule so it wouldn't be |
||||||||||||
requiresTypeChecking: true, | ||||||||||||
}, | ||||||||||||
messages: { | ||||||||||||
forbiddenFunctionSpread: | ||||||||||||
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. [Feature] If we add the rule as-is (only looking at functions), it'd be a breaking change to later add in Maps, Sets, etc. I think the rule should include all of what's requested in the issue to start. LMK if any of them give you grief, happy to help! |
||||||||||||
'Spreading a function is almost always a mistake. Did you forget to call the function?', | ||||||||||||
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. [Proofreading] Maybe...
Suggested change
|
||||||||||||
}, | ||||||||||||
schema: [], | ||||||||||||
type: 'suggestion', | ||||||||||||
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. I think this is a "problem" rather than a "suggestion", WDYT? 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. IMO this is most of the time a problem, but untill today there is nothing warning the user that it is one. 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. Not sure... 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. It's a |
||||||||||||
}, | ||||||||||||
create: (context): TSESLint.RuleListener => { | ||||||||||||
const listener = (node: TSESTree.SpreadElement): void => { | ||||||||||||
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. [Style] Is there a reason you'd wanted to split this out instead of having it inline at the bottom of the function? Inline would mean an inferrable type for return {
SpreadElement(node): void {
// ...
}
}; |
||||||||||||
const services = getParserServices(context); | ||||||||||||
const checker = services.program.getTypeChecker(); | ||||||||||||
|
||||||||||||
const tsNode = services.esTreeNodeToTSNodeMap.get(node.argument); | ||||||||||||
const type = checker.getTypeAtLocation(tsNode); | ||||||||||||
Comment on lines
+27
to
+30
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. [Cleanup] We have a new thing for this 😄 https://typescript-eslint.io/blog/announcing-typescript-eslint-v6#type-checker-wrapper-apis
Suggested change
|
||||||||||||
|
||||||||||||
if ( | ||||||||||||
type.getProperties().length === 0 && | ||||||||||||
type.getCallSignatures().length > 0 | ||||||||||||
) { | ||||||||||||
context.report({ | ||||||||||||
node, | ||||||||||||
messageId: 'forbiddenFunctionSpread', | ||||||||||||
}); | ||||||||||||
} | ||||||||||||
}; | ||||||||||||
return { | ||||||||||||
SpreadElement: listener, | ||||||||||||
}; | ||||||||||||
}, | ||||||||||||
}); |
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. maybe also add tests for spreading EDIT: hmm, should we include a custom iterable as well? might be overkill, IDK 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. Do you mean in the valid part? 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. yup 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. +1 on adding a test for a custom iterable :) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { RuleTester } from '@typescript-eslint/rule-tester'; | ||
|
||
import rule from '../../src/rules/no-misused-spread'; | ||
import { getFixturesRootDir } from '../RuleTester'; | ||
|
||
const rootDir = getFixturesRootDir(); | ||
const ruleTester = new RuleTester({ | ||
parser: '@typescript-eslint/parser', | ||
parserOptions: { | ||
tsconfigRootDir: rootDir, | ||
project: './tsconfig.json', | ||
}, | ||
}); | ||
|
||
ruleTester.run('no-misused-spread', rule, { | ||
valid: [ | ||
` | ||
const a = () => ({ value: 33 }); | ||
const b = { | ||
...a(), | ||
name: 'name', | ||
}; | ||
`, | ||
` | ||
const a = (x: number) => ({ value: x }); | ||
const b = { | ||
...a(42), | ||
name: 'name', | ||
}; | ||
`, | ||
` | ||
interface FuncWithProps { | ||
property?: string; | ||
(): number; | ||
} | ||
const funcWithProps: FuncWithProps = () => 1; | ||
funcWithProps.property = 'foo'; | ||
const spreadFuncWithProps = { ...funcWithProps }; | ||
`, | ||
` | ||
type FuncWithProps = { | ||
property?: string; | ||
(): number; | ||
}; | ||
const funcWithProps: FuncWithProps = () => 1; | ||
funcWithProps.property = 'foo'; | ||
const spreadFuncWithProps = { ...funcWithProps }; | ||
`, | ||
` | ||
const a = { value: 33 }; | ||
const b = { | ||
...a, | ||
name: 'name', | ||
}; | ||
`, | ||
` | ||
const a = {}; | ||
const b = { | ||
...a, | ||
name: 'name', | ||
}; | ||
`, | ||
` | ||
const a = () => ({ value: 33 }); | ||
const b = ({ value: number }) => ({ value: value }); | ||
b({ ...a() }); | ||
`, | ||
` | ||
const a = [33]; | ||
const b = { | ||
...a, | ||
name: 'name', | ||
}; | ||
`, | ||
], | ||
invalid: [ | ||
{ | ||
code: ` | ||
const a = () => ({ value: 33 }); | ||
const b = { | ||
...a, | ||
name: 'name', | ||
}; | ||
`, | ||
errors: [{ line: 4, column: 3, messageId: 'forbiddenFunctionSpread' }], | ||
}, | ||
{ | ||
code: ` | ||
const a = (x: number) => ({ value: x }); | ||
const b = { | ||
...a, | ||
name: 'name', | ||
}; | ||
`, | ||
errors: [{ line: 4, column: 3, messageId: 'forbiddenFunctionSpread' }], | ||
}, | ||
{ | ||
code: ` | ||
const a = () => ({ value: 33 }); | ||
const b = ({ value: number }) => ({ value: value }); | ||
b({ ...a }); | ||
`, | ||
errors: [{ line: 4, column: 5, messageId: 'forbiddenFunctionSpread' }], | ||
}, | ||
], | ||
}); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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.
[Nit]
This line should probably need to be one line above