Skip to content

Commit

Permalink
Merge branch 'custom-reactive-function-option'
Browse files Browse the repository at this point in the history
  • Loading branch information
joshwilsonvu committed Dec 30, 2023
2 parents 9f3a192 + 8846509 commit 87e2f9b
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 7 deletions.
19 changes: 18 additions & 1 deletion docs/reactivity.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,19 @@ return <Component staticName={props.name} />
</details>
<!-- AUTO-GENERATED-CONTENT:START (OPTIONS) -->
## Rule Options
Options shown here are the defaults. Manually configuring an array will *replace* the defaults.
```js
{
"solid/reactivity": ["warn", {
// List of function names to consider as reactive functions (allow signals to be safely passed as arguments). In addition, any create* or use* functions are automatically included.
customReactiveFunctions: [], // Array<string>
}]
}
```
<!-- AUTO-GENERATED-CONTENT:END -->
<!-- AUTO-GENERATED-CONTENT:START (CASES) -->
Expand Down Expand Up @@ -607,6 +619,11 @@ X.createFoo(() => bar());
const [bar, setBar] = createSignal();
X.Y.createFoo(() => bar());

/* eslint solid/reactivity: ["error", { "customReactiveFunctions": ["customQuery"] }] */
function customQuery(v) {}
const [signal, setSignal] = createSignal();
customQuery(() => signal());

const [signal, setSignal] = createSignal(1);
const element = document.getElementById("id");
element.addEventListener(
Expand Down
50 changes: 44 additions & 6 deletions src/rules/reactivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,42 @@ const getReturnedVar = (id: T.Node, scope: Scope): Variable | null => {
return null;
};

export default createRule({
type MessageIds =
| "noWrite"
| "untrackedReactive"
| "expectedFunctionGotExpression"
| "badSignal"
| "badUnnamedDerivedSignal"
| "shouldDestructure"
| "shouldAssign"
| "noAsyncTrackedScope";
type Options = [{ customReactiveFunctions: string[] }];

export default createRule<Options, MessageIds>({
meta: {
type: "problem",
docs: {
description:
"Enforce that reactivity (props, signals, memos, etc.) is properly used, so changes in those values will be tracked and update the view as expected.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/reactivity.md",
},
schema: [],
schema: [
{
type: "object",
properties: {
customReactiveFunctions: {
description:
"List of function names to consider as reactive functions (allow signals to be safely passed as arguments). In addition, any create* or use* functions are automatically included.",
type: "array",
items: {
type: "string",
},
default: [],
},
},
additionalProperties: false,
},
],
messages: {
noWrite: "The reactive variable '{{name}}' should not be reassigned or altered directly.",
untrackedReactive:
Expand All @@ -243,8 +270,12 @@ export default createRule({
"This tracked scope should not be async. Solid's reactivity only tracks synchronously.",
},
},
defaultOptions: [] as const,
create(context) {
defaultOptions: [
{
customReactiveFunctions: [],
},
],
create(context, [options]) {
const warnShouldDestructure = (node: T.Node, nth?: string) =>
context.report({
node,
Expand Down Expand Up @@ -994,7 +1025,10 @@ export default createRule({
pushTrackedScope(arg1, "function");
}
}
} else if (/^(?:use|create)[A-Z]/.test(callee.name)) {
} else if (
/^(?:use|create)[A-Z]/.test(callee.name) ||
options.customReactiveFunctions.includes(callee.name)
) {
// Custom hooks parameters may or may not be tracking scopes, no way to know.
// Assume all identifier/function arguments are tracked scopes, and use "called-function"
// to allow async handlers (permissive). Assume non-resolvable args are reactive expressions.
Expand All @@ -1019,7 +1053,11 @@ export default createRule({
) {
// Like `on*` event handlers, mark all `addEventListener` listeners as called functions.
pushTrackedScope(node.arguments[1], "called-function");
} else if (property.type === "Identifier" && /^(?:use|create)[A-Z]/.test(property.name)) {
} else if (
property.type === "Identifier" &&
(/^(?:use|create)[A-Z]/.test(property.name) ||
options.customReactiveFunctions.includes(property.name))
) {
// Handle custom hook parameters for property access custom hooks
for (const arg of node.arguments) {
if (isFunctionNode(arg)) {
Expand Down
6 changes: 6 additions & 0 deletions test/rules/reactivity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ export const cases = run("reactivity", rule, {
X.createFoo(() => bar());`,
`const [bar, setBar] = createSignal();
X . Y\n. createFoo(() => bar());`,
{
code: `function customQuery(v) {}
const [signal, setSignal] = createSignal();
customQuery(() => signal());`,
options: [{ customReactiveFunctions: ["customQuery"] }], // only needed when not create*/use*
},
// Event listeners
`const [signal, setSignal] = createSignal(1);
const element = document.getElementById("id");
Expand Down

0 comments on commit 87e2f9b

Please sign in to comment.