-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(eslint-plugin): add signalStoreFeatureShouldUseGenericType rule (#…
- Loading branch information
1 parent
b58ea22
commit e2ab916
Showing
10 changed files
with
243 additions
and
8 deletions.
There are no files selected for viewing
110 changes: 110 additions & 0 deletions
110
...les/eslint-plugin/spec/rules/signals/signal-store-feature-should-use-generic-type.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import type { ESLintUtils, TSESLint } from '@typescript-eslint/utils'; | ||
import * as path from 'path'; | ||
import rule, { | ||
messageId, | ||
} from '../../../src/rules/signals/signal-store-feature-should-use-generic-type'; | ||
import { ruleTester, fromFixture } from '../../utils'; | ||
|
||
type MessageIds = ESLintUtils.InferMessageIdsTypeFromRule<typeof rule>; | ||
type Options = readonly ESLintUtils.InferOptionsTypeFromRule<typeof rule>[]; | ||
type RunTests = TSESLint.RunTests<MessageIds, Options>; | ||
|
||
const valid: () => RunTests['valid'] = () => [ | ||
`const withY = <Y>() => signalStoreFeature({ state: type<{ y: Y }>() }, withState({}));`, | ||
`export const withY = <Y>() => signalStoreFeature(type<{ state: { y: Y } }>(), withState({}));`, | ||
`const withY = <_>() => { return signalStoreFeature({ state: type<{ y: number }>() }, withState({})); }`, | ||
`export const withY = <_>() => { return signalStoreFeature(type<{ state: { y: number } }>(), withState({})); }`, | ||
`function withY<Y>() { return signalStoreFeature({ state: type<{ y: Y }>() }, withState({})); }`, | ||
`export function withY<_>() { return signalStoreFeature(type<{ state: { y: number } }>(), withState({})); }`, | ||
`function withY<_>() { | ||
const feature = signalStoreFeature(type<{ state: { y: number } }>(), withState({})); | ||
return feature; | ||
}`, | ||
]; | ||
|
||
const invalid: () => RunTests['invalid'] = () => [ | ||
fromFixture( | ||
` | ||
const withY = () => signalStoreFeature({ state: type<{ y: number }>() }, withState({})); | ||
~~~~~~~~~~~~~~~~~~ [${messageId}]`, | ||
{ | ||
output: ` | ||
const withY = <_>() => signalStoreFeature({ state: type<{ y: number }>() }, withState({}));`, | ||
} | ||
), | ||
fromFixture( | ||
` | ||
const withY = () => signalStoreFeature(type<{ state: { y: number } }>(), withState({})); | ||
~~~~~~~~~~~~~~~~~~ [${messageId}]`, | ||
{ | ||
output: ` | ||
const withY = <_>() => signalStoreFeature(type<{ state: { y: number } }>(), withState({}));`, | ||
} | ||
), | ||
fromFixture( | ||
` | ||
const withY = () => { return signalStoreFeature({ state: type<{ y: number }>() }, withState({})); } | ||
~~~~~~~~~~~~~~~~~~ [${messageId}]`, | ||
{ | ||
output: ` | ||
const withY = <_>() => { return signalStoreFeature({ state: type<{ y: number }>() }, withState({})); }`, | ||
} | ||
), | ||
fromFixture( | ||
` | ||
const withY = () => { return signalStoreFeature(type<{ state: { y: number } }>(), withState({})); } | ||
~~~~~~~~~~~~~~~~~~ [${messageId}]`, | ||
{ | ||
output: ` | ||
const withY = <_>() => { return signalStoreFeature(type<{ state: { y: number } }>(), withState({})); }`, | ||
} | ||
), | ||
fromFixture( | ||
` | ||
const withY = () => { return signalStoreFeature(type<{ state: { y: number } }>(), withState({})); } | ||
~~~~~~~~~~~~~~~~~~ [${messageId}]`, | ||
{ | ||
output: ` | ||
const withY = <_>() => { return signalStoreFeature(type<{ state: { y: number } }>(), withState({})); }`, | ||
} | ||
), | ||
|
||
fromFixture( | ||
` | ||
function withY() { return signalStoreFeature(type<{ state: { y: number } }>(), withState({})); } | ||
~~~~~~~~~~~~~~~~~~ [${messageId}]`, | ||
{ | ||
output: ` | ||
function withY<_>() { return signalStoreFeature(type<{ state: { y: number } }>(), withState({})); }`, | ||
} | ||
), | ||
fromFixture( | ||
` | ||
function withY() { return signalStoreFeature({ state: type<{ y: number }>() }, withState({})); } | ||
~~~~~~~~~~~~~~~~~~ [${messageId}]`, | ||
{ | ||
output: ` | ||
function withY<_>() { return signalStoreFeature({ state: type<{ y: number }>() }, withState({})); }`, | ||
} | ||
), | ||
fromFixture( | ||
` | ||
function withY() { | ||
const feature = signalStoreFeature({ state: type<{ y: number }>() }, withState({})); | ||
~~~~~~~~~~~~~~~~~~ [${messageId}] | ||
return feature; | ||
}`, | ||
{ | ||
output: ` | ||
function withY<_>() { | ||
const feature = signalStoreFeature({ state: type<{ y: number }>() }, withState({})); | ||
return feature; | ||
}`, | ||
} | ||
), | ||
]; | ||
|
||
ruleTester().run(path.parse(__filename).name, rule, { | ||
valid: valid(), | ||
invalid: invalid(), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
modules/eslint-plugin/src/rules/signals/signal-store-feature-should-use-generic-type.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils'; | ||
import * as path from 'path'; | ||
import { createRule } from '../../rule-creator'; | ||
import { | ||
isArrowFunctionExpression, | ||
isCallExpression, | ||
isFunctionDeclaration, | ||
isIdentifier, | ||
} from '../../utils'; | ||
|
||
export const messageId = 'signalStoreFeatureShouldUseGenericType'; | ||
|
||
type MessageIds = typeof messageId; | ||
type Options = readonly []; | ||
|
||
export default createRule<Options, MessageIds>({ | ||
name: path.parse(__filename).name, | ||
meta: { | ||
type: 'problem', | ||
ngrxModule: 'signals', | ||
docs: { | ||
description: `A custom Signal Store feature that accepts an input should define a generic type.`, | ||
}, | ||
fixable: 'code', | ||
schema: [], | ||
messages: { | ||
[messageId]: `Add an unused generic type to the function creating the signal store feature.`, | ||
}, | ||
}, | ||
defaultOptions: [], | ||
create: (context) => { | ||
function report( | ||
signalStoreFeature: TSESTree.CallExpression, | ||
func?: TSESTree.Node | ||
) { | ||
if ( | ||
!func || | ||
(!isFunctionDeclaration(func) && !isArrowFunctionExpression(func)) | ||
) { | ||
return; | ||
} | ||
const parentHasGenerics = | ||
func.typeParameters && func.typeParameters.params.length > 0; | ||
if (!parentHasGenerics) { | ||
context.report({ | ||
node: signalStoreFeature.callee, | ||
messageId, | ||
fix(fixer) { | ||
if (isFunctionDeclaration(func)) { | ||
if (func.id) { | ||
return fixer.insertTextAfter(func.id, '<_>'); | ||
} | ||
} | ||
|
||
return fixer.insertTextBefore(func, '<_>'); | ||
}, | ||
}); | ||
} | ||
} | ||
|
||
function hasInputAsArgument(node: TSESTree.CallExpression) { | ||
const [inputArg] = node.arguments; | ||
return ( | ||
!isCallExpression(inputArg) || | ||
(isIdentifier(inputArg.callee) && inputArg.callee.name === 'type') | ||
); | ||
} | ||
|
||
return { | ||
[`ArrowFunctionExpression > CallExpression[callee.name=signalStoreFeature]`]( | ||
node: TSESTree.CallExpression | ||
) { | ||
if (hasInputAsArgument(node)) { | ||
report(node, node.parent); | ||
} | ||
}, | ||
[`ArrowFunctionExpression > BlockStatement CallExpression[callee.name=signalStoreFeature]`]( | ||
node: TSESTree.CallExpression | ||
) { | ||
if (hasInputAsArgument(node)) { | ||
let parent: TSESTree.Node | undefined = node.parent; | ||
while (parent && !isArrowFunctionExpression(parent)) { | ||
parent = parent.parent; | ||
} | ||
report(node, parent); | ||
} | ||
}, | ||
[`FunctionDeclaration > BlockStatement CallExpression[callee.name=signalStoreFeature]`]( | ||
node: TSESTree.CallExpression | ||
) { | ||
if (hasInputAsArgument(node)) { | ||
let parent: TSESTree.Node | undefined = node.parent; | ||
while (parent && !isFunctionDeclaration(parent)) { | ||
parent = parent.parent; | ||
} | ||
report(node, parent); | ||
} | ||
}, | ||
}; | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
...ntent/guide/eslint-plugin/rules/signal-store-feature-should-use-generic-type.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# signal-store-feature-should-use-generic-type | ||
|
||
A custom Signal Store feature that accepts an input should define a generic type. | ||
|
||
- **Type**: problem | ||
- **Fixable**: Yes | ||
- **Suggestion**: No | ||
- **Requires type checking**: No | ||
- **Configurable**: No | ||
|
||
<!-- Everything above this generated, do not edit --> | ||
<!-- MANUAL-DOC:START --> |