gitnexus 1.6.13-rc.56 → 1.6.13-rc.57
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/core/group/extractors/http-patterns/python.js +23 -4
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +14 -10
- package/dist/core/ingestion/route-extractors/fastapi-router-bindings.d.ts +19 -15
- package/dist/core/ingestion/route-extractors/fastapi-router-bindings.js +43 -21
- package/dist/core/ingestion/route-extractors/fastapi-router-prefixes.d.ts +21 -0
- package/dist/core/ingestion/route-extractors/fastapi-router-prefixes.js +153 -0
- package/dist/storage/parse-cache.js +4 -1
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Python from 'tree-sitter-python';
|
|
2
2
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
3
3
|
import { normalizeExtractedRoutePath } from '../../../ingestion/route-extractors/route-path.js';
|
|
4
|
+
import { extractFastAPIRouterBindings, } from '../../../ingestion/route-extractors/fastapi-router-bindings.js';
|
|
5
|
+
import { mergeMountPrefixes, resolveFastAPIRouterPrefixes, } from '../../../ingestion/route-extractors/fastapi-router-prefixes.js';
|
|
4
6
|
import { extractPythonModuleConstants, parseConstOperands, resolveOperands, } from '../../../ingestion/route-extractors/python-const-resolver.js';
|
|
5
7
|
/**
|
|
6
8
|
* Python HTTP plugin. Handles:
|
|
@@ -883,6 +885,10 @@ const NONLITERAL_ROUTE_DECORATOR_RE = /@\s*(?:app|router)\s*\.\s*(?:get|post|put
|
|
|
883
885
|
function buildPythonRepoContext(files, parser, readFile, parseSource) {
|
|
884
886
|
const prefixesByLongKey = new Map();
|
|
885
887
|
const prefixesByShortKey = new Map();
|
|
888
|
+
const routerIncludes = [];
|
|
889
|
+
const routerImports = [];
|
|
890
|
+
const routerModuleAliases = [];
|
|
891
|
+
const routerConstructorPrefixes = [];
|
|
886
892
|
// Single read pass (#2393): slurp every `.py` file's content ONCE. This used to
|
|
887
893
|
// be two passes — the include_router pre-pass below and the #2391 constant cost
|
|
888
894
|
// gate each re-read every `.py` file. The composed-route cost gate is computed
|
|
@@ -900,6 +906,15 @@ function buildPythonRepoContext(files, parser, readFile, parseSource) {
|
|
|
900
906
|
if (!hasComposedRoute && NONLITERAL_ROUTE_DECORATOR_RE.test(src))
|
|
901
907
|
hasComposedRoute = true;
|
|
902
908
|
}
|
|
909
|
+
for (const [rel, src] of pyContents) {
|
|
910
|
+
if (src.includes('include_router')) {
|
|
911
|
+
extractFastAPIRouterBindings(rel, src, routerIncludes, routerImports, routerModuleAliases, routerConstructorPrefixes);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
// Resolve against every repo path, empty files included, so module
|
|
915
|
+
// ambiguity matches ingestion (which passes all scanned paths).
|
|
916
|
+
const { prefixesByFile, resolvedIncludes } = resolveFastAPIRouterPrefixes(files, routerIncludes, routerImports, routerModuleAliases, routerConstructorPrefixes);
|
|
917
|
+
const resolvedIncludeKeys = new Set([...resolvedIncludes].map((inc) => JSON.stringify([inc.filePath, inc.routerExpr, inc.prefix])));
|
|
903
918
|
// Single PARSE pass (#2391): parse each `.py` at most once and feed BOTH the
|
|
904
919
|
// include_router prefix pre-pass and the composed-constant map below. This used
|
|
905
920
|
// to be two loops, so an include_router file in a composed repo was parsed
|
|
@@ -968,6 +983,8 @@ function buildPythonRepoContext(files, parser, readFile, parseSource) {
|
|
|
968
983
|
if (prefix === null)
|
|
969
984
|
continue;
|
|
970
985
|
const moduleShort = modNode.text;
|
|
986
|
+
if (resolvedIncludeKeys.has(JSON.stringify([rel, `${moduleShort}.router`, prefix])))
|
|
987
|
+
continue;
|
|
971
988
|
const aliasLong = localNameToModuleAlias.get(moduleShort);
|
|
972
989
|
const sameFileImport = localNameToModule.get(moduleShort);
|
|
973
990
|
const longKey = aliasLong ?? sameFileImport?.moduleLong;
|
|
@@ -993,6 +1010,8 @@ function buildPythonRepoContext(files, parser, readFile, parseSource) {
|
|
|
993
1010
|
const prefix = unquoteLiteral(prefixNode.text);
|
|
994
1011
|
if (prefix === null)
|
|
995
1012
|
continue;
|
|
1013
|
+
if (resolvedIncludeKeys.has(JSON.stringify([rel, nameNode.text, prefix])))
|
|
1014
|
+
continue;
|
|
996
1015
|
if (localImp.moduleLong) {
|
|
997
1016
|
recordPrefix(prefixesByLongKey, localImp.moduleLong, prefix);
|
|
998
1017
|
}
|
|
@@ -1013,6 +1032,7 @@ function buildPythonRepoContext(files, parser, readFile, parseSource) {
|
|
|
1013
1032
|
}
|
|
1014
1033
|
}
|
|
1015
1034
|
return {
|
|
1035
|
+
prefixesByFile,
|
|
1016
1036
|
prefixesByLongKey,
|
|
1017
1037
|
prefixesByShortKey,
|
|
1018
1038
|
constantsByFile,
|
|
@@ -1145,14 +1165,13 @@ export const PYTHON_HTTP_PLUGIN = {
|
|
|
1145
1165
|
// by the literal and the #2391 non-literal (resolved) router loops so both
|
|
1146
1166
|
// stack prefixes identically.
|
|
1147
1167
|
const emitRouterProvider = (httpMethod, rawPath, line) => {
|
|
1148
|
-
//
|
|
1149
|
-
//
|
|
1150
|
-
// graph nodes and group contracts agree on which prefix applies.
|
|
1168
|
+
// Exact file matches plus the legacy long/short fallback. This mirrors
|
|
1169
|
+
// ingestion so graph Route nodes and contracts agree on mounted paths.
|
|
1151
1170
|
const longKey = fileRel ? fileLongKey(fileRel) : '';
|
|
1152
1171
|
const longPrefixes = longKey ? ctx?.prefixesByLongKey.get(longKey) : undefined;
|
|
1153
1172
|
const shortKey = fileRel ? fileShortKey(fileRel) : '';
|
|
1154
1173
|
const shortPrefixes = longPrefixes || !shortKey ? undefined : ctx?.prefixesByShortKey.get(shortKey);
|
|
1155
|
-
const prefixSet = longPrefixes ?? shortPrefixes;
|
|
1174
|
+
const prefixSet = mergeMountPrefixes(fileRel ? ctx?.prefixesByFile.get(fileRel.replace(/\\/g, '/')) : undefined, longPrefixes ?? shortPrefixes);
|
|
1156
1175
|
// Stack the same-file APIRouter(prefix=...) under any cross-file
|
|
1157
1176
|
// include_router prefix.
|
|
1158
1177
|
const localPath = constructorPrefix ? joinPrefix(constructorPrefix, rawPath) : rawPath;
|
|
@@ -30,6 +30,7 @@ import { classifyContentLanguages } from '../content-language-classification.js'
|
|
|
30
30
|
import { SCOPE_RESOLVERS } from '../scope-resolution/pipeline/registry.js';
|
|
31
31
|
import { DATA_ROUTE_TABLE_SOURCE } from '../route-extractors/data-route-table.js';
|
|
32
32
|
import { createWorkerPool, workerPoolDisabledByEnv, resolveAutoPoolSize, envWorkerPoolSize, resolveHostParallelism, WorkerPoolInitializationError, WorkerPoolDisabledError, } from '../workers/worker-pool.js';
|
|
33
|
+
import { mergeMountPrefixes, resolveFastAPIRouterPrefixes, } from '../route-extractors/fastapi-router-prefixes.js';
|
|
33
34
|
import { normalizeExtractedRoutePath } from '../route-extractors/route-path.js';
|
|
34
35
|
import { resolveOperands } from '../route-extractors/python-const-resolver.js';
|
|
35
36
|
import { prepareRouteConstantsByProvider } from '../language-provider.js';
|
|
@@ -1326,11 +1327,9 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
1326
1327
|
}
|
|
1327
1328
|
m.set(alias.localName, alias.moduleKeyLong);
|
|
1328
1329
|
}
|
|
1329
|
-
//
|
|
1330
|
-
//
|
|
1331
|
-
//
|
|
1332
|
-
// remain the fallback when no long key is known (e.g. Shape A includes
|
|
1333
|
-
// without a corresponding import statement).
|
|
1330
|
+
// Exact-file matches handle import-resolved mounts (including nested
|
|
1331
|
+
// routers). These long/short maps preserve the older fallback for mounts
|
|
1332
|
+
// whose import cannot be resolved to one file.
|
|
1334
1333
|
const prefixesByLongKey = new Map();
|
|
1335
1334
|
const prefixesByShortKey = new Map();
|
|
1336
1335
|
// Constructor prefixes are `router`-only (the apply gate below and the
|
|
@@ -1338,6 +1337,7 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
1338
1337
|
// flat file-key → prefix map suffices — mirrors the group layer's shape.
|
|
1339
1338
|
const constructorPrefixesByLongKey = new Map();
|
|
1340
1339
|
const constructorPrefixesByShortKey = new Map();
|
|
1340
|
+
const { prefixesByFile, resolvedIncludes } = resolveFastAPIRouterPrefixes(allPaths, allRouterIncludes, allRouterImports, allRouterModuleAliases, allRouterConstructorPrefixes);
|
|
1341
1341
|
const recordPrefix = (target, key, prefix) => {
|
|
1342
1342
|
let set = target.get(key);
|
|
1343
1343
|
if (!set) {
|
|
@@ -1347,6 +1347,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
1347
1347
|
set.add(prefix);
|
|
1348
1348
|
};
|
|
1349
1349
|
for (const inc of allRouterIncludes) {
|
|
1350
|
+
// Unprefixed includes only exist as propagation edges; recording `''`
|
|
1351
|
+
// here would shadow a real short-key prefix for the same module.
|
|
1352
|
+
if (resolvedIncludes.has(inc) || !inc.prefix)
|
|
1353
|
+
continue;
|
|
1350
1354
|
// Shape A: `<module>.router`. The worker emits `routerExpr` already
|
|
1351
1355
|
// including `.router`, so split it back. We only know a short module
|
|
1352
1356
|
// key here — the call site doesn't carry the dotted package path. If
|
|
@@ -1377,7 +1381,8 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
1377
1381
|
recordPrefix(prefixesByShortKey, localImp.moduleKey, inc.prefix);
|
|
1378
1382
|
}
|
|
1379
1383
|
}
|
|
1380
|
-
if (
|
|
1384
|
+
if (prefixesByFile.size > 0 ||
|
|
1385
|
+
prefixesByLongKey.size > 0 ||
|
|
1381
1386
|
prefixesByShortKey.size > 0 ||
|
|
1382
1387
|
allRouterConstructorPrefixes.length > 0) {
|
|
1383
1388
|
const fileLongKey = (rel) => {
|
|
@@ -1415,15 +1420,14 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
1415
1420
|
expanded.push(dr);
|
|
1416
1421
|
continue;
|
|
1417
1422
|
}
|
|
1418
|
-
//
|
|
1419
|
-
//
|
|
1420
|
-
// between e.g. `api/users.py` and `admin/users.py`.
|
|
1423
|
+
// Exact file matches plus the legacy long/short fallback for mounts
|
|
1424
|
+
// whose import could not be resolved.
|
|
1421
1425
|
const longKey = fileLongKey(dr.filePath);
|
|
1422
1426
|
const longPrefixes = longKey ? prefixesByLongKey.get(longKey) : undefined;
|
|
1423
1427
|
const shortPrefixes = longPrefixes
|
|
1424
1428
|
? undefined
|
|
1425
1429
|
: prefixesByShortKey.get(fileShortKey(dr.filePath));
|
|
1426
|
-
const prefixes = longPrefixes ?? shortPrefixes;
|
|
1430
|
+
const prefixes = mergeMountPrefixes(prefixesByFile.get(dr.filePath.replace(/\\/g, '/')), longPrefixes ?? shortPrefixes);
|
|
1427
1431
|
// Constructor prefixes are keyed like include_router prefixes:
|
|
1428
1432
|
// long-key entries are precise, while short-key entries are only
|
|
1429
1433
|
// valid for repo-root/single-segment files where `fileLongKey`
|
|
@@ -13,17 +13,19 @@
|
|
|
13
13
|
* extraction here lets unit tests import the function directly without
|
|
14
14
|
* booting a worker, satisfying DoD §2.7.
|
|
15
15
|
*
|
|
16
|
-
* Worker phase is per-file, so the
|
|
17
|
-
* `pipeline-phases/parse-impl.ts`. Here we
|
|
16
|
+
* Worker phase is per-file, so the cross-file resolution lives in
|
|
17
|
+
* `fastapi-router-prefixes.ts` and `pipeline-phases/parse-impl.ts`. Here we extract raw record
|
|
18
18
|
* kinds and let the pipeline aggregate them across files:
|
|
19
19
|
*
|
|
20
20
|
* • {@link ExtractedRouterInclude} — every
|
|
21
|
-
* `<host>.include_router(<routerExpr>, prefix='/x')`
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* `
|
|
21
|
+
* `<host>.include_router(<routerExpr>, prefix='/x')` mount, plus
|
|
22
|
+
* unprefixed `<host>.include_router(<routerExpr>[, <other kwargs>])`
|
|
23
|
+
* calls recorded with `prefix: ''`. `<routerExpr>` is either
|
|
24
|
+
* `<module>.router` (Shape A) or a bare local name (Shape B). The
|
|
25
|
+
* extractor leaves `<host>` unconstrained (production code uses `app`,
|
|
26
|
+
* `api`, `application`, `asgi_app`, …); unprefixed records are only
|
|
27
|
+
* propagation edges, and `fastapi-router-prefixes.ts` passes a parent
|
|
28
|
+
* prefix through them only when the host is `router`.
|
|
27
29
|
*
|
|
28
30
|
* • {@link ExtractedRouterImport} — every
|
|
29
31
|
* `from <module> import router [as <alias>]`, captured for both
|
|
@@ -37,25 +39,25 @@
|
|
|
37
39
|
* • short key — basename without `.py` (`users`)
|
|
38
40
|
* • long key — `<parent-dir>/<basename>` (`api/users`)
|
|
39
41
|
*
|
|
40
|
-
* Imports
|
|
41
|
-
*
|
|
42
|
-
* long key first and falls back to the short key, so cross-package
|
|
43
|
-
* collisions are eliminated for Shape B and minimised for Shape A.
|
|
42
|
+
* Imports retain their module path for exact-file resolution. The short and
|
|
43
|
+
* long keys remain for the conservative fallback when that resolution fails.
|
|
44
44
|
*
|
|
45
45
|
* The functions in this module are pure (no Worker / parentPort
|
|
46
46
|
* dependency) so they can be unit-tested directly without booting a
|
|
47
47
|
* worker thread.
|
|
48
48
|
*/
|
|
49
49
|
/**
|
|
50
|
-
* One `<host>.include_router(<routerExpr>, prefix='/x')` site
|
|
50
|
+
* One `<host>.include_router(<routerExpr>, prefix='/x')` site, or a
|
|
51
|
+
* simple child include without a local prefix.
|
|
51
52
|
*
|
|
52
53
|
* `routerExpr` is the raw text of the first argument — either
|
|
53
54
|
* `<module>.router` (Shape A) or a bare local name (Shape B).
|
|
54
|
-
*
|
|
55
|
-
*
|
|
55
|
+
* Cross-file resolution uses {@link ExtractedRouterImport} records
|
|
56
|
+
* emitted by the same file.
|
|
56
57
|
*/
|
|
57
58
|
export interface ExtractedRouterInclude {
|
|
58
59
|
filePath: string;
|
|
60
|
+
host: string;
|
|
59
61
|
routerExpr: string;
|
|
60
62
|
prefix: string;
|
|
61
63
|
lineNumber: number;
|
|
@@ -76,6 +78,7 @@ export interface ExtractedRouterInclude {
|
|
|
76
78
|
export interface ExtractedRouterImport {
|
|
77
79
|
filePath: string;
|
|
78
80
|
localName: string;
|
|
81
|
+
modulePath: string;
|
|
79
82
|
moduleKey: string;
|
|
80
83
|
moduleKeyLong?: string;
|
|
81
84
|
}
|
|
@@ -98,6 +101,7 @@ export interface ExtractedRouterModuleAlias {
|
|
|
98
101
|
filePath: string;
|
|
99
102
|
/** Local name in the importing file (== imported name or its alias). */
|
|
100
103
|
localName: string;
|
|
104
|
+
modulePath: string;
|
|
101
105
|
/** Long key (`<parent>/<stem>`) — non-empty for every emitted record. */
|
|
102
106
|
moduleKeyLong: string;
|
|
103
107
|
}
|
|
@@ -13,17 +13,19 @@
|
|
|
13
13
|
* extraction here lets unit tests import the function directly without
|
|
14
14
|
* booting a worker, satisfying DoD §2.7.
|
|
15
15
|
*
|
|
16
|
-
* Worker phase is per-file, so the
|
|
17
|
-
* `pipeline-phases/parse-impl.ts`. Here we
|
|
16
|
+
* Worker phase is per-file, so the cross-file resolution lives in
|
|
17
|
+
* `fastapi-router-prefixes.ts` and `pipeline-phases/parse-impl.ts`. Here we extract raw record
|
|
18
18
|
* kinds and let the pipeline aggregate them across files:
|
|
19
19
|
*
|
|
20
20
|
* • {@link ExtractedRouterInclude} — every
|
|
21
|
-
* `<host>.include_router(<routerExpr>, prefix='/x')`
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* `
|
|
21
|
+
* `<host>.include_router(<routerExpr>, prefix='/x')` mount, plus
|
|
22
|
+
* unprefixed `<host>.include_router(<routerExpr>[, <other kwargs>])`
|
|
23
|
+
* calls recorded with `prefix: ''`. `<routerExpr>` is either
|
|
24
|
+
* `<module>.router` (Shape A) or a bare local name (Shape B). The
|
|
25
|
+
* extractor leaves `<host>` unconstrained (production code uses `app`,
|
|
26
|
+
* `api`, `application`, `asgi_app`, …); unprefixed records are only
|
|
27
|
+
* propagation edges, and `fastapi-router-prefixes.ts` passes a parent
|
|
28
|
+
* prefix through them only when the host is `router`.
|
|
27
29
|
*
|
|
28
30
|
* • {@link ExtractedRouterImport} — every
|
|
29
31
|
* `from <module> import router [as <alias>]`, captured for both
|
|
@@ -37,10 +39,8 @@
|
|
|
37
39
|
* • short key — basename without `.py` (`users`)
|
|
38
40
|
* • long key — `<parent-dir>/<basename>` (`api/users`)
|
|
39
41
|
*
|
|
40
|
-
* Imports
|
|
41
|
-
*
|
|
42
|
-
* long key first and falls back to the short key, so cross-package
|
|
43
|
-
* collisions are eliminated for Shape B and minimised for Shape A.
|
|
42
|
+
* Imports retain their module path for exact-file resolution. The short and
|
|
43
|
+
* long keys remain for the conservative fallback when that resolution fails.
|
|
44
44
|
*
|
|
45
45
|
* The functions in this module are pure (no Worker / parentPort
|
|
46
46
|
* dependency) so they can be unit-tested directly without booting a
|
|
@@ -49,10 +49,18 @@
|
|
|
49
49
|
// `<host>.include_router(<module>.router, ..., prefix='/x')` (Shape A).
|
|
50
50
|
// `<host>` is left unrestricted — common production names include
|
|
51
51
|
// `app`, `api`, `application`, `asgi_app`. Pinning to the literal
|
|
52
|
-
// `app` would silently drop these.
|
|
53
|
-
|
|
52
|
+
// `app` would silently drop these. Arguments before `prefix=` may hold one
|
|
53
|
+
// level of nested calls (`dependencies=[Depends(auth)]`); the two
|
|
54
|
+
// alternatives start on disjoint characters, so matching stays linear.
|
|
55
|
+
const INCLUDE_ROUTER_ATTR_RE = /\b([A-Za-z_][\w.]*)\.include_router\s*\(\s*([A-Za-z_][\w]*)\.router\b(?:[^()]|\([^()]*\))*?\bprefix\s*=\s*(['"])([^'"]*)\3/g;
|
|
54
56
|
// `<host>.include_router(<local_name>, ..., prefix='/x')` (Shape B).
|
|
55
|
-
const INCLUDE_ROUTER_NAME_RE = /\b(
|
|
57
|
+
const INCLUDE_ROUTER_NAME_RE = /\b([A-Za-z_][\w.]*)\.include_router\s*\(\s*([A-Za-z_][\w]*)\b(?:[^()]|\([^()]*\))*?\bprefix\s*=\s*(['"])([^'"]*)\3/g;
|
|
58
|
+
// A child router may be included without a local prefix and inherit the
|
|
59
|
+
// prefix of its parent router when that parent is mounted elsewhere. Other
|
|
60
|
+
// keyword arguments (with one level of nested calls, like the prefixed
|
|
61
|
+
// patterns) and a trailing comma are allowed; a `prefix=` anywhere in the
|
|
62
|
+
// call declines the match so it never double-fires with those patterns.
|
|
63
|
+
const INCLUDE_ROUTER_UNPREFIXED_RE = /\b([A-Za-z_][\w.]*)\.include_router\s*\(\s*([A-Za-z_]\w*(?:\.router)?)\s*(?:,(?!(?:[^()]|\([^()]*\))*?\bprefix\s*=)(?:[^()]|\([^()]*\))*)?\)/g;
|
|
56
64
|
// Module path: a sequence of dots (`.`, `..`, `...`) for "current
|
|
57
65
|
// package" imports, OR an optional leading-dot prefix followed by a
|
|
58
66
|
// dotted identifier (`api.users`, `.api.users`, `..siblings.users`).
|
|
@@ -175,6 +183,7 @@ export function extractFastAPIRouterBindings(filePath, content, outIncludes, out
|
|
|
175
183
|
outImports.push({
|
|
176
184
|
filePath,
|
|
177
185
|
localName,
|
|
186
|
+
modulePath: moduleText,
|
|
178
187
|
moduleKey: moduleShort,
|
|
179
188
|
...(moduleLong ? { moduleKeyLong: moduleLong } : {}),
|
|
180
189
|
});
|
|
@@ -199,6 +208,7 @@ export function extractFastAPIRouterBindings(filePath, content, outIncludes, out
|
|
|
199
208
|
outModuleAliases.push({
|
|
200
209
|
filePath,
|
|
201
210
|
localName,
|
|
211
|
+
modulePath: `${moduleText}.${importedName}`,
|
|
202
212
|
moduleKeyLong: aliasLong,
|
|
203
213
|
});
|
|
204
214
|
}
|
|
@@ -231,8 +241,9 @@ export function extractFastAPIRouterBindings(filePath, content, outIncludes, out
|
|
|
231
241
|
while ((m = INCLUDE_ROUTER_ATTR_RE.exec(content)) !== null) {
|
|
232
242
|
outIncludes.push({
|
|
233
243
|
filePath,
|
|
234
|
-
|
|
235
|
-
|
|
244
|
+
host: m[1],
|
|
245
|
+
routerExpr: `${m[2]}.router`,
|
|
246
|
+
prefix: m[4],
|
|
236
247
|
lineNumber: content.substring(0, m.index).split('\n').length,
|
|
237
248
|
});
|
|
238
249
|
}
|
|
@@ -245,14 +256,25 @@ export function extractFastAPIRouterBindings(filePath, content, outIncludes, out
|
|
|
245
256
|
// is intentionally permissive and would re-capture `<mod>.router`
|
|
246
257
|
// as the bare name `mod`. Discriminate by re-checking the
|
|
247
258
|
// immediate source around the captured argument position.
|
|
248
|
-
const argStart = m.index + m[0].indexOf(m[
|
|
249
|
-
const dotProbe = content.slice(argStart + m[
|
|
259
|
+
const argStart = m.index + m[0].indexOf(m[2], m[0].indexOf('(') + 1);
|
|
260
|
+
const dotProbe = content.slice(argStart + m[2].length, argStart + m[2].length + 8);
|
|
250
261
|
if (/^\s*\.\s*router/.test(dotProbe))
|
|
251
262
|
continue;
|
|
252
263
|
outIncludes.push({
|
|
253
264
|
filePath,
|
|
254
|
-
|
|
255
|
-
|
|
265
|
+
host: m[1],
|
|
266
|
+
routerExpr: m[2],
|
|
267
|
+
prefix: m[4],
|
|
268
|
+
lineNumber: content.substring(0, m.index).split('\n').length,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
INCLUDE_ROUTER_UNPREFIXED_RE.lastIndex = 0;
|
|
272
|
+
while ((m = INCLUDE_ROUTER_UNPREFIXED_RE.exec(content)) !== null) {
|
|
273
|
+
outIncludes.push({
|
|
274
|
+
filePath,
|
|
275
|
+
host: m[1],
|
|
276
|
+
routerExpr: m[2],
|
|
277
|
+
prefix: '',
|
|
256
278
|
lineNumber: content.substring(0, m.index).split('\n').length,
|
|
257
279
|
});
|
|
258
280
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExtractedRouterConstructorPrefix, ExtractedRouterImport, ExtractedRouterInclude, ExtractedRouterModuleAlias } from './fastapi-router-bindings.js';
|
|
2
|
+
export interface ResolvedFastAPIRouterPrefixes {
|
|
3
|
+
prefixesByFile: Map<string, Set<string>>;
|
|
4
|
+
resolvedIncludes: Set<ExtractedRouterInclude>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Prefixes that apply to one router file: exact import-resolved mounts plus
|
|
8
|
+
* the legacy long/short-key prefixes of mounts the resolver could not bind.
|
|
9
|
+
* Shared by ingestion and the group extractor so both surfaces agree.
|
|
10
|
+
*/
|
|
11
|
+
export declare function mergeMountPrefixes(exact: ReadonlySet<string> | undefined, legacy: ReadonlySet<string> | undefined): ReadonlySet<string> | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Carry mounted prefixes through exact, import-resolved router includes.
|
|
14
|
+
*
|
|
15
|
+
* Only a host literally named `router` passes its mounted prefix on to the
|
|
16
|
+
* routers it includes, and resolution is per file rather than per variable:
|
|
17
|
+
* `api_router = APIRouter(); api_router.include_router(x.router)` does not
|
|
18
|
+
* pass through, and a prefixed `api_router.include_router(...)` is treated
|
|
19
|
+
* as a root mount. Unprefixed includes from any other host are ignored.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveFastAPIRouterPrefixes(files: Iterable<string>, includes: readonly ExtractedRouterInclude[], imports: readonly ExtractedRouterImport[], moduleAliases: readonly ExtractedRouterModuleAlias[], constructorPrefixes?: readonly ExtractedRouterConstructorPrefix[]): ResolvedFastAPIRouterPrefixes;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { normalizeExtractedRoutePath } from './route-path.js';
|
|
2
|
+
/** Resolve only imports that identify one Python file in this repository. */
|
|
3
|
+
function resolveModuleFile(importer, modulePath, files, absoluteModules) {
|
|
4
|
+
const leadingDots = /^\.+/.exec(modulePath)?.[0].length ?? 0;
|
|
5
|
+
const dotted = modulePath.slice(leadingDots);
|
|
6
|
+
if (!dotted)
|
|
7
|
+
return undefined;
|
|
8
|
+
const moduleSegments = dotted.split('.');
|
|
9
|
+
if (!moduleSegments.every((part) => /^[A-Za-z_]\w*$/.test(part)))
|
|
10
|
+
return undefined;
|
|
11
|
+
let stem;
|
|
12
|
+
if (leadingDots > 0) {
|
|
13
|
+
const directory = importer.replace(/\\/g, '/').split('/').slice(0, -1);
|
|
14
|
+
const parentLevels = leadingDots - 1;
|
|
15
|
+
if (parentLevels > directory.length)
|
|
16
|
+
return undefined;
|
|
17
|
+
stem = [...directory.slice(0, directory.length - parentLevels), ...moduleSegments].join('/');
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
stem = moduleSegments.join('/');
|
|
21
|
+
}
|
|
22
|
+
if (leadingDots === 0)
|
|
23
|
+
return absoluteModules.get(stem) ?? undefined;
|
|
24
|
+
const candidates = [`${stem}.py`, `${stem}/__init__.py`].filter((candidate) => files.has(candidate));
|
|
25
|
+
return candidates.length === 1 ? candidates[0] : undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Prefixes that apply to one router file: exact import-resolved mounts plus
|
|
29
|
+
* the legacy long/short-key prefixes of mounts the resolver could not bind.
|
|
30
|
+
* Shared by ingestion and the group extractor so both surfaces agree.
|
|
31
|
+
*/
|
|
32
|
+
export function mergeMountPrefixes(exact, legacy) {
|
|
33
|
+
if (!exact)
|
|
34
|
+
return legacy;
|
|
35
|
+
if (!legacy)
|
|
36
|
+
return exact;
|
|
37
|
+
return new Set([...exact, ...legacy]);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Carry mounted prefixes through exact, import-resolved router includes.
|
|
41
|
+
*
|
|
42
|
+
* Only a host literally named `router` passes its mounted prefix on to the
|
|
43
|
+
* routers it includes, and resolution is per file rather than per variable:
|
|
44
|
+
* `api_router = APIRouter(); api_router.include_router(x.router)` does not
|
|
45
|
+
* pass through, and a prefixed `api_router.include_router(...)` is treated
|
|
46
|
+
* as a root mount. Unprefixed includes from any other host are ignored.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveFastAPIRouterPrefixes(files, includes, imports, moduleAliases, constructorPrefixes = []) {
|
|
49
|
+
const fileSet = new Set([...files].map((file) => file.replace(/\\/g, '/')));
|
|
50
|
+
const constructorPrefixByFile = new Map(constructorPrefixes.map((ctor) => [ctor.filePath.replace(/\\/g, '/'), ctor.prefix]));
|
|
51
|
+
const absoluteModules = new Map();
|
|
52
|
+
for (const file of fileSet) {
|
|
53
|
+
if (!file.endsWith('.py'))
|
|
54
|
+
continue;
|
|
55
|
+
const stem = file.endsWith('/__init__.py')
|
|
56
|
+
? file.slice(0, -'/__init__.py'.length)
|
|
57
|
+
: file.slice(0, -'.py'.length);
|
|
58
|
+
const parts = stem.split('/');
|
|
59
|
+
for (let i = 0; i < parts.length; i++) {
|
|
60
|
+
const suffix = parts.slice(i).join('/');
|
|
61
|
+
const previous = absoluteModules.get(suffix);
|
|
62
|
+
absoluteModules.set(suffix, previous === undefined ? file : previous === file ? file : null);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const importsByFile = new Map();
|
|
66
|
+
for (const imp of [...imports, ...moduleAliases]) {
|
|
67
|
+
const modulePath = imp.modulePath;
|
|
68
|
+
if (!modulePath)
|
|
69
|
+
continue;
|
|
70
|
+
const file = imp.filePath.replace(/\\/g, '/');
|
|
71
|
+
const bindings = importsByFile.get(file) ?? new Map();
|
|
72
|
+
bindings.set(imp.localName, { modulePath });
|
|
73
|
+
importsByFile.set(file, bindings);
|
|
74
|
+
}
|
|
75
|
+
const prefixesByFile = new Map();
|
|
76
|
+
const resolvedIncludes = new Set();
|
|
77
|
+
const childIncludes = new Map();
|
|
78
|
+
const bareMounts = new Set();
|
|
79
|
+
for (const inc of includes) {
|
|
80
|
+
const source = inc.filePath.replace(/\\/g, '/');
|
|
81
|
+
const localName = inc.routerExpr.endsWith('.router')
|
|
82
|
+
? inc.routerExpr.slice(0, -'.router'.length)
|
|
83
|
+
: inc.routerExpr;
|
|
84
|
+
const modulePath = importsByFile.get(source)?.get(localName)?.modulePath;
|
|
85
|
+
if (!modulePath)
|
|
86
|
+
continue;
|
|
87
|
+
const target = resolveModuleFile(source, modulePath, fileSet, absoluteModules);
|
|
88
|
+
if (!target)
|
|
89
|
+
continue;
|
|
90
|
+
if (inc.host === 'router') {
|
|
91
|
+
const children = childIncludes.get(source) ?? [];
|
|
92
|
+
children.push({ target, prefix: inc.prefix, include: inc });
|
|
93
|
+
childIncludes.set(source, children);
|
|
94
|
+
}
|
|
95
|
+
else if (inc.prefix) {
|
|
96
|
+
const prefixes = prefixesByFile.get(target) ?? new Set();
|
|
97
|
+
prefixes.add(inc.prefix);
|
|
98
|
+
prefixesByFile.set(target, prefixes);
|
|
99
|
+
resolvedIncludes.add(inc);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
bareMounts.add(target);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// A router mounted without a prefix still seeds traversal (with an empty
|
|
106
|
+
// prefix) so its own `APIRouter(prefix=...)` reaches its children. Like its
|
|
107
|
+
// own routes, it only does so when no prefixed mount targets the file.
|
|
108
|
+
const roots = [
|
|
109
|
+
...[...prefixesByFile].flatMap(([file, prefixes]) => [...prefixes].map((prefix) => ({ file, prefix }))),
|
|
110
|
+
...[...bareMounts]
|
|
111
|
+
.filter((file) => !prefixesByFile.has(file))
|
|
112
|
+
.map((file) => ({ file, prefix: '' })),
|
|
113
|
+
];
|
|
114
|
+
// `expanded` memoizes (file, prefix) frames so diamond-shaped include graphs
|
|
115
|
+
// stay linear in distinct prefixes; the per-path `visited` set still stops
|
|
116
|
+
// cycles whose edges keep growing the prefix.
|
|
117
|
+
const expanded = new Set();
|
|
118
|
+
for (const { file, prefix } of roots) {
|
|
119
|
+
const stack = [{ file, prefix, visited: new Set([file]) }];
|
|
120
|
+
for (let current = stack.pop(); current; current = stack.pop()) {
|
|
121
|
+
const frameKey = `${current.file}\0${current.prefix}`;
|
|
122
|
+
if (expanded.has(frameKey))
|
|
123
|
+
continue;
|
|
124
|
+
expanded.add(frameKey);
|
|
125
|
+
// The parent's own `APIRouter(prefix=...)` sits between its mount
|
|
126
|
+
// prefix and the child include prefix.
|
|
127
|
+
const ctorPrefix = constructorPrefixByFile.get(current.file);
|
|
128
|
+
const parentPrefix = ctorPrefix
|
|
129
|
+
? normalizeExtractedRoutePath(ctorPrefix, current.prefix)
|
|
130
|
+
: current.prefix;
|
|
131
|
+
for (const edge of childIncludes.get(current.file) ?? []) {
|
|
132
|
+
if (current.visited.has(edge.target))
|
|
133
|
+
continue;
|
|
134
|
+
const normalized = normalizeExtractedRoutePath(edge.prefix, parentPrefix);
|
|
135
|
+
// An all-empty chain adds no prefix; record nothing so the child
|
|
136
|
+
// keeps its legacy fallback, but keep walking for deeper prefixes.
|
|
137
|
+
const joined = normalized === '/' ? '' : normalized;
|
|
138
|
+
if (joined) {
|
|
139
|
+
const targetPrefixes = prefixesByFile.get(edge.target) ?? new Set();
|
|
140
|
+
targetPrefixes.add(joined);
|
|
141
|
+
prefixesByFile.set(edge.target, targetPrefixes);
|
|
142
|
+
resolvedIncludes.add(edge.include);
|
|
143
|
+
}
|
|
144
|
+
stack.push({
|
|
145
|
+
file: edge.target,
|
|
146
|
+
prefix: joined,
|
|
147
|
+
visited: new Set([...current.visited, edge.target]),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { prefixesByFile, resolvedIncludes };
|
|
153
|
+
}
|
|
@@ -810,7 +810,10 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
|
|
|
810
810
|
// v122 (#3414): Python decorator identity models restoring helper calls and
|
|
811
811
|
// treats match-pattern captures and nested nonlocal rebinds as shadowing.
|
|
812
812
|
// Warm v121 captures carry the old verdicts.
|
|
813
|
-
|
|
813
|
+
// v123 (#3408): FastAPI include records now carry `host`, router imports carry
|
|
814
|
+
// `modulePath`, and unprefixed child includes are emitted. Warm v122 records
|
|
815
|
+
// lack them, so nested router-prefix propagation would stay inert.
|
|
816
|
+
const SCHEMA_BUMP = 123;
|
|
814
817
|
const GITNEXUS_PKG_VERSION = (() => {
|
|
815
818
|
try {
|
|
816
819
|
// package.json sits at gitnexus/package.json — two levels up from
|
package/package.json
CHANGED