@kernlang/review 3.3.4 → 3.3.6
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/dist/cache.js +1 -1
- package/dist/concept-rules/auth-drift.d.ts +29 -0
- package/dist/concept-rules/auth-drift.js +127 -0
- package/dist/concept-rules/auth-drift.js.map +1 -0
- package/dist/concept-rules/contract-drift.d.ts +21 -0
- package/dist/concept-rules/contract-drift.js +65 -0
- package/dist/concept-rules/contract-drift.js.map +1 -0
- package/dist/concept-rules/contract-method-drift.d.ts +22 -0
- package/dist/concept-rules/contract-method-drift.js +105 -0
- package/dist/concept-rules/contract-method-drift.js.map +1 -0
- package/dist/concept-rules/cross-stack-utils.d.ts +96 -0
- package/dist/concept-rules/cross-stack-utils.js +259 -0
- package/dist/concept-rules/cross-stack-utils.js.map +1 -0
- package/dist/concept-rules/duplicate-route.d.ts +20 -0
- package/dist/concept-rules/duplicate-route.js +112 -0
- package/dist/concept-rules/duplicate-route.js.map +1 -0
- package/dist/concept-rules/index.js +26 -1
- package/dist/concept-rules/index.js.map +1 -1
- package/dist/concept-rules/missing-response-model.d.ts +10 -0
- package/dist/concept-rules/missing-response-model.js +38 -0
- package/dist/concept-rules/missing-response-model.js.map +1 -0
- package/dist/concept-rules/orphan-route.d.ts +20 -0
- package/dist/concept-rules/orphan-route.js +96 -0
- package/dist/concept-rules/orphan-route.js.map +1 -0
- package/dist/concept-rules/sync-handler-does-io.d.ts +9 -0
- package/dist/concept-rules/sync-handler-does-io.js +56 -0
- package/dist/concept-rules/sync-handler-does-io.js.map +1 -0
- package/dist/concept-rules/tainted-across-wire.d.ts +33 -0
- package/dist/concept-rules/tainted-across-wire.js +95 -0
- package/dist/concept-rules/tainted-across-wire.js.map +1 -0
- package/dist/concept-rules/untyped-api-response.d.ts +30 -0
- package/dist/concept-rules/untyped-api-response.js +73 -0
- package/dist/concept-rules/untyped-api-response.js.map +1 -0
- package/dist/concept-rules/untyped-both-ends-response.d.ts +10 -0
- package/dist/concept-rules/untyped-both-ends-response.js +55 -0
- package/dist/concept-rules/untyped-both-ends-response.js.map +1 -0
- package/dist/external-tools.d.ts +17 -4
- package/dist/external-tools.js +12 -1
- package/dist/external-tools.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +115 -9
- package/dist/index.js.map +1 -1
- package/dist/llm-bridge.d.ts +38 -1
- package/dist/llm-bridge.js +172 -12
- package/dist/llm-bridge.js.map +1 -1
- package/dist/llm-review.js +29 -11
- package/dist/llm-review.js.map +1 -1
- package/dist/mappers/ts-concepts.js +650 -11
- package/dist/mappers/ts-concepts.js.map +1 -1
- package/dist/rules/index.js +17 -1
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/kern-source.js +37 -5
- package/dist/rules/kern-source.js.map +1 -1
- package/dist/rules/set-setter-collision.d.ts +21 -0
- package/dist/rules/set-setter-collision.js +74 -0
- package/dist/rules/set-setter-collision.js.map +1 -0
- package/dist/rules/suggest-kern-primitive.d.ts +30 -0
- package/dist/rules/suggest-kern-primitive.js +543 -0
- package/dist/rules/suggest-kern-primitive.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -10,6 +10,24 @@ const EXTRACTOR_VERSION = '1.0.0';
|
|
|
10
10
|
// ── Network effect signatures ────────────────────────────────────────────
|
|
11
11
|
const NETWORK_CALLS = new Set(['fetch', 'axios', 'got', 'request', 'superagent', 'ky']);
|
|
12
12
|
const NETWORK_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'request']);
|
|
13
|
+
// ── Wrapped HTTP-client detection ────────────────────────────────────────
|
|
14
|
+
// ~75% of production React/Next/Expo apps route through a wrapper (custom
|
|
15
|
+
// ApiClient class, axios.create instance, tRPC-generated client). The fixed
|
|
16
|
+
// NETWORK_CALLS set misses those, so the cross-stack wedge rules
|
|
17
|
+
// (contract-drift, untyped-api-response, tainted-across-wire) silently
|
|
18
|
+
// find nothing on real repos. collectClientIdentifiers() scans the file
|
|
19
|
+
// for wrapper patterns and returns the local identifiers that behave like
|
|
20
|
+
// HTTP clients; extractEffects() then treats `<name>.get/post/…` calls on
|
|
21
|
+
// those identifiers as network effects.
|
|
22
|
+
const CLIENT_HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete']);
|
|
23
|
+
// Names considered client-shaped for imported identifiers. Kept narrow on
|
|
24
|
+
// purpose — false positives here cascade into the wedge rules and poison
|
|
25
|
+
// the pitch. Matches: api, http, client, apiClient, httpClient, fetcher,
|
|
26
|
+
// requester, ApiClient, HttpClient, MyApiClient, etc.
|
|
27
|
+
const CLIENT_NAME_PATTERN = /^(api|http|client|apiClient|httpClient|fetcher|requester)$|Client$/;
|
|
28
|
+
// Wrapper factories — if a variable is initialized with one of these calls,
|
|
29
|
+
// the variable is a client instance (axios.create, ky.create, got.extend).
|
|
30
|
+
const CLIENT_FACTORY_CALLS = new Set(['axios.create', 'ky.create', 'ky.extend', 'got.extend']);
|
|
13
31
|
const DB_CALLS = new Set([
|
|
14
32
|
'query',
|
|
15
33
|
'execute',
|
|
@@ -237,6 +255,7 @@ function hasIntentComment(text) {
|
|
|
237
255
|
}
|
|
238
256
|
// ── effect ───────────────────────────────────────────────────────────────
|
|
239
257
|
function extractEffects(sf, filePath, nodes) {
|
|
258
|
+
const clientIdents = collectClientIdentifiers(sf);
|
|
240
259
|
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
241
260
|
const callee = call.getExpression();
|
|
242
261
|
let funcName = '';
|
|
@@ -250,18 +269,29 @@ function extractEffects(sf, filePath, nodes) {
|
|
|
250
269
|
objName = pa.getExpression().getText();
|
|
251
270
|
}
|
|
252
271
|
// Network effects
|
|
253
|
-
|
|
254
|
-
|
|
272
|
+
const isDirectNetwork = NETWORK_CALLS.has(funcName);
|
|
273
|
+
const isKnownLibraryMethod = NETWORK_METHODS.has(funcName) && /axios|got|ky|http|request|superagent/i.test(objName);
|
|
274
|
+
const isWrappedClientCall = CLIENT_HTTP_METHODS.has(funcName) && clientIdents.has(objName);
|
|
275
|
+
if (isDirectNetwork || isKnownLibraryMethod || isWrappedClientCall) {
|
|
255
276
|
const isAsync = isInAsyncContext(call);
|
|
256
277
|
nodes.push({
|
|
257
278
|
id: conceptId(filePath, 'effect', call.getStart()),
|
|
258
279
|
kind: 'effect',
|
|
259
280
|
primarySpan: span(filePath, call),
|
|
260
281
|
evidence: call.getText().substring(0, 120),
|
|
261
|
-
confidence:
|
|
282
|
+
confidence: isDirectNetwork ? 1.0 : isWrappedClientCall ? 0.75 : 0.8,
|
|
262
283
|
language: 'ts',
|
|
263
284
|
containerId: getContainerId(call, filePath),
|
|
264
|
-
payload: {
|
|
285
|
+
payload: {
|
|
286
|
+
kind: 'effect',
|
|
287
|
+
subtype: 'network',
|
|
288
|
+
async: isAsync,
|
|
289
|
+
target: extractTarget(call),
|
|
290
|
+
responseAsserted: isResponseAsserted(call, isWrappedClientCall),
|
|
291
|
+
bodyKind: extractBodyKind(call, funcName),
|
|
292
|
+
method: extractHttpMethod(call, funcName, isDirectNetwork, isKnownLibraryMethod, isWrappedClientCall),
|
|
293
|
+
hasAuthHeader: extractHasAuthHeader(call, funcName),
|
|
294
|
+
},
|
|
265
295
|
});
|
|
266
296
|
continue;
|
|
267
297
|
}
|
|
@@ -295,7 +325,7 @@ function extractEffects(sf, filePath, nodes) {
|
|
|
295
325
|
}
|
|
296
326
|
}
|
|
297
327
|
// ── entrypoint ───────────────────────────────────────────────────────────
|
|
298
|
-
const ROUTE_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'all'
|
|
328
|
+
const ROUTE_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'all']);
|
|
299
329
|
function extractEntrypoints(sf, filePath, nodes) {
|
|
300
330
|
// Express/Fastify route handlers: app.get('/path', handler)
|
|
301
331
|
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
@@ -304,14 +334,20 @@ function extractEntrypoints(sf, filePath, nodes) {
|
|
|
304
334
|
continue;
|
|
305
335
|
const pa = callee;
|
|
306
336
|
const methodName = pa.getName();
|
|
307
|
-
if (!ROUTE_METHODS.has(methodName))
|
|
308
|
-
continue;
|
|
309
337
|
const objText = pa.getExpression().getText();
|
|
310
338
|
if (!/app|router|server/i.test(objText))
|
|
311
339
|
continue;
|
|
312
340
|
const args = call.getArguments();
|
|
313
341
|
if (args.length < 2)
|
|
314
342
|
continue;
|
|
343
|
+
if (methodName === 'use') {
|
|
344
|
+
const mount = extractRouteMount(call, filePath);
|
|
345
|
+
if (mount)
|
|
346
|
+
nodes.push(mount);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (!ROUTE_METHODS.has(methodName))
|
|
350
|
+
continue;
|
|
315
351
|
// First arg is the route path
|
|
316
352
|
let routePath;
|
|
317
353
|
if (args[0].getKind() === SyntaxKind.StringLiteral) {
|
|
@@ -329,7 +365,7 @@ function extractEntrypoints(sf, filePath, nodes) {
|
|
|
329
365
|
kind: 'entrypoint',
|
|
330
366
|
subtype: 'route',
|
|
331
367
|
name: routePath || methodName,
|
|
332
|
-
httpMethod: methodName
|
|
368
|
+
httpMethod: methodName.toUpperCase(),
|
|
333
369
|
},
|
|
334
370
|
});
|
|
335
371
|
}
|
|
@@ -364,6 +400,119 @@ function extractEntrypoints(sf, filePath, nodes) {
|
|
|
364
400
|
}
|
|
365
401
|
}
|
|
366
402
|
}
|
|
403
|
+
// Express `app.use('/api/prefix', ...middlewares, subRouter)`: emit a
|
|
404
|
+
// route-mount concept so `collectRoutesAcrossGraph` can join the sub-router's
|
|
405
|
+
// bare paths (`router.get('/foo')`) with the mount prefix (`/api/prefix/foo`).
|
|
406
|
+
// Mirrors the FastAPI `app.include_router()` emission in review-python.
|
|
407
|
+
// Caller must have already filtered to `.use()` calls whose object matches
|
|
408
|
+
// `app|router|server`.
|
|
409
|
+
function extractRouteMount(call, mountFile) {
|
|
410
|
+
const args = call.getArguments();
|
|
411
|
+
if (args.length < 2)
|
|
412
|
+
return undefined;
|
|
413
|
+
if (args[0].getKind() !== SyntaxKind.StringLiteral)
|
|
414
|
+
return undefined;
|
|
415
|
+
const prefix = args[0].getLiteralValue();
|
|
416
|
+
if (!prefix.startsWith('/'))
|
|
417
|
+
return undefined;
|
|
418
|
+
// The sub-router is the LAST arg; intermediate args are middlewares.
|
|
419
|
+
const last = args[args.length - 1];
|
|
420
|
+
if (last.getKind() !== SyntaxKind.Identifier)
|
|
421
|
+
return undefined;
|
|
422
|
+
const ident = last;
|
|
423
|
+
const routerName = ident.getText();
|
|
424
|
+
const sourceModule = resolveImportedFileSuffix(ident, mountFile);
|
|
425
|
+
// Emit even when resolution fails — the same-file fallback in
|
|
426
|
+
// resolveMountPrefix can still match if the router is declared locally.
|
|
427
|
+
return {
|
|
428
|
+
id: conceptId(mountFile, 'entrypoint', call.getStart()),
|
|
429
|
+
kind: 'entrypoint',
|
|
430
|
+
primarySpan: span(mountFile, call),
|
|
431
|
+
evidence: call.getText().substring(0, 120),
|
|
432
|
+
confidence: 0.9,
|
|
433
|
+
language: 'ts',
|
|
434
|
+
containerId: getContainerId(call, mountFile),
|
|
435
|
+
payload: {
|
|
436
|
+
kind: 'entrypoint',
|
|
437
|
+
subtype: 'route-mount',
|
|
438
|
+
name: prefix,
|
|
439
|
+
routerName,
|
|
440
|
+
sourceModule,
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
// Given an identifier used in a mount call, resolve it back to the file it
|
|
445
|
+
// was imported from, and return a trailing path suffix (relative to the mount
|
|
446
|
+
// file's directory, with extension) usable by `cross-stack-utils`'s
|
|
447
|
+
// `routeFile.endsWith('/' + sourceModule)` matcher.
|
|
448
|
+
//
|
|
449
|
+
// Returns `undefined` when the identifier is declared locally (no import),
|
|
450
|
+
// when the import cannot be resolved to a source file, or when the import
|
|
451
|
+
// is external (node_modules).
|
|
452
|
+
function resolveImportedFileSuffix(ident, mountFile) {
|
|
453
|
+
const symbol = ident.getSymbol();
|
|
454
|
+
if (!symbol)
|
|
455
|
+
return undefined;
|
|
456
|
+
for (const decl of symbol.getDeclarations()) {
|
|
457
|
+
const kind = decl.getKind();
|
|
458
|
+
if (kind !== SyntaxKind.ImportClause && kind !== SyntaxKind.ImportSpecifier)
|
|
459
|
+
continue;
|
|
460
|
+
const importDecl = decl.getFirstAncestorByKind(SyntaxKind.ImportDeclaration);
|
|
461
|
+
if (!importDecl)
|
|
462
|
+
continue;
|
|
463
|
+
const resolved = importDecl.getModuleSpecifierSourceFile();
|
|
464
|
+
if (resolved) {
|
|
465
|
+
return pathSuffixBetween(mountFile, resolved.getFilePath());
|
|
466
|
+
}
|
|
467
|
+
// ts-morph couldn't resolve (no project config) — fall back to parsing the
|
|
468
|
+
// specifier string and swapping common JS→TS extensions.
|
|
469
|
+
const specifier = importDecl.getModuleSpecifierValue();
|
|
470
|
+
const guess = guessResolvedSuffix(specifier, mountFile);
|
|
471
|
+
if (guess)
|
|
472
|
+
return guess;
|
|
473
|
+
}
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
function pathSuffixBetween(mountFile, targetFile) {
|
|
477
|
+
// Return targetFile relative to the mount file's directory when possible.
|
|
478
|
+
// Fallback: the last 2-3 path components of the target. This just needs to
|
|
479
|
+
// be a unique trailing suffix for `routeFile.endsWith('/' + suffix)` to match.
|
|
480
|
+
const parts = targetFile.split('/');
|
|
481
|
+
const mountParts = mountFile.split('/');
|
|
482
|
+
// Find longest common prefix
|
|
483
|
+
let commonLen = 0;
|
|
484
|
+
while (commonLen < parts.length - 1 &&
|
|
485
|
+
commonLen < mountParts.length - 1 &&
|
|
486
|
+
parts[commonLen] === mountParts[commonLen]) {
|
|
487
|
+
commonLen++;
|
|
488
|
+
}
|
|
489
|
+
const tail = parts.slice(commonLen).join('/');
|
|
490
|
+
return tail || parts.slice(-2).join('/');
|
|
491
|
+
}
|
|
492
|
+
function guessResolvedSuffix(specifier, mountFile) {
|
|
493
|
+
if (!specifier.startsWith('.'))
|
|
494
|
+
return undefined;
|
|
495
|
+
const mountDir = mountFile.split('/').slice(0, -1).join('/');
|
|
496
|
+
const segments = [];
|
|
497
|
+
for (const seg of specifier.split('/')) {
|
|
498
|
+
if (seg === '' || seg === '.')
|
|
499
|
+
continue;
|
|
500
|
+
if (seg === '..') {
|
|
501
|
+
if (mountDir.length === 0)
|
|
502
|
+
continue;
|
|
503
|
+
segments.pop();
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
segments.push(seg);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (segments.length === 0)
|
|
510
|
+
return undefined;
|
|
511
|
+
const base = segments[segments.length - 1];
|
|
512
|
+
const swapped = base.replace(/\.(js|mjs|cjs)$/i, '.ts');
|
|
513
|
+
segments[segments.length - 1] = /\.(ts|tsx)$/i.test(swapped) ? swapped : `${swapped}.ts`;
|
|
514
|
+
return segments.join('/');
|
|
515
|
+
}
|
|
367
516
|
// ── Next.js handlers & server actions ────────────────────────────────────
|
|
368
517
|
const NEXTJS_HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']);
|
|
369
518
|
function hasUseServerDirective(sf) {
|
|
@@ -987,10 +1136,500 @@ function extractTarget(call) {
|
|
|
987
1136
|
if (first.getKind() === SyntaxKind.StringLiteral) {
|
|
988
1137
|
return first.getLiteralValue();
|
|
989
1138
|
}
|
|
990
|
-
if (first.getKind() === SyntaxKind.
|
|
991
|
-
first.
|
|
992
|
-
|
|
1139
|
+
if (first.getKind() === SyntaxKind.NoSubstitutionTemplateLiteral) {
|
|
1140
|
+
return first.getLiteralValue();
|
|
1141
|
+
}
|
|
1142
|
+
if (first.getKind() === SyntaxKind.TemplateExpression) {
|
|
1143
|
+
return extractTemplateUrl(first);
|
|
1144
|
+
}
|
|
1145
|
+
return undefined;
|
|
1146
|
+
}
|
|
1147
|
+
// Convert a template literal like `${API_BASE}/api/review/${slug}` into a
|
|
1148
|
+
// server-route-shaped path like `/api/review/:slug` so cross-stack rules can
|
|
1149
|
+
// correlate it against backend routes. Without this every `fetch(` \`${BASE}/…\` `)`
|
|
1150
|
+
// call is silently dropped by `normalizeClientUrl` (which rejects targets that
|
|
1151
|
+
// start with \`$ instead of \`/).
|
|
1152
|
+
function extractTemplateUrl(tmpl) {
|
|
1153
|
+
const head = tmpl.getHead();
|
|
1154
|
+
let out = head.getLiteralText();
|
|
1155
|
+
const spans = tmpl.getTemplateSpans();
|
|
1156
|
+
for (let i = 0; i < spans.length; i++) {
|
|
1157
|
+
const span = spans[i];
|
|
1158
|
+
const expr = span.getExpression();
|
|
1159
|
+
const exprText = expr.getText();
|
|
1160
|
+
const isBareIdent = expr.getKind() === SyntaxKind.Identifier;
|
|
1161
|
+
if (i === 0 && out === '' && isBareIdent && looksLikeBaseUrlName(exprText)) {
|
|
1162
|
+
// Drop a leading `${BASE_URL}` interpolation so the path starts with `/`.
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
out += `:${isBareIdent ? exprText : 'param'}`;
|
|
1166
|
+
}
|
|
1167
|
+
out += span.getLiteral().getLiteralText();
|
|
1168
|
+
}
|
|
1169
|
+
return out.length > 0 ? out : undefined;
|
|
1170
|
+
}
|
|
1171
|
+
function looksLikeBaseUrlName(name) {
|
|
1172
|
+
return (/(^|_)(base|url|host|origin|endpoint|api|server)(_|$)/i.test(name) ||
|
|
1173
|
+
/(Base|Url|Host|Origin|Endpoint|Api|Server)$/.test(name));
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Given a network call (fetch/axios/…), decide whether the eventual JSON
|
|
1177
|
+
* payload is consumed with a type annotation, `as T` cast, or `satisfies T`
|
|
1178
|
+
* clause. Returns:
|
|
1179
|
+
* - `true` — the call-site is typed; the consumer enforces a shape.
|
|
1180
|
+
* - `false` — the call-site is awaited/.then()'d but no assertion appears.
|
|
1181
|
+
* - `undefined` — no `.json()` consumption in scope, or the pattern is
|
|
1182
|
+
* too complex to analyze.
|
|
1183
|
+
*
|
|
1184
|
+
* Powers the `untyped-api-response` cross-stack rule (the frontend treats
|
|
1185
|
+
* the server's declared response shape as `any`). Kept intentionally
|
|
1186
|
+
* conservative — false positives here poison the pitch.
|
|
1187
|
+
*/
|
|
1188
|
+
function isResponseAsserted(call, isWrappedClientCall = false) {
|
|
1189
|
+
// Generic type argument on the call itself, e.g. `apiClient.get<User>(url)`.
|
|
1190
|
+
// Wrapped clients (~75% of prod apps) almost always rely on this — the
|
|
1191
|
+
// wrapper pre-parses JSON and returns a typed payload, so the caller never
|
|
1192
|
+
// sees a `.json()` call. Treating the generic as the assertion is the only
|
|
1193
|
+
// way the `untyped-api-response` rule can fire on wrapped-client codebases.
|
|
1194
|
+
if (call.getTypeArguments().length > 0)
|
|
1195
|
+
return true;
|
|
1196
|
+
// Walk outward from the network call looking for JSON consumption. Only
|
|
1197
|
+
// after we've seen `.json()` (or a `.then(r => r.json())` callback) does
|
|
1198
|
+
// it make sense to rule the *payload* typed vs untyped — a raw Response
|
|
1199
|
+
// assigned to a variable like `const res = await fetch(...)` tells us
|
|
1200
|
+
// nothing about how the caller will later parse it. Codex review on
|
|
1201
|
+
// 550a57ec caught the false positive for the split pattern
|
|
1202
|
+
// `const res = await fetch(url); const data: User[] = await res.json();`
|
|
1203
|
+
// where the raw Response variable has no annotation.
|
|
1204
|
+
//
|
|
1205
|
+
// Wrapped clients pre-parse JSON inside the wrapper, so there's no
|
|
1206
|
+
// `.json()` call to observe at the call-site. For those we treat the
|
|
1207
|
+
// wrapped call itself as if JSON consumption had already happened — the
|
|
1208
|
+
// payload is whatever `apiClient.get(...)` returns, and we classify the
|
|
1209
|
+
// enclosing binding's type annotation directly.
|
|
1210
|
+
let cursor = call;
|
|
1211
|
+
let sawJsonConsumption = isWrappedClientCall;
|
|
1212
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
1213
|
+
const parent = cursor.getParent();
|
|
1214
|
+
if (!parent)
|
|
1215
|
+
return undefined;
|
|
1216
|
+
if (parent.getKind() === SyntaxKind.AwaitExpression || parent.getKind() === SyntaxKind.ParenthesizedExpression) {
|
|
1217
|
+
cursor = parent;
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
if (parent.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
1221
|
+
const pa = parent;
|
|
1222
|
+
const parentCall = pa.getParent();
|
|
1223
|
+
if (pa.getName() === 'then' && parentCall?.getKind() === SyntaxKind.CallExpression) {
|
|
1224
|
+
// Count this as JSON consumption only if the `.then` callback
|
|
1225
|
+
// clearly parses JSON. A `.then(r => r.status)` chain doesn't.
|
|
1226
|
+
if (callbackCallsJson(parentCall)) {
|
|
1227
|
+
sawJsonConsumption = true;
|
|
1228
|
+
}
|
|
1229
|
+
cursor = parentCall;
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (pa.getName() === 'json' && parentCall?.getKind() === SyntaxKind.CallExpression) {
|
|
1233
|
+
// `(…).json()` — we've now reached the JSON payload.
|
|
1234
|
+
sawJsonConsumption = true;
|
|
1235
|
+
cursor = parentCall;
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
return undefined;
|
|
1239
|
+
}
|
|
1240
|
+
// From here on, every branch is about classifying the payload.
|
|
1241
|
+
// If we haven't actually reached the payload yet, we're looking at a
|
|
1242
|
+
// raw Response and can't honestly say whether it's typed — bail with
|
|
1243
|
+
// `undefined` instead of misclassifying it as untyped.
|
|
1244
|
+
if (!sawJsonConsumption)
|
|
1245
|
+
return undefined;
|
|
1246
|
+
if (parent.getKind() === SyntaxKind.VariableDeclaration) {
|
|
1247
|
+
const decl = parent;
|
|
1248
|
+
if (decl.getTypeNode())
|
|
1249
|
+
return true;
|
|
1250
|
+
return containsAssertion(decl.getInitializer());
|
|
1251
|
+
}
|
|
1252
|
+
if (parent.getKind() === SyntaxKind.ReturnStatement ||
|
|
1253
|
+
parent.getKind() === SyntaxKind.ArrowFunction ||
|
|
1254
|
+
parent.getKind() === SyntaxKind.BinaryExpression) {
|
|
1255
|
+
return containsAssertion(cursor);
|
|
1256
|
+
}
|
|
1257
|
+
if (parent.getKind() === SyntaxKind.AsExpression ||
|
|
1258
|
+
parent.getKind() === SyntaxKind.TypeAssertionExpression ||
|
|
1259
|
+
parent.getKind() === SyntaxKind.SatisfiesExpression) {
|
|
1260
|
+
return true;
|
|
1261
|
+
}
|
|
1262
|
+
return undefined;
|
|
1263
|
+
}
|
|
1264
|
+
return undefined;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Peek into a `.then(...)` call's first argument and return true when the
|
|
1268
|
+
* callback body contains a `.json()` call. Used to tell whether a
|
|
1269
|
+
* `.then(r => r.json())` chain actually resolves to JSON — as opposed to
|
|
1270
|
+
* `.then(r => r.status)` which resolves to a number and should not be
|
|
1271
|
+
* treated as JSON consumption.
|
|
1272
|
+
*/
|
|
1273
|
+
/**
|
|
1274
|
+
* Classify the body payload of a network call (fetch/axios/…):
|
|
1275
|
+
* - `'none'` — no options arg, or no body property found.
|
|
1276
|
+
* - `'static'` — body is a literal (string/object/array) with no dynamic
|
|
1277
|
+
* interpolation.
|
|
1278
|
+
* - `'dynamic'` — body reads from a variable, uses a template literal
|
|
1279
|
+
* containing `${…}`, or calls `JSON.stringify(x)` on a non-literal `x`.
|
|
1280
|
+
*
|
|
1281
|
+
* Feeds the `tainted-across-wire` cross-stack rule. Conservative by design:
|
|
1282
|
+
* when unsure we return `undefined` (fallthrough) so the rule stays silent
|
|
1283
|
+
* instead of over-reporting.
|
|
1284
|
+
*/
|
|
1285
|
+
/**
|
|
1286
|
+
* Network libraries split into two call-site conventions:
|
|
1287
|
+
* fetch-style — `fetch(url, options)` where `options.body` carries the
|
|
1288
|
+
* payload. `ky` and generic `request()` follow this shape.
|
|
1289
|
+
* axios-style — `axios.post(url, data, config)` where the second arg IS
|
|
1290
|
+
* the payload directly. `got.post`, `superagent.post`, and most axios
|
|
1291
|
+
* method calls (post/put/patch) match this.
|
|
1292
|
+
* Gemini review on d8f95d49 caught that the flat "object literal at arg1?
|
|
1293
|
+
* look for .body" logic returned `none` for legitimate axios payloads like
|
|
1294
|
+
* `axios.post('/api', { name: 'foo' })` — the whole object IS the body.
|
|
1295
|
+
*/
|
|
1296
|
+
const AXIOS_STYLE_METHODS = new Set(['post', 'put', 'patch']);
|
|
1297
|
+
function extractBodyKind(call, funcName) {
|
|
1298
|
+
const args = call.getArguments();
|
|
1299
|
+
if (args.length < 2)
|
|
1300
|
+
return 'none';
|
|
1301
|
+
const arg = args[1];
|
|
1302
|
+
// Axios-style: the 2nd arg *is* the body.
|
|
1303
|
+
if (AXIOS_STYLE_METHODS.has(funcName)) {
|
|
1304
|
+
return classifyBodyExpression(arg);
|
|
1305
|
+
}
|
|
1306
|
+
// Fetch-style: the 2nd arg is an options object; body lives in `.body`.
|
|
1307
|
+
if (arg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
1308
|
+
const obj = arg;
|
|
1309
|
+
const bodyProp = obj.getProperty('body');
|
|
1310
|
+
if (!bodyProp)
|
|
1311
|
+
return 'none';
|
|
1312
|
+
// Shorthand `{ method: 'POST', body }` — the body is a variable reference
|
|
1313
|
+
// by definition, so it's dynamic. Codex flagged the earlier branch that
|
|
1314
|
+
// returned `undefined` here as a false negative: a shorthand in a
|
|
1315
|
+
// fetch options object is a common RequestInit pattern, not an
|
|
1316
|
+
// unclassifiable construct.
|
|
1317
|
+
if (bodyProp.getKind() === SyntaxKind.ShorthandPropertyAssignment)
|
|
1318
|
+
return 'dynamic';
|
|
1319
|
+
if (bodyProp.getKind() !== SyntaxKind.PropertyAssignment)
|
|
1320
|
+
return undefined;
|
|
1321
|
+
const initializer = bodyProp.getInitializer();
|
|
1322
|
+
return classifyBodyExpression(initializer);
|
|
1323
|
+
}
|
|
1324
|
+
// Options is passed as a variable (common pattern: `fetch(url, opts)`) —
|
|
1325
|
+
// we don't have enough info to tell without type checker dataflow.
|
|
1326
|
+
return undefined;
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Derive the HTTP method of a network call. Returns uppercase method string
|
|
1330
|
+
* (`GET`, `POST`, …) when confident, `undefined` when the method lives in a
|
|
1331
|
+
* runtime variable we can't read statically (e.g. `axios({ method: verb })`).
|
|
1332
|
+
* Feeds the `contract-method-drift` cross-stack rule.
|
|
1333
|
+
*
|
|
1334
|
+
* Resolution rules (in order):
|
|
1335
|
+
* 1. Wrapped client (`apiClient.post(…)`) or library method (`axios.get(…)`)
|
|
1336
|
+
* → `funcName.toUpperCase()`. `funcName === 'request'` is intentionally
|
|
1337
|
+
* skipped — for `axios.request({ method })` we'd need to read the config.
|
|
1338
|
+
* 2. Raw `fetch(url, { method: 'POST' })` — read the string literal. Any
|
|
1339
|
+
* spread (`{ method: 'GET', ...opts }`) downgrades to `undefined` since
|
|
1340
|
+
* we can't tell if opts overrides method at runtime.
|
|
1341
|
+
* 3. Raw `fetch(url)` / `axios(url)` with no options arg → `GET` (WHATWG +
|
|
1342
|
+
* axios spec default).
|
|
1343
|
+
* 4. Raw call with variable options arg → `undefined` (requires dataflow).
|
|
1344
|
+
*/
|
|
1345
|
+
function extractHttpMethod(call, funcName, isDirectNetwork, isKnownLibraryMethod, isWrappedClientCall) {
|
|
1346
|
+
if (isKnownLibraryMethod || isWrappedClientCall) {
|
|
1347
|
+
if (funcName === 'request')
|
|
1348
|
+
return undefined;
|
|
1349
|
+
return funcName.toUpperCase();
|
|
1350
|
+
}
|
|
1351
|
+
if (!isDirectNetwork)
|
|
1352
|
+
return undefined;
|
|
1353
|
+
const args = call.getArguments();
|
|
1354
|
+
if (args.length < 2)
|
|
1355
|
+
return 'GET';
|
|
1356
|
+
const opts = args[1];
|
|
1357
|
+
if (opts.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
1358
|
+
const obj = opts;
|
|
1359
|
+
const hasSpread = obj.getProperties().some((p) => p.getKind() === SyntaxKind.SpreadAssignment);
|
|
1360
|
+
if (hasSpread)
|
|
1361
|
+
return undefined;
|
|
1362
|
+
const methodProp = obj.getProperty('method');
|
|
1363
|
+
if (!methodProp)
|
|
1364
|
+
return 'GET';
|
|
1365
|
+
if (methodProp.getKind() !== SyntaxKind.PropertyAssignment)
|
|
1366
|
+
return undefined;
|
|
1367
|
+
const initializer = methodProp.getInitializer();
|
|
1368
|
+
if (!initializer)
|
|
1369
|
+
return undefined;
|
|
1370
|
+
const iKind = initializer.getKind();
|
|
1371
|
+
if (iKind === SyntaxKind.StringLiteral) {
|
|
1372
|
+
return initializer.getLiteralValue().toUpperCase();
|
|
1373
|
+
}
|
|
1374
|
+
if (iKind === SyntaxKind.NoSubstitutionTemplateLiteral) {
|
|
1375
|
+
return initializer.getLiteralValue().toUpperCase();
|
|
1376
|
+
}
|
|
1377
|
+
return undefined;
|
|
993
1378
|
}
|
|
994
1379
|
return undefined;
|
|
995
1380
|
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Detect whether a network call's options literal carries an `Authorization`
|
|
1383
|
+
* header. Returns `true`/`false` when the options object is inspectable,
|
|
1384
|
+
* `undefined` when it's a variable, spread, or missing. Only looks at raw
|
|
1385
|
+
* `fetch(…)` — wrapped clients typically inject auth inside the wrapper.
|
|
1386
|
+
* Feeds the `auth-drift` rule, which only fires when the mapper is confident.
|
|
1387
|
+
*/
|
|
1388
|
+
function extractHasAuthHeader(call, funcName) {
|
|
1389
|
+
if (funcName !== 'fetch')
|
|
1390
|
+
return undefined;
|
|
1391
|
+
const args = call.getArguments();
|
|
1392
|
+
if (args.length < 2)
|
|
1393
|
+
return false;
|
|
1394
|
+
const opts = args[1];
|
|
1395
|
+
if (opts.getKind() !== SyntaxKind.ObjectLiteralExpression)
|
|
1396
|
+
return undefined;
|
|
1397
|
+
const obj = opts;
|
|
1398
|
+
if (obj.getProperties().some((p) => p.getKind() === SyntaxKind.SpreadAssignment))
|
|
1399
|
+
return undefined;
|
|
1400
|
+
// Cookie / session auth: `fetch(url, { credentials: 'include' })` or
|
|
1401
|
+
// `'same-origin'` sends session cookies without an Authorization header.
|
|
1402
|
+
// We can't tell from the call alone whether the server accepts that auth
|
|
1403
|
+
// channel, so downgrade to `undefined` and let auth-drift stay silent.
|
|
1404
|
+
// Codex review: without this, cookie-based apps fire auth-drift on every
|
|
1405
|
+
// protected route even though they're authenticated.
|
|
1406
|
+
const credentialsProp = obj.getProperty('credentials');
|
|
1407
|
+
if (credentialsProp && credentialsProp.getKind() === SyntaxKind.PropertyAssignment) {
|
|
1408
|
+
const init = credentialsProp.getInitializer();
|
|
1409
|
+
if (init) {
|
|
1410
|
+
const k = init.getKind();
|
|
1411
|
+
if (k === SyntaxKind.StringLiteral || k === SyntaxKind.NoSubstitutionTemplateLiteral) {
|
|
1412
|
+
const v = init.getLiteralValue();
|
|
1413
|
+
if (v === 'include' || v === 'same-origin')
|
|
1414
|
+
return undefined;
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
// Runtime-computed credentials flag — can't tell.
|
|
1418
|
+
return undefined;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
const headersProp = obj.getProperty('headers');
|
|
1423
|
+
if (!headersProp)
|
|
1424
|
+
return false;
|
|
1425
|
+
if (headersProp.getKind() !== SyntaxKind.PropertyAssignment)
|
|
1426
|
+
return undefined;
|
|
1427
|
+
const headersInit = headersProp.getInitializer();
|
|
1428
|
+
if (!headersInit)
|
|
1429
|
+
return undefined;
|
|
1430
|
+
if (headersInit.getKind() !== SyntaxKind.ObjectLiteralExpression)
|
|
1431
|
+
return undefined;
|
|
1432
|
+
const headersObj = headersInit;
|
|
1433
|
+
if (headersObj.getProperties().some((p) => p.getKind() === SyntaxKind.SpreadAssignment))
|
|
1434
|
+
return undefined;
|
|
1435
|
+
for (const prop of headersObj.getProperties()) {
|
|
1436
|
+
if (prop.getKind() === SyntaxKind.PropertyAssignment) {
|
|
1437
|
+
const pa = prop;
|
|
1438
|
+
if (/^['"]?authorization['"]?$/i.test(pa.getName()))
|
|
1439
|
+
return true;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
return false;
|
|
1443
|
+
}
|
|
1444
|
+
function classifyBodyExpression(expr) {
|
|
1445
|
+
if (!expr)
|
|
1446
|
+
return undefined;
|
|
1447
|
+
const k = expr.getKind();
|
|
1448
|
+
if (k === SyntaxKind.StringLiteral || k === SyntaxKind.NoSubstitutionTemplateLiteral)
|
|
1449
|
+
return 'static';
|
|
1450
|
+
if (k === SyntaxKind.NumericLiteral || k === SyntaxKind.TrueKeyword || k === SyntaxKind.FalseKeyword)
|
|
1451
|
+
return 'static';
|
|
1452
|
+
if (k === SyntaxKind.NullKeyword || k === SyntaxKind.UndefinedKeyword)
|
|
1453
|
+
return 'none';
|
|
1454
|
+
// Template literal with ${…} → dynamic. Template without substitutions is
|
|
1455
|
+
// handled above as NoSubstitutionTemplateLiteral.
|
|
1456
|
+
if (k === SyntaxKind.TemplateExpression)
|
|
1457
|
+
return 'dynamic';
|
|
1458
|
+
// Plain object/array literal — dynamic iff any value inside is non-literal.
|
|
1459
|
+
if (k === SyntaxKind.ObjectLiteralExpression || k === SyntaxKind.ArrayLiteralExpression) {
|
|
1460
|
+
return objectOrArrayIsDynamic(expr) ? 'dynamic' : 'static';
|
|
1461
|
+
}
|
|
1462
|
+
// `JSON.stringify(x)` — dynamic when x is non-literal, static otherwise.
|
|
1463
|
+
if (k === SyntaxKind.CallExpression) {
|
|
1464
|
+
const call = expr;
|
|
1465
|
+
const calleeText = call.getExpression().getText();
|
|
1466
|
+
if (calleeText === 'JSON.stringify') {
|
|
1467
|
+
const arg = call.getArguments()[0];
|
|
1468
|
+
return classifyBodyExpression(arg);
|
|
1469
|
+
}
|
|
1470
|
+
return 'dynamic';
|
|
1471
|
+
}
|
|
1472
|
+
// Identifier, property access, element access, binary expression, etc. —
|
|
1473
|
+
// something that reads a variable or constructs a value at runtime.
|
|
1474
|
+
if (k === SyntaxKind.Identifier ||
|
|
1475
|
+
k === SyntaxKind.PropertyAccessExpression ||
|
|
1476
|
+
k === SyntaxKind.ElementAccessExpression ||
|
|
1477
|
+
k === SyntaxKind.BinaryExpression ||
|
|
1478
|
+
k === SyntaxKind.ConditionalExpression ||
|
|
1479
|
+
k === SyntaxKind.SpreadElement) {
|
|
1480
|
+
return 'dynamic';
|
|
1481
|
+
}
|
|
1482
|
+
return undefined;
|
|
1483
|
+
}
|
|
1484
|
+
function objectOrArrayIsDynamic(expr) {
|
|
1485
|
+
// Walk top-level values; recurse into nested objects/arrays.
|
|
1486
|
+
if (expr.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
1487
|
+
const obj = expr;
|
|
1488
|
+
for (const prop of obj.getProperties()) {
|
|
1489
|
+
if (prop.getKind() === SyntaxKind.ShorthandPropertyAssignment)
|
|
1490
|
+
return true;
|
|
1491
|
+
if (prop.getKind() === SyntaxKind.SpreadAssignment)
|
|
1492
|
+
return true;
|
|
1493
|
+
if (prop.getKind() !== SyntaxKind.PropertyAssignment)
|
|
1494
|
+
return true;
|
|
1495
|
+
const init = prop.getInitializer();
|
|
1496
|
+
const kind = classifyBodyExpression(init);
|
|
1497
|
+
if (kind !== 'static' && kind !== 'none')
|
|
1498
|
+
return true;
|
|
1499
|
+
}
|
|
1500
|
+
return false;
|
|
1501
|
+
}
|
|
1502
|
+
if (expr.getKind() === SyntaxKind.ArrayLiteralExpression) {
|
|
1503
|
+
const arr = expr;
|
|
1504
|
+
for (const el of arr.getElements()) {
|
|
1505
|
+
const kind = classifyBodyExpression(el);
|
|
1506
|
+
if (kind !== 'static' && kind !== 'none')
|
|
1507
|
+
return true;
|
|
1508
|
+
}
|
|
1509
|
+
return false;
|
|
1510
|
+
}
|
|
1511
|
+
return true;
|
|
1512
|
+
}
|
|
1513
|
+
function callbackCallsJson(thenCall) {
|
|
1514
|
+
const callback = thenCall.getArguments()[0];
|
|
1515
|
+
if (!callback)
|
|
1516
|
+
return false;
|
|
1517
|
+
const k = callback.getKind();
|
|
1518
|
+
if (k !== SyntaxKind.ArrowFunction && k !== SyntaxKind.FunctionExpression)
|
|
1519
|
+
return false;
|
|
1520
|
+
const callbackText = callback.getText();
|
|
1521
|
+
// Text-based check is sufficient here: the callback bodies we care about
|
|
1522
|
+
// are short and the helper is advisory. The rule treats `undefined` as
|
|
1523
|
+
// "unknown" and stays silent, so a miss here is never a false positive.
|
|
1524
|
+
return /\.json\s*\(\s*\)/.test(callbackText);
|
|
1525
|
+
}
|
|
1526
|
+
function containsAssertion(node) {
|
|
1527
|
+
if (!node)
|
|
1528
|
+
return false;
|
|
1529
|
+
const k = node.getKind();
|
|
1530
|
+
if (k === SyntaxKind.AsExpression ||
|
|
1531
|
+
k === SyntaxKind.TypeAssertionExpression ||
|
|
1532
|
+
k === SyntaxKind.SatisfiesExpression) {
|
|
1533
|
+
return true;
|
|
1534
|
+
}
|
|
1535
|
+
// A single-level unwrap of `await` / `(...)` is enough for the common
|
|
1536
|
+
// `const x = (await fetch(...).then(r => r.json())) as User` shape.
|
|
1537
|
+
if (k === SyntaxKind.AwaitExpression || k === SyntaxKind.ParenthesizedExpression) {
|
|
1538
|
+
const child = node.getExpression();
|
|
1539
|
+
return containsAssertion(child);
|
|
1540
|
+
}
|
|
1541
|
+
return false;
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Scan `sf` for identifiers that behave like HTTP client wrappers, so that
|
|
1545
|
+
* `extractEffects` can emit `effect.network` for `<name>.get/post/…` calls
|
|
1546
|
+
* that would otherwise slip through the fixed NETWORK_CALLS/library-method
|
|
1547
|
+
* filter.
|
|
1548
|
+
*
|
|
1549
|
+
* Three evidence sources (all single-file, no cross-file graph resolution):
|
|
1550
|
+
* 1. Local class declarations whose bodies call a known network primitive
|
|
1551
|
+
* somewhere — that class IS a client wrapper. The class name is used
|
|
1552
|
+
* in pass 2 to mark `new <ClassName>()` instances.
|
|
1553
|
+
* 2. Local variable initializers that match a client factory
|
|
1554
|
+
* (`axios.create(...)`, `ky.create(...)`, `got.extend(...)`) or
|
|
1555
|
+
* `new <ClientClass>(...)` where <ClientClass> was found in pass 1.
|
|
1556
|
+
* 3. Imported identifiers from a relative / alias path whose local name
|
|
1557
|
+
* matches CLIENT_NAME_PATTERN. Third-party imports are skipped —
|
|
1558
|
+
* library HTTP clients are already covered by NETWORK_CALLS.
|
|
1559
|
+
*
|
|
1560
|
+
* False-positive surface is narrow on purpose: a match only translates into
|
|
1561
|
+
* a network effect when the identifier is later called with `.get/post/put/
|
|
1562
|
+
* patch/delete`, so a name match alone never produces a finding.
|
|
1563
|
+
*/
|
|
1564
|
+
function collectClientIdentifiers(sf) {
|
|
1565
|
+
const clients = new Set();
|
|
1566
|
+
// Pass 1 — wrapper classes defined in this file.
|
|
1567
|
+
const clientClassNames = new Set();
|
|
1568
|
+
for (const cls of sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration)) {
|
|
1569
|
+
if (classCallsNetwork(cls)) {
|
|
1570
|
+
const name = cls.getName();
|
|
1571
|
+
if (name)
|
|
1572
|
+
clientClassNames.add(name);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
// Pass 2 — local instances: `const api = axios.create(...)`, `new ApiClient()`.
|
|
1576
|
+
for (const decl of sf.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
1577
|
+
const nameNode = decl.getNameNode();
|
|
1578
|
+
if (nameNode.getKind() !== SyntaxKind.Identifier)
|
|
1579
|
+
continue;
|
|
1580
|
+
const init = decl.getInitializer();
|
|
1581
|
+
if (!init)
|
|
1582
|
+
continue;
|
|
1583
|
+
const identName = nameNode.getText();
|
|
1584
|
+
if (init.getKind() === SyntaxKind.NewExpression) {
|
|
1585
|
+
const className = init.getExpression().getText();
|
|
1586
|
+
if (clientClassNames.has(className))
|
|
1587
|
+
clients.add(identName);
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
if (init.getKind() === SyntaxKind.CallExpression) {
|
|
1591
|
+
const calleeText = init.getExpression().getText();
|
|
1592
|
+
if (CLIENT_FACTORY_CALLS.has(calleeText))
|
|
1593
|
+
clients.add(identName);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
// Pass 3 — imported clients with client-shaped names from local paths.
|
|
1597
|
+
for (const imp of sf.getImportDeclarations()) {
|
|
1598
|
+
const spec = imp.getModuleSpecifierValue();
|
|
1599
|
+
if (!spec)
|
|
1600
|
+
continue;
|
|
1601
|
+
const isLocal = spec.startsWith('.') || spec.startsWith('@/') || spec.startsWith('~/') || spec.startsWith('@shared/');
|
|
1602
|
+
if (!isLocal)
|
|
1603
|
+
continue;
|
|
1604
|
+
for (const named of imp.getNamedImports()) {
|
|
1605
|
+
const local = named.getAliasNode()?.getText() ?? named.getNameNode().getText();
|
|
1606
|
+
if (CLIENT_NAME_PATTERN.test(local))
|
|
1607
|
+
clients.add(local);
|
|
1608
|
+
}
|
|
1609
|
+
const def = imp.getDefaultImport();
|
|
1610
|
+
if (def && CLIENT_NAME_PATTERN.test(def.getText()))
|
|
1611
|
+
clients.add(def.getText());
|
|
1612
|
+
}
|
|
1613
|
+
return clients;
|
|
1614
|
+
}
|
|
1615
|
+
function classCallsNetwork(cls) {
|
|
1616
|
+
for (const call of cls.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
1617
|
+
const callee = call.getExpression();
|
|
1618
|
+
const k = callee.getKind();
|
|
1619
|
+
if (k === SyntaxKind.Identifier) {
|
|
1620
|
+
if (NETWORK_CALLS.has(callee.getText()))
|
|
1621
|
+
return true;
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
if (k === SyntaxKind.PropertyAccessExpression) {
|
|
1625
|
+
const pa = callee;
|
|
1626
|
+
const methodName = pa.getName();
|
|
1627
|
+
const objText = pa.getExpression().getText();
|
|
1628
|
+
if (NETWORK_METHODS.has(methodName) && /^(axios|got|ky|superagent|request|http)$/.test(objText)) {
|
|
1629
|
+
return true;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
return false;
|
|
1634
|
+
}
|
|
996
1635
|
//# sourceMappingURL=ts-concepts.js.map
|