|
1 | | -import { getUpstreamVariables, getDownstreamRefs, getCallExpr } from "./ast.js"; |
| 1 | +import { getDownstreamRefs, getCallExpr } from "./ast.js"; |
| 2 | + |
| 3 | +/** |
| 4 | + * @import {Scope} from 'eslint' |
| 5 | + * @import {Rule} from 'eslint' |
| 6 | + * @import {AST} from 'eslint' |
| 7 | + */ |
2 | 8 |
|
3 | 9 | export const isReactFunctionalComponent = (node) => |
4 | 10 | (node.type === "FunctionDeclaration" || |
@@ -130,23 +136,24 @@ export const isPropCallback = (context, ref) => |
130 | 136 | // NOTE: Global variables (like `JSON` in `JSON.stringify()`) have an empty `defs`; fortunately `[].some() === false`. |
131 | 137 | // Also, I'm not sure so far when `defs.length > 1`... haven't seen it with shadowed variables or even redeclared variables with `var`. |
132 | 138 | export const isState = (context, ref) => |
133 | | - getUpstreamReactVariables(context, ref.resolved).notEmptyEvery((variable) => |
134 | | - variable.defs.some((def) => isUseState(def.node)), |
| 139 | + getUpstreamRefs(context, ref).notEmptyEvery((ref) => |
| 140 | + ref.resolved.defs.some((def) => isUseState(def.node)), |
135 | 141 | ); |
136 | 142 | // Returns false for props of HOCs like `withRouter` because they usually have side effects. |
137 | 143 | export const isProp = (context, ref) => |
138 | | - getUpstreamReactVariables(context, ref.resolved).notEmptyEvery((variable) => |
139 | | - variable.defs.some((def) => isPropDef(def)), |
| 144 | + getUpstreamRefs(context, ref).notEmptyEvery((ref) => |
| 145 | + ref.resolved.defs.some((def) => isPropDef(def)), |
140 | 146 | ); |
141 | 147 | export const isRef = (context, ref) => |
142 | | - getUpstreamReactVariables(context, ref.resolved).notEmptyEvery((variable) => |
143 | | - variable.defs.some((def) => isUseRef(def.node)), |
| 148 | + getUpstreamRefs(context, ref).notEmptyEvery((ref) => |
| 149 | + ref.resolved.defs.some((def) => isUseRef(def.node)), |
144 | 150 | ); |
145 | 151 |
|
146 | 152 | // TODO: Surely can be simplified/re-use other functions. |
147 | 153 | // Needs a better API too so we can more easily get names etc. for messages. |
148 | 154 | export const getUseStateNode = (context, ref) => { |
149 | | - return getUpstreamReactVariables(context, ref.resolved) |
| 155 | + return getUpstreamRefs(context, ref) |
| 156 | + .map((ref) => ref.resolved) |
150 | 157 | .find((variable) => variable.defs.some((def) => isUseState(def.node))) |
151 | 158 | ?.defs.find((def) => isUseState(def.node))?.node; |
152 | 159 | }; |
@@ -186,25 +193,43 @@ export const isImmediateCall = (node) => { |
186 | 193 | export const isArgsAllLiterals = (context, callExpr) => |
187 | 194 | callExpr.arguments |
188 | 195 | .flatMap((arg) => getDownstreamRefs(context, arg)) |
189 | | - .flatMap((ref) => getUpstreamReactVariables(context, ref.resolved)) |
190 | | - .length === 0; |
191 | | - |
192 | | -export const getUpstreamReactVariables = (context, variable) => |
193 | | - getUpstreamVariables( |
194 | | - context, |
195 | | - variable, |
196 | | - // Stop at the *usage* of `useState` - don't go up to the `useState` variable. |
197 | | - // Not needed for props - they don't go "too far". |
198 | | - // We could remove this and check for the `useState` variable instead, |
199 | | - // but then all our tests need to import it so we can traverse up to it. |
200 | | - // And would need to change `getUseStateNode()` too? |
201 | | - // TODO: Could probably organize these filters better. |
202 | | - (node) => !isUseState(node), |
203 | | - ).filter((variable) => |
204 | | - variable.defs.every( |
205 | | - (def) => |
206 | | - isPropDef(def) || |
207 | | - // Ignore variables declared inside an anonymous function, like in `array.map()`. |
208 | | - def.type !== "Parameter", |
209 | | - ), |
210 | | - ); |
| 196 | + .flatMap((ref) => getUpstreamRefs(context, ref)).length === 0; |
| 197 | + |
| 198 | +/** |
| 199 | + * @param {Rule.RuleContext} context |
| 200 | + * @param {Scope.Reference} ref |
| 201 | + * |
| 202 | + * @returns {Scope.Reference[]} |
| 203 | + */ |
| 204 | +export const getUpstreamRefs = (context, ref) => { |
| 205 | + if (!ref.resolved) { |
| 206 | + // I think this only happens when: |
| 207 | + // 1. Import statement is missing |
| 208 | + // 2. ESLint globals are misconfigured |
| 209 | + return []; |
| 210 | + } else if ( |
| 211 | + // Ignore references to function parameters, aside from props. |
| 212 | + ref.resolved.defs.every( |
| 213 | + (def) => def.type === "Parameter" && !isPropDef(def), |
| 214 | + ) |
| 215 | + ) { |
| 216 | + return []; |
| 217 | + } |
| 218 | + |
| 219 | + const upstreamRefs = ref.resolved.defs |
| 220 | + // TODO: https://github.com/NickvanDyke/eslint-plugin-react-you-might-not-need-an-effect/issues/34 |
| 221 | + // `init` covers for arrow functions; also needs `body` to descend into function declarations |
| 222 | + // But then for function parameters (including props), `def.node.body` is the body of the function that they belong to, |
| 223 | + // so we get *all* the downstream refs in it... |
| 224 | + // We only want to descend when we're traversing up the function itself; no its parameters. |
| 225 | + // Probably similar logic to in `getUpstreamReactVariables`. |
| 226 | + .filter((def) => !!def.node.init) |
| 227 | + // Stop before we get to `useState()` - we want references to the state and setter. |
| 228 | + // May not be necessary if we adapt the check in `isState()`? |
| 229 | + .filter((def) => !isUseState(def.node)) |
| 230 | + .flatMap((def) => getDownstreamRefs(context, def.node.init)) |
| 231 | + .flatMap((ref) => getUpstreamRefs(context, ref)); |
| 232 | + |
| 233 | + // Ultimately return only leaf refs |
| 234 | + return upstreamRefs.length === 0 ? [ref] : upstreamRefs; |
| 235 | +}; |
0 commit comments