120
let lastEffect = null;
121
const codePathReactHooksMapStack = [];
122
const codePathSegmentStack = [];
123
- const useEventViolations = new Set();
124
-
125
- // For a given AST node, iterate through the top level statements and add all useEvent
126
- // definitions. We can do this in non-Program nodes because we can rely on the assumption that
127
- // useEvent functions can only be declared within a component or hook at its top level.
128
- function addAllUseEventViolations(node) {
129
- if (node.body.type !== 'BlockStatement') return;
130
- for (const statement of node.body.body) {
131
- if (statement.type !== 'VariableDeclaration') continue;
132
- for (const declaration of statement.declarations) {
133
- if (
134
- declaration.type === 'VariableDeclarator' &&
135
- declaration.init &&
136
- declaration.init.type === 'CallExpression' &&
137
- declaration.init.callee &&
138
- isUseEventIdentifier(declaration.init.callee)
139
- ) {
140
- useEventViolations.add(declaration.id);
123
+ const useEventFunctions = new WeakSet();
124
+
125
+ // For a given scope, iterate through the references and add all useEvent definitions. We can
126
+ // do this in non-Program nodes because we can rely on the assumption that useEvent functions
127
+ // can only be declared within a component or hook at its top level.
128
+ function recordAllUseEventFunctions(scope) {
129
+ for (const reference of scope.references) {
130
+ const parent = reference.identifier.parent;
131
+ if (
132
+ parent.type === 'VariableDeclarator' &&
133
+ parent.init &&
134
+ parent.init.type === 'CallExpression' &&
135
+ parent.init.callee &&
136
+ isUseEventIdentifier(parent.init.callee)
137
+ ) {
138
+ for (const ref of reference.resolved.references) {
139
+ if (ref !== reference) {
140
+ useEventFunctions.add(ref.identifier);
141
+ }
142
}
143
}
144
}
145
}
146
146
- // Resolve a useEvent violation, ie the useEvent created function was called.
147
- function resolveUseEventViolation(scope, ident) {
148
- if (scope.references == null || useEventViolations.size === 0) return;
149
- for (const ref of scope.references) {
150
- if (ref.resolved == null) continue;
151
- const [useEventFunctionIdentifier] = ref.resolved.identifiers;
152
- if (ident.name === useEventFunctionIdentifier.name) {
153
- useEventViolations.delete(useEventFunctionIdentifier);
154
- break;
155
- }
156
- }
157
- }
158
-
147
return {
148
// Maintain code segment path stack as we traverse.
149
onCodePathSegmentStart: segment => codePathSegmentStack.push(segment),
555
reactHooks.push(node.callee);
556
}
557
570
- const scope = context.getScope();
571
- // useEvent: Resolve a function created with useEvent that is invoked locally at least once.
572
- // OK - onClick();
573
- resolveUseEventViolation(scope, node.callee);
574
-
558
// useEvent: useEvent functions can be passed by reference within useEffect as well as in
559
// another useEvent
560
if (
570
},
571
572
Identifier(node) {
590
- // OK - useEffect(() => { setInterval(onClick, ...) }, []);
591
- if (lastEffect != null && node.parent.type === 'CallExpression') {
592
- resolveUseEventViolation(context.getScope(), node);
573
+ // This identifier resolves to a useEvent function, but isn't being referenced in an
574
+ // effect or another event function. It isn't being called either.
575
+ if (
576
+ lastEffect == null &&
577
+ useEventFunctions.has(node) &&
578
+ node.parent.type !== 'CallExpression'
579
+ ) {
580
+ context.report({
581
+ node,
582
+ message:
583
+ `\`${context.getSource(
584
+ node,
585
+ )}\` is a function created with React Hook "useEvent", and can only be called from ` +
586
+ 'the same component. They cannot be assigned to variables or passed down.',
587
+ });
588
}
589
},
590
597
FunctionDeclaration(node) {
598
// function MyComponent() { const onClick = useEvent(...) }
599
if (isInsideComponentOrHook(node)) {
605
- addAllUseEventViolations(node);
600
+ recordAllUseEventFunctions(context.getScope());
601
}
602
},
603
604
ArrowFunctionExpression(node) {
605
// const MyComponent = () => { const onClick = useEvent(...) }
606
if (isInsideComponentOrHook(node)) {
612
- addAllUseEventViolations(node);
613
- }
614
- },
615
-
616
- 'Program:exit'(_node) {
617
- for (const node of useEventViolations.values()) {
618
- context.report({
619
- node,
620
- message:
621
- `\`${context.getSource(
622
- node,
623
- )}\` is a function created with React Hook "useEvent", and can only be called from ` +
624
- 'the same component. They cannot be assigned to variables or passed down.',
625
- });
607
+ recordAllUseEventFunctions(context.getScope());
608
}
609
},
610
};