@behavioral-contracts/verify-cli 1.0.0

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 (80) hide show
  1. package/LICENSE +119 -0
  2. package/README.md +694 -0
  3. package/dist/analyze-results.js +253 -0
  4. package/dist/analyzer.d.ts +366 -0
  5. package/dist/analyzer.d.ts.map +1 -0
  6. package/dist/analyzer.js +2592 -0
  7. package/dist/analyzer.js.map +1 -0
  8. package/dist/analyzers/async-error-analyzer.d.ts +72 -0
  9. package/dist/analyzers/async-error-analyzer.d.ts.map +1 -0
  10. package/dist/analyzers/async-error-analyzer.js +243 -0
  11. package/dist/analyzers/async-error-analyzer.js.map +1 -0
  12. package/dist/analyzers/event-listener-analyzer.d.ts +102 -0
  13. package/dist/analyzers/event-listener-analyzer.d.ts.map +1 -0
  14. package/dist/analyzers/event-listener-analyzer.js +253 -0
  15. package/dist/analyzers/event-listener-analyzer.js.map +1 -0
  16. package/dist/analyzers/react-query-analyzer.d.ts +66 -0
  17. package/dist/analyzers/react-query-analyzer.d.ts.map +1 -0
  18. package/dist/analyzers/react-query-analyzer.js +341 -0
  19. package/dist/analyzers/react-query-analyzer.js.map +1 -0
  20. package/dist/analyzers/return-value-analyzer.d.ts +61 -0
  21. package/dist/analyzers/return-value-analyzer.d.ts.map +1 -0
  22. package/dist/analyzers/return-value-analyzer.js +225 -0
  23. package/dist/analyzers/return-value-analyzer.js.map +1 -0
  24. package/dist/code-snippet.d.ts +48 -0
  25. package/dist/code-snippet.d.ts.map +1 -0
  26. package/dist/code-snippet.js +84 -0
  27. package/dist/code-snippet.js.map +1 -0
  28. package/dist/corpus-loader.d.ts +33 -0
  29. package/dist/corpus-loader.d.ts.map +1 -0
  30. package/dist/corpus-loader.js +155 -0
  31. package/dist/corpus-loader.js.map +1 -0
  32. package/dist/fixture-tester.d.ts +28 -0
  33. package/dist/fixture-tester.d.ts.map +1 -0
  34. package/dist/fixture-tester.js +176 -0
  35. package/dist/fixture-tester.js.map +1 -0
  36. package/dist/index.d.ts +6 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +375 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/package-discovery.d.ts +62 -0
  41. package/dist/package-discovery.d.ts.map +1 -0
  42. package/dist/package-discovery.js +299 -0
  43. package/dist/package-discovery.js.map +1 -0
  44. package/dist/reporter.d.ts +43 -0
  45. package/dist/reporter.d.ts.map +1 -0
  46. package/dist/reporter.js +347 -0
  47. package/dist/reporter.js.map +1 -0
  48. package/dist/reporters/benchmarking.d.ts +70 -0
  49. package/dist/reporters/benchmarking.d.ts.map +1 -0
  50. package/dist/reporters/benchmarking.js +191 -0
  51. package/dist/reporters/benchmarking.js.map +1 -0
  52. package/dist/reporters/d3-visualizer.d.ts +40 -0
  53. package/dist/reporters/d3-visualizer.d.ts.map +1 -0
  54. package/dist/reporters/d3-visualizer.js +803 -0
  55. package/dist/reporters/d3-visualizer.js.map +1 -0
  56. package/dist/reporters/health-score.d.ts +33 -0
  57. package/dist/reporters/health-score.d.ts.map +1 -0
  58. package/dist/reporters/health-score.js +149 -0
  59. package/dist/reporters/health-score.js.map +1 -0
  60. package/dist/reporters/index.d.ts +11 -0
  61. package/dist/reporters/index.d.ts.map +1 -0
  62. package/dist/reporters/index.js +11 -0
  63. package/dist/reporters/index.js.map +1 -0
  64. package/dist/reporters/package-breakdown.d.ts +48 -0
  65. package/dist/reporters/package-breakdown.d.ts.map +1 -0
  66. package/dist/reporters/package-breakdown.js +185 -0
  67. package/dist/reporters/package-breakdown.js.map +1 -0
  68. package/dist/reporters/positive-evidence.d.ts +42 -0
  69. package/dist/reporters/positive-evidence.d.ts.map +1 -0
  70. package/dist/reporters/positive-evidence.js +436 -0
  71. package/dist/reporters/positive-evidence.js.map +1 -0
  72. package/dist/tsconfig-generator.d.ts +17 -0
  73. package/dist/tsconfig-generator.d.ts.map +1 -0
  74. package/dist/tsconfig-generator.js +107 -0
  75. package/dist/tsconfig-generator.js.map +1 -0
  76. package/dist/types.d.ts +298 -0
  77. package/dist/types.d.ts.map +1 -0
  78. package/dist/types.js +5 -0
  79. package/dist/types.js.map +1 -0
  80. package/package.json +59 -0
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Event Listener Analyzer
3
+ *
4
+ * Detects missing required event listeners on instances of event-emitting classes.
5
+ * This is a PATTERN-BASED analyzer that works for ANY package requiring event listeners.
6
+ *
7
+ * Examples:
8
+ * - ws.WebSocket requires 'error' listener
9
+ * - bull.Queue requires 'error' and 'failed' listeners
10
+ * - archiver requires 'error' listener
11
+ * - socket.io.Server requires 'error' listener
12
+ *
13
+ * Contract Configuration:
14
+ * ```yaml
15
+ * detection:
16
+ * class_names: ["WebSocket"]
17
+ * require_instance_tracking: true
18
+ * required_event_listeners:
19
+ * - event: "error"
20
+ * required: true
21
+ * severity: error
22
+ * ```
23
+ */
24
+ import * as ts from 'typescript';
25
+ export class EventListenerAnalyzer {
26
+ sourceFile;
27
+ contracts;
28
+ // private typeChecker: ts.TypeChecker; // Reserved for future type-aware detection
29
+ // Track instances requiring event listeners
30
+ trackedInstances = new Map();
31
+ constructor(sourceFile, contracts, _typeChecker // Prefixed with _ to indicate intentionally unused
32
+ ) {
33
+ this.sourceFile = sourceFile;
34
+ this.contracts = contracts;
35
+ // this.typeChecker = typeChecker; // Reserved for future type-aware detection
36
+ }
37
+ /**
38
+ * Main analysis entry point
39
+ *
40
+ * Steps:
41
+ * 1. Find all instance declarations from contracts with required_event_listeners
42
+ * 2. Find all .on(), .addEventListener(), .once() calls for tracked instances
43
+ * 3. Check if all required listeners are attached
44
+ * 4. Report violations for missing required listeners
45
+ */
46
+ analyze(functionNode) {
47
+ const violations = [];
48
+ // Step 1: Find all instance declarations requiring event listeners
49
+ this.findInstanceDeclarations(functionNode);
50
+ // Step 2: Find all event listener attachments
51
+ this.findEventListeners(functionNode);
52
+ // Step 3: Validate all required listeners are attached
53
+ for (const [varName, instance] of this.trackedInstances.entries()) {
54
+ for (const requiredListener of instance.requiredListeners) {
55
+ if (!instance.attachedEvents.has(requiredListener.event)) {
56
+ violations.push({
57
+ packageName: instance.packageName,
58
+ className: instance.className,
59
+ variableName: varName,
60
+ missingEvent: requiredListener.event,
61
+ requiredListener,
62
+ declarationNode: instance.declarationNode,
63
+ });
64
+ }
65
+ }
66
+ }
67
+ // Clear tracking for next function
68
+ this.trackedInstances.clear();
69
+ return violations;
70
+ }
71
+ /**
72
+ * Find all instance declarations from contracts with required event listeners
73
+ *
74
+ * Patterns detected:
75
+ * - const ws = new WebSocket(url)
76
+ * - const queue = new Queue('tasks')
77
+ * - this.client = axios.create()
78
+ */
79
+ findInstanceDeclarations(node) {
80
+ const self = this;
81
+ function visit(node) {
82
+ // Pattern: const ws = new WebSocket(url)
83
+ if (ts.isVariableDeclaration(node) && node.initializer) {
84
+ self.checkNewExpression(node);
85
+ }
86
+ // Pattern: this.ws = new WebSocket(url)
87
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
88
+ if (ts.isNewExpression(node.right)) {
89
+ self.checkNewExpressionForAssignment(node);
90
+ }
91
+ }
92
+ ts.forEachChild(node, visit);
93
+ }
94
+ visit(node);
95
+ }
96
+ /**
97
+ * Check if a new expression creates an instance requiring event listeners
98
+ */
99
+ checkNewExpression(declaration) {
100
+ if (!declaration.initializer || !ts.isNewExpression(declaration.initializer)) {
101
+ return;
102
+ }
103
+ const newExpr = declaration.initializer;
104
+ const className = this.getClassName(newExpr);
105
+ if (!className)
106
+ return;
107
+ // Check if any contract declares this class with required event listeners
108
+ for (const [packageName, contract] of this.contracts.entries()) {
109
+ if (!contract.detection?.required_event_listeners)
110
+ continue;
111
+ if (!contract.detection.class_names?.includes(className))
112
+ continue;
113
+ // This class requires event listeners - track it
114
+ const varName = declaration.name.getText(this.sourceFile);
115
+ this.trackedInstances.set(varName, {
116
+ packageName,
117
+ className,
118
+ requiredListeners: contract.detection.required_event_listeners,
119
+ declarationNode: declaration,
120
+ attachedEvents: new Set(),
121
+ });
122
+ }
123
+ }
124
+ /**
125
+ * Check assignment expressions like: this.ws = new WebSocket(url)
126
+ */
127
+ checkNewExpressionForAssignment(assignment) {
128
+ if (!ts.isNewExpression(assignment.right))
129
+ return;
130
+ const newExpr = assignment.right;
131
+ const className = this.getClassName(newExpr);
132
+ if (!className)
133
+ return;
134
+ // Check if any contract declares this class with required event listeners
135
+ for (const [packageName, contract] of this.contracts.entries()) {
136
+ if (!contract.detection?.required_event_listeners)
137
+ continue;
138
+ if (!contract.detection.class_names?.includes(className))
139
+ continue;
140
+ // Extract variable name from left side (e.g., "ws" from "this.ws")
141
+ const varName = this.getVariableNameFromExpression(assignment.left);
142
+ if (!varName)
143
+ return;
144
+ this.trackedInstances.set(varName, {
145
+ packageName,
146
+ className,
147
+ requiredListeners: contract.detection.required_event_listeners,
148
+ declarationNode: assignment,
149
+ attachedEvents: new Set(),
150
+ });
151
+ }
152
+ }
153
+ /**
154
+ * Extract class name from new expression
155
+ * Examples:
156
+ * - new WebSocket(url) → "WebSocket"
157
+ * - new Queue('tasks') → "Queue"
158
+ */
159
+ getClassName(newExpr) {
160
+ const expr = newExpr.expression;
161
+ if (ts.isIdentifier(expr)) {
162
+ return expr.text;
163
+ }
164
+ if (ts.isPropertyAccessExpression(expr)) {
165
+ return expr.name.text;
166
+ }
167
+ return null;
168
+ }
169
+ /**
170
+ * Extract variable name from expression
171
+ * Examples:
172
+ * - this.ws → "ws"
173
+ * - connection → "connection"
174
+ */
175
+ getVariableNameFromExpression(expr) {
176
+ if (ts.isIdentifier(expr)) {
177
+ return expr.text;
178
+ }
179
+ if (ts.isPropertyAccessExpression(expr)) {
180
+ return expr.name.text;
181
+ }
182
+ return null;
183
+ }
184
+ /**
185
+ * Find all event listener attachments
186
+ *
187
+ * Patterns detected:
188
+ * - ws.on('error', handler)
189
+ * - ws.addEventListener('error', handler)
190
+ * - ws.once('error', handler)
191
+ * - this.ws.on('error', handler)
192
+ */
193
+ findEventListeners(node) {
194
+ const self = this;
195
+ function visit(node) {
196
+ if (ts.isCallExpression(node)) {
197
+ self.checkEventListenerCall(node);
198
+ }
199
+ ts.forEachChild(node, visit);
200
+ }
201
+ visit(node);
202
+ }
203
+ /**
204
+ * Check if a call expression is attaching an event listener
205
+ */
206
+ checkEventListenerCall(call) {
207
+ // Must be a method call like ws.on(...)
208
+ if (!ts.isPropertyAccessExpression(call.expression)) {
209
+ return;
210
+ }
211
+ const propAccess = call.expression;
212
+ const methodName = propAccess.name.text;
213
+ // Only interested in event listener methods
214
+ if (!['on', 'addEventListener', 'once'].includes(methodName)) {
215
+ return;
216
+ }
217
+ // Extract variable name (e.g., "ws" from "ws.on(...)")
218
+ const varName = this.getVariableNameFromExpression(propAccess.expression);
219
+ if (!varName)
220
+ return;
221
+ // Check if this variable is tracked
222
+ const instance = this.trackedInstances.get(varName);
223
+ if (!instance)
224
+ return;
225
+ // Extract event name from first argument
226
+ const eventName = this.getEventName(call);
227
+ if (!eventName)
228
+ return;
229
+ // Mark this event as attached
230
+ instance.attachedEvents.add(eventName);
231
+ }
232
+ /**
233
+ * Extract event name from listener call
234
+ * Examples:
235
+ * - ws.on('error', handler) → "error"
236
+ * - ws.addEventListener("message", handler) → "message"
237
+ */
238
+ getEventName(call) {
239
+ if (call.arguments.length === 0)
240
+ return null;
241
+ const firstArg = call.arguments[0];
242
+ // String literal: 'error'
243
+ if (ts.isStringLiteral(firstArg)) {
244
+ return firstArg.text;
245
+ }
246
+ // No string literal: "message"
247
+ if (ts.isNoSubstitutionTemplateLiteral(firstArg)) {
248
+ return firstArg.text;
249
+ }
250
+ return null;
251
+ }
252
+ }
253
+ //# sourceMappingURL=event-listener-analyzer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-listener-analyzer.js","sourceRoot":"","sources":["../../src/analyzers/event-listener-analyzer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * React Query specific analyzer
3
+ * Detects error handling patterns for useQuery, useMutation, useInfiniteQuery
4
+ */
5
+ import * as ts from 'typescript';
6
+ import type { HookCall, VariableUsage, HookErrorHandling } from '../types.js';
7
+ export declare class ReactQueryAnalyzer {
8
+ private sourceFile;
9
+ constructor(sourceFile: ts.SourceFile, _checker: ts.TypeChecker);
10
+ /**
11
+ * Detects if a call expression is a React Query hook
12
+ */
13
+ isReactQueryHook(node: ts.CallExpression): string | null;
14
+ /**
15
+ * Detects if a new expression is QueryClient
16
+ */
17
+ isQueryClient(node: ts.NewExpression): boolean;
18
+ /**
19
+ * Extracts hook call information including options and return values
20
+ */
21
+ extractHookCall(node: ts.CallExpression, hookName: string): HookCall | null;
22
+ /**
23
+ * Parses hook options object to detect callbacks
24
+ */
25
+ private parseHookOptions;
26
+ /**
27
+ * Parses retry option to determine type
28
+ */
29
+ private parseRetryOption;
30
+ /**
31
+ * Parses destructured return values from hook
32
+ */
33
+ private parseReturnValues;
34
+ /**
35
+ * Tracks how a variable is used in the component scope
36
+ */
37
+ trackVariableUsage(variableName: string, componentNode: ts.Node): VariableUsage;
38
+ /**
39
+ * Checks if an expression references a specific variable
40
+ */
41
+ private referencesVariable;
42
+ /**
43
+ * Analyzes error handling for a React Query hook call
44
+ */
45
+ analyzeHookErrorHandling(hookCall: HookCall, componentNode: ts.Node): HookErrorHandling;
46
+ /**
47
+ * Finds the component (function) containing a node
48
+ */
49
+ findContainingComponent(node: ts.Node): ts.Node | null;
50
+ /**
51
+ * Heuristic to check if a function looks like a React component
52
+ */
53
+ private looksLikeComponent;
54
+ /**
55
+ * Detects QueryClient configuration and global error handlers
56
+ */
57
+ detectGlobalHandlers(sourceFile: ts.SourceFile): {
58
+ hasQueryCacheOnError: boolean;
59
+ hasMutationCacheOnError: boolean;
60
+ };
61
+ /**
62
+ * Checks if a QueryCache/MutationCache has onError callback
63
+ */
64
+ private hasOnErrorCallback;
65
+ }
66
+ //# sourceMappingURL=react-query-analyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-query-analyzer.d.ts","sourceRoot":"","sources":["../../src/analyzers/react-query-analyzer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAE9E,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,UAAU,CAAgB;gBAEtB,UAAU,EAAE,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,EAAE,CAAC,WAAW;IAI/D;;OAEG;IACH,gBAAgB,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,GAAG,MAAM,GAAG,IAAI;IAexD;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,GAAG,OAAO;IAQ9C;;OAEG;IACH,eAAe,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,EAAE,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IA+B3E;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAwBxB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAaxB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAqBzB;;OAEG;IACH,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,EAAE,CAAC,IAAI,GAAG,aAAa;IA0D/E;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA2B1B;;OAEG;IACH,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,CAAC,IAAI,GAAG,iBAAiB;IAgDvF;;OAEG;IACH,uBAAuB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,IAAI,GAAG,IAAI;IAiBtD;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAmB1B;;OAEG;IACH,oBAAoB,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,GAAG;QAC/C,oBAAoB,EAAE,OAAO,CAAC;QAC9B,uBAAuB,EAAE,OAAO,CAAC;KAClC;IAoCD;;OAEG;IACH,OAAO,CAAC,kBAAkB;CAgB3B"}
@@ -0,0 +1,341 @@
1
+ /**
2
+ * React Query specific analyzer
3
+ * Detects error handling patterns for useQuery, useMutation, useInfiniteQuery
4
+ */
5
+ import * as ts from 'typescript';
6
+ export class ReactQueryAnalyzer {
7
+ sourceFile;
8
+ constructor(sourceFile, _checker) {
9
+ this.sourceFile = sourceFile;
10
+ }
11
+ /**
12
+ * Detects if a call expression is a React Query hook
13
+ */
14
+ isReactQueryHook(node) {
15
+ if (!ts.isIdentifier(node.expression)) {
16
+ return null;
17
+ }
18
+ const functionName = node.expression.text;
19
+ const reactQueryHooks = ['useQuery', 'useMutation', 'useInfiniteQuery'];
20
+ if (reactQueryHooks.includes(functionName)) {
21
+ return functionName;
22
+ }
23
+ return null;
24
+ }
25
+ /**
26
+ * Detects if a new expression is QueryClient
27
+ */
28
+ isQueryClient(node) {
29
+ if (!ts.isIdentifier(node.expression)) {
30
+ return false;
31
+ }
32
+ return node.expression.text === 'QueryClient';
33
+ }
34
+ /**
35
+ * Extracts hook call information including options and return values
36
+ */
37
+ extractHookCall(node, hookName) {
38
+ const location = this.sourceFile.getLineAndCharacterOfPosition(node.getStart());
39
+ const hookCall = {
40
+ hookName: hookName,
41
+ location: {
42
+ file: this.sourceFile.fileName,
43
+ line: location.line + 1,
44
+ column: location.character + 1,
45
+ },
46
+ returnValues: new Map(),
47
+ options: {},
48
+ };
49
+ // Extract options object (first argument for hooks)
50
+ if (node.arguments.length > 0) {
51
+ const optionsArg = node.arguments[0];
52
+ if (ts.isObjectLiteralExpression(optionsArg)) {
53
+ this.parseHookOptions(optionsArg, hookCall);
54
+ }
55
+ }
56
+ // Extract return values from destructuring
57
+ const parent = node.parent;
58
+ if (parent && ts.isVariableDeclaration(parent)) {
59
+ this.parseReturnValues(parent, hookCall);
60
+ }
61
+ return hookCall;
62
+ }
63
+ /**
64
+ * Parses hook options object to detect callbacks
65
+ */
66
+ parseHookOptions(options, hookCall) {
67
+ for (const property of options.properties) {
68
+ if (!ts.isPropertyAssignment(property))
69
+ continue;
70
+ if (!ts.isIdentifier(property.name))
71
+ continue;
72
+ const propName = property.name.text;
73
+ switch (propName) {
74
+ case 'onError':
75
+ hookCall.options.onError = true;
76
+ break;
77
+ case 'onMutate':
78
+ hookCall.options.onMutate = true;
79
+ break;
80
+ case 'onSuccess':
81
+ hookCall.options.onSuccess = true;
82
+ break;
83
+ case 'retry':
84
+ hookCall.options.retry = this.parseRetryOption(property.initializer);
85
+ break;
86
+ }
87
+ }
88
+ }
89
+ /**
90
+ * Parses retry option to determine type
91
+ */
92
+ parseRetryOption(node) {
93
+ if (ts.isNumericLiteral(node)) {
94
+ return 'number';
95
+ }
96
+ if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) {
97
+ return 'boolean';
98
+ }
99
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
100
+ return 'function';
101
+ }
102
+ return 'default';
103
+ }
104
+ /**
105
+ * Parses destructured return values from hook
106
+ */
107
+ parseReturnValues(declaration, hookCall) {
108
+ if (!declaration.name || !ts.isObjectBindingPattern(declaration.name)) {
109
+ return;
110
+ }
111
+ for (const element of declaration.name.elements) {
112
+ if (!ts.isBindingElement(element))
113
+ continue;
114
+ // Handle both: { error } and { error: customError }
115
+ const propertyName = element.propertyName
116
+ ? (ts.isIdentifier(element.propertyName) ? element.propertyName.text : '')
117
+ : (ts.isIdentifier(element.name) ? element.name.text : '');
118
+ const variableName = ts.isIdentifier(element.name) ? element.name.text : '';
119
+ if (propertyName && variableName) {
120
+ hookCall.returnValues.set(variableName, propertyName);
121
+ }
122
+ }
123
+ }
124
+ /**
125
+ * Tracks how a variable is used in the component scope
126
+ */
127
+ trackVariableUsage(variableName, componentNode) {
128
+ const usage = {
129
+ variableName,
130
+ propertyName: '',
131
+ declaredAt: {
132
+ file: this.sourceFile.fileName,
133
+ line: 0,
134
+ },
135
+ usedIn: {
136
+ conditionals: 0,
137
+ jsxExpressions: 0,
138
+ callbacks: 0,
139
+ },
140
+ };
141
+ const self = this;
142
+ // Walk the component to find variable usage
143
+ function visit(node) {
144
+ // Check for if statements: if (isError) { ... }
145
+ if (ts.isIfStatement(node)) {
146
+ const condition = node.expression;
147
+ if (self.referencesVariable(condition, variableName)) {
148
+ usage.usedIn.conditionals++;
149
+ }
150
+ }
151
+ // Check for ternary: isError ? <Error /> : <Success />
152
+ if (ts.isConditionalExpression(node)) {
153
+ if (self.referencesVariable(node.condition, variableName)) {
154
+ usage.usedIn.conditionals++;
155
+ }
156
+ }
157
+ // Check for JSX: {isError && <Error />}
158
+ if (ts.isJsxExpression(node)) {
159
+ if (node.expression && self.referencesVariable(node.expression, variableName)) {
160
+ usage.usedIn.jsxExpressions++;
161
+ }
162
+ }
163
+ // Check for logical expressions: isError && doSomething()
164
+ if (ts.isBinaryExpression(node)) {
165
+ if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
166
+ node.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
167
+ if (self.referencesVariable(node.left, variableName)) {
168
+ usage.usedIn.conditionals++;
169
+ }
170
+ }
171
+ }
172
+ ts.forEachChild(node, visit);
173
+ }
174
+ visit(componentNode);
175
+ return usage;
176
+ }
177
+ /**
178
+ * Checks if an expression references a specific variable
179
+ */
180
+ referencesVariable(node, variableName) {
181
+ if (ts.isIdentifier(node) && node.text === variableName) {
182
+ return true;
183
+ }
184
+ // Check for property access: error.message
185
+ if (ts.isPropertyAccessExpression(node)) {
186
+ if (ts.isIdentifier(node.expression) && node.expression.text === variableName) {
187
+ return true;
188
+ }
189
+ }
190
+ // Check for optional chaining: error?.message
191
+ if (ts.isNonNullExpression(node)) {
192
+ return this.referencesVariable(node.expression, variableName);
193
+ }
194
+ let found = false;
195
+ ts.forEachChild(node, (child) => {
196
+ if (this.referencesVariable(child, variableName)) {
197
+ found = true;
198
+ }
199
+ });
200
+ return found;
201
+ }
202
+ /**
203
+ * Analyzes error handling for a React Query hook call
204
+ */
205
+ analyzeHookErrorHandling(hookCall, componentNode) {
206
+ const analysis = {
207
+ hasErrorStateCheck: false,
208
+ hasOnErrorCallback: false,
209
+ hasGlobalHandler: false, // TODO: Detect from QueryClient
210
+ errorCheckedBeforeDataAccess: false,
211
+ };
212
+ // Check if onError callback is present
213
+ if (hookCall.options.onError) {
214
+ analysis.hasOnErrorCallback = true;
215
+ }
216
+ // Track error/isError variables
217
+ const errorVars = Array.from(hookCall.returnValues.entries())
218
+ .filter(([_, propName]) => propName === 'error' || propName === 'isError')
219
+ .map(([varName, _]) => varName);
220
+ // Check if any error variables are used
221
+ for (const errorVar of errorVars) {
222
+ const usage = this.trackVariableUsage(errorVar, componentNode);
223
+ if (usage.usedIn.conditionals > 0 || usage.usedIn.jsxExpressions > 0) {
224
+ analysis.hasErrorStateCheck = true;
225
+ break;
226
+ }
227
+ }
228
+ // Check for optimistic update pattern (onMutate + onError)
229
+ if (hookCall.hookName === 'useMutation') {
230
+ if (hookCall.options.onMutate && hookCall.options.onError) {
231
+ // TODO: Verify rollback logic in onError
232
+ analysis.hasOptimisticUpdateRollback = true;
233
+ }
234
+ else if (hookCall.options.onMutate && !hookCall.options.onError) {
235
+ analysis.hasOptimisticUpdateRollback = false;
236
+ }
237
+ }
238
+ // Analyze retry configuration
239
+ if (hookCall.options.retry) {
240
+ analysis.retryAnalysis = {
241
+ type: hookCall.options.retry,
242
+ avoidsClientErrors: hookCall.options.retry === 'function', // Assume function checks status
243
+ };
244
+ }
245
+ return analysis;
246
+ }
247
+ /**
248
+ * Finds the component (function) containing a node
249
+ */
250
+ findContainingComponent(node) {
251
+ let current = node.parent;
252
+ while (current) {
253
+ // Check for function component
254
+ if (ts.isFunctionDeclaration(current) ||
255
+ ts.isFunctionExpression(current) ||
256
+ ts.isArrowFunction(current)) {
257
+ // Verify it looks like a React component (PascalCase name or returns JSX)
258
+ if (this.looksLikeComponent(current)) {
259
+ return current;
260
+ }
261
+ }
262
+ current = current.parent;
263
+ }
264
+ return null;
265
+ }
266
+ /**
267
+ * Heuristic to check if a function looks like a React component
268
+ */
269
+ looksLikeComponent(node) {
270
+ // Check for PascalCase name
271
+ if (ts.isFunctionDeclaration(node) && node.name) {
272
+ const name = node.name.text;
273
+ if (name && name[0] === name[0].toUpperCase()) {
274
+ return true;
275
+ }
276
+ }
277
+ // Check if it returns JSX
278
+ // This is a simplified check - a full check would need to walk the function body
279
+ const bodyText = node.body?.getText(this.sourceFile) || '';
280
+ if (bodyText.includes('<') && bodyText.includes('/>')) {
281
+ return true;
282
+ }
283
+ return false;
284
+ }
285
+ /**
286
+ * Detects QueryClient configuration and global error handlers
287
+ */
288
+ detectGlobalHandlers(sourceFile) {
289
+ const result = {
290
+ hasQueryCacheOnError: false,
291
+ hasMutationCacheOnError: false,
292
+ };
293
+ const self = this;
294
+ function visit(node) {
295
+ // Look for: new QueryClient({ ... })
296
+ if (ts.isNewExpression(node) && self.isQueryClient(node)) {
297
+ if (node.arguments && node.arguments.length > 0) {
298
+ const config = node.arguments[0];
299
+ if (ts.isObjectLiteralExpression(config)) {
300
+ // Check for queryCache and mutationCache
301
+ for (const prop of config.properties) {
302
+ if (!ts.isPropertyAssignment(prop))
303
+ continue;
304
+ if (!ts.isIdentifier(prop.name))
305
+ continue;
306
+ if (prop.name.text === 'queryCache') {
307
+ result.hasQueryCacheOnError = self.hasOnErrorCallback(prop.initializer);
308
+ }
309
+ else if (prop.name.text === 'mutationCache') {
310
+ result.hasMutationCacheOnError = self.hasOnErrorCallback(prop.initializer);
311
+ }
312
+ }
313
+ }
314
+ }
315
+ }
316
+ ts.forEachChild(node, visit);
317
+ }
318
+ visit(sourceFile);
319
+ return result;
320
+ }
321
+ /**
322
+ * Checks if a QueryCache/MutationCache has onError callback
323
+ */
324
+ hasOnErrorCallback(node) {
325
+ // Look for: new QueryCache({ onError: ... })
326
+ if (ts.isNewExpression(node) && node.arguments && node.arguments.length > 0) {
327
+ const config = node.arguments[0];
328
+ if (ts.isObjectLiteralExpression(config)) {
329
+ for (const prop of config.properties) {
330
+ if (ts.isPropertyAssignment(prop) &&
331
+ ts.isIdentifier(prop.name) &&
332
+ prop.name.text === 'onError') {
333
+ return true;
334
+ }
335
+ }
336
+ }
337
+ }
338
+ return false;
339
+ }
340
+ }
341
+ //# sourceMappingURL=react-query-analyzer.js.map