Skip to content

Commit eae38f2

Browse files
committed
Fire only on imported names in reactivity, handling aliases. Closes #6.
1 parent d6fc94a commit eae38f2

File tree

4 files changed

+76
-27
lines changed

4 files changed

+76
-27
lines changed

src/rules/reactivity.ts

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
isFunctionNode,
1313
ProgramOrFunctionNode,
1414
isProgramOrFunctionNode,
15+
trackImports,
1516
} from "../utils";
1617

1718
const { findVariable, getFunctionHeadLocation } = ASTUtils;
@@ -258,6 +259,9 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
258259
const scopeStack = new ScopeStack();
259260
const { currentScope } = scopeStack;
260261

262+
/** Tracks imports from 'solid-js', handling aliases. */
263+
const { matchImport, handleImportDeclaration } = trackImports();
264+
261265
/** Populates the function stack. */
262266
const onFunctionEnter = (node: ProgramOrFunctionNode) => {
263267
if (isFunctionNode(node) && scopeStack.syncCallbacks.has(node)) {
@@ -512,7 +516,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
512516
) {
513517
if (
514518
node.callee.type === "Identifier" &&
515-
["untrack", "batch", "onCleanup", "onError", "produce"].includes(node.callee.name)
519+
matchImport(["untrack", "batch", "onCleanup", "onError", "produce"], node.callee.name)
516520
) {
517521
// These Solid APIs take callbacks that run in the current scope
518522
scopeStack.syncCallbacks.add(node.arguments[0]);
@@ -530,7 +534,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
530534
}
531535
if (
532536
node.callee.type === "Identifier" &&
533-
["createSignal", "createStore"].includes(node.callee.name) &&
537+
matchImport(["createSignal", "createStore"], node.callee.name) &&
534538
node.parent?.type === "VariableDeclarator"
535539
) {
536540
// Allow using reactive variables in state setter if the current scope is tracked.
@@ -564,37 +568,37 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
564568
// Mark return values of certain functions as reactive
565569
if (init.type === "CallExpression" && init.callee.type === "Identifier") {
566570
const { callee } = init;
567-
if (callee.name === "createSignal" || callee.name === "useTransition") {
571+
if (matchImport(["createSignal", "useTransition"], callee.name)) {
568572
const signal = id && getNthDestructuredVar(id, 0, context.getScope());
569573
if (signal) {
570574
scopeStack.pushSignal(signal, currentScope().node);
571575
} else {
572576
warnShouldDestructure(id ?? init, "first");
573577
}
574-
} else if (callee.name === "createMemo" || callee.name === "createSelector") {
578+
} else if (matchImport(["createMemo", "createSelector"], callee.name)) {
575579
const memo = id && getReturnedVar(id, context.getScope());
576580
// memos act like signals
577581
if (memo) {
578582
scopeStack.pushSignal(memo, currentScope().node);
579583
} else {
580584
warnShouldAssign(id ?? init);
581585
}
582-
} else if (callee.name === "createStore") {
586+
} else if (matchImport("createStore", callee.name)) {
583587
const store = id && getNthDestructuredVar(id, 0, context.getScope());
584588
// stores act like props
585589
if (store) {
586590
scopeStack.pushProps(store, currentScope().node);
587591
} else {
588592
warnShouldDestructure(id ?? init, "first");
589593
}
590-
} else if (callee.name === "mergeProps") {
594+
} else if (matchImport("mergeProps", callee.name)) {
591595
const merged = id && getReturnedVar(id, context.getScope());
592596
if (merged) {
593597
scopeStack.pushProps(merged, currentScope().node);
594598
} else {
595599
warnShouldAssign(id ?? init);
596600
}
597-
} else if (callee.name === "splitProps") {
601+
} else if (matchImport("splitProps", callee.name)) {
598602
// splitProps can return an unbounded array of props variables, though it's most often two
599603
if (id?.type === "ArrayPattern") {
600604
const vars = id.elements
@@ -614,13 +618,13 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
614618
scopeStack.pushProps(vars, currentScope().node);
615619
}
616620
}
617-
} else if (callee.name === "createResource") {
621+
} else if (matchImport("createResource", callee.name)) {
618622
// createResource return value has reactive .loading and .error
619623
const resourceReturn = id && getNthDestructuredVar(id, 0, context.getScope());
620624
if (resourceReturn) {
621625
scopeStack.pushProps(resourceReturn, currentScope().node);
622626
}
623-
} else if (callee.name === "createMutable") {
627+
} else if (matchImport("createMutable", callee.name)) {
624628
const mutable = id && getReturnedVar(id, context.getScope());
625629
if (mutable) {
626630
scopeStack.pushProps(mutable, currentScope().node);
@@ -669,23 +673,26 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
669673
const callee = node.callee;
670674
const arg0 = node.arguments[0];
671675
if (
672-
[
673-
"createMemo",
674-
"children",
675-
"createEffect",
676-
"createRenderEffect",
677-
"createDeferred",
678-
"createComputed",
679-
"createSelector",
680-
].includes(callee.name) ||
681-
(callee.name === "createResource" && node.arguments.length >= 2)
676+
matchImport(
677+
[
678+
"createMemo",
679+
"children",
680+
"createEffect",
681+
"createRenderEffect",
682+
"createDeferred",
683+
"createComputed",
684+
"createSelector",
685+
],
686+
callee.name
687+
) ||
688+
(matchImport("createResource", callee.name) && node.arguments.length >= 2)
682689
) {
683690
// createEffect, createMemo, etc. fn arg, and createResource optional
684691
// `source` first argument may be a signal
685692
pushTrackedScope(arg0, "function");
686693
} else if (
694+
matchImport("onMount", callee.name) ||
687695
[
688-
"onMount",
689696
"setInterval",
690697
"setTimeout",
691698
"setImmediate",
@@ -698,9 +705,9 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
698705
// to updates to reactive variables; it's okay to poll the current
699706
// value. Consider them event-handler tracked scopes for our purposes.
700707
pushTrackedScope(arg0, "called-function");
701-
} else if (callee.name === "createMutable" && arg0) {
708+
} else if (matchImport("createMutable", callee.name) && arg0) {
702709
pushTrackedScope(arg0, "expression");
703-
} else if (callee.name === "on") {
710+
} else if (matchImport("on", callee.name)) {
704711
// on accepts a signal or an array of signals as its first argument,
705712
// and a tracking function as its second
706713
if (arg0) {
@@ -716,7 +723,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
716723
// Since dependencies are known, function can be async
717724
pushTrackedScope(node.arguments[1], "called-function");
718725
}
719-
} else if (callee.name === "runWithOwner") {
726+
} else if (matchImport("runWithOwner", callee.name)) {
720727
// runWithOwner(owner, fn) only creates a tracked scope if `owner =
721728
// getOwner()` runs in a tracked scope. If owner is a variable,
722729
// attempt to detect if it's a tracked scope or not, but if this
@@ -733,7 +740,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
733740
decl.node.type === "VariableDeclarator" &&
734741
decl.node.init?.type === "CallExpression" &&
735742
decl.node.init.callee.type === "Identifier" &&
736-
decl.node.init.callee.name === "getOwner"
743+
matchImport("getOwner", decl.node.init.callee.name)
737744
) {
738745
// Check if the function in which getOwner() is called is a tracked scope. If the scopeStack
739746
// has moved on from that scope already, assume it's tracked, since that's less intrusive.
@@ -770,7 +777,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
770777
// function, a tracked scope expecting a reactive function. All of the
771778
// track function's references where it's called push a tracked scope.
772779
if (node.init?.type === "CallExpression" && node.init.callee.type === "Identifier") {
773-
if (["createReactive", "createReaction"].includes(node.init.callee.name)) {
780+
if (matchImport(["createReactive", "createReaction"], node.init.callee.name)) {
774781
const track = getReturnedVar(node.id, context.getScope());
775782
if (track) {
776783
for (const reference of track.references) {
@@ -808,6 +815,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
808815
};
809816

810817
return {
818+
ImportDeclaration: handleImportDeclaration,
811819
JSXExpressionContainer(node: T.JSXExpressionContainer) {
812820
checkForTrackedScopes(node);
813821
},
@@ -843,7 +851,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
843851
if (element.openingElement.name.type === "JSXIdentifier") {
844852
const tagName = element.openingElement.name.name;
845853
if (
846-
tagName === "For" &&
854+
matchImport("For", tagName) &&
847855
node.params.length === 2 &&
848856
node.params[1].type === "Identifier"
849857
) {
@@ -852,7 +860,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
852860
scopeStack.pushSignal(index, currentScope().node);
853861
}
854862
} else if (
855-
tagName === "Index" &&
863+
matchImport("Index", tagName) &&
856864
node.params.length >= 1 &&
857865
node.params[0].type === "Identifier"
858866
) {

src/utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,24 @@ export const getCommentAfter = (
8383
sourceCode
8484
.getCommentsAfter(node)
8585
.find((comment) => comment.loc!.start.line === node.loc!.end.line);
86+
87+
export const trackImports = (fromModule = /^solid-js(?:\/?|\b)/) => {
88+
const importMap = new Map<string, string>();
89+
const handleImportDeclaration = (node: T.ImportDeclaration) => {
90+
if (fromModule.test(node.source.value)) {
91+
for (const specifier of node.specifiers) {
92+
if (specifier.type === "ImportSpecifier") {
93+
importMap.set(specifier.imported.name, specifier.local.name);
94+
}
95+
}
96+
}
97+
};
98+
const matchImport = (imports: string | Array<string>, str: string) => {
99+
const importArr = Array.isArray(imports) ? imports : [imports];
100+
return importArr
101+
.map((i) => importMap.get(i))
102+
.filter(Boolean)
103+
.includes(str);
104+
};
105+
return { matchImport, handleImportDeclaration };
106+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { createSignal as fooBar } from 'solid-js';
2+
3+
const [signal] = fooBar(5);
4+
console.log(signal());

test/rules/reactivity.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@ import { AST_NODE_TYPES as T } from "@typescript-eslint/utils";
22
import { run } from "../ruleTester";
33
import rule from "../../src/rules/reactivity";
44

5+
// Don't bother checking for imports for every test
6+
jest.mock("../../src/utils", () => {
7+
return {
8+
...jest.requireActual("../../src/utils"),
9+
trackImports: () => {
10+
// eslint-disable-next-line @typescript-eslint/no-empty-function
11+
const handleImportDeclaration = () => {};
12+
const matchImport = (imports: string | Array<string>, str: string) => {
13+
const importArr = Array.isArray(imports) ? imports : [imports];
14+
return importArr.includes(str);
15+
};
16+
return { matchImport, handleImportDeclaration };
17+
},
18+
};
19+
});
20+
521
export const cases = run("reactivity", rule, {
622
valid: [
723
`function MyComponent(props) {

0 commit comments

Comments
 (0)