@kernlang/review 3.3.5 → 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/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,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: auth-drift
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — fires when a frontend network call targets a server
|
|
5
|
+
* route file that declares an auth guard (FastAPI `Depends(get_current_user)`,
|
|
6
|
+
* Flask `@login_required`, early-return `if (!req.user)` Express patterns),
|
|
7
|
+
* but the client call sends no Authorization header.
|
|
8
|
+
*
|
|
9
|
+
* Real-bug classes:
|
|
10
|
+
* - Protected endpoint called from a public page or before login finished.
|
|
11
|
+
* - Frontend forgot to add the Bearer token after a refactor that moved
|
|
12
|
+
* auth out of a wrapper.
|
|
13
|
+
* - Endpoint was auth-gated late in the PR but the client still fires
|
|
14
|
+
* unauthenticated.
|
|
15
|
+
*
|
|
16
|
+
* v1 scope:
|
|
17
|
+
* - Only fires on raw `fetch(...)`. Wrapped clients typically inject auth
|
|
18
|
+
* inside the wrapper; the TS mapper already reports
|
|
19
|
+
* `hasAuthHeader: undefined` for them.
|
|
20
|
+
* - Auth guard is "present" when any guard concept of subtype 'auth' lives
|
|
21
|
+
* in the same file as a matching server route. File granularity is
|
|
22
|
+
* coarse on purpose — FastAPI's idiomatic `APIRouter(dependencies=[...])`
|
|
23
|
+
* puts the guard on the router, not each handler.
|
|
24
|
+
*
|
|
25
|
+
* Confidence: `CROSS_STACK_EXACT_CONFIDENCE` (0.9). Graph mode only.
|
|
26
|
+
*/
|
|
27
|
+
import type { ReviewFinding } from '../types.js';
|
|
28
|
+
import type { ConceptRuleContext } from './index.js';
|
|
29
|
+
export declare function authDrift(ctx: ConceptRuleContext): ReviewFinding[];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: auth-drift
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — fires when a frontend network call targets a server
|
|
5
|
+
* route file that declares an auth guard (FastAPI `Depends(get_current_user)`,
|
|
6
|
+
* Flask `@login_required`, early-return `if (!req.user)` Express patterns),
|
|
7
|
+
* but the client call sends no Authorization header.
|
|
8
|
+
*
|
|
9
|
+
* Real-bug classes:
|
|
10
|
+
* - Protected endpoint called from a public page or before login finished.
|
|
11
|
+
* - Frontend forgot to add the Bearer token after a refactor that moved
|
|
12
|
+
* auth out of a wrapper.
|
|
13
|
+
* - Endpoint was auth-gated late in the PR but the client still fires
|
|
14
|
+
* unauthenticated.
|
|
15
|
+
*
|
|
16
|
+
* v1 scope:
|
|
17
|
+
* - Only fires on raw `fetch(...)`. Wrapped clients typically inject auth
|
|
18
|
+
* inside the wrapper; the TS mapper already reports
|
|
19
|
+
* `hasAuthHeader: undefined` for them.
|
|
20
|
+
* - Auth guard is "present" when any guard concept of subtype 'auth' lives
|
|
21
|
+
* in the same file as a matching server route. File granularity is
|
|
22
|
+
* coarse on purpose — FastAPI's idiomatic `APIRouter(dependencies=[...])`
|
|
23
|
+
* puts the guard on the router, not each handler.
|
|
24
|
+
*
|
|
25
|
+
* Confidence: `CROSS_STACK_EXACT_CONFIDENCE` (0.9). Graph mode only.
|
|
26
|
+
*/
|
|
27
|
+
import { createFingerprint } from '../types.js';
|
|
28
|
+
import { API_PATH_RE, CROSS_STACK_EXACT_CONFIDENCE, collectRoutesAcrossGraph, findMatchingRoute, normalizeClientUrl, } from './cross-stack-utils.js';
|
|
29
|
+
export function authDrift(ctx) {
|
|
30
|
+
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
31
|
+
return [];
|
|
32
|
+
const serverRoutes = collectRoutesAcrossGraph(ctx.allConcepts);
|
|
33
|
+
if (serverRoutes.length === 0)
|
|
34
|
+
return [];
|
|
35
|
+
// Map each file to the set of containerIds that have an auth guard AND
|
|
36
|
+
// record which files have BOTH guarded and unguarded routes ("mixed").
|
|
37
|
+
// Codex review flagged that file-level presence was too coarse: a file
|
|
38
|
+
// with `/api/me` (guarded) + `/api/public` (not) would false-positive on
|
|
39
|
+
// the public endpoint. The fix: only fire when we can prove the SPECIFIC
|
|
40
|
+
// route is guarded (matching containerId), OR when every route in the
|
|
41
|
+
// file shares the file's guard scope. Mixed files stay silent.
|
|
42
|
+
const authGuardContainers = collectAuthGuardContainers(ctx.allConcepts);
|
|
43
|
+
if (authGuardContainers.size === 0)
|
|
44
|
+
return [];
|
|
45
|
+
const findings = [];
|
|
46
|
+
for (const [, conceptMap] of ctx.allConcepts) {
|
|
47
|
+
for (const node of conceptMap.nodes) {
|
|
48
|
+
if (node.kind !== 'effect' || node.payload.kind !== 'effect' || node.payload.subtype !== 'network')
|
|
49
|
+
continue;
|
|
50
|
+
if (node.primarySpan.file !== ctx.filePath)
|
|
51
|
+
continue;
|
|
52
|
+
// Must be explicitly `false`. `undefined` = mapper couldn't tell; stay silent.
|
|
53
|
+
if (node.payload.hasAuthHeader !== false)
|
|
54
|
+
continue;
|
|
55
|
+
const target = node.payload.target;
|
|
56
|
+
if (typeof target !== 'string')
|
|
57
|
+
continue;
|
|
58
|
+
const normalized = normalizeClientUrl(target);
|
|
59
|
+
if (!normalized || !API_PATH_RE.test(normalized))
|
|
60
|
+
continue;
|
|
61
|
+
const route = findMatchingRoute(normalized, serverRoutes);
|
|
62
|
+
if (!route?.node)
|
|
63
|
+
continue;
|
|
64
|
+
const serverFile = route.node.primarySpan.file;
|
|
65
|
+
const fileGuards = authGuardContainers.get(serverFile);
|
|
66
|
+
if (!fileGuards)
|
|
67
|
+
continue;
|
|
68
|
+
// Proof that this specific route is guarded:
|
|
69
|
+
// (a) the route's containerId matches an auth-guard containerId
|
|
70
|
+
// (FastAPI pattern: `@router.get` + `Depends(...)` share the
|
|
71
|
+
// function body), OR
|
|
72
|
+
// (b) the file contains exactly one route (Express pattern: guard
|
|
73
|
+
// is inside the handler callback, not the app.get call, so
|
|
74
|
+
// containers differ but there's no ambiguity about which route
|
|
75
|
+
// the guard protects).
|
|
76
|
+
// Mixed multi-route files with no container match fall through to
|
|
77
|
+
// silent — Codex review called out this false-positive class.
|
|
78
|
+
const routeContainer = route.node.containerId;
|
|
79
|
+
const routeIsGuardedByContainer = routeContainer !== undefined && fileGuards.routeContainers.has(routeContainer);
|
|
80
|
+
const routeIsGuardedBySingleRouteFile = fileGuards.totalRoutesInFile === 1;
|
|
81
|
+
if (!routeIsGuardedByContainer && !routeIsGuardedBySingleRouteFile)
|
|
82
|
+
continue;
|
|
83
|
+
findings.push({
|
|
84
|
+
source: 'kern',
|
|
85
|
+
ruleId: 'auth-drift',
|
|
86
|
+
severity: 'warning',
|
|
87
|
+
category: 'bug',
|
|
88
|
+
message: `Frontend calls \`${target}\` without an Authorization header, but the server route requires authentication (guard declared in ${shortPath(serverFile)}). Add the Authorization header or change the server guard.`,
|
|
89
|
+
primarySpan: node.primarySpan,
|
|
90
|
+
fingerprint: createFingerprint('auth-drift', node.primarySpan.startLine, node.primarySpan.startCol),
|
|
91
|
+
confidence: node.confidence * CROSS_STACK_EXACT_CONFIDENCE,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return findings;
|
|
96
|
+
}
|
|
97
|
+
function collectAuthGuardContainers(allConcepts) {
|
|
98
|
+
const result = new Map();
|
|
99
|
+
for (const [filePath, map] of allConcepts) {
|
|
100
|
+
const routeContainers = new Set();
|
|
101
|
+
let routeCount = 0;
|
|
102
|
+
for (const node of map.nodes) {
|
|
103
|
+
if (node.kind === 'entrypoint' && node.payload.kind === 'entrypoint' && node.payload.subtype === 'route') {
|
|
104
|
+
routeCount++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (node.kind !== 'guard' || node.payload.kind !== 'guard')
|
|
108
|
+
continue;
|
|
109
|
+
if (node.payload.subtype !== 'auth')
|
|
110
|
+
continue;
|
|
111
|
+
// The guard's containerId is the scope that enforces auth. Routes
|
|
112
|
+
// sharing that containerId (same function body) are considered
|
|
113
|
+
// guarded.
|
|
114
|
+
if (node.containerId !== undefined)
|
|
115
|
+
routeContainers.add(node.containerId);
|
|
116
|
+
}
|
|
117
|
+
if (routeContainers.size > 0) {
|
|
118
|
+
result.set(filePath, { routeContainers, totalRoutesInFile: routeCount });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function shortPath(filePath) {
|
|
124
|
+
const parts = filePath.split('/');
|
|
125
|
+
return parts.slice(-2).join('/');
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=auth-drift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-drift.js","sourceRoot":"","sources":["../../src/concept-rules/auth-drift.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,15 +17,14 @@
|
|
|
17
17
|
* review silently returns no findings (can't correlate from one file).
|
|
18
18
|
*/
|
|
19
19
|
import { createFingerprint } from '../types.js';
|
|
20
|
-
import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE,
|
|
20
|
+
import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE, collectRoutesAcrossGraph, hasMatchingRoute, normalizeClientUrl, } from './cross-stack-utils.js';
|
|
21
21
|
export function contractDrift(ctx) {
|
|
22
22
|
// Graph mode only — URL correlation is useless within a single file.
|
|
23
23
|
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
24
24
|
return [];
|
|
25
|
-
const serverRoutes =
|
|
25
|
+
const serverRoutes = collectRoutesAcrossGraph(ctx.allConcepts);
|
|
26
26
|
const clientCalls = [];
|
|
27
27
|
for (const [, conceptMap] of ctx.allConcepts) {
|
|
28
|
-
collectRoutes(conceptMap, serverRoutes);
|
|
29
28
|
for (const node of conceptMap.nodes) {
|
|
30
29
|
if (node.kind !== 'effect' || node.payload.kind !== 'effect' || node.payload.subtype !== 'network')
|
|
31
30
|
continue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract-drift.js","sourceRoot":"","sources":["../../src/concept-rules/contract-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,gCAAgC,EAChC,
|
|
1
|
+
{"version":3,"file":"contract-drift.js","sourceRoot":"","sources":["../../src/concept-rules/contract-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,gCAAgC,EAChC,wBAAwB,EACxB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,wBAAwB,CAAC;AAShC,MAAM,UAAU,aAAa,CAAC,GAAuB;IACnD,qEAAqE;IACrE,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,MAAM,WAAW,GAAiB,EAAE,CAAC;IAErC,KAAK,MAAM,CAAC,EAAE,UAAU,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC;QAC7C,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC;YACpC,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS;gBAAE,SAAS;YAC7G,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;YACnC,IAAI,OAAO,MAAM,KAAK,QAAQ;gBAAE,SAAS;YACzC,MAAM,UAAU,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;YAC9C,IAAI,CAAC,UAAU,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC;gBAAE,SAAS;YAC3D,WAAW,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;IAED,wEAAwE;IACxE,0EAA0E;IAC1E,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAErE,MAAM,QAAQ,GAAoB,EAAE,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,wEAAwE;QACxE,4CAA4C;QAC5C,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,KAAK,GAAG,CAAC,QAAQ;YAAE,SAAS;QAC1D,IAAI,gBAAgB,CAAC,IAAI,CAAC,cAAc,EAAE,YAAY,CAAC;YAAE,SAAS;QAElE,QAAQ,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,gBAAgB;YACxB,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,oBAAoB,IAAI,CAAC,MAAM,2KAA2K;YACnN,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW;YAClC,WAAW,EAAE,iBAAiB,CAAC,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC;YACjH,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,gCAAgC;SACpE,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: contract-method-drift
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — fires when a frontend network call targets an API path
|
|
5
|
+
* the server DOES define, but only for different HTTP methods. Sibling to
|
|
6
|
+
* `contract-drift`:
|
|
7
|
+
* - contract-drift : "no server route exists at this path"
|
|
8
|
+
* - contract-method-drift : "server routes exist at this path, just not
|
|
9
|
+
* for your verb"
|
|
10
|
+
*
|
|
11
|
+
* Keeping them separate matters: the messages and fixes differ (rename the
|
|
12
|
+
* client URL vs. change the verb), the fingerprints can't collide on the
|
|
13
|
+
* same line, and the existing contract-drift `continue` gate stays intact.
|
|
14
|
+
*
|
|
15
|
+
* Confidence multiplier: `CROSS_STACK_EXACT_CONFIDENCE` (0.9) — once the path
|
|
16
|
+
* matches, a method mismatch is unambiguous.
|
|
17
|
+
*
|
|
18
|
+
* Requires graph mode; silent in single-file review.
|
|
19
|
+
*/
|
|
20
|
+
import type { ReviewFinding } from '../types.js';
|
|
21
|
+
import type { ConceptRuleContext } from './index.js';
|
|
22
|
+
export declare function contractMethodDrift(ctx: ConceptRuleContext): ReviewFinding[];
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: contract-method-drift
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — fires when a frontend network call targets an API path
|
|
5
|
+
* the server DOES define, but only for different HTTP methods. Sibling to
|
|
6
|
+
* `contract-drift`:
|
|
7
|
+
* - contract-drift : "no server route exists at this path"
|
|
8
|
+
* - contract-method-drift : "server routes exist at this path, just not
|
|
9
|
+
* for your verb"
|
|
10
|
+
*
|
|
11
|
+
* Keeping them separate matters: the messages and fixes differ (rename the
|
|
12
|
+
* client URL vs. change the verb), the fingerprints can't collide on the
|
|
13
|
+
* same line, and the existing contract-drift `continue` gate stays intact.
|
|
14
|
+
*
|
|
15
|
+
* Confidence multiplier: `CROSS_STACK_EXACT_CONFIDENCE` (0.9) — once the path
|
|
16
|
+
* matches, a method mismatch is unambiguous.
|
|
17
|
+
*
|
|
18
|
+
* Requires graph mode; silent in single-file review.
|
|
19
|
+
*/
|
|
20
|
+
import { createFingerprint } from '../types.js';
|
|
21
|
+
import { API_PATH_RE, CROSS_STACK_EXACT_CONFIDENCE, collectRoutesAcrossGraph, findRoutesAtPath, normalizeClientUrl, } from './cross-stack-utils.js';
|
|
22
|
+
// Verbs the TS/Python mappers emit for handlers that intentionally accept any
|
|
23
|
+
// method (Express `app.all()` emits `ALL`; `app.use()` emits `undefined`).
|
|
24
|
+
const WILDCARD_METHODS = new Set(['ALL', 'ANY']);
|
|
25
|
+
export function contractMethodDrift(ctx) {
|
|
26
|
+
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
27
|
+
return [];
|
|
28
|
+
const serverRoutes = collectRoutesAcrossGraph(ctx.allConcepts);
|
|
29
|
+
if (serverRoutes.length === 0)
|
|
30
|
+
return [];
|
|
31
|
+
const clientCalls = [];
|
|
32
|
+
for (const [, conceptMap] of ctx.allConcepts) {
|
|
33
|
+
for (const node of conceptMap.nodes) {
|
|
34
|
+
if (node.kind !== 'effect' || node.payload.kind !== 'effect' || node.payload.subtype !== 'network')
|
|
35
|
+
continue;
|
|
36
|
+
const target = node.payload.target;
|
|
37
|
+
const method = node.payload.method;
|
|
38
|
+
if (typeof target !== 'string' || typeof method !== 'string')
|
|
39
|
+
continue;
|
|
40
|
+
const normalized = normalizeClientUrl(target);
|
|
41
|
+
if (!normalized || !API_PATH_RE.test(normalized))
|
|
42
|
+
continue;
|
|
43
|
+
clientCalls.push({ target, normalizedPath: normalized, method, node });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (clientCalls.length === 0)
|
|
47
|
+
return [];
|
|
48
|
+
const findings = [];
|
|
49
|
+
for (const call of clientCalls) {
|
|
50
|
+
if (call.node.primarySpan.file !== ctx.filePath)
|
|
51
|
+
continue;
|
|
52
|
+
const routesAtPath = findRoutesAtPath(call.normalizedPath, serverRoutes);
|
|
53
|
+
if (routesAtPath.length === 0)
|
|
54
|
+
continue;
|
|
55
|
+
const hasMethodMatch = routesAtPath.some((r) => methodMatches(r.method, call.method));
|
|
56
|
+
if (hasMethodMatch)
|
|
57
|
+
continue;
|
|
58
|
+
const serverMethods = collectKnownMethods(routesAtPath);
|
|
59
|
+
if (serverMethods.length === 0)
|
|
60
|
+
continue;
|
|
61
|
+
const methodList = serverMethods.join(', ');
|
|
62
|
+
const hint = serverMethods.length === 1 ? ` Did you mean \`${serverMethods[0]} ${call.target}\`?` : '';
|
|
63
|
+
findings.push({
|
|
64
|
+
source: 'kern',
|
|
65
|
+
ruleId: 'contract-method-drift',
|
|
66
|
+
severity: 'warning',
|
|
67
|
+
category: 'bug',
|
|
68
|
+
message: `Frontend calls \`${call.method} ${call.target}\` but the server only defines [${methodList}] for this path.${hint}`,
|
|
69
|
+
primarySpan: call.node.primarySpan,
|
|
70
|
+
fingerprint: createFingerprint('contract-method-drift', call.node.primarySpan.startLine, call.node.primarySpan.startCol),
|
|
71
|
+
confidence: call.node.confidence * CROSS_STACK_EXACT_CONFIDENCE,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return findings;
|
|
75
|
+
}
|
|
76
|
+
function methodMatches(routeMethod, clientMethod) {
|
|
77
|
+
if (!routeMethod)
|
|
78
|
+
return true;
|
|
79
|
+
const r = routeMethod.toUpperCase();
|
|
80
|
+
if (WILDCARD_METHODS.has(r))
|
|
81
|
+
return true;
|
|
82
|
+
const c = clientMethod.toUpperCase();
|
|
83
|
+
if (r === c)
|
|
84
|
+
return true;
|
|
85
|
+
// Express and Starlette/FastAPI both auto-respond to HEAD on GET routes
|
|
86
|
+
// (returning headers, no body). Firing method-drift on `HEAD /api/x`
|
|
87
|
+
// against `app.get('/api/x')` is a false positive — the server DOES
|
|
88
|
+
// satisfy the request. Codex review called this out.
|
|
89
|
+
if (c === 'HEAD' && r === 'GET')
|
|
90
|
+
return true;
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
function collectKnownMethods(routes) {
|
|
94
|
+
const set = new Set();
|
|
95
|
+
for (const r of routes) {
|
|
96
|
+
if (!r.method)
|
|
97
|
+
continue;
|
|
98
|
+
const m = r.method.toUpperCase();
|
|
99
|
+
if (WILDCARD_METHODS.has(m))
|
|
100
|
+
continue;
|
|
101
|
+
set.add(m);
|
|
102
|
+
}
|
|
103
|
+
return Array.from(set).sort();
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=contract-method-drift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-method-drift.js","sourceRoot":"","sources":["../../src/concept-rules/contract-method-drift.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,6 +17,14 @@ import type { ConceptMap, ConceptNode } from '@kernlang/core';
|
|
|
17
17
|
* untyped-api-response can bump its own multiplier).
|
|
18
18
|
*/
|
|
19
19
|
export declare const CROSS_STACK_HEURISTIC_CONFIDENCE = 0.7;
|
|
20
|
+
/**
|
|
21
|
+
* Multiplier for rules where the correlation is unambiguous: the path matches
|
|
22
|
+
* exactly AND a second dimension (HTTP method, auth header, …) disagrees.
|
|
23
|
+
* `contract-method-drift`, `duplicate-route`, and `auth-drift` use this —
|
|
24
|
+
* once the path matches, a verb mismatch, duplicate declaration, or missing
|
|
25
|
+
* Authorization header is a real bug, not a heuristic.
|
|
26
|
+
*/
|
|
27
|
+
export declare const CROSS_STACK_EXACT_CONFIDENCE = 0.9;
|
|
20
28
|
/** Client URLs we consider "internal" to the reviewed project. */
|
|
21
29
|
export declare const API_PATH_RE: RegExp;
|
|
22
30
|
export interface ServerRoute {
|
|
@@ -25,11 +33,41 @@ export interface ServerRoute {
|
|
|
25
33
|
/** Present when the caller needs to cite the server route in a finding. */
|
|
26
34
|
node?: ConceptNode;
|
|
27
35
|
}
|
|
36
|
+
export declare function hasFastApiEvidence(map: ConceptMap): boolean;
|
|
37
|
+
export declare function isFastApiRouteMissingResponseModel(node: ConceptNode, map?: ConceptMap): boolean;
|
|
28
38
|
/**
|
|
29
39
|
* Pull every server-side route out of a concept map. Callers typically fold
|
|
30
40
|
* this across `ctx.allConcepts` to collect routes for the whole project.
|
|
41
|
+
*
|
|
42
|
+
* Per-file use (legacy signature): just emits the decorator path as-is.
|
|
43
|
+
*
|
|
44
|
+
* Cross-project use (preferred): call `collectRoutesAcrossGraph` instead,
|
|
45
|
+
* which joins route-mount concepts (FastAPI `app.include_router(prefix=…)`)
|
|
46
|
+
* with the per-file route decorators so `@router.get("/current")` mounted
|
|
47
|
+
* under `prefix="/api/nutrition-goals"` surfaces as `/api/nutrition-goals/current`.
|
|
48
|
+
* Without that join the wedge rules silently find nothing on every FastAPI
|
|
49
|
+
* app that follows the standard APIRouter pattern.
|
|
31
50
|
*/
|
|
32
51
|
export declare function collectRoutes(map: ConceptMap, routes: ServerRoute[]): void;
|
|
52
|
+
/**
|
|
53
|
+
* Graph-wide route collection with FastAPI router-prefix expansion.
|
|
54
|
+
*
|
|
55
|
+
* Walks every concept map twice:
|
|
56
|
+
* 1. Collect `route-mount` concepts (FastAPI `app.include_router(<router>,
|
|
57
|
+
* prefix=…)` calls). Each mount carries `prefix`, `routerName`, and —
|
|
58
|
+
* when the router was imported from another module — `sourceModule`
|
|
59
|
+
* like `app.api.nutrition_goals`.
|
|
60
|
+
* 2. For each per-file `route` concept, look up a matching mount by
|
|
61
|
+
* `sourceModule` ↔ file path suffix (Python `app.api.nutrition_goals`
|
|
62
|
+
* resolves to any file path ending in `app/api/nutrition_goals.py`),
|
|
63
|
+
* falling back to a project-wide `routerName` match when the mount
|
|
64
|
+
* is in the same file as the routes.
|
|
65
|
+
*
|
|
66
|
+
* Per-file routes with no mount are still emitted with their declared path
|
|
67
|
+
* — Flask / Express routes and FastAPI apps that decorate directly on
|
|
68
|
+
* `@app.get(...)` already carry the full path.
|
|
69
|
+
*/
|
|
70
|
+
export declare function collectRoutesAcrossGraph(allConcepts: ReadonlyMap<string, ConceptMap>): ServerRoute[];
|
|
33
71
|
/**
|
|
34
72
|
* Strip scheme/host, query string, and fragment from a client URL so it can
|
|
35
73
|
* match against a server route template. Returns undefined when the input
|
|
@@ -48,3 +86,11 @@ export declare function normalizeClientUrl(raw: string): string | undefined;
|
|
|
48
86
|
export declare function findMatchingRoute(clientPath: string, routes: readonly ServerRoute[]): ServerRoute | undefined;
|
|
49
87
|
/** Boolean-returning thin wrapper preserved for callers that just need a yes/no. */
|
|
50
88
|
export declare function hasMatchingRoute(clientPath: string, routes: readonly ServerRoute[]): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Return every server route whose path template matches the client path,
|
|
91
|
+
* regardless of HTTP method. Used by `contract-method-drift` and
|
|
92
|
+
* `orphan-route` to distinguish "no server exists here" (contract-drift
|
|
93
|
+
* territory) from "server exists but only responds to a different verb /
|
|
94
|
+
* no one calls it" (method-drift / orphan-route territory).
|
|
95
|
+
*/
|
|
96
|
+
export declare function findRoutesAtPath(clientPath: string, routes: readonly ServerRoute[]): ServerRoute[];
|
|
@@ -16,11 +16,48 @@
|
|
|
16
16
|
* untyped-api-response can bump its own multiplier).
|
|
17
17
|
*/
|
|
18
18
|
export const CROSS_STACK_HEURISTIC_CONFIDENCE = 0.7;
|
|
19
|
+
/**
|
|
20
|
+
* Multiplier for rules where the correlation is unambiguous: the path matches
|
|
21
|
+
* exactly AND a second dimension (HTTP method, auth header, …) disagrees.
|
|
22
|
+
* `contract-method-drift`, `duplicate-route`, and `auth-drift` use this —
|
|
23
|
+
* once the path matches, a verb mismatch, duplicate declaration, or missing
|
|
24
|
+
* Authorization header is a real bug, not a heuristic.
|
|
25
|
+
*/
|
|
26
|
+
export const CROSS_STACK_EXACT_CONFIDENCE = 0.9;
|
|
19
27
|
/** Client URLs we consider "internal" to the reviewed project. */
|
|
20
28
|
export const API_PATH_RE = /^\/api\//;
|
|
29
|
+
export function hasFastApiEvidence(map) {
|
|
30
|
+
if (map.language !== 'py')
|
|
31
|
+
return false;
|
|
32
|
+
return map.edges.some((edge) => {
|
|
33
|
+
if (edge.kind !== 'dependency' || edge.payload.kind !== 'dependency')
|
|
34
|
+
return false;
|
|
35
|
+
return edge.payload.specifier === 'fastapi' || edge.payload.specifier.startsWith('fastapi.');
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function isFastApiRouteMissingResponseModel(node, map) {
|
|
39
|
+
if (node.language !== 'py')
|
|
40
|
+
return false;
|
|
41
|
+
if (node.kind !== 'entrypoint' || node.payload.kind !== 'entrypoint')
|
|
42
|
+
return false;
|
|
43
|
+
if (node.payload.subtype !== 'route')
|
|
44
|
+
return false;
|
|
45
|
+
if (node.payload.responseModel)
|
|
46
|
+
return false;
|
|
47
|
+
return map ? hasFastApiEvidence(map) : false;
|
|
48
|
+
}
|
|
21
49
|
/**
|
|
22
50
|
* Pull every server-side route out of a concept map. Callers typically fold
|
|
23
51
|
* this across `ctx.allConcepts` to collect routes for the whole project.
|
|
52
|
+
*
|
|
53
|
+
* Per-file use (legacy signature): just emits the decorator path as-is.
|
|
54
|
+
*
|
|
55
|
+
* Cross-project use (preferred): call `collectRoutesAcrossGraph` instead,
|
|
56
|
+
* which joins route-mount concepts (FastAPI `app.include_router(prefix=…)`)
|
|
57
|
+
* with the per-file route decorators so `@router.get("/current")` mounted
|
|
58
|
+
* under `prefix="/api/nutrition-goals"` surfaces as `/api/nutrition-goals/current`.
|
|
59
|
+
* Without that join the wedge rules silently find nothing on every FastAPI
|
|
60
|
+
* app that follows the standard APIRouter pattern.
|
|
24
61
|
*/
|
|
25
62
|
export function collectRoutes(map, routes) {
|
|
26
63
|
for (const node of map.nodes) {
|
|
@@ -32,6 +69,100 @@ export function collectRoutes(map, routes) {
|
|
|
32
69
|
routes.push({ path, method: node.payload.httpMethod, node });
|
|
33
70
|
}
|
|
34
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Graph-wide route collection with FastAPI router-prefix expansion.
|
|
74
|
+
*
|
|
75
|
+
* Walks every concept map twice:
|
|
76
|
+
* 1. Collect `route-mount` concepts (FastAPI `app.include_router(<router>,
|
|
77
|
+
* prefix=…)` calls). Each mount carries `prefix`, `routerName`, and —
|
|
78
|
+
* when the router was imported from another module — `sourceModule`
|
|
79
|
+
* like `app.api.nutrition_goals`.
|
|
80
|
+
* 2. For each per-file `route` concept, look up a matching mount by
|
|
81
|
+
* `sourceModule` ↔ file path suffix (Python `app.api.nutrition_goals`
|
|
82
|
+
* resolves to any file path ending in `app/api/nutrition_goals.py`),
|
|
83
|
+
* falling back to a project-wide `routerName` match when the mount
|
|
84
|
+
* is in the same file as the routes.
|
|
85
|
+
*
|
|
86
|
+
* Per-file routes with no mount are still emitted with their declared path
|
|
87
|
+
* — Flask / Express routes and FastAPI apps that decorate directly on
|
|
88
|
+
* `@app.get(...)` already carry the full path.
|
|
89
|
+
*/
|
|
90
|
+
export function collectRoutesAcrossGraph(allConcepts) {
|
|
91
|
+
const routes = [];
|
|
92
|
+
// Build the mount index first so each route can look up its prefix.
|
|
93
|
+
const mountsByModule = new Map();
|
|
94
|
+
const mountsByRouter = new Map();
|
|
95
|
+
for (const [mountFile, map] of allConcepts) {
|
|
96
|
+
for (const node of map.nodes) {
|
|
97
|
+
if (node.kind !== 'entrypoint' || node.payload.kind !== 'entrypoint')
|
|
98
|
+
continue;
|
|
99
|
+
if (node.payload.subtype !== 'route-mount')
|
|
100
|
+
continue;
|
|
101
|
+
const prefix = node.payload.name;
|
|
102
|
+
const routerName = node.payload.routerName;
|
|
103
|
+
const sourceModule = node.payload.sourceModule;
|
|
104
|
+
if (sourceModule) {
|
|
105
|
+
const list = mountsByModule.get(sourceModule) ?? [];
|
|
106
|
+
list.push(prefix);
|
|
107
|
+
mountsByModule.set(sourceModule, list);
|
|
108
|
+
}
|
|
109
|
+
if (routerName) {
|
|
110
|
+
const list = mountsByRouter.get(routerName) ?? [];
|
|
111
|
+
list.push({ prefix, mountFile });
|
|
112
|
+
mountsByRouter.set(routerName, list);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const [routeFile, map] of allConcepts) {
|
|
117
|
+
for (const node of map.nodes) {
|
|
118
|
+
if (node.kind !== 'entrypoint' || node.payload.kind !== 'entrypoint')
|
|
119
|
+
continue;
|
|
120
|
+
if (node.payload.subtype !== 'route')
|
|
121
|
+
continue;
|
|
122
|
+
const path = node.payload.name;
|
|
123
|
+
if (typeof path !== 'string' || !path.startsWith('/'))
|
|
124
|
+
continue;
|
|
125
|
+
const prefix = resolveMountPrefix(routeFile, node.payload.routerName, mountsByModule, mountsByRouter);
|
|
126
|
+
const fullPath = prefix ? joinPaths(prefix, path) : path;
|
|
127
|
+
routes.push({ path: fullPath, method: node.payload.httpMethod, node });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return routes;
|
|
131
|
+
}
|
|
132
|
+
function resolveMountPrefix(routeFile, routerName, mountsByModule, mountsByRouter) {
|
|
133
|
+
// Module-based match. TS mounts emit a `sourceModule` that already carries a
|
|
134
|
+
// code extension (e.g. `routes/review.ts`) — use it as a path suffix directly.
|
|
135
|
+
// Python mounts emit a dotted module name (`app.api.nutrition_goals`) — translate
|
|
136
|
+
// to `app/api/nutrition_goals.py` first. The leading-slash boundary check in
|
|
137
|
+
// both branches prevents `blog/api.py` from false-matching module `api`.
|
|
138
|
+
for (const [sourceModule, prefixes] of mountsByModule) {
|
|
139
|
+
if (prefixes.length === 0)
|
|
140
|
+
continue;
|
|
141
|
+
const relTail = /\.(ts|tsx|js|jsx|mjs|cjs)$/i.test(sourceModule)
|
|
142
|
+
? sourceModule
|
|
143
|
+
: `${sourceModule.replace(/\./g, '/')}.py`;
|
|
144
|
+
if (routeFile === relTail || routeFile.endsWith(`/${relTail}`))
|
|
145
|
+
return prefixes[0];
|
|
146
|
+
}
|
|
147
|
+
// Same-file match: `router = APIRouter(); app.include_router(router, prefix=…)`.
|
|
148
|
+
// The mount has no `sourceModule` but shares the file with the routes.
|
|
149
|
+
if (routerName) {
|
|
150
|
+
const entries = mountsByRouter.get(routerName);
|
|
151
|
+
if (entries) {
|
|
152
|
+
const sameFile = entries.find((e) => e.mountFile === routeFile);
|
|
153
|
+
if (sameFile)
|
|
154
|
+
return sameFile.prefix;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
function joinPaths(prefix, path) {
|
|
160
|
+
const trimmedPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
161
|
+
const trimmedPath = path.startsWith('/') ? path : `/${path}`;
|
|
162
|
+
if (trimmedPath === '/')
|
|
163
|
+
return trimmedPrefix || '/';
|
|
164
|
+
return `${trimmedPrefix}${trimmedPath}`;
|
|
165
|
+
}
|
|
35
166
|
/**
|
|
36
167
|
* Strip scheme/host, query string, and fragment from a client URL so it can
|
|
37
168
|
* match against a server route template. Returns undefined when the input
|
|
@@ -89,6 +220,36 @@ export function findMatchingRoute(clientPath, routes) {
|
|
|
89
220
|
export function hasMatchingRoute(clientPath, routes) {
|
|
90
221
|
return findMatchingRoute(clientPath, routes) !== undefined;
|
|
91
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Return every server route whose path template matches the client path,
|
|
225
|
+
* regardless of HTTP method. Used by `contract-method-drift` and
|
|
226
|
+
* `orphan-route` to distinguish "no server exists here" (contract-drift
|
|
227
|
+
* territory) from "server exists but only responds to a different verb /
|
|
228
|
+
* no one calls it" (method-drift / orphan-route territory).
|
|
229
|
+
*/
|
|
230
|
+
export function findRoutesAtPath(clientPath, routes) {
|
|
231
|
+
const clientSegments = trimTrailing(clientPath).split('/');
|
|
232
|
+
const matches = [];
|
|
233
|
+
for (const route of routes) {
|
|
234
|
+
const routeSegments = trimTrailing(route.path).split('/');
|
|
235
|
+
if (routeSegments.length !== clientSegments.length)
|
|
236
|
+
continue;
|
|
237
|
+
let matched = true;
|
|
238
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
239
|
+
const rs = routeSegments[i];
|
|
240
|
+
const cs = clientSegments[i];
|
|
241
|
+
if (isParamSegment(rs))
|
|
242
|
+
continue;
|
|
243
|
+
if (rs !== cs) {
|
|
244
|
+
matched = false;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (matched)
|
|
249
|
+
matches.push(route);
|
|
250
|
+
}
|
|
251
|
+
return matches;
|
|
252
|
+
}
|
|
92
253
|
function trimTrailing(path) {
|
|
93
254
|
return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path;
|
|
94
255
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cross-stack-utils.js","sourceRoot":"","sources":["../../src/concept-rules/cross-stack-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,GAAG,CAAC;AAEpD,kEAAkE;AAClE,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;AAStC
|
|
1
|
+
{"version":3,"file":"cross-stack-utils.js","sourceRoot":"","sources":["../../src/concept-rules/cross-stack-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,GAAG,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,GAAG,CAAC;AAEhD,kEAAkE;AAClE,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;AAStC,MAAM,UAAU,kBAAkB,CAAC,GAAe;IAChD,IAAI,GAAG,CAAC,QAAQ,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACxC,OAAO,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QAC7B,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY;YAAE,OAAO,KAAK,CAAC;QACnF,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;IAC/F,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,kCAAkC,CAAC,IAAiB,EAAE,GAAgB;IACpF,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACzC,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,KAAK,CAAC;IACnF,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC;IACnD,IAAI,IAAI,CAAC,OAAO,CAAC,aAAa;QAAE,OAAO,KAAK,CAAC;IAC7C,OAAO,GAAG,CAAC,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AAC/C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,GAAe,EAAE,MAAqB;IAClE,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,OAAO;YAAE,SAAS;QACnH,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;QAC/B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAChE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,wBAAwB,CAAC,WAA4C;IACnF,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,oEAAoE;IACpE,MAAM,cAAc,GAAG,IAAI,GAAG,EAAoB,CAAC;IACnD,MAAM,cAAc,GAAG,IAAI,GAAG,EAAwD,CAAC;IACvF,KAAK,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,IAAI,WAAW,EAAE,CAAC;QAC3C,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YAC7B,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY;gBAAE,SAAS;YAC/E,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,aAAa;gBAAE,SAAS;YACrD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YACjC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;YAC3C,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;YAC/C,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;gBACpD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAClB,cAAc,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;YACzC,CAAC;YACD,IAAI,UAAU,EAAE,CAAC;gBACf,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;gBAClD,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;gBACjC,cAAc,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YACvC,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,IAAI,WAAW,EAAE,CAAC;QAC3C,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YAC7B,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY;gBAAE,SAAS;YAC/E,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,OAAO;gBAAE,SAAS;YAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YAC/B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAS;YAEhE,MAAM,MAAM,GAAG,kBAAkB,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,cAAc,EAAE,cAAc,CAAC,CAAC;YACtG,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YACzD,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,kBAAkB,CACzB,SAAiB,EACjB,UAA8B,EAC9B,cAA6C,EAC7C,cAAiF;IAEjF,6EAA6E;IAC7E,+EAA+E;IAC/E,kFAAkF;IAClF,6EAA6E;IAC7E,yEAAyE;IACzE,KAAK,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,IAAI,cAAc,EAAE,CAAC;QACtD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACpC,MAAM,OAAO,GAAG,6BAA6B,CAAC,IAAI,CAAC,YAAY,CAAC;YAC9D,CAAC,CAAC,YAAY;YACd,CAAC,CAAC,GAAG,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC;QAC7C,IAAI,SAAS,KAAK,OAAO,IAAI,SAAS,CAAC,QAAQ,CAAC,IAAI,OAAO,EAAE,CAAC;YAAE,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrF,CAAC;IACD,iFAAiF;IACjF,uEAAuE;IACvE,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC/C,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;YAChE,IAAI,QAAQ;gBAAE,OAAO,QAAQ,CAAC,MAAM,CAAC;QACvC,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,SAAS,CAAC,MAAc,EAAE,IAAY;IAC7C,MAAM,aAAa,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAC1E,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;IAC7D,IAAI,WAAW,KAAK,GAAG;QAAE,OAAO,aAAa,IAAI,GAAG,CAAC;IACrD,OAAO,GAAG,aAAa,GAAG,WAAW,EAAE,CAAC;AAC1C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,IAAI,GAAG,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACrB,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACnE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAChC,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5D,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3D,GAAG,GAAG,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACtD,CAAC;IACD,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,CAAC,KAAK,CAAC,CAAC;QAAE,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpC,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,CAAC,KAAK,CAAC,CAAC;QAAE,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpC,OAAO,GAAG,IAAI,SAAS,CAAC;AAC1B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAAkB,EAAE,MAA8B;IAClF,MAAM,cAAc,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3D,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,aAAa,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC1D,IAAI,aAAa,CAAC,MAAM,KAAK,cAAc,CAAC,MAAM;YAAE,SAAS;QAC7D,IAAI,OAAO,GAAG,IAAI,CAAC;QACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9C,MAAM,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;YAC5B,MAAM,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;YAC7B,IAAI,cAAc,CAAC,EAAE,CAAC;gBAAE,SAAS;YACjC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;gBACd,OAAO,GAAG,KAAK,CAAC;gBAChB,MAAM;YACR,CAAC;QACH,CAAC;QACD,IAAI,OAAO;YAAE,OAAO,KAAK,CAAC;IAC5B,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,MAA8B;IACjF,OAAO,iBAAiB,CAAC,UAAU,EAAE,MAAM,CAAC,KAAK,SAAS,CAAC;AAC7D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,MAA8B;IACjF,MAAM,cAAc,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3D,MAAM,OAAO,GAAkB,EAAE,CAAC;IAClC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,aAAa,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC1D,IAAI,aAAa,CAAC,MAAM,KAAK,cAAc,CAAC,MAAM;YAAE,SAAS;QAC7D,IAAI,OAAO,GAAG,IAAI,CAAC;QACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9C,MAAM,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;YAC5B,MAAM,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;YAC7B,IAAI,cAAc,CAAC,EAAE,CAAC;gBAAE,SAAS;YACjC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;gBACd,OAAO,GAAG,KAAK,CAAC;gBAChB,MAAM;YACR,CAAC;QACH,CAAC;QACD,IAAI,OAAO;YAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IAChC,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1E,CAAC;AAED,SAAS,cAAc,CAAC,GAAW;IACjC,OAAO,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3E,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
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 type { ReviewFinding } from '../types.js';
|
|
19
|
+
import type { ConceptRuleContext } from './index.js';
|
|
20
|
+
export declare function duplicateRoute(ctx: ConceptRuleContext): ReviewFinding[];
|