Skip to content

Commit 7714756

Browse files
committed
WIP getReferences
1 parent 525636c commit 7714756

File tree

5 files changed

+77
-51
lines changed

5 files changed

+77
-51
lines changed

src/rules/reactivity.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export default createRule({
260260
const { currentScope, parentScope } = scopeStack;
261261

262262
/** Tracks imports from 'solid-js', handling aliases. */
263-
const { matchImport, handleImportDeclaration } = trackImports();
263+
const { matchImport } = trackImports(sourceCode.ast);
264264

265265
/** Workaround for #61 */
266266
const markPropsOnCondition = (node: FunctionNode, cb: (props: T.Identifier) => boolean) => {
@@ -1095,7 +1095,6 @@ export default createRule({
10951095
};
10961096

10971097
return {
1098-
ImportDeclaration: handleImportDeclaration,
10991098
JSXExpressionContainer(node: T.JSXExpressionContainer) {
11001099
checkForTrackedScopes(node);
11011100
},
@@ -1107,7 +1106,7 @@ export default createRule({
11071106
checkForSyncCallbacks(node);
11081107

11091108
// ensure calls to reactive primitives use the results.
1110-
const parent = node.parent && ignoreTransparentWrappers(node.parent, true);
1109+
const parent = node.parent && ignoreTransparentWrappers(node.parent, "up");
11111110
if (parent?.type !== "AssignmentExpression" && parent?.type !== "VariableDeclarator") {
11121111
checkForReactiveAssignment(null, node);
11131112
}

src/rules/reactivity/pluginApi.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,11 @@ export interface ReactivityPluginApi {
4545
* - `.foo`: when the 'foo' property is accessed
4646
* - `.*`: when any direct property is accessed
4747
* - `.**`: when any direct or nested property is accessed
48+
* - `=`: defines the declaration scope, no other effect; implicitly current scope if not given,
49+
* only one `=` is allowed
4850
* @example
4951
* if (isCall(node, "createSignal")) {
50-
* reactive(node, '[0]()');
52+
* reactive(node, '[0]=()');
5153
* } else if (isCall(node, "createMemo")) {
5254
* reactive(node, '()');
5355
* } else if (isCall(node, "splitProps")) {
@@ -73,6 +75,7 @@ export interface ReactivityPluginApi {
7375

7476
export interface ReactivityPlugin {
7577
package: string;
78+
version: string;
7679
create: (api: ReactivityPluginApi) => TSESLint.RuleListener;
7780
}
7881

src/rules/reactivity/rule.ts

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { TSESLint, TSESTree as T, ASTUtils } from "@typescript-eslint/utils";
2-
import invariant from 'tiny-invariant'
1+
import { TSESLint, TSESTree as T, ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
2+
import invariant from "tiny-invariant";
33
import {
44
ProgramOrFunctionNode,
55
FunctionNode,
@@ -10,6 +10,7 @@ import {
1010
import { ReactivityScope, VirtualReference } from "./analyze";
1111
import type { ReactivityPlugin, ReactivityPluginApi } from "./pluginApi";
1212

13+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
1314
const { findVariable } = ASTUtils;
1415

1516
function parsePath(path: string): Array<string> | null {
@@ -24,18 +25,7 @@ function parsePath(path: string): Array<string> | null {
2425
return null;
2526
}
2627

27-
type MessageIds =
28-
| "noWrite"
29-
| "untrackedReactive"
30-
| "expectedFunctionGotExpression"
31-
| "badSignal"
32-
| "badUnnamedDerivedSignal"
33-
| "shouldDestructure"
34-
| "shouldAssign"
35-
| "noAsyncTrackedScope"
36-
| "jsxReactiveVariable";
37-
38-
const rule: TSESLint.RuleModule<MessageIds, []> = {
28+
export default createRule({
3929
meta: {
4030
type: "problem",
4131
docs: {
@@ -64,10 +54,11 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
6454
jsxReactiveVariable: "This variable should not be used as a JSX element.",
6555
},
6656
},
57+
defaultOptions: [],
6758
create(context) {
6859
const sourceCode = context.getSourceCode();
6960

70-
const { handleImportDeclaration, matchImport, matchLocalToModule } = trackImports();
61+
const { matchImport, matchLocalToModule } = trackImports(sourceCode.ast);
7162

7263
const root = new ReactivityScope(sourceCode.ast, null);
7364
const syncCallbacks = new Set<FunctionNode>();
@@ -116,20 +107,40 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
116107
}
117108

118109
function VirtualReference(node: T.Node): VirtualReference {
119-
return { node, declarationScope: }
110+
return { node, declarationScope: null };
120111
}
121112

122113
/**
123114
* Given what's usually a CallExpression and a description of how the expression must be used
124115
* in order to be accessed reactively, return a list of virtual references for each place where
125116
* a reactive expression is accessed.
126-
* `path` is a string formatted according to `pluginApi`.
117+
* `path` is a array of segments parsed by `parsePath` according to `pluginApi`.
127118
*/
128-
function* getReferences(node: T.Expression, path: string, allowMutable = false): Generator<VirtualReference> {
119+
function getReferences(
120+
node: T.Node,
121+
path: string | null,
122+
allowMutable = false
123+
): Array<VirtualReference> {
129124
node = ignoreTransparentWrappers(node, "up");
130-
if (!path) {
125+
const parsedPathOuter = path != null ? parsePath(path) : null;
126+
const eqCount = parsedPathOuter?.reduce((c, segment) => c + +(segment === '='), 0) ?? 0;
127+
if (eqCount > 1) {
128+
throw new Error(`'${path}' must have 0 or 1 '=' characters, has ${eqCount}`)
129+
}
130+
const hasEq = eqCount === 1;
131+
132+
let declarationScope = hasEq ? null : context.getScope();
133+
134+
function* recursiveGenerator(node: T.Node, parsedPath: Array<string> | null) {
135+
136+
137+
if (!parsedPath) {
131138
yield VirtualReference(node);
132139
} else if (node.parent?.type === "VariableDeclarator" && node.parent.init === node) {
140+
yield getReferences(node.parent.id);
141+
} else if (node.type === "Identifier") {
142+
143+
}
133144
const { id } = node.parent;
134145
if (id.type === "Identifier") {
135146
const variable = findVariable(context.getScope(), id);
@@ -144,31 +155,34 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
144155
context.report({ node: reference.identifier, messageId: "noWrite" });
145156
}
146157
} else {
147-
yield* getReferences(reference.identifier, path);
158+
yield* getReferences(reference.identifier, parsedPath, allowMutable);
148159
}
149160
}
150161
}
151162
} else if (id.type === "ArrayPattern") {
152-
const parsedPath = parsePath(path)
153-
if (parsedPath) {
154-
const newPath = path.substring(match[0].length);
155-
const index = match[1]
156-
if (index === '*') {
157-
158-
} else {
159-
163+
if (parsedPath[0] === "[]") {
164+
for (const el of id.elements) {
165+
if (!el) {
166+
// ignore
167+
} else if (el.type === "Identifier") {
168+
yield* getReferences(el, parsedPath.slice(1), allowMutable);
169+
} else if (el.type === "RestElement") {
170+
yield* getReferences(el.argument, parsedPath, allowMutable);
171+
}
160172
}
173+
} else {
161174
}
162-
163175
}
164-
}
176+
177+
178+
return Array.from(recursiveGenerator(node, parsePath(path)));
165179
}
166180

167181
function distributeReferences(root: ReactivityScope, references: Array<VirtualReference>) {
168182
references.forEach((ref) => {
169183
const range = ref.node.range;
170184
const scope = root.deepestScopeContaining(range);
171-
invariant(scope != null)
185+
invariant(scope != null);
172186
scope.references.push(ref);
173187
});
174188
}
@@ -238,4 +252,4 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
238252
},
239253
};
240254
},
241-
};
255+
});

src/utils.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ export function trace(node: T.Node, initialScope: TSESLint.Scope.Scope): T.Node
7070
}
7171

7272
/** Get the relevant node when wrapped by a node that doesn't change the behavior */
73-
export function ignoreTransparentWrappers(node: T.Expression, dir = 'down'): T.Expression {
73+
export function ignoreTransparentWrappers(node: T.Node, dir = "down"): T.Node {
7474
if (node.type === "TSAsExpression" || node.type === "TSNonNullExpression") {
75-
const next = dir === 'up' ? node.parent as T.Expression : node.expression;
75+
const next = dir === "up" ? node.parent : node.expression;
7676
if (next) {
7777
return ignoreTransparentWrappers(next, dir);
7878
}
@@ -122,27 +122,37 @@ export const getCommentAfter = (
122122
.getCommentsAfter(node)
123123
.find((comment) => comment.loc!.start.line === node.loc!.end.line);
124124

125-
export const trackImports = (fromModule = /^solid-js(?:\/?|\b)/) => {
126-
const importMap = new Map<string, string>();
127-
const moduleMap = new Map<string, string>();
125+
export const trackImports = (program: T.Program) => {
126+
const solidRegex = /^solid-js(?:\/?|\b)/;
127+
const importMap = new Map<string, { imported: string; source: string }>();
128128

129-
const handleImportDeclaration = (node: T.ImportDeclaration) => {
130-
if (fromModule.test(node.source.value)) {
129+
for (const node of program.body) {
130+
if (node.type === "ImportDeclaration") {
131131
for (const specifier of node.specifiers) {
132-
if (specifier.type === "ImportSpecifier") {
133-
importMap.set(specifier.imported.name, specifier.local.name);
134-
moduleMap.set(specifier.local.name, node.source.value);
132+
if (specifier.type === "ImportSpecifier" && specifier.importKind !== "type") {
133+
importMap.set(specifier.local.name, {
134+
imported: specifier.imported.name,
135+
source: node.source.value,
136+
});
135137
}
136138
}
137139
}
138-
};
139-
const matchImport = (imports: string | Array<string>, str: string): string | undefined => {
140+
}
141+
const matchImport = (
142+
imports: string | Array<string>,
143+
local: string,
144+
module = solidRegex
145+
): string | undefined => {
146+
const match = importMap.get(local);
147+
if (!match || !module.test(match.source)) {
148+
return;
149+
}
140150
const importArr = Array.isArray(imports) ? imports : [imports];
141-
return importArr.find((i) => importMap.get(i) === str);
151+
return importArr.find((i) => i === match.imported);
142152
};
143-
const matchLocalToModule = (local: string): string | undefined => moduleMap.get(local);
153+
const matchLocalToModule = (local: string): string | undefined => importMap.get(local)?.source;
144154

145-
return { matchImport, handleImportDeclaration, matchLocalToModule };
155+
return { matchImport, matchLocalToModule };
146156
};
147157

148158
export function appendImports(

test/rules/reactivity/api.test.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)