@kernlang/review 3.3.5 → 3.3.7
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/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.js +2 -3
- package/dist/concept-rules/contract-drift.js.map +1 -1
- 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 +46 -0
- package/dist/concept-rules/cross-stack-utils.js +161 -0
- package/dist/concept-rules/cross-stack-utils.js.map +1 -1
- 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 +14 -0
- 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.js +2 -5
- package/dist/concept-rules/tainted-across-wire.js.map +1 -1
- package/dist/concept-rules/untyped-api-response.js +8 -6
- package/dist/concept-rules/untyped-api-response.js.map +1 -1
- 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/index.js +40 -3
- package/dist/index.js.map +1 -1
- package/dist/llm-bridge.d.ts +12 -0
- package/dist/llm-bridge.js +131 -7
- package/dist/llm-bridge.js.map +1 -1
- package/dist/mappers/ts-concepts.js +406 -13
- package/dist/mappers/ts-concepts.js.map +1 -1
- package/dist/rules/index.js +16 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/kern-source.js +2 -0
- 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/package.json +2 -2
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: duplicate-route
|
|
3
|
+
*
|
|
4
|
+
* Server-side rule — fires when two or more route decorators declare the
|
|
5
|
+
* same `{path, method}` combination in the reviewed project. Real-bug
|
|
6
|
+
* classes:
|
|
7
|
+
* - Someone renamed a handler but forgot to delete the old one; both fire,
|
|
8
|
+
* order-dependent, one silently shadows the other.
|
|
9
|
+
* - A copy-paste left an `@router.get("/users")` pair intact.
|
|
10
|
+
* - A FastAPI router was mounted twice under the same prefix by accident.
|
|
11
|
+
*
|
|
12
|
+
* Fires on the SECOND and later occurrences (the first is the canonical
|
|
13
|
+
* declaration; duplicates are the bug). No-verb routes (`app.use`) key
|
|
14
|
+
* under `ANY` so two `use('/x')` calls still surface as duplicates.
|
|
15
|
+
*
|
|
16
|
+
* Path-only scope: does not cross-correlate client calls. Graph mode only.
|
|
17
|
+
*/
|
|
18
|
+
import { createFingerprint } from '../types.js';
|
|
19
|
+
import { CROSS_STACK_EXACT_CONFIDENCE, collectRoutesAcrossGraph } from './cross-stack-utils.js';
|
|
20
|
+
export function duplicateRoute(ctx) {
|
|
21
|
+
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
22
|
+
return [];
|
|
23
|
+
const routes = collectRoutesAcrossGraph(ctx.allConcepts);
|
|
24
|
+
if (routes.length < 2)
|
|
25
|
+
return [];
|
|
26
|
+
// Group by `${METHOD} ${path}` for exact duplicates, BUT wildcard routes
|
|
27
|
+
// (`ALL`/`ANY`/undefined) shadow specific verbs on the same path. Codex
|
|
28
|
+
// review caught that the naïve keying missed `app.all('/x')` +
|
|
29
|
+
// `app.get('/x')` collisions. We do two passes:
|
|
30
|
+
// 1. Group routes at each path by wildcard-vs-specific classification.
|
|
31
|
+
// 2. Within a path, if there's a wildcard route AND any specific verb
|
|
32
|
+
// route, they collide (pick the later one as the "duplicate").
|
|
33
|
+
// 3. Also flag same-path-same-method duplicates as before.
|
|
34
|
+
const byKey = new Map();
|
|
35
|
+
const byPath = new Map();
|
|
36
|
+
for (const r of routes) {
|
|
37
|
+
const method = (r.method ?? 'ANY').toUpperCase();
|
|
38
|
+
const key = `${method} ${r.path}`;
|
|
39
|
+
const keyList = byKey.get(key) ?? [];
|
|
40
|
+
keyList.push(r);
|
|
41
|
+
byKey.set(key, keyList);
|
|
42
|
+
const pathList = byPath.get(r.path) ?? [];
|
|
43
|
+
pathList.push(r);
|
|
44
|
+
byPath.set(r.path, pathList);
|
|
45
|
+
}
|
|
46
|
+
// Wildcard-vs-specific collisions: when a path has a wildcard-accepting
|
|
47
|
+
// route and any specific-verb route, the handlers shadow each other.
|
|
48
|
+
const WILDCARD = new Set(['ALL', 'ANY']);
|
|
49
|
+
for (const [, pathRoutes] of byPath) {
|
|
50
|
+
if (pathRoutes.length < 2)
|
|
51
|
+
continue;
|
|
52
|
+
const wildcards = pathRoutes.filter((r) => {
|
|
53
|
+
const m = (r.method ?? 'ANY').toUpperCase();
|
|
54
|
+
return WILDCARD.has(m);
|
|
55
|
+
});
|
|
56
|
+
if (wildcards.length === 0)
|
|
57
|
+
continue;
|
|
58
|
+
// Synthesize a collision key so the existing emission loop fires.
|
|
59
|
+
// Keep the wildcard as canonical (first), flag every specific-verb route
|
|
60
|
+
// on this path as the duplicate.
|
|
61
|
+
const specifics = pathRoutes.filter((r) => {
|
|
62
|
+
const m = (r.method ?? 'ANY').toUpperCase();
|
|
63
|
+
return !WILDCARD.has(m);
|
|
64
|
+
});
|
|
65
|
+
if (specifics.length === 0)
|
|
66
|
+
continue;
|
|
67
|
+
const collisionKey = `* ${wildcards[0].path}`;
|
|
68
|
+
if (!byKey.has(collisionKey))
|
|
69
|
+
byKey.set(collisionKey, [wildcards[0], ...specifics]);
|
|
70
|
+
}
|
|
71
|
+
const findings = [];
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
for (const [key, list] of byKey) {
|
|
74
|
+
if (list.length < 2)
|
|
75
|
+
continue;
|
|
76
|
+
const isWildcardCollision = key.startsWith('* ');
|
|
77
|
+
const duplicates = list.slice(1);
|
|
78
|
+
for (const dup of duplicates) {
|
|
79
|
+
if (!dup.node || dup.node.primarySpan.file !== ctx.filePath)
|
|
80
|
+
continue;
|
|
81
|
+
const fingerprint = createFingerprint('duplicate-route', dup.node.primarySpan.startLine, dup.node.primarySpan.startCol);
|
|
82
|
+
if (seen.has(fingerprint))
|
|
83
|
+
continue;
|
|
84
|
+
seen.add(fingerprint);
|
|
85
|
+
const first = list[0];
|
|
86
|
+
const firstFile = first.node?.primarySpan.file ?? 'another file';
|
|
87
|
+
const firstLine = first.node?.primarySpan.startLine;
|
|
88
|
+
const firstRef = firstLine != null ? `${shortPath(firstFile)}:${firstLine}` : shortPath(firstFile);
|
|
89
|
+
const firstMethod = (first.method ?? 'ANY').toUpperCase();
|
|
90
|
+
const dupMethod = (dup.method ?? 'ANY').toUpperCase();
|
|
91
|
+
const message = isWildcardCollision
|
|
92
|
+
? `Route \`${dupMethod} ${dup.path}\` is shadowed by wildcard route \`${firstMethod} ${dup.path}\` at ${firstRef}. The wildcard handler will match ${dupMethod} requests depending on registration order.`
|
|
93
|
+
: `Duplicate route declaration: \`${key}\` is already declared at ${firstRef}. One of the two handlers will silently shadow the other.`;
|
|
94
|
+
findings.push({
|
|
95
|
+
source: 'kern',
|
|
96
|
+
ruleId: 'duplicate-route',
|
|
97
|
+
severity: 'warning',
|
|
98
|
+
category: 'bug',
|
|
99
|
+
message,
|
|
100
|
+
primarySpan: dup.node.primarySpan,
|
|
101
|
+
fingerprint,
|
|
102
|
+
confidence: dup.node.confidence * CROSS_STACK_EXACT_CONFIDENCE,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return findings;
|
|
107
|
+
}
|
|
108
|
+
function shortPath(filePath) {
|
|
109
|
+
const parts = filePath.split('/');
|
|
110
|
+
return parts.slice(-2).join('/');
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=duplicate-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duplicate-route.js","sourceRoot":"","sources":["../../src/concept-rules/duplicate-route.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,21 +4,35 @@
|
|
|
4
4
|
* These rules work on any language that emits concepts.
|
|
5
5
|
* Language-agnostic by design.
|
|
6
6
|
*/
|
|
7
|
+
import { authDrift } from './auth-drift.js';
|
|
7
8
|
import { boundaryMutation } from './boundary-mutation.js';
|
|
8
9
|
import { contractDrift } from './contract-drift.js';
|
|
10
|
+
import { contractMethodDrift } from './contract-method-drift.js';
|
|
11
|
+
import { duplicateRoute } from './duplicate-route.js';
|
|
9
12
|
import { ignoredError } from './ignored-error.js';
|
|
13
|
+
import { missingResponseModel } from './missing-response-model.js';
|
|
14
|
+
import { orphanRoute } from './orphan-route.js';
|
|
15
|
+
import { syncHandlerDoesIo } from './sync-handler-does-io.js';
|
|
10
16
|
import { taintedAcrossWire } from './tainted-across-wire.js';
|
|
11
17
|
import { unguardedEffect } from './unguarded-effect.js';
|
|
12
18
|
import { unrecoveredEffect } from './unrecovered-effect.js';
|
|
13
19
|
import { untypedApiResponse } from './untyped-api-response.js';
|
|
20
|
+
import { untypedBothEndsResponse } from './untyped-both-ends-response.js';
|
|
14
21
|
export const conceptRules = [
|
|
22
|
+
authDrift,
|
|
15
23
|
boundaryMutation,
|
|
16
24
|
contractDrift,
|
|
25
|
+
contractMethodDrift,
|
|
26
|
+
duplicateRoute,
|
|
17
27
|
ignoredError,
|
|
28
|
+
missingResponseModel,
|
|
29
|
+
syncHandlerDoesIo,
|
|
30
|
+
orphanRoute,
|
|
18
31
|
taintedAcrossWire,
|
|
19
32
|
unguardedEffect,
|
|
20
33
|
unrecoveredEffect,
|
|
21
34
|
untypedApiResponse,
|
|
35
|
+
untypedBothEndsResponse,
|
|
22
36
|
];
|
|
23
37
|
export function runConceptRules(concepts, filePath, allConcepts, graphImports) {
|
|
24
38
|
const ctx = { concepts, filePath, allConcepts, graphImports };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/concept-rules/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/concept-rules/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAa1E,MAAM,CAAC,MAAM,YAAY,GAAkB;IACzC,SAAS;IACT,gBAAgB;IAChB,aAAa;IACb,mBAAmB;IACnB,cAAc;IACd,YAAY;IACZ,oBAAoB;IACpB,iBAAiB;IACjB,WAAW;IACX,iBAAiB;IACjB,eAAe;IACf,iBAAiB;IACjB,kBAAkB;IAClB,uBAAuB;CACxB,CAAC;AAEF,MAAM,UAAU,eAAe,CAC7B,QAAoB,EACpB,QAAgB,EAChB,WAAqC,EACrC,YAAoC;IAEpC,MAAM,GAAG,GAAuB,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;IAClF,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: missing-response-model
|
|
3
|
+
*
|
|
4
|
+
* Fires on Python route decorators that do not declare a FastAPI
|
|
5
|
+
* `response_model=...`. Kept Python-scoped because other route mappers do
|
|
6
|
+
* not currently surface an equivalent response-schema signal.
|
|
7
|
+
*/
|
|
8
|
+
import type { ReviewFinding } from '../types.js';
|
|
9
|
+
import type { ConceptRuleContext } from './index.js';
|
|
10
|
+
export declare function missingResponseModel(ctx: ConceptRuleContext): ReviewFinding[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: missing-response-model
|
|
3
|
+
*
|
|
4
|
+
* Fires on Python route decorators that do not declare a FastAPI
|
|
5
|
+
* `response_model=...`. Kept Python-scoped because other route mappers do
|
|
6
|
+
* not currently surface an equivalent response-schema signal.
|
|
7
|
+
*/
|
|
8
|
+
import { createFingerprint } from '../types.js';
|
|
9
|
+
import { CROSS_STACK_HEURISTIC_CONFIDENCE, hasFastApiEvidence } from './cross-stack-utils.js';
|
|
10
|
+
export function missingResponseModel(ctx) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
if (!hasFastApiEvidence(ctx.concepts))
|
|
13
|
+
return findings;
|
|
14
|
+
for (const node of ctx.concepts.nodes) {
|
|
15
|
+
if (node.kind !== 'entrypoint')
|
|
16
|
+
continue;
|
|
17
|
+
if (node.payload.kind !== 'entrypoint')
|
|
18
|
+
continue;
|
|
19
|
+
if (node.payload.subtype !== 'route')
|
|
20
|
+
continue;
|
|
21
|
+
if (node.language !== 'py')
|
|
22
|
+
continue;
|
|
23
|
+
if (node.payload.responseModel)
|
|
24
|
+
continue;
|
|
25
|
+
findings.push({
|
|
26
|
+
source: 'kern',
|
|
27
|
+
ruleId: 'missing-response-model',
|
|
28
|
+
severity: 'warning',
|
|
29
|
+
category: 'bug',
|
|
30
|
+
message: `Route \`${node.payload.name}\` has no FastAPI response_model. Add response_model=... so backend response-shape drift is caught at the contract boundary.`,
|
|
31
|
+
primarySpan: node.primarySpan,
|
|
32
|
+
fingerprint: createFingerprint('missing-response-model', node.primarySpan.startLine, node.primarySpan.startCol),
|
|
33
|
+
confidence: node.confidence * CROSS_STACK_HEURISTIC_CONFIDENCE,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return findings;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=missing-response-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missing-response-model.js","sourceRoot":"","sources":["../../src/concept-rules/missing-response-model.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,gCAAgC,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAG9F,MAAM,UAAU,oBAAoB,CAAC,GAAuB;IAC1D,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAEvD,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtC,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY;YAAE,SAAS;QACzC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY;YAAE,SAAS;QACjD,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,OAAO;YAAE,SAAS;QAC/C,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI;YAAE,SAAS;QACrC,IAAI,IAAI,CAAC,OAAO,CAAC,aAAa;YAAE,SAAS;QAEzC,QAAQ,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,wBAAwB;YAChC,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,WAAW,IAAI,CAAC,OAAO,CAAC,IAAI,8HAA8H;YACnK,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,WAAW,EAAE,iBAAiB,CAAC,wBAAwB,EAAE,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC;YAC/G,UAAU,EAAE,IAAI,CAAC,UAAU,GAAG,gCAAgC;SAC/D,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: orphan-route
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — mirror of contract-drift. Fires when a server-side route
|
|
5
|
+
* exists that no client call in the reviewed graph targets. Real-bug classes:
|
|
6
|
+
* - Handler was kept live after the frontend renamed the URL it hits.
|
|
7
|
+
* - Endpoint was written before the UI that would call it (forgotten TODO).
|
|
8
|
+
* - A dev-only test endpoint made it into production code.
|
|
9
|
+
*
|
|
10
|
+
* v1 scope: path-only match (any client call whose normalized path matches
|
|
11
|
+
* the route template suppresses the finding, regardless of HTTP method). The
|
|
12
|
+
* method axis is left to `contract-method-drift` — compounding both here
|
|
13
|
+
* would double-fire on the same line.
|
|
14
|
+
*
|
|
15
|
+
* Requires graph mode: silent in single-file review. Single-file can't know
|
|
16
|
+
* "no one calls this" because callers live in other files by definition.
|
|
17
|
+
*/
|
|
18
|
+
import type { ReviewFinding } from '../types.js';
|
|
19
|
+
import type { ConceptRuleContext } from './index.js';
|
|
20
|
+
export declare function orphanRoute(ctx: ConceptRuleContext): ReviewFinding[];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: orphan-route
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — mirror of contract-drift. Fires when a server-side route
|
|
5
|
+
* exists that no client call in the reviewed graph targets. Real-bug classes:
|
|
6
|
+
* - Handler was kept live after the frontend renamed the URL it hits.
|
|
7
|
+
* - Endpoint was written before the UI that would call it (forgotten TODO).
|
|
8
|
+
* - A dev-only test endpoint made it into production code.
|
|
9
|
+
*
|
|
10
|
+
* v1 scope: path-only match (any client call whose normalized path matches
|
|
11
|
+
* the route template suppresses the finding, regardless of HTTP method). The
|
|
12
|
+
* method axis is left to `contract-method-drift` — compounding both here
|
|
13
|
+
* would double-fire on the same line.
|
|
14
|
+
*
|
|
15
|
+
* Requires graph mode: silent in single-file review. Single-file can't know
|
|
16
|
+
* "no one calls this" because callers live in other files by definition.
|
|
17
|
+
*/
|
|
18
|
+
import { createFingerprint } from '../types.js';
|
|
19
|
+
import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE, collectRoutesAcrossGraph, findRoutesAtPath, normalizeClientUrl, } from './cross-stack-utils.js';
|
|
20
|
+
export function orphanRoute(ctx) {
|
|
21
|
+
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
22
|
+
return [];
|
|
23
|
+
const serverRoutes = collectRoutesAcrossGraph(ctx.allConcepts);
|
|
24
|
+
if (serverRoutes.length === 0)
|
|
25
|
+
return [];
|
|
26
|
+
// Collect every client-call path in the graph once so each route checks
|
|
27
|
+
// against a shared set rather than re-walking allConcepts.
|
|
28
|
+
//
|
|
29
|
+
// Codex review: if ANY network effect has an unresolved target (imported
|
|
30
|
+
// constant, URL builder, variable expression), the rule MUST abstain —
|
|
31
|
+
// the unresolved call could be hitting any of the "orphan" routes and
|
|
32
|
+
// we'd fire a false positive. Only run the rule when every client call
|
|
33
|
+
// is statically resolvable.
|
|
34
|
+
const clientPaths = new Set();
|
|
35
|
+
let hasUnresolvedTarget = false;
|
|
36
|
+
for (const [, conceptMap] of ctx.allConcepts) {
|
|
37
|
+
for (const node of conceptMap.nodes) {
|
|
38
|
+
if (node.kind !== 'effect' || node.payload.kind !== 'effect' || node.payload.subtype !== 'network')
|
|
39
|
+
continue;
|
|
40
|
+
const target = node.payload.target;
|
|
41
|
+
if (typeof target !== 'string') {
|
|
42
|
+
hasUnresolvedTarget = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const normalized = normalizeClientUrl(target);
|
|
46
|
+
if (!normalized) {
|
|
47
|
+
hasUnresolvedTarget = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!API_PATH_RE.test(normalized))
|
|
51
|
+
continue;
|
|
52
|
+
clientPaths.add(normalized);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Gate: backend-only project (no client calls) — silent.
|
|
56
|
+
// Gate: any unresolved client targets — silent (Codex P2).
|
|
57
|
+
if (clientPaths.size === 0)
|
|
58
|
+
return [];
|
|
59
|
+
if (hasUnresolvedTarget)
|
|
60
|
+
return [];
|
|
61
|
+
const findings = [];
|
|
62
|
+
const seenFingerprints = new Set();
|
|
63
|
+
for (const route of serverRoutes) {
|
|
64
|
+
if (!route.node || route.node.primarySpan.file !== ctx.filePath)
|
|
65
|
+
continue;
|
|
66
|
+
if (clientCallMatches(route.path, clientPaths))
|
|
67
|
+
continue;
|
|
68
|
+
const fingerprint = createFingerprint('orphan-route', route.node.primarySpan.startLine, route.node.primarySpan.startCol);
|
|
69
|
+
// Router-mount expansion can cause the same per-file route to surface
|
|
70
|
+
// twice under different prefixes when two mounts share a router (rare
|
|
71
|
+
// but legal). Dedupe by fingerprint so we emit one finding per span.
|
|
72
|
+
if (seenFingerprints.has(fingerprint))
|
|
73
|
+
continue;
|
|
74
|
+
seenFingerprints.add(fingerprint);
|
|
75
|
+
const methodLabel = route.method ? `${route.method} ` : '';
|
|
76
|
+
findings.push({
|
|
77
|
+
source: 'kern',
|
|
78
|
+
ruleId: 'orphan-route',
|
|
79
|
+
severity: 'warning',
|
|
80
|
+
category: 'bug',
|
|
81
|
+
message: `Server defines \`${methodLabel}${route.path}\` but no client in the reviewed project calls this path. Either remove the handler or add the frontend caller.`,
|
|
82
|
+
primarySpan: route.node.primarySpan,
|
|
83
|
+
fingerprint,
|
|
84
|
+
confidence: route.node.confidence * CROSS_STACK_HEURISTIC_CONFIDENCE,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return findings;
|
|
88
|
+
}
|
|
89
|
+
function clientCallMatches(routePath, clientPaths) {
|
|
90
|
+
for (const cp of clientPaths) {
|
|
91
|
+
if (findRoutesAtPath(cp, [{ path: routePath, method: undefined }]).length > 0)
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=orphan-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orphan-route.js","sourceRoot":"","sources":["../../src/concept-rules/orphan-route.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: sync-handler-does-io
|
|
3
|
+
*
|
|
4
|
+
* Fires when a route handler is explicitly synchronous and performs
|
|
5
|
+
* network/db/fs I/O in the same function container.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReviewFinding } from '../types.js';
|
|
8
|
+
import type { ConceptRuleContext } from './index.js';
|
|
9
|
+
export declare function syncHandlerDoesIo(ctx: ConceptRuleContext): ReviewFinding[];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: sync-handler-does-io
|
|
3
|
+
*
|
|
4
|
+
* Fires when a route handler is explicitly synchronous and performs
|
|
5
|
+
* network/db/fs I/O in the same function container.
|
|
6
|
+
*/
|
|
7
|
+
import { createFingerprint } from '../types.js';
|
|
8
|
+
const BLOCKING_IO_SUBTYPES = new Set(['network', 'db', 'fs']);
|
|
9
|
+
export function syncHandlerDoesIo(ctx) {
|
|
10
|
+
const effectsByContainer = new Map();
|
|
11
|
+
for (const node of ctx.concepts.nodes) {
|
|
12
|
+
if (node.kind !== 'effect')
|
|
13
|
+
continue;
|
|
14
|
+
if (node.payload.kind !== 'effect')
|
|
15
|
+
continue;
|
|
16
|
+
if (!BLOCKING_IO_SUBTYPES.has(node.payload.subtype))
|
|
17
|
+
continue;
|
|
18
|
+
if (!node.containerId)
|
|
19
|
+
continue;
|
|
20
|
+
const existing = effectsByContainer.get(node.containerId) ?? [];
|
|
21
|
+
existing.push(node);
|
|
22
|
+
effectsByContainer.set(node.containerId, existing);
|
|
23
|
+
}
|
|
24
|
+
const findings = [];
|
|
25
|
+
for (const node of ctx.concepts.nodes) {
|
|
26
|
+
if (node.kind !== 'entrypoint')
|
|
27
|
+
continue;
|
|
28
|
+
if (node.payload.kind !== 'entrypoint')
|
|
29
|
+
continue;
|
|
30
|
+
if (node.payload.subtype !== 'route')
|
|
31
|
+
continue;
|
|
32
|
+
if (node.payload.isAsync !== false)
|
|
33
|
+
continue;
|
|
34
|
+
if (!node.containerId)
|
|
35
|
+
continue;
|
|
36
|
+
const effects = effectsByContainer.get(node.containerId);
|
|
37
|
+
if (!effects || effects.length === 0)
|
|
38
|
+
continue;
|
|
39
|
+
const firstEffect = effects[0];
|
|
40
|
+
if (firstEffect.payload.kind !== 'effect')
|
|
41
|
+
continue;
|
|
42
|
+
findings.push({
|
|
43
|
+
source: 'kern',
|
|
44
|
+
ruleId: 'sync-handler-does-io',
|
|
45
|
+
severity: 'warning',
|
|
46
|
+
category: 'bug',
|
|
47
|
+
message: `Sync route handler \`${node.payload.name}\` performs ${firstEffect.payload.subtype} I/O. Make the handler async and use non-blocking I/O, or move the blocking work out of the request path.`,
|
|
48
|
+
primarySpan: node.primarySpan,
|
|
49
|
+
relatedSpans: effects.map((effect) => effect.primarySpan),
|
|
50
|
+
fingerprint: createFingerprint('sync-handler-does-io', node.primarySpan.startLine, node.primarySpan.startCol),
|
|
51
|
+
confidence: 0.9,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return findings;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=sync-handler-does-io.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-handler-does-io.js","sourceRoot":"","sources":["../../src/concept-rules/sync-handler-does-io.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGhD,MAAM,oBAAoB,GAAG,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAE9D,MAAM,UAAU,iBAAiB,CAAC,GAAuB;IACvD,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAyB,CAAC;IAE5D,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtC,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;YAAE,SAAS;QACrC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ;YAAE,SAAS;QAC7C,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;YAAE,SAAS;QAC9D,IAAI,CAAC,IAAI,CAAC,WAAW;YAAE,SAAS;QAEhC,MAAM,QAAQ,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;QAChE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpB,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACrD,CAAC;IAED,MAAM,QAAQ,GAAoB,EAAE,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtC,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY;YAAE,SAAS;QACzC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY;YAAE,SAAS;QACjD,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,OAAO;YAAE,SAAS;QAC/C,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,KAAK;YAAE,SAAS;QAC7C,IAAI,CAAC,IAAI,CAAC,WAAW;YAAE,SAAS;QAEhC,MAAM,OAAO,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACzD,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAC/C,MAAM,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QAC/B,IAAI,WAAW,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ;YAAE,SAAS;QAEpD,QAAQ,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,sBAAsB;YAC9B,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,wBAAwB,IAAI,CAAC,OAAO,CAAC,IAAI,eAAe,WAAW,CAAC,OAAO,CAAC,OAAO,2GAA2G;YACvM,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC;YACzD,WAAW,EAAE,iBAAiB,CAAC,sBAAsB,EAAE,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC;YAC7G,UAAU,EAAE,GAAG;SAChB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -29,14 +29,11 @@
|
|
|
29
29
|
* missing server matches (contract-drift owns that class).
|
|
30
30
|
*/
|
|
31
31
|
import { createFingerprint } from '../types.js';
|
|
32
|
-
import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE,
|
|
32
|
+
import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE, collectRoutesAcrossGraph, findMatchingRoute, normalizeClientUrl, } from './cross-stack-utils.js';
|
|
33
33
|
export function taintedAcrossWire(ctx) {
|
|
34
34
|
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
35
35
|
return [];
|
|
36
|
-
const serverRoutes =
|
|
37
|
-
for (const [, conceptMap] of ctx.allConcepts) {
|
|
38
|
-
collectRoutes(conceptMap, serverRoutes);
|
|
39
|
-
}
|
|
36
|
+
const serverRoutes = collectRoutesAcrossGraph(ctx.allConcepts);
|
|
40
37
|
if (serverRoutes.length === 0)
|
|
41
38
|
return [];
|
|
42
39
|
// Build the set of files that contain at least one validation guard.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tainted-across-wire.js","sourceRoot":"","sources":["../../src/concept-rules/tainted-across-wire.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,gCAAgC,EAChC,
|
|
1
|
+
{"version":3,"file":"tainted-across-wire.js","sourceRoot":"","sources":["../../src/concept-rules/tainted-across-wire.ts"],"names":[],"mappings":"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"}
|
|
@@ -26,14 +26,11 @@
|
|
|
26
26
|
* False positives here would poison the pitch.
|
|
27
27
|
*/
|
|
28
28
|
import { createFingerprint } from '../types.js';
|
|
29
|
-
import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE,
|
|
29
|
+
import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE, collectRoutesAcrossGraph, findMatchingRoute, isFastApiRouteMissingResponseModel, normalizeClientUrl, } from './cross-stack-utils.js';
|
|
30
30
|
export function untypedApiResponse(ctx) {
|
|
31
31
|
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
32
32
|
return [];
|
|
33
|
-
const serverRoutes =
|
|
34
|
-
for (const [, conceptMap] of ctx.allConcepts) {
|
|
35
|
-
collectRoutes(conceptMap, serverRoutes);
|
|
36
|
-
}
|
|
33
|
+
const serverRoutes = collectRoutesAcrossGraph(ctx.allConcepts);
|
|
37
34
|
if (serverRoutes.length === 0)
|
|
38
35
|
return [];
|
|
39
36
|
const findings = [];
|
|
@@ -50,8 +47,13 @@ export function untypedApiResponse(ctx) {
|
|
|
50
47
|
const normalized = normalizeClientUrl(target);
|
|
51
48
|
if (!normalized || !API_PATH_RE.test(normalized))
|
|
52
49
|
continue;
|
|
53
|
-
|
|
50
|
+
const matchedRoute = findMatchingRoute(normalized, serverRoutes);
|
|
51
|
+
if (!matchedRoute)
|
|
52
|
+
continue;
|
|
53
|
+
if (matchedRoute.node &&
|
|
54
|
+
isFastApiRouteMissingResponseModel(matchedRoute.node, ctx.allConcepts.get(matchedRoute.node.primarySpan.file))) {
|
|
54
55
|
continue;
|
|
56
|
+
}
|
|
55
57
|
findings.push({
|
|
56
58
|
source: 'kern',
|
|
57
59
|
ruleId: 'untyped-api-response',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"untyped-api-response.js","sourceRoot":"","sources":["../../src/concept-rules/untyped-api-response.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,gCAAgC,EAChC,
|
|
1
|
+
{"version":3,"file":"untyped-api-response.js","sourceRoot":"","sources":["../../src/concept-rules/untyped-api-response.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,gCAAgC,EAChC,wBAAwB,EACxB,iBAAiB,EACjB,kCAAkC,EAClC,kBAAkB,GACnB,MAAM,wBAAwB,CAAC;AAGhC,MAAM,UAAU,kBAAkB,CAAC,GAAuB;IACxD,IAAI,CAAC,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAE9D,MAAM,YAAY,GAAG,wBAAwB,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC/D,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEzC,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,0EAA0E;IAC1E,MAAM,aAAa,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC;IACxE,KAAK,MAAM,IAAI,IAAI,aAAa,CAAC,KAAK,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS;YAAE,SAAS;QAC7G,IAAI,IAAI,CAAC,OAAO,CAAC,gBAAgB,KAAK,KAAK;YAAE,SAAS,CAAC,yBAAyB;QAChF,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;QACnC,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,SAAS;QACzC,MAAM,UAAU,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAC9C,IAAI,CAAC,UAAU,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC;YAAE,SAAS;QAC3D,MAAM,YAAY,GAAG,iBAAiB,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;QACjE,IAAI,CAAC,YAAY;YAAE,SAAS;QAC5B,IACE,YAAY,CAAC,IAAI;YACjB,kCAAkC,CAAC,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,EAC9G,CAAC;YACD,SAAS;QACX,CAAC;QAED,QAAQ,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,sBAAsB;YAC9B,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,mBAAmB,MAAM,wPAAwP;YAC1R,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,WAAW,EAAE,iBAAiB,CAAC,sBAAsB,EAAE,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC;YAC7G,uEAAuE;YACvE,sEAAsE;YACtE,wCAAwC;YACxC,UAAU,EAAE,IAAI,CAAC,UAAU,GAAG,gCAAgC;SAC/D,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: untyped-both-ends-response
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack linker for the response typing wedge. Fires when the client
|
|
5
|
+
* consumes a matching API response without a type assertion and the Python
|
|
6
|
+
* server route also has no `response_model=...`.
|
|
7
|
+
*/
|
|
8
|
+
import type { ReviewFinding } from '../types.js';
|
|
9
|
+
import type { ConceptRuleContext } from './index.js';
|
|
10
|
+
export declare function untypedBothEndsResponse(ctx: ConceptRuleContext): ReviewFinding[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: untyped-both-ends-response
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack linker for the response typing wedge. Fires when the client
|
|
5
|
+
* consumes a matching API response without a type assertion and the Python
|
|
6
|
+
* server route also has no `response_model=...`.
|
|
7
|
+
*/
|
|
8
|
+
import { createFingerprint } from '../types.js';
|
|
9
|
+
import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE, collectRoutesAcrossGraph, findMatchingRoute, isFastApiRouteMissingResponseModel, normalizeClientUrl, } from './cross-stack-utils.js';
|
|
10
|
+
export function untypedBothEndsResponse(ctx) {
|
|
11
|
+
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
12
|
+
return [];
|
|
13
|
+
const allConcepts = ctx.allConcepts;
|
|
14
|
+
const routesMissingModel = collectRoutesAcrossGraph(allConcepts).filter((route) => routeMissingResponseModel(route, allConcepts));
|
|
15
|
+
if (routesMissingModel.length === 0)
|
|
16
|
+
return [];
|
|
17
|
+
const findings = [];
|
|
18
|
+
const localConcepts = allConcepts.get(ctx.filePath) ?? ctx.concepts;
|
|
19
|
+
for (const node of localConcepts.nodes) {
|
|
20
|
+
if (node.language !== 'ts')
|
|
21
|
+
continue;
|
|
22
|
+
if (node.kind !== 'effect' || node.payload.kind !== 'effect' || node.payload.subtype !== 'network')
|
|
23
|
+
continue;
|
|
24
|
+
if (node.payload.responseAsserted !== false)
|
|
25
|
+
continue;
|
|
26
|
+
const target = node.payload.target;
|
|
27
|
+
if (typeof target !== 'string')
|
|
28
|
+
continue;
|
|
29
|
+
const normalized = normalizeClientUrl(target);
|
|
30
|
+
if (!normalized || !API_PATH_RE.test(normalized))
|
|
31
|
+
continue;
|
|
32
|
+
const matchedRoute = findMatchingRoute(normalized, routesMissingModel);
|
|
33
|
+
if (!matchedRoute?.node)
|
|
34
|
+
continue;
|
|
35
|
+
findings.push({
|
|
36
|
+
source: 'kern',
|
|
37
|
+
ruleId: 'untyped-both-ends-response',
|
|
38
|
+
severity: 'warning',
|
|
39
|
+
category: 'bug',
|
|
40
|
+
message: `Response for \`${target}\` is untyped on both ends: the client consumes it without a type assertion and the matching backend route has no response_model.`,
|
|
41
|
+
primarySpan: node.primarySpan,
|
|
42
|
+
relatedSpans: [matchedRoute.node.primarySpan],
|
|
43
|
+
fingerprint: createFingerprint('untyped-both-ends-response', node.primarySpan.startLine, node.primarySpan.startCol),
|
|
44
|
+
confidence: Math.min(node.confidence, matchedRoute.node.confidence) * CROSS_STACK_HEURISTIC_CONFIDENCE,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return findings;
|
|
48
|
+
}
|
|
49
|
+
function routeMissingResponseModel(route, allConcepts) {
|
|
50
|
+
const node = route.node;
|
|
51
|
+
if (!node)
|
|
52
|
+
return false;
|
|
53
|
+
return isFastApiRouteMissingResponseModel(node, allConcepts.get(node.primarySpan.file));
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=untyped-both-ends-response.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"untyped-both-ends-response.js","sourceRoot":"","sources":["../../src/concept-rules/untyped-both-ends-response.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,gCAAgC,EAChC,wBAAwB,EACxB,iBAAiB,EACjB,kCAAkC,EAClC,kBAAkB,GAEnB,MAAM,wBAAwB,CAAC;AAGhC,MAAM,UAAU,uBAAuB,CAAC,GAAuB;IAC7D,IAAI,CAAC,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC9D,MAAM,WAAW,GAAG,GAAG,CAAC,WAAW,CAAC;IAEpC,MAAM,kBAAkB,GAAG,wBAAwB,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAChF,yBAAyB,CAAC,KAAK,EAAE,WAAW,CAAC,CAC9C,CAAC;IACF,IAAI,kBAAkB,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAE/C,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,MAAM,aAAa,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC;IAEpE,KAAK,MAAM,IAAI,IAAI,aAAa,CAAC,KAAK,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI;YAAE,SAAS;QACrC,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS;YAAE,SAAS;QAC7G,IAAI,IAAI,CAAC,OAAO,CAAC,gBAAgB,KAAK,KAAK;YAAE,SAAS;QACtD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;QACnC,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,SAAS;QACzC,MAAM,UAAU,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAC9C,IAAI,CAAC,UAAU,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC;YAAE,SAAS;QAC3D,MAAM,YAAY,GAAG,iBAAiB,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;QACvE,IAAI,CAAC,YAAY,EAAE,IAAI;YAAE,SAAS;QAElC,QAAQ,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,4BAA4B;YACpC,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,kBAAkB,MAAM,mIAAmI;YACpK,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,YAAY,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC;YAC7C,WAAW,EAAE,iBAAiB,CAC5B,4BAA4B,EAC5B,IAAI,CAAC,WAAW,CAAC,SAAS,EAC1B,IAAI,CAAC,WAAW,CAAC,QAAQ,CAC1B;YACD,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,gCAAgC;SACvG,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,yBAAyB,CAAC,KAAkB,EAAE,WAA4C;IACjG,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;IACxB,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,OAAO,kCAAkC,CAAC,IAAI,EAAE,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;AAC1F,CAAC"}
|