Skip to content

Commit 539e4df

Browse files
authored
Exp: modify isReadOnly to support more read cases (#29)
* fix: segregate read only to implementations moves isRead only to manually handle react hook primitives creates another helper to check if a value is being returned for a custom hook also added additional check to `isReadOnly` to avoid raising flags when working with callExpressions * additional case
1 parent 874d916 commit 539e4df

File tree

4 files changed

+410
-263
lines changed

4 files changed

+410
-263
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ yarn-error.log*
2424

2525
# misc
2626
.DS_Store
27+
.eslintcache

src/StateSnapshot.js

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import {
22
callExpressions,
33
functionTypes,
4-
getParentOfNodeType,
5-
isInHookDeps,
4+
isDepthSameAsRootComponent,
5+
isInCustomHookDef,
6+
isInReactHookDeps,
7+
isInReactHooks,
68
isInSomething,
79
isReadOnly,
810
} from './lib/utils'
@@ -79,8 +81,9 @@ export default {
7981
if (
8082
node.parent.type === 'MemberExpression' &&
8183
node.parent.property === node
82-
)
84+
) {
8385
return
86+
}
8487

8588
const kind = which(node.name, scope)
8689
if (kind === 'state' && isInRender(node)) {
@@ -94,16 +97,35 @@ export default {
9497
// ignore the error if the snapshot
9598
// is just being read in the hook and is a part of the dependency array
9699

97-
if (
98-
isReadOnly(node) &&
99-
(isInHookDeps(node) ||
100+
// Valids
101+
// - allowed to read at root level for computation (new hook / render level defs) , basically anything defined at the root of the component definition
102+
// [x] allowed to read in a useEffect and useCallback if added in deps
103+
104+
// Invalids
105+
// [x] if being used in a callback that isn't useEffect or useCallback
106+
107+
if (isReadOnly(node)) {
108+
if (isInReactHooks(node) && !isInReactHookDeps(node)) {
109+
return context.report({
110+
node,
111+
message: SNAPSHOT_CALLBACK_MESSAGE,
112+
})
113+
}
114+
if (
115+
isDepthSameAsRootComponent(node) ||
100116
isInJSXContainer(node) ||
101-
isInDeclaration(node))
102-
) {
103-
return
117+
isInCustomHookDef(node)
118+
) {
119+
return
120+
}
121+
} else {
122+
return context.report({
123+
node,
124+
message: SNAPSHOT_CALLBACK_MESSAGE,
125+
})
104126
}
105127

106-
if (isInCallback(node)) {
128+
if (isInCallback(node) && !isInReactHooks(node)) {
107129
return context.report({
108130
node,
109131
message: SNAPSHOT_CALLBACK_MESSAGE,
@@ -218,8 +240,9 @@ function isComputedIdentifier(node, scope) {
218240
}
219241
})
220242

221-
if (!isIt && scope.upper)
243+
if (!isIt && scope.upper) {
222244
return (isIt = isComputedIdentifier(node, scope.upper))
245+
}
223246

224247
return isIt
225248
}
@@ -325,8 +348,9 @@ function isUsedInUseProxy(node, scope) {
325348
}
326349
}
327350
})
328-
if (!isUsed && scope.upper)
351+
if (!isUsed && scope.upper) {
329352
return (isUsed = isUsedInUseProxy(node, scope.upper))
353+
}
330354
return isUsed
331355
}
332356

@@ -372,12 +396,12 @@ function isInJSXContainer(node) {
372396
)
373397
}
374398

375-
function isInDeclaration(node) {
376-
const _parentDeclaration = getParentOfNodeType(node, 'VariableDeclarator')
399+
// function isInDeclaration(node) {
400+
// const _parentDeclaration = getParentOfNodeType(node, 'VariableDeclarator')
377401

378-
if (_parentDeclaration?.init?.callee?.name === 'useSnapshot') {
379-
return true
380-
}
402+
// if (_parentDeclaration?.init?.callee?.name === 'useSnapshot') {
403+
// return true
404+
// }
381405

382-
return false
383-
}
406+
// return false
407+
// }

src/lib/utils.js

Lines changed: 89 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export const writingOpExpressionTypes = ['UpdateExpression']
1616
*
1717
* isInSomething(thisNode,"FunctionExpression") // true
1818
* isInSomething(thisNode,"ExpressionStatement") // false
19-
*
2019
*/
2120
export function isInSomething(node, thing) {
2221
if (node.parent && node.parent.type !== thing) {
@@ -30,18 +29,16 @@ export function isInSomething(node, thing) {
3029
/**
3130
* @param {any} node ASTNode
3231
*
33-
*
3432
* @example
3533
*
36-
*const stateOne = proxy({
34+
* const stateOne = proxy({
3735
* count: 0,
3836
* inc: function () {
3937
* ++state.count //<== taking node from here
4038
* },
4139
* })
4240
*
4341
* nearestCalleeName(node) //=> "proxy" //<= proxy is the nearest callee
44-
*
4542
*/
4643
export function nearestCalleeName(node) {
4744
if (!(node && node.parent)) {
@@ -81,9 +78,12 @@ export function nearestCalleeName(node) {
8178
*
8279
* getParentOfNodeType(thisNode,"FunctionExpression") // ASTNode.type: "FunctionExpression"
8380
* getParentOfNodeType(thisNode,"ExpressionStatement") // null
84-
*
8581
*/
8682
export function getParentOfNodeType(node, nodeType) {
83+
if (!node?.parent) {
84+
return null
85+
}
86+
8787
if (node.parent && node.parent.type !== nodeType) {
8888
return getParentOfNodeType(node.parent, nodeType)
8989
} else if (node.parent && node.parent.type === nodeType) {
@@ -111,11 +111,14 @@ export function isReadOnly(node) {
111111
return isReadOnly(node.parent)
112112
}
113113

114+
if (node.parent.type === 'CallExpression') {
115+
return true
116+
}
117+
114118
return true
115119
}
116120

117121
/**
118-
*
119122
* @param {*} node
120123
* @returns {boolean} true if the node is on the left
121124
* of an assignment expression aka being modified
@@ -152,28 +155,43 @@ export function returnFirstCallback(node) {
152155
* resulting node or not
153156
* @returns {* | boolean} - either true/false or the CallExpression node that belongs to the hook
154157
*/
155-
export function isInHook(node, returnHook = false) {
158+
export function isInReactHooks(node, returnHook = false) {
156159
const hookDef = getNearestHook(node)
157160
if (returnHook) {
158161
return hookDef
159162
}
160163
return hookDef ? true : false
161164
}
162165

166+
function isReactPrimitive(node) {
167+
if (
168+
node.type === 'Identifier' &&
169+
(node.name === 'useEffect' || node.name === 'useCallback')
170+
) {
171+
return true
172+
}
173+
174+
if (node.type === 'MemberExpression') {
175+
const flatExpr = flattenMemberExpression(node)
176+
return flatExpr.endsWith('useEffect') || flatExpr.endsWith('useCallback')
177+
}
178+
179+
return false
180+
}
181+
163182
export function getNearestHook(node) {
164183
if (!node.parent || !node.parent.type) return false
165184

166-
if (
167-
functionTypes.includes(node.type) &&
168-
node.parent.type == 'CallExpression' &&
169-
node.parent.callee.type === 'Identifier' &&
170-
(node.parent.callee.name === 'useEffect' ||
171-
node.parent.callee.name === 'useCallback')
172-
) {
173-
return node.parent
174-
} else {
175-
return getNearestHook(node.parent)
185+
const parentCaller = getParentOfNodeType(node, 'CallExpression')
186+
if (!parentCaller) {
187+
return false
188+
}
189+
190+
if (!isReactPrimitive(parentCaller.callee)) {
191+
return getNearestHook(parentCaller)
176192
}
193+
194+
return parentCaller
177195
}
178196

179197
/**
@@ -182,8 +200,8 @@ export function getNearestHook(node) {
182200
* @param {*} node
183201
* @returns {boolean}
184202
*/
185-
export function isInHookDeps(node) {
186-
const hookNode = isInHook(node, true)
203+
export function isInReactHookDeps(node) {
204+
const hookNode = isInReactHooks(node, true)
187205
if (!hookNode) {
188206
return false
189207
}
@@ -271,3 +289,55 @@ function flattenMemberExpression(expr, key = '') {
271289
return path
272290
}
273291
}
292+
293+
export function isDepthSameAsRootComponent(node) {
294+
const varDef = getParentOfNodeType(node, 'VariableDeclaration')
295+
const parentNormalFunc = getParentOfNodeType(varDef, 'FunctionDeclaration')
296+
const parentArrFunc = getParentOfNodeType(varDef, 'ArrowFunctionExpression')
297+
298+
const parentFunc = parentNormalFunc || parentArrFunc
299+
if (!parentFunc) {
300+
return false
301+
}
302+
303+
if (
304+
parentFunc?.parent.type === 'VariableDeclarator' &&
305+
parentFunc?.parent.parent.type === 'VariableDeclaration' &&
306+
parentFunc?.parent.parent.parent.type === 'Program'
307+
) {
308+
return true
309+
}
310+
}
311+
312+
export function isInCustomHookDef(node) {
313+
const nearestReturn = getParentOfNodeType(node, 'ReturnStatement')
314+
const returnInBlock = getParentOfNodeType(nearestReturn, 'BlockStatement')
315+
const normalFuncDef = getParentOfNodeType(
316+
returnInBlock,
317+
'ArrowFunctionExpression'
318+
)
319+
const arrowFuncDef = getParentOfNodeType(returnInBlock, 'FunctionExpression')
320+
321+
const nearestFuncDef = normalFuncDef || arrowFuncDef || false
322+
323+
if (!nearestFuncDef) {
324+
return false
325+
}
326+
327+
const varDeclaratorOfFunc = getParentOfNodeType(
328+
nearestFuncDef,
329+
'VariableDeclarator'
330+
)
331+
const varDefOfFunc = getParentOfNodeType(
332+
varDeclaratorOfFunc,
333+
'VariableDeclaration'
334+
)
335+
336+
const varDefOnRoot = varDefOfFunc.parent?.type === 'Program' || false
337+
338+
return (
339+
varDefOnRoot &&
340+
varDeclaratorOfFunc.id?.type === 'Identifier' &&
341+
varDeclaratorOfFunc.id?.name.startsWith('use')
342+
)
343+
}

0 commit comments

Comments
 (0)