Skip to content

feat: add fixer to require-store-callbacks-use-set-param #1165

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

Merged
merged 5 commits into from
Mar 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clear-mugs-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-svelte': minor
---

Adds a suggestion to the `require-store-callbacks-use-set-param` rule to automatically rename or add function parameters.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
| [svelte/no-shorthand-style-property-overrides](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-shorthand-style-property-overrides/) | disallow shorthand style properties that override related longhand properties | :star: |
| [svelte/no-store-async](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-store-async/) | disallow using async/await inside svelte stores because it causes issues with the auto-unsubscribing features | :star: |
| [svelte/no-unknown-style-directive-property](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unknown-style-directive-property/) | disallow unknown `style:property` | :star: |
| [svelte/require-store-callbacks-use-set-param](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-store-callbacks-use-set-param/) | store callbacks must use `set` param | |
| [svelte/require-store-callbacks-use-set-param](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-store-callbacks-use-set-param/) | store callbacks must use `set` param | :bulb: |
| [svelte/require-store-reactive-access](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-store-reactive-access/) | disallow to use of the store itself as an operand. Need to use $ prefix or get function. | :star::wrench: |
| [svelte/valid-compile](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-compile/) | disallow warnings when compiling. | |
| [svelte/valid-style-parse](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-style-parse/) | require valid style element parsing | |
Expand Down
2 changes: 1 addition & 1 deletion docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
| [svelte/no-shorthand-style-property-overrides](./rules/no-shorthand-style-property-overrides.md) | disallow shorthand style properties that override related longhand properties | :star: |
| [svelte/no-store-async](./rules/no-store-async.md) | disallow using async/await inside svelte stores because it causes issues with the auto-unsubscribing features | :star: |
| [svelte/no-unknown-style-directive-property](./rules/no-unknown-style-directive-property.md) | disallow unknown `style:property` | :star: |
| [svelte/require-store-callbacks-use-set-param](./rules/require-store-callbacks-use-set-param.md) | store callbacks must use `set` param | |
| [svelte/require-store-callbacks-use-set-param](./rules/require-store-callbacks-use-set-param.md) | store callbacks must use `set` param | :bulb: |
| [svelte/require-store-reactive-access](./rules/require-store-reactive-access.md) | disallow to use of the store itself as an operand. Need to use $ prefix or get function. | :star::wrench: |
| [svelte/valid-compile](./rules/valid-compile.md) | disallow warnings when compiling. | |
| [svelte/valid-style-parse](./rules/valid-style-parse.md) | require valid style element parsing | |
Expand Down
2 changes: 2 additions & 0 deletions docs/rules/require-store-callbacks-use-set-param.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ since: 'v2.12.0'

> store callbacks must use `set` param

- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).

## :book: Rule Details

This rule disallows if `readable` / `writable` store's setter function doesn't use `set` parameter.<br>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,7 @@ import type { Variable } from '@typescript-eslint/scope-manager';
import { createRule } from '../utils/index.js';
import type { RuleContext, RuleFixer } from '../types.js';
import { extractStoreReferences } from './reference-helpers/svelte-store.js';
import { getScope } from '../utils/ast-utils.js';

function findVariableForName(
context: RuleContext,
node: TSESTree.Node,
name: string,
expectedName: string
): { hasConflict: boolean; variable: Variable | null } {
const scope = getScope(context, node);
let variable: Variable | null = null;

for (const ref of scope.references) {
if (ref.identifier.name === expectedName) {
return { hasConflict: true, variable: null };
}
}

for (const v of scope.variables) {
if (v.name === expectedName) {
return { hasConflict: true, variable: null };
}
if (v.name === name) {
variable = v;
}
}

return { hasConflict: false, variable };
}
import { findVariableForReplacement } from '../utils/ast-utils.js';

function createFixer(node: TSESTree.Node, variable: Variable | null, name: string) {
return function* fix(fixer: RuleFixer) {
Expand Down Expand Up @@ -92,7 +65,7 @@ export default createRule('derived-has-same-inputs-outputs', {
if (fnParam.type !== 'Identifier') return;
const expectedName = `$${args.name}`;
if (expectedName !== fnParam.name) {
const { hasConflict, variable } = findVariableForName(
const { hasConflict, variable } = findVariableForReplacement(
context,
fn.body,
fnParam.name,
Expand Down Expand Up @@ -136,7 +109,7 @@ export default createRule('derived-has-same-inputs-outputs', {
if (element && element.type === 'Identifier' && argName) {
const expectedName = `$${argName}`;
if (expectedName !== element.name) {
const { hasConflict, variable } = findVariableForName(
const { hasConflict, variable } = findVariableForReplacement(
context,
fn.body,
element.name,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { findVariableForReplacement } from '../utils/ast-utils.js';
import type { SuggestionReportDescriptor } from '../types.js';
import { createRule } from '../utils/index.js';
import { extractStoreReferences } from './reference-helpers/svelte-store.js';
import { getSourceCode } from '../utils/compat.js';

export default createRule('require-store-callbacks-use-set-param', {
meta: {
Expand All @@ -8,9 +11,12 @@ export default createRule('require-store-callbacks-use-set-param', {
category: 'Possible Errors',
recommended: false
},
hasSuggestions: true,
schema: [],
messages: {
unexpected: 'Store callbacks must use `set` param.'
unexpected: 'Store callbacks must use `set` param.',
updateParam: 'Rename parameter from {{oldName}} to `set`.',
addParam: 'Add a `set` parameter.'
},
type: 'suggestion'
},
Expand All @@ -24,10 +30,48 @@ export default createRule('require-store-callbacks-use-set-param', {
}
const param = fn.params[0];
if (!param || (param.type === 'Identifier' && param.name !== 'set')) {
const { hasConflict, variable } = findVariableForReplacement(
context,
fn.body,
param ? param.name : null,
'set'
);
const suggest: SuggestionReportDescriptor[] = [];
if (!hasConflict) {
if (param) {
suggest.push({
messageId: 'updateParam',
data: { oldName: param.name },
*fix(fixer) {
yield fixer.replaceText(param, 'set');

if (variable) {
for (const ref of variable.references) {
yield fixer.replaceText(ref.identifier, 'set');
}
}
}
});
} else {
const token = getSourceCode(context).getTokenBefore(fn.body, {
filter: (token) => token.type === 'Punctuator' && token.value === '(',
includeComments: false
});
if (token) {
suggest.push({
messageId: 'addParam',
fix(fixer) {
return fixer.insertTextAfter(token, 'set');
}
});
}
}
}
context.report({
node: fn,
loc: fn.loc,
messageId: 'unexpected'
messageId: 'unexpected',
suggest
});
}
}
Expand Down
34 changes: 34 additions & 0 deletions packages/eslint-plugin-svelte/src/utils/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,3 +685,37 @@ function getSimpleNameFromNode(

return getSourceCode(context).getText(node);
}

/**
* Finds the variable for a given name in the specified node's scope.
* Also determines if the replacement name is already in use.
*
* If the `name` is set to null, this assumes you're adding a new variable
* and reports if it is already in use.
*/
export function findVariableForReplacement(
context: RuleContext,
node: TSESTree.Node,
name: string | null,
replacementName: string
): { hasConflict: boolean; variable: Variable | null } {
const scope = getScope(context, node);
let variable: Variable | null = null;

for (const ref of scope.references) {
if (ref.identifier.name === replacementName) {
return { hasConflict: true, variable: null };
}
}

for (const v of scope.variables) {
if (v.name === replacementName) {
return { hasConflict: true, variable: null };
}
if (v.name === name) {
variable = v;
}
}

return { hasConflict: false, variable };
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,198 @@
- message: Store callbacks must use `set` param.
line: 4
column: 18
suggestions: null
suggestions:
- desc: Add a `set` parameter.
messageId: addParam
output: |
<script>
import { readable, writable } from 'svelte/store';

readable(false, (set) => true);
readable(false, (foo) => true);

writable(false, () => true);
writable(false, (foo) => true);

readable(false, (foo) => {
foo;
insideACallback(() => {
foo;
});
conflictingName(() => {
const foo = 303;
foo;
});
});

readable(false, () => {
const set = 303;
});

insideACallback(() => {
const set = 303;
readable(false, () => {
set;
});
});
</script>
- message: Store callbacks must use `set` param.
line: 5
column: 18
suggestions: null
suggestions:
- desc: Rename parameter from foo to `set`.
messageId: updateParam
output: |
<script>
import { readable, writable } from 'svelte/store';

readable(false, () => true);
readable(false, (set) => true);

writable(false, () => true);
writable(false, (foo) => true);

readable(false, (foo) => {
foo;
insideACallback(() => {
foo;
});
conflictingName(() => {
const foo = 303;
foo;
});
});

readable(false, () => {
const set = 303;
});

insideACallback(() => {
const set = 303;
readable(false, () => {
set;
});
});
</script>
- message: Store callbacks must use `set` param.
line: 7
column: 18
suggestions: null
suggestions:
- desc: Add a `set` parameter.
messageId: addParam
output: |
<script>
import { readable, writable } from 'svelte/store';

readable(false, () => true);
readable(false, (foo) => true);

writable(false, (set) => true);
writable(false, (foo) => true);

readable(false, (foo) => {
foo;
insideACallback(() => {
foo;
});
conflictingName(() => {
const foo = 303;
foo;
});
});

readable(false, () => {
const set = 303;
});

insideACallback(() => {
const set = 303;
readable(false, () => {
set;
});
});
</script>
- message: Store callbacks must use `set` param.
line: 8
column: 18
suggestions:
- desc: Rename parameter from foo to `set`.
messageId: updateParam
output: |
<script>
import { readable, writable } from 'svelte/store';

readable(false, () => true);
readable(false, (foo) => true);

writable(false, () => true);
writable(false, (set) => true);

readable(false, (foo) => {
foo;
insideACallback(() => {
foo;
});
conflictingName(() => {
const foo = 303;
foo;
});
});

readable(false, () => {
const set = 303;
});

insideACallback(() => {
const set = 303;
readable(false, () => {
set;
});
});
</script>
- message: Store callbacks must use `set` param.
line: 10
column: 18
suggestions:
- desc: Rename parameter from foo to `set`.
messageId: updateParam
output: |
<script>
import { readable, writable } from 'svelte/store';

readable(false, () => true);
readable(false, (foo) => true);

writable(false, () => true);
writable(false, (foo) => true);

readable(false, (set) => {
set;
insideACallback(() => {
set;
});
conflictingName(() => {
const foo = 303;
foo;
});
});

readable(false, () => {
const set = 303;
});

insideACallback(() => {
const set = 303;
readable(false, () => {
set;
});
});
</script>
- message: Store callbacks must use `set` param.
line: 21
column: 18
suggestions: null
- message: Store callbacks must use `set` param.
line: 27
column: 19
suggestions: null
Loading
Loading