Skip to content

Commit cce3fc1

Browse files
authored
Merge pull request #68 from solidjs-community/fix/props-marked-exit
Mark props by name on enter pass instead of exit.
2 parents d8784e3 + 17c92c2 commit cce3fc1

File tree

3 files changed

+58
-24
lines changed

3 files changed

+58
-24
lines changed

docs/reactivity.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ const Component = (props) => {
4848
return <div>{value()}</div>;
4949
};
5050

51+
const Component = (props) => {
52+
const derived = () => props.value;
53+
const oops = derived();
54+
return <div>{oops}</div>;
55+
};
56+
5157
function Component(something) {
5258
console.log(something.a);
5359
return <div />;

src/rules/reactivity.ts

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -271,11 +271,31 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
271271
/** Tracks imports from 'solid-js', handling aliases. */
272272
const { matchImport, handleImportDeclaration } = trackImports();
273273

274+
/** Workaround for #61 */
275+
const markPropsOnCondition = (node: FunctionNode, cb: (props: T.Identifier) => boolean) => {
276+
if (
277+
node.params.length === 1 &&
278+
node.params[0].type === "Identifier" &&
279+
node.parent?.type !== "JSXExpressionContainer" && // "render props" aren't components
280+
node.parent?.type !== "TemplateLiteral" && // inline functions in tagged template literals aren't components
281+
cb(node.params[0])
282+
) {
283+
// This function is a component, consider its parameter a props
284+
const propsParam = findVariable(context.getScope(), node.params[0]);
285+
if (propsParam) {
286+
scopeStack.pushProps(propsParam, node);
287+
}
288+
}
289+
};
290+
274291
/** Populates the function stack. */
275292
const onFunctionEnter = (node: ProgramOrFunctionNode) => {
276-
if (isFunctionNode(node) && scopeStack.syncCallbacks.has(node)) {
277-
// Ignore sync callbacks like Array#forEach and certain Solid primitives
278-
return;
293+
if (isFunctionNode(node)) {
294+
markPropsOnCondition(node, (props) => isPropsByName(props.name));
295+
if (scopeStack.syncCallbacks.has(node)) {
296+
// Ignore sync callbacks like Array#forEach and certain Solid primitives
297+
return;
298+
}
279299
}
280300
scopeStack.push(new ScopeStackItem(node));
281301
};
@@ -381,34 +401,26 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
381401

382402
/** Performs all analysis and reporting. */
383403
const onFunctionExit = (currentScopeNode: ProgramOrFunctionNode) => {
404+
// If this function is a component, add its props as a reactive variable
405+
if (isFunctionNode(currentScopeNode)) {
406+
markPropsOnCondition(
407+
currentScopeNode,
408+
(props) =>
409+
!isPropsByName(props.name) && // already added in markPropsOnEnter
410+
currentScope().hasJSX &&
411+
// begins with lowercase === not component
412+
(currentScopeNode.type !== "FunctionDeclaration" ||
413+
!currentScopeNode.id?.name?.match(/^[a-z]/))
414+
);
415+
}
416+
384417
// Ignore sync callbacks like Array#forEach and certain Solid primitives.
385418
// In this case only, currentScopeNode !== currentScope().node, but we're
386419
// returning early so it doesn't matter.
387420
if (isFunctionNode(currentScopeNode) && scopeStack.syncCallbacks.has(currentScopeNode)) {
388421
return;
389422
}
390423

391-
// If this function is a component, add its props as a reactive variable
392-
if (isFunctionNode(currentScopeNode) && currentScopeNode.params.length === 1) {
393-
const paramsNode = currentScopeNode.params[0];
394-
if (
395-
paramsNode?.type === "Identifier" &&
396-
((currentScope().hasJSX &&
397-
(currentScopeNode.type !== "FunctionDeclaration" ||
398-
!currentScopeNode.id?.name?.match(/^[a-z]/))) ||
399-
// begins with lowercase === not component
400-
isPropsByName(paramsNode.name)) &&
401-
currentScopeNode.parent?.type !== "JSXExpressionContainer" && // "render props" aren't components
402-
currentScopeNode.parent?.type !== "TemplateLiteral" // inline functions in tagged template literals aren't components
403-
) {
404-
// This function is a component, consider its parameter a props
405-
const propsParam = findVariable(context.getScope(), paramsNode);
406-
if (propsParam) {
407-
scopeStack.pushProps(propsParam);
408-
}
409-
}
410-
}
411-
412424
// Iterate through all usages of (derived) signals in the current scope
413425
for (const { reference, declarationScope } of scopeStack.consumeSignalReferencesInScope()) {
414426
const identifier = reference.identifier;

test/rules/reactivity.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,22 @@ export const cases = run("reactivity", rule, {
320320
},
321321
],
322322
},
323+
// mark `props` as props by name before we've determined if Component is a component in :exit
324+
{
325+
code: `
326+
const Component = props => {
327+
const derived = () => props.value;
328+
const oops = derived();
329+
return <div>{oops}</div>;
330+
}`,
331+
errors: [
332+
{
333+
messageId: "untrackedReactive",
334+
data: { name: "derived" },
335+
type: T.CallExpression,
336+
},
337+
],
338+
},
323339
// treat first parameter of uppercase function with JSX as a props
324340
{
325341
code: `

0 commit comments

Comments
 (0)