@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.
- package/LICENSE +119 -0
- package/README.md +694 -0
- package/dist/analyze-results.js +253 -0
- package/dist/analyzer.d.ts +366 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +2592 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/analyzers/async-error-analyzer.d.ts +72 -0
- package/dist/analyzers/async-error-analyzer.d.ts.map +1 -0
- package/dist/analyzers/async-error-analyzer.js +243 -0
- package/dist/analyzers/async-error-analyzer.js.map +1 -0
- package/dist/analyzers/event-listener-analyzer.d.ts +102 -0
- package/dist/analyzers/event-listener-analyzer.d.ts.map +1 -0
- package/dist/analyzers/event-listener-analyzer.js +253 -0
- package/dist/analyzers/event-listener-analyzer.js.map +1 -0
- package/dist/analyzers/react-query-analyzer.d.ts +66 -0
- package/dist/analyzers/react-query-analyzer.d.ts.map +1 -0
- package/dist/analyzers/react-query-analyzer.js +341 -0
- package/dist/analyzers/react-query-analyzer.js.map +1 -0
- package/dist/analyzers/return-value-analyzer.d.ts +61 -0
- package/dist/analyzers/return-value-analyzer.d.ts.map +1 -0
- package/dist/analyzers/return-value-analyzer.js +225 -0
- package/dist/analyzers/return-value-analyzer.js.map +1 -0
- package/dist/code-snippet.d.ts +48 -0
- package/dist/code-snippet.d.ts.map +1 -0
- package/dist/code-snippet.js +84 -0
- package/dist/code-snippet.js.map +1 -0
- package/dist/corpus-loader.d.ts +33 -0
- package/dist/corpus-loader.d.ts.map +1 -0
- package/dist/corpus-loader.js +155 -0
- package/dist/corpus-loader.js.map +1 -0
- package/dist/fixture-tester.d.ts +28 -0
- package/dist/fixture-tester.d.ts.map +1 -0
- package/dist/fixture-tester.js +176 -0
- package/dist/fixture-tester.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +375 -0
- package/dist/index.js.map +1 -0
- package/dist/package-discovery.d.ts +62 -0
- package/dist/package-discovery.d.ts.map +1 -0
- package/dist/package-discovery.js +299 -0
- package/dist/package-discovery.js.map +1 -0
- package/dist/reporter.d.ts +43 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +347 -0
- package/dist/reporter.js.map +1 -0
- package/dist/reporters/benchmarking.d.ts +70 -0
- package/dist/reporters/benchmarking.d.ts.map +1 -0
- package/dist/reporters/benchmarking.js +191 -0
- package/dist/reporters/benchmarking.js.map +1 -0
- package/dist/reporters/d3-visualizer.d.ts +40 -0
- package/dist/reporters/d3-visualizer.d.ts.map +1 -0
- package/dist/reporters/d3-visualizer.js +803 -0
- package/dist/reporters/d3-visualizer.js.map +1 -0
- package/dist/reporters/health-score.d.ts +33 -0
- package/dist/reporters/health-score.d.ts.map +1 -0
- package/dist/reporters/health-score.js +149 -0
- package/dist/reporters/health-score.js.map +1 -0
- package/dist/reporters/index.d.ts +11 -0
- package/dist/reporters/index.d.ts.map +1 -0
- package/dist/reporters/index.js +11 -0
- package/dist/reporters/index.js.map +1 -0
- package/dist/reporters/package-breakdown.d.ts +48 -0
- package/dist/reporters/package-breakdown.d.ts.map +1 -0
- package/dist/reporters/package-breakdown.js +185 -0
- package/dist/reporters/package-breakdown.js.map +1 -0
- package/dist/reporters/positive-evidence.d.ts +42 -0
- package/dist/reporters/positive-evidence.d.ts.map +1 -0
- package/dist/reporters/positive-evidence.js +436 -0
- package/dist/reporters/positive-evidence.js.map +1 -0
- package/dist/tsconfig-generator.d.ts +17 -0
- package/dist/tsconfig-generator.d.ts.map +1 -0
- package/dist/tsconfig-generator.js +107 -0
- package/dist/tsconfig-generator.js.map +1 -0
- package/dist/types.d.ts +298 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- 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
|