diff --git a/docs/reactivity.md b/docs/reactivity.md index 07c6296..81be87c 100644 --- a/docs/reactivity.md +++ b/docs/reactivity.md @@ -166,7 +166,19 @@ return - +## 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 + }] +} +``` + @@ -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( diff --git a/src/rules/reactivity.ts b/src/rules/reactivity.ts index 4d7c551..911d147 100644 --- a/src/rules/reactivity.ts +++ b/src/rules/reactivity.ts @@ -216,7 +216,18 @@ 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({ meta: { type: "problem", docs: { @@ -224,7 +235,23 @@ export default createRule({ "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: @@ -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, @@ -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. @@ -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)) { diff --git a/test/rules/reactivity.test.ts b/test/rules/reactivity.test.ts index f315828..b9c74d9 100644 --- a/test/rules/reactivity.test.ts +++ b/test/rules/reactivity.test.ts @@ -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");