@pyreon/lint 0.12.13 → 0.12.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +55 -2
  2. package/lib/analysis/cli.js.html +1 -1
  3. package/lib/analysis/index.js.html +1 -1
  4. package/lib/cli.js +960 -162
  5. package/lib/cli.js.map +1 -1
  6. package/lib/index.js +935 -161
  7. package/lib/index.js.map +1 -1
  8. package/lib/types/index.d.ts +96 -23
  9. package/lib/types/index.d.ts.map +1 -1
  10. package/package.json +2 -1
  11. package/schema/pyreonlintrc.schema.json +64 -0
  12. package/src/cli.ts +44 -2
  13. package/src/config/presets.ts +13 -1
  14. package/src/index.ts +7 -0
  15. package/src/lint.ts +37 -6
  16. package/src/lsp/index.ts +15 -2
  17. package/src/rules/architecture/dev-guard-warnings.ts +172 -17
  18. package/src/rules/architecture/no-circular-import.ts +7 -0
  19. package/src/rules/architecture/no-process-dev-gate.ts +18 -45
  20. package/src/rules/architecture/require-browser-smoke-test.ts +227 -0
  21. package/src/rules/form/no-submit-without-validation.ts +9 -0
  22. package/src/rules/form/no-unregistered-field.ts +9 -0
  23. package/src/rules/hooks/no-raw-addeventlistener.ts +7 -0
  24. package/src/rules/hooks/no-raw-localstorage.ts +12 -1
  25. package/src/rules/hooks/no-raw-setinterval.ts +14 -0
  26. package/src/rules/index.ts +4 -1
  27. package/src/rules/jsx/no-props-destructure.ts +20 -6
  28. package/src/rules/lifecycle/no-dom-in-setup.ts +67 -7
  29. package/src/rules/reactivity/no-bare-signal-in-jsx.ts +12 -1
  30. package/src/rules/reactivity/no-unbatched-updates.ts +3 -0
  31. package/src/rules/router/no-imperative-navigate-in-render.ts +131 -35
  32. package/src/rules/ssr/no-window-in-ssr.ts +418 -35
  33. package/src/rules/store/no-duplicate-store-id.ts +11 -0
  34. package/src/rules/store/no-mutate-store-state.ts +11 -1
  35. package/src/rules/styling/no-dynamic-styled.ts +13 -24
  36. package/src/rules/styling/no-theme-outside-provider.ts +34 -2
  37. package/src/runner.ts +100 -10
  38. package/src/tests/runner.test.ts +1573 -21
  39. package/src/types.ts +74 -3
  40. package/src/utils/component-context.ts +106 -0
  41. package/src/utils/exempt-paths.ts +39 -0
  42. package/src/utils/file-roles.ts +32 -0
  43. package/src/utils/imports.ts +4 -1
  44. package/src/utils/validate-options.ts +68 -0
  45. package/src/watcher.ts +17 -0
@@ -1,4 +1,6 @@
1
1
  import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { isPathExempt } from '../../utils/exempt-paths'
3
+ import { createComponentContextTracker } from '../../utils/component-context'
2
4
  import { getSpan, isCallTo } from '../../utils/ast'
3
5
 
4
6
  const TIMER_FNS = new Set(['setInterval', 'setTimeout'])
@@ -10,16 +12,28 @@ export const noRawSetInterval: Rule = {
10
12
  description: 'Suggest wrapping setInterval/setTimeout in onMount for automatic cleanup.',
11
13
  severity: 'info',
12
14
  fixable: false,
15
+ schema: { exemptPaths: 'string[]' },
13
16
  },
14
17
  create(context) {
18
+ // Configurable `exemptPaths` — for packages that IMPLEMENT
19
+ // `useInterval` / `useTimeout` (they can't use themselves).
20
+ if (isPathExempt(context)) return {}
21
+
22
+ // Only flag when *inside* a component / hook setup body. Module-level
23
+ // timers, utility functions, and test callbacks have their own
24
+ // lifecycle and don't need component-tied cleanup.
25
+ const ctx = createComponentContextTracker()
26
+
15
27
  let mountDepth = 0
16
28
  const callbacks: VisitorCallbacks = {
29
+ ...ctx.callbacks,
17
30
  CallExpression(node: any) {
18
31
  if (isCallTo(node, 'onMount')) {
19
32
  mountDepth++
20
33
  }
21
34
 
22
35
  if (mountDepth > 0) return
36
+ if (!ctx.isInComponentOrHook()) return
23
37
 
24
38
  const callee = node.callee
25
39
  if (!callee || callee.type !== 'Identifier') return
@@ -10,6 +10,7 @@ import { noCrossLayerImport } from './architecture/no-cross-layer-import'
10
10
  import { noDeepImport } from './architecture/no-deep-import'
11
11
  import { noErrorWithoutPrefix } from './architecture/no-error-without-prefix'
12
12
  import { noProcessDevGate } from './architecture/no-process-dev-gate'
13
+ import { requireBrowserSmokeTest } from './architecture/require-browser-smoke-test'
13
14
  import { noSubmitWithoutValidation } from './form/no-submit-without-validation'
14
15
  // Form
15
16
  import { noUnregisteredField } from './form/no-unregistered-field'
@@ -108,13 +109,14 @@ export const allRules: Rule[] = [
108
109
  noWindowInSsr,
109
110
  noMismatchRisk,
110
111
  preferRequestContext,
111
- // Architecture (6)
112
+ // Architecture (7)
112
113
  noCircularImport,
113
114
  noDeepImport,
114
115
  noCrossLayerImport,
115
116
  devGuardWarnings,
116
117
  noErrorWithoutPrefix,
117
118
  noProcessDevGate,
119
+ requireBrowserSmokeTest,
118
120
  // Store (3)
119
121
  noStoreOutsideProvider,
120
122
  noMutateStoreState,
@@ -187,6 +189,7 @@ export {
187
189
  noPeekInTracked,
188
190
  noProcessDevGate,
189
191
  noPropsDestructure,
192
+ requireBrowserSmokeTest,
190
193
  // Hooks
191
194
  noRawAddEventListener,
192
195
  noRawLocalStorage,
@@ -42,25 +42,40 @@ export const noPropsDestructure: Rule = {
42
42
  },
43
43
  create(context) {
44
44
  let functionDepth = 0
45
+ // oxc visitor doesn't pass `parent` to callbacks — previous
46
+ // `parent?.type === 'CallExpression'` check was silently inert. Pre-mark
47
+ // function nodes that appear as CallExpression arguments on the way in.
48
+ const callArgFns = new WeakSet<any>()
45
49
 
46
50
  const callbacks: VisitorCallbacks = {
51
+ CallExpression(node: any) {
52
+ for (const arg of node.arguments ?? []) {
53
+ if (
54
+ arg?.type === 'ArrowFunctionExpression' ||
55
+ arg?.type === 'FunctionExpression' ||
56
+ arg?.type === 'FunctionDeclaration'
57
+ ) {
58
+ callArgFns.add(arg)
59
+ }
60
+ }
61
+ },
47
62
  ArrowFunctionExpression(node: any) {
48
63
  functionDepth++
49
- checkFunction(node, context, functionDepth)
64
+ checkFunction(node, context, functionDepth, callArgFns)
50
65
  },
51
66
  'ArrowFunctionExpression:exit'() {
52
67
  functionDepth--
53
68
  },
54
69
  FunctionDeclaration(node: any) {
55
70
  functionDepth++
56
- checkFunction(node, context, functionDepth)
71
+ checkFunction(node, context, functionDepth, callArgFns)
57
72
  },
58
73
  'FunctionDeclaration:exit'() {
59
74
  functionDepth--
60
75
  },
61
76
  FunctionExpression(node: any) {
62
77
  functionDepth++
63
- checkFunction(node, context, functionDepth)
78
+ checkFunction(node, context, functionDepth, callArgFns)
64
79
  },
65
80
  'FunctionExpression:exit'() {
66
81
  functionDepth--
@@ -70,7 +85,7 @@ export const noPropsDestructure: Rule = {
70
85
  },
71
86
  }
72
87
 
73
- function checkFunction(node: any, context: any, depth: number) {
88
+ function checkFunction(node: any, context: any, depth: number, callArgFns: WeakSet<any>) {
74
89
  const params = node.params
75
90
  if (!params || params.length === 0) return
76
91
 
@@ -82,8 +97,7 @@ function checkFunction(node: any, context: any, depth: number) {
82
97
 
83
98
  // Skip functions passed as arguments to HOC factories
84
99
  // e.g. createLink(({ href, ...rest }) => <a {...rest} />)
85
- const parent = node.parent
86
- if (parent?.type === 'CallExpression' && parent.arguments?.includes(node)) return
100
+ if (callArgFns.has(node)) return
87
101
 
88
102
  const body = node.body
89
103
  if (!body) return
@@ -18,12 +18,74 @@ export const noDomInSetup: Rule = {
18
18
  fixable: false,
19
19
  },
20
20
  create(context) {
21
- let safeDepth = 0 // inside onMount or effect
21
+ let safeDepth = 0
22
+ function isSafeContextCall(node: any): boolean {
23
+ // Lifecycle + effect hooks only run post-mount in a browser.
24
+ // `onUnmount` / `onCleanup` fire after the component has mounted so
25
+ // the DOM exists. `renderEffect` is the signal-system equivalent of
26
+ // `effect`. `requestAnimationFrame` only schedules its callback
27
+ // inside a browser frame, so its body is always post-setup execution.
28
+ return (
29
+ isCallTo(node, 'onMount') ||
30
+ isCallTo(node, 'onUnmount') ||
31
+ isCallTo(node, 'onCleanup') ||
32
+ isCallTo(node, 'effect') ||
33
+ isCallTo(node, 'renderEffect') ||
34
+ isCallTo(node, 'requestAnimationFrame')
35
+ )
36
+ }
37
+ // `if (typeof document === 'undefined') return|throw` at the head of a
38
+ // function makes the rest of the body implicitly browser-safe — the
39
+ // SSR path bailed out. Same heuristic as `no-window-in-ssr`.
40
+ function isNegatedTypeofDocument(test: any): boolean {
41
+ if (!test) return false
42
+ if (
43
+ test.type === 'BinaryExpression' &&
44
+ (test.operator === '===' || test.operator === '==') &&
45
+ test.left?.type === 'UnaryExpression' &&
46
+ test.left.operator === 'typeof' &&
47
+ test.left.argument?.type === 'Identifier' &&
48
+ (test.left.argument.name === 'document' || test.left.argument.name === 'window')
49
+ )
50
+ return true
51
+ return false
52
+ }
53
+ function isEarlyReturnDocumentGuard(stmt: any): boolean {
54
+ if (!stmt || stmt.type !== 'IfStatement') return false
55
+ if (!isNegatedTypeofDocument(stmt.test)) return false
56
+ const c = stmt.consequent
57
+ const isTerminator = (s: any): boolean =>
58
+ s?.type === 'ReturnStatement' || s?.type === 'ThrowStatement'
59
+ if (isTerminator(c)) return true
60
+ if (c?.type === 'BlockStatement' && c.body.length === 1 && isTerminator(c.body[0]))
61
+ return true
62
+ return false
63
+ }
64
+ // Per-function depth bumps from early-return guards — popped on exit.
65
+ const earlyReturnStack: number[] = []
66
+ function pushFunctionScope(node: any) {
67
+ const body = node?.body
68
+ const stmts = body?.type === 'BlockStatement' ? body.body : null
69
+ let bumps = 0
70
+ if (stmts && stmts.length > 0 && isEarlyReturnDocumentGuard(stmts[0])) {
71
+ bumps = 1
72
+ safeDepth++
73
+ }
74
+ earlyReturnStack.push(bumps)
75
+ }
76
+ function popFunctionScope() {
77
+ const bumps = earlyReturnStack.pop() ?? 0
78
+ if (bumps > 0) safeDepth -= bumps
79
+ }
22
80
  const callbacks: VisitorCallbacks = {
81
+ FunctionDeclaration: pushFunctionScope,
82
+ 'FunctionDeclaration:exit': popFunctionScope,
83
+ FunctionExpression: pushFunctionScope,
84
+ 'FunctionExpression:exit': popFunctionScope,
85
+ ArrowFunctionExpression: pushFunctionScope,
86
+ 'ArrowFunctionExpression:exit': popFunctionScope,
23
87
  CallExpression(node: any) {
24
- if (isCallTo(node, 'onMount') || isCallTo(node, 'effect')) {
25
- safeDepth++
26
- }
88
+ if (isSafeContextCall(node)) safeDepth++
27
89
 
28
90
  if (safeDepth > 0) return
29
91
 
@@ -43,9 +105,7 @@ export const noDomInSetup: Rule = {
43
105
  }
44
106
  },
45
107
  'CallExpression:exit'(node: any) {
46
- if (isCallTo(node, 'onMount') || isCallTo(node, 'effect')) {
47
- safeDepth--
48
- }
108
+ if (isSafeContextCall(node)) safeDepth--
49
109
  },
50
110
  }
51
111
  return callbacks
@@ -1,6 +1,17 @@
1
1
  import type { Rule, VisitorCallbacks } from '../../types'
2
2
  import { getSpan } from '../../utils/ast'
3
3
 
4
+ // `use…`/`get…`/`is…`/`has…` are conventional hook/getter prefixes — not
5
+ // signal reads. `[A-Z]…` covers component invocations. The skip-names set
6
+ // covers framework VNode-producing helpers whose call sites always produce
7
+ // JSX, not signal values:
8
+ // - `render` — `@pyreon/ui-core` render-prop helper
9
+ // - `h` — `@pyreon/core` hyperscript (JSX runtime)
10
+ // - `cloneVNode` — `@pyreon/core` VNode-tree cloner (used by kinetic)
11
+ // Matching is on full identifier name, so user-defined signals with these
12
+ // exact names would slip through; rename to `rendered`/`hyperscript`/etc.
13
+ // or move the read outside JSX.
14
+ const SKIP_NAMES = new Set(['render', 'h', 'cloneVNode'])
4
15
  const SKIP_PREFIXES = /^(use|get|is|has|[A-Z])/
5
16
 
6
17
  export const noBareSignalInJsx: Rule = {
@@ -35,7 +46,7 @@ export const noBareSignalInJsx: Rule = {
35
46
  if (!callee || callee.type !== 'Identifier') return
36
47
 
37
48
  const name: string = callee.name
38
- if (SKIP_PREFIXES.test(name)) return
49
+ if (SKIP_NAMES.has(name) || SKIP_PREFIXES.test(name)) return
39
50
 
40
51
  const span = getSpan(node)
41
52
  const source = context.getSourceText()
@@ -1,5 +1,6 @@
1
1
  import type { Rule, VisitorCallbacks } from '../../types'
2
2
  import { getSpan, isCallTo, isSetCall } from '../../utils/ast'
3
+ import { isPathExempt } from '../../utils/exempt-paths'
3
4
 
4
5
  interface ScopeInfo {
5
6
  setCalls: Array<{ span: { start: number; end: number } }>
@@ -15,8 +16,10 @@ export const noUnbatchedUpdates: Rule = {
15
16
  description: 'Warn when 3+ .set() calls occur in the same function without batch().',
16
17
  severity: 'warn',
17
18
  fixable: false,
19
+ schema: { exemptPaths: 'string[]' },
18
20
  },
19
21
  create(context) {
22
+ if (isPathExempt(context)) return {}
20
23
  const scopeStack: ScopeInfo[] = []
21
24
  let batchDepth = 0
22
25
 
@@ -11,73 +11,169 @@ export const noImperativeNavigateInRender: Rule = {
11
11
  fixable: false,
12
12
  },
13
13
  create(context) {
14
- // Track depth of component functions and safe callback wrappers
15
- // We detect components via VariableDeclarator with PascalCase name + ArrowFunctionExpression init,
16
- // or FunctionDeclaration with PascalCase name.
17
- // "Safe" = onMount/effect/onUnmount callbacks or JSX event handlers.
14
+ // The rule fires when `navigate()` / `router.push()` runs synchronously
15
+ // as part of rendering the component the infinite-render-loop case.
16
+ //
17
+ // The render path consists of the component body itself PLUS any nested
18
+ // function that is called synchronously from the render body (IIFEs,
19
+ // locally-bound fns that are invoked immediately). Nested functions
20
+ // that are stored (assigned to a const, returned, passed as a callback
21
+ // to an event handler / setTimeout / .then) are deferred execution and
22
+ // don't contribute to the render-loop bug.
18
23
  let componentBodyDepth = 0
19
- let safeDepth = 0
24
+ // Depth inside a nested non-component function AT ALL — used to scope
25
+ // call-tracking (we only record nested-fn calls while inside a
26
+ // component body, not in module-level code).
27
+ let nestedFnDepth = 0
28
+ // Arrow/Function expressions that are the direct init of a PascalCase
29
+ // `VariableDeclarator` (= component assignment) — marked so the
30
+ // ArrowFn/FunctionExpression visitor knows to NOT count them as nested.
31
+ const componentInits = new WeakSet<any>()
32
+ // Names of locally-bound nested fns (e.g. `const fn = () => router.push(...)`)
33
+ // whose body contains a dangerous navigation call. Populated during the
34
+ // nested fn's walk; checked on synchronous `fn()` calls in the render
35
+ // body. Stack-scoped to the enclosing component so nested components
36
+ // don't leak bindings.
37
+ const dangerousBindings: Array<Set<string>> = []
38
+ // When we're inside a nested fn whose binding might be dangerous, mark
39
+ // the current nested fn as "contains navigate" if we see one.
40
+ const nestedFnStack: Array<{ containsNavigate: boolean; bindingName: string | null }> = []
41
+
42
+ function isComponentFunctionDecl(node: any): boolean {
43
+ return /^[A-Z]/.test(node.id?.name ?? '')
44
+ }
45
+
46
+ function enterNestedFn(node: any, bindingName: string | null) {
47
+ nestedFnDepth++
48
+ nestedFnStack.push({ containsNavigate: false, bindingName })
49
+ }
50
+ function exitNestedFn() {
51
+ nestedFnDepth--
52
+ const frame = nestedFnStack.pop()
53
+ if (!frame) return
54
+ if (frame.containsNavigate && frame.bindingName && dangerousBindings.length > 0) {
55
+ dangerousBindings[dangerousBindings.length - 1]!.add(frame.bindingName)
56
+ }
57
+ }
58
+
59
+ function isNavigateCall(node: any): boolean {
60
+ return isCallTo(node, 'navigate') || isMemberCallTo(node, 'router', 'push')
61
+ }
20
62
 
21
63
  const callbacks: VisitorCallbacks = {
22
64
  FunctionDeclaration(node: any) {
23
- const name: string = node.id?.name ?? ''
24
- if (/^[A-Z]/.test(name)) {
65
+ if (isComponentFunctionDecl(node)) {
25
66
  componentBodyDepth++
67
+ dangerousBindings.push(new Set())
68
+ } else if (componentBodyDepth > 0) {
69
+ enterNestedFn(node, node.id?.type === 'Identifier' ? node.id.name : null)
26
70
  }
27
71
  },
28
72
  'FunctionDeclaration:exit'(node: any) {
29
- const name: string = node.id?.name ?? ''
30
- if (/^[A-Z]/.test(name)) {
73
+ if (isComponentFunctionDecl(node)) {
31
74
  componentBodyDepth--
75
+ dangerousBindings.pop()
76
+ } else if (componentBodyDepth > 0) {
77
+ exitNestedFn()
32
78
  }
33
79
  },
34
- // For arrow functions, we use VariableDeclarator to detect component assignment
35
80
  VariableDeclarator(node: any) {
36
- const name: string = node.id?.name ?? ''
37
- if (/^[A-Z]/.test(name) && node.init?.type === 'ArrowFunctionExpression') {
81
+ if (
82
+ /^[A-Z]/.test(node.id?.name ?? '') &&
83
+ (node.init?.type === 'ArrowFunctionExpression' || node.init?.type === 'FunctionExpression')
84
+ ) {
38
85
  componentBodyDepth++
86
+ dangerousBindings.push(new Set())
87
+ componentInits.add(node.init)
39
88
  }
40
89
  },
41
90
  'VariableDeclarator:exit'(node: any) {
42
- const name: string = node.id?.name ?? ''
43
- if (/^[A-Z]/.test(name) && node.init?.type === 'ArrowFunctionExpression') {
91
+ if (
92
+ /^[A-Z]/.test(node.id?.name ?? '') &&
93
+ (node.init?.type === 'ArrowFunctionExpression' || node.init?.type === 'FunctionExpression')
94
+ ) {
44
95
  componentBodyDepth--
96
+ dangerousBindings.pop()
97
+ }
98
+ },
99
+ ArrowFunctionExpression(node: any) {
100
+ if (componentInits.has(node)) return
101
+ if (componentBodyDepth > 0) {
102
+ // Binding name comes from the parent VariableDeclarator if the
103
+ // arrow is its init — e.g. `const fn = () => …`. Parent is not
104
+ // passed by oxc, so we rely on order: `VariableDeclarator` visits
105
+ // before its init. We patch the binding name in a pre-visitor pass.
106
+ enterNestedFn(node, bindingAssignmentNames.get(node) ?? null)
107
+ }
108
+ },
109
+ 'ArrowFunctionExpression:exit'(node: any) {
110
+ if (componentInits.has(node)) return
111
+ if (componentBodyDepth > 0) exitNestedFn()
112
+ },
113
+ FunctionExpression(node: any) {
114
+ if (componentInits.has(node)) return
115
+ if (componentBodyDepth > 0) {
116
+ enterNestedFn(node, bindingAssignmentNames.get(node) ?? null)
45
117
  }
46
118
  },
47
- // Track safe callback boundaries: onMount(() => ...), effect(() => ...), etc.
119
+ 'FunctionExpression:exit'(node: any) {
120
+ if (componentInits.has(node)) return
121
+ if (componentBodyDepth > 0) exitNestedFn()
122
+ },
48
123
  CallExpression(node: any) {
49
124
  if (componentBodyDepth <= 0) return
50
125
 
51
- // Check if this is a safe wrapper entering
52
- if (isSafeWrapperCall(node)) {
53
- safeDepth++
126
+ // Direct `navigate()` / `router.push()` call. At render-body depth
127
+ // (nestedFnDepth === 0) it's the classic infinite-loop bug. Inside
128
+ // a nested fn, mark the enclosing frame as dangerous (so a later
129
+ // sync call of that fn's binding re-surfaces the issue).
130
+ if (isNavigateCall(node)) {
131
+ if (nestedFnDepth === 0) {
132
+ context.report({
133
+ message:
134
+ 'Imperative navigation at the top level of a component — this runs on every render and causes infinite loops. Move inside `onMount`, `effect`, or an event handler.',
135
+ span: getSpan(node),
136
+ })
137
+ } else if (nestedFnStack.length > 0) {
138
+ nestedFnStack[nestedFnStack.length - 1]!.containsNavigate = true
139
+ }
140
+ return
54
141
  }
55
142
 
56
- // Only report if we're in a component body and NOT inside a safe callback
57
- if (safeDepth > 0) return
58
-
59
- if (isCallTo(node, 'navigate') || isMemberCallTo(node, 'router', 'push')) {
143
+ // Sync invocation (at render-body depth) of a locally-bound fn
144
+ // whose body contains `navigate()`. `const fn = () => router.push();
145
+ // fn()` IS the infinite-loop bug — the previous rewrite missed it.
146
+ if (
147
+ nestedFnDepth === 0 &&
148
+ node.callee?.type === 'Identifier' &&
149
+ dangerousBindings.length > 0 &&
150
+ dangerousBindings[dangerousBindings.length - 1]!.has(node.callee.name)
151
+ ) {
60
152
  context.report({
61
153
  message:
62
- 'Imperative navigation at the top level of a component — this runs on every render and causes infinite loops. Move inside `onMount`, `effect`, or an event handler.',
154
+ 'Synchronous call of a nested function that performs imperative navigation — this runs during render and causes infinite loops. Move the call inside `onMount`, `effect`, or an event handler.',
63
155
  span: getSpan(node),
64
156
  })
65
157
  }
66
158
  },
67
- 'CallExpression:exit'(node: any) {
68
- if (componentBodyDepth <= 0) return
69
- if (isSafeWrapperCall(node)) {
70
- safeDepth--
159
+ }
160
+
161
+ // Pre-walk: for each `VariableDeclarator` whose init is an ArrowFn or
162
+ // FunctionExpression, stash the binding name against the init node so
163
+ // the ArrowFn/FunctionExpression visitor can retrieve it without needing
164
+ // parent-in-visitor (oxc doesn't pass parent).
165
+ const bindingAssignmentNames = new WeakMap<any, string>()
166
+ callbacks.VariableDeclaration = (node: any) => {
167
+ for (const decl of node.declarations ?? []) {
168
+ if (
169
+ decl.id?.type === 'Identifier' &&
170
+ (decl.init?.type === 'ArrowFunctionExpression' ||
171
+ decl.init?.type === 'FunctionExpression')
172
+ ) {
173
+ bindingAssignmentNames.set(decl.init, decl.id.name)
71
174
  }
72
- },
175
+ }
73
176
  }
74
177
  return callbacks
75
178
  },
76
179
  }
77
-
78
- function isSafeWrapperCall(node: any): boolean {
79
- const callee = node.callee
80
- if (!callee || callee.type !== 'Identifier') return false
81
- const name: string = callee.name
82
- return name === 'onMount' || name === 'effect' || name === 'onUnmount'
83
- }