-
-
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 preferProtectedState rule (#4488)
Closes #4474 Co-authored-by: Rainer Hahnekamp <[email protected]>
- Loading branch information
1 parent
8c499cf
commit 32c772d
Showing
9 changed files
with
216 additions
and
5 deletions.
There are no files selected for viewing
82 changes: 82 additions & 0 deletions
82
modules/eslint-plugin/spec/rules/signals/prefer-protected-state.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,82 @@ | ||
import type { ESLintUtils, TSESLint } from '@typescript-eslint/utils'; | ||
import * as path from 'path'; | ||
import rule, { | ||
preferProtectedState, | ||
preferProtectedStateSuggest, | ||
} from '../../../src/rules/signals/prefer-protected-state'; | ||
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 mySignalStore = signalStore();`, | ||
`const mySignalStore = signalStore({ protectedState: true });`, | ||
`const mySignalStore = signalStore({ providedIn: 'root' });`, | ||
`const mySignalStore = signalStore({ providedIn: 'root', protectedState: true });`, | ||
]; | ||
|
||
const invalid: () => RunTests['invalid'] = () => [ | ||
fromFixture( | ||
` | ||
const mySignalStore = signalStore({ providedIn: 'root', protectedState: false, }); | ||
~~~~~~~~~~~~~~~~~~~~~ [${preferProtectedState} suggest]`, | ||
{ | ||
suggestions: [ | ||
{ | ||
messageId: preferProtectedStateSuggest, | ||
output: ` | ||
const mySignalStore = signalStore({ providedIn: 'root', });`, | ||
}, | ||
], | ||
} | ||
), | ||
fromFixture( | ||
` | ||
const mySignalStore = signalStore({ providedIn: 'root', protectedState: false , }); | ||
~~~~~~~~~~~~~~~~~~~~~ [${preferProtectedState} suggest]`, | ||
{ | ||
suggestions: [ | ||
{ | ||
messageId: preferProtectedStateSuggest, | ||
output: ` | ||
const mySignalStore = signalStore({ providedIn: 'root', });`, | ||
}, | ||
], | ||
} | ||
), | ||
fromFixture( | ||
` | ||
const mySignalStore = signalStore({ protectedState: false, }); | ||
~~~~~~~~~~~~~~~~~~~~~ [${preferProtectedState} suggest]`, | ||
{ | ||
suggestions: [ | ||
{ | ||
messageId: preferProtectedStateSuggest, | ||
output: ` | ||
const mySignalStore = signalStore();`, | ||
}, | ||
], | ||
} | ||
), | ||
fromFixture( | ||
` | ||
const mySignalStore = signalStore({ protectedState: false, providedIn: 'root' }); | ||
~~~~~~~~~~~~~~~~~~~~~ [${preferProtectedState} suggest]`, | ||
{ | ||
suggestions: [ | ||
{ | ||
messageId: preferProtectedStateSuggest, | ||
output: ` | ||
const mySignalStore = signalStore({ providedIn: 'root' });`, | ||
}, | ||
], | ||
} | ||
), | ||
]; | ||
|
||
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
78 changes: 78 additions & 0 deletions
78
modules/eslint-plugin/src/rules/signals/prefer-protected-state.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,78 @@ | ||
import { type TSESTree } from '@typescript-eslint/utils'; | ||
import * as path from 'path'; | ||
import { createRule } from '../../rule-creator'; | ||
|
||
export const preferProtectedState = 'preferProtectedState'; | ||
export const preferProtectedStateSuggest = 'preferProtectedStateSuggest'; | ||
|
||
type MessageIds = | ||
| typeof preferProtectedState | ||
| typeof preferProtectedStateSuggest; | ||
type Options = readonly []; | ||
|
||
export default createRule<Options, MessageIds>({ | ||
name: path.parse(__filename).name, | ||
meta: { | ||
type: 'suggestion', | ||
hasSuggestions: true, | ||
ngrxModule: 'signals', | ||
docs: { | ||
description: `A Signal Store prefers protected state`, | ||
}, | ||
schema: [], | ||
messages: { | ||
[preferProtectedState]: | ||
'{ protectedState: false } should be removed to prevent external state mutations.', | ||
[preferProtectedStateSuggest]: 'Remove `{protectedState: false}`.', | ||
}, | ||
}, | ||
defaultOptions: [], | ||
create: (context) => { | ||
return { | ||
[`CallExpression[callee.name=signalStore][arguments.length>0] > ObjectExpression[properties.length>0] > Property[key.name=protectedState][value.value=false]`]( | ||
node: TSESTree.Property | ||
) { | ||
context.report({ | ||
node, | ||
messageId: preferProtectedState, | ||
suggest: [ | ||
{ | ||
messageId: preferProtectedStateSuggest, | ||
fix: (fixer) => { | ||
const getRangeToBeRemoved = (): Parameters< | ||
typeof fixer.removeRange | ||
>[0] => { | ||
const parentObject = node.parent as TSESTree.ObjectExpression; | ||
const parentObjectHasOnlyOneProperty = | ||
parentObject.properties.length === 1; | ||
|
||
if (parentObjectHasOnlyOneProperty) { | ||
/** | ||
* Remove the entire object if it contains only one property - the relevant one | ||
*/ | ||
return parentObject.range; | ||
} | ||
|
||
const tokenAfter = context.sourceCode.getTokenAfter(node); | ||
const tokenAfterIsComma = tokenAfter?.value?.trim() === ','; | ||
/** | ||
* Remove the specific property if there is more than one property in the parent | ||
*/ | ||
return [ | ||
node.range[0], | ||
/** | ||
* remove trailing comma as well | ||
*/ | ||
tokenAfterIsComma ? tokenAfter.range[1] : node.range[1], | ||
]; | ||
}; | ||
|
||
return fixer.removeRange(getRangeToBeRemoved()); | ||
}, | ||
}, | ||
], | ||
}); | ||
}, | ||
}; | ||
}, | ||
}); |
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
44 changes: 44 additions & 0 deletions
44
projects/ngrx.io/content/guide/eslint-plugin/rules/prefer-protected-state.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,44 @@ | ||
# prefer-protected-state | ||
|
||
A Signal Store prefers protected state. | ||
|
||
- **Type**: suggestion | ||
- **Fixable**: No | ||
- **Suggestion**: Yes | ||
- **Requires type checking**: No | ||
- **Configurable**: No | ||
|
||
<!-- Everything above this generated, do not edit --> | ||
<!-- MANUAL-DOC:START --> | ||
|
||
## Rule Details | ||
|
||
This rule ensures that state changes are only managed by the Signal Store to prevent unintended modifications and provide clear ownership of where changes occur. | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```ts | ||
// SUGGESTION ❗ | ||
const Store = signalStore( | ||
{ protectedState: false }, | ||
~~~~~~~~~~~~~~~~~~~~~ [warning] | ||
withState({}), | ||
); | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```ts | ||
// GOOD ✅ | ||
const Store = signalStore( | ||
withState({}), | ||
); | ||
``` | ||
|
||
```ts | ||
// GOOD ✅ | ||
const Store = signalStore( | ||
{ protectedState: true }, | ||
withState({}), | ||
); | ||
``` |