Skip to content

Commit

Permalink
WIP getReferences
Browse files Browse the repository at this point in the history
  • Loading branch information
joshwilsonvu committed Aug 26, 2023
1 parent 525636c commit 7714756
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 51 deletions.
5 changes: 2 additions & 3 deletions src/rules/reactivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export default createRule({
const { currentScope, parentScope } = scopeStack;

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

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

return {
ImportDeclaration: handleImportDeclaration,
JSXExpressionContainer(node: T.JSXExpressionContainer) {
checkForTrackedScopes(node);
},
Expand All @@ -1107,7 +1106,7 @@ export default createRule({
checkForSyncCallbacks(node);

// ensure calls to reactive primitives use the results.
const parent = node.parent && ignoreTransparentWrappers(node.parent, true);
const parent = node.parent && ignoreTransparentWrappers(node.parent, "up");
if (parent?.type !== "AssignmentExpression" && parent?.type !== "VariableDeclarator") {
checkForReactiveAssignment(null, node);
}
Expand Down
5 changes: 4 additions & 1 deletion src/rules/reactivity/pluginApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ export interface ReactivityPluginApi {
* - `.foo`: when the 'foo' property is accessed
* - `.*`: when any direct property is accessed
* - `.**`: when any direct or nested property is accessed
* - `=`: defines the declaration scope, no other effect; implicitly current scope if not given,
* only one `=` is allowed
* @example
* if (isCall(node, "createSignal")) {
* reactive(node, '[0]()');
* reactive(node, '[0]=()');
* } else if (isCall(node, "createMemo")) {
* reactive(node, '()');
* } else if (isCall(node, "splitProps")) {
Expand All @@ -73,6 +75,7 @@ export interface ReactivityPluginApi {

export interface ReactivityPlugin {
package: string;
version: string;
create: (api: ReactivityPluginApi) => TSESLint.RuleListener;
}

Expand Down
78 changes: 46 additions & 32 deletions src/rules/reactivity/rule.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TSESLint, TSESTree as T, ASTUtils } from "@typescript-eslint/utils";
import invariant from 'tiny-invariant'
import { TSESLint, TSESTree as T, ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
import invariant from "tiny-invariant";
import {
ProgramOrFunctionNode,
FunctionNode,
Expand All @@ -10,6 +10,7 @@ import {
import { ReactivityScope, VirtualReference } from "./analyze";
import type { ReactivityPlugin, ReactivityPluginApi } from "./pluginApi";

const createRule = ESLintUtils.RuleCreator.withoutDocs;
const { findVariable } = ASTUtils;

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

type MessageIds =
| "noWrite"
| "untrackedReactive"
| "expectedFunctionGotExpression"
| "badSignal"
| "badUnnamedDerivedSignal"
| "shouldDestructure"
| "shouldAssign"
| "noAsyncTrackedScope"
| "jsxReactiveVariable";

const rule: TSESLint.RuleModule<MessageIds, []> = {
export default createRule({
meta: {
type: "problem",
docs: {
Expand Down Expand Up @@ -64,10 +54,11 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
jsxReactiveVariable: "This variable should not be used as a JSX element.",
},
},
defaultOptions: [],
create(context) {
const sourceCode = context.getSourceCode();

const { handleImportDeclaration, matchImport, matchLocalToModule } = trackImports();
const { matchImport, matchLocalToModule } = trackImports(sourceCode.ast);

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

function VirtualReference(node: T.Node): VirtualReference {
return { node, declarationScope: }
return { node, declarationScope: null };
}

/**
* Given what's usually a CallExpression and a description of how the expression must be used
* in order to be accessed reactively, return a list of virtual references for each place where
* a reactive expression is accessed.
* `path` is a string formatted according to `pluginApi`.
* `path` is a array of segments parsed by `parsePath` according to `pluginApi`.
*/
function* getReferences(node: T.Expression, path: string, allowMutable = false): Generator<VirtualReference> {
function getReferences(
node: T.Node,
path: string | null,
allowMutable = false
): Array<VirtualReference> {
node = ignoreTransparentWrappers(node, "up");
if (!path) {
const parsedPathOuter = path != null ? parsePath(path) : null;
const eqCount = parsedPathOuter?.reduce((c, segment) => c + +(segment === '='), 0) ?? 0;
if (eqCount > 1) {
throw new Error(`'${path}' must have 0 or 1 '=' characters, has ${eqCount}`)
}
const hasEq = eqCount === 1;

let declarationScope = hasEq ? null : context.getScope();

function* recursiveGenerator(node: T.Node, parsedPath: Array<string> | null) {


if (!parsedPath) {
yield VirtualReference(node);
} else if (node.parent?.type === "VariableDeclarator" && node.parent.init === node) {
yield getReferences(node.parent.id);
} else if (node.type === "Identifier") {

}
const { id } = node.parent;
if (id.type === "Identifier") {
const variable = findVariable(context.getScope(), id);
Expand All @@ -144,31 +155,34 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
context.report({ node: reference.identifier, messageId: "noWrite" });
}
} else {
yield* getReferences(reference.identifier, path);
yield* getReferences(reference.identifier, parsedPath, allowMutable);
}
}
}
} else if (id.type === "ArrayPattern") {
const parsedPath = parsePath(path)
if (parsedPath) {
const newPath = path.substring(match[0].length);
const index = match[1]
if (index === '*') {

} else {

if (parsedPath[0] === "[]") {
for (const el of id.elements) {
if (!el) {
// ignore
} else if (el.type === "Identifier") {
yield* getReferences(el, parsedPath.slice(1), allowMutable);
} else if (el.type === "RestElement") {
yield* getReferences(el.argument, parsedPath, allowMutable);
}
}
} else {
}

}
}


return Array.from(recursiveGenerator(node, parsePath(path)));
}

function distributeReferences(root: ReactivityScope, references: Array<VirtualReference>) {
references.forEach((ref) => {
const range = ref.node.range;
const scope = root.deepestScopeContaining(range);
invariant(scope != null)
invariant(scope != null);
scope.references.push(ref);
});
}
Expand Down Expand Up @@ -238,4 +252,4 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
},
};
},
};
});
40 changes: 25 additions & 15 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ export function trace(node: T.Node, initialScope: TSESLint.Scope.Scope): T.Node
}

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

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

const handleImportDeclaration = (node: T.ImportDeclaration) => {
if (fromModule.test(node.source.value)) {
for (const node of program.body) {
if (node.type === "ImportDeclaration") {
for (const specifier of node.specifiers) {
if (specifier.type === "ImportSpecifier") {
importMap.set(specifier.imported.name, specifier.local.name);
moduleMap.set(specifier.local.name, node.source.value);
if (specifier.type === "ImportSpecifier" && specifier.importKind !== "type") {
importMap.set(specifier.local.name, {
imported: specifier.imported.name,
source: node.source.value,
});
}
}
}
};
const matchImport = (imports: string | Array<string>, str: string): string | undefined => {
}
const matchImport = (
imports: string | Array<string>,
local: string,
module = solidRegex
): string | undefined => {
const match = importMap.get(local);
if (!match || !module.test(match.source)) {
return;
}
const importArr = Array.isArray(imports) ? imports : [imports];
return importArr.find((i) => importMap.get(i) === str);
return importArr.find((i) => i === match.imported);
};
const matchLocalToModule = (local: string): string | undefined => moduleMap.get(local);
const matchLocalToModule = (local: string): string | undefined => importMap.get(local)?.source;

return { matchImport, handleImportDeclaration, matchLocalToModule };
return { matchImport, matchLocalToModule };
};

export function appendImports(
Expand Down
Empty file.

0 comments on commit 7714756

Please sign in to comment.