gitnexus 1.6.4-rc.36 → 1.6.4-rc.38
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.
|
@@ -41,35 +41,76 @@ export function resolvePythonImportTarget(parsedImport, workspaceIndex) {
|
|
|
41
41
|
const pathLike = parsedImport.targetRaw.replace(/\./g, '/');
|
|
42
42
|
if (pathLike.includes('/')) {
|
|
43
43
|
const [leadingSegment] = pathLike.split('/').filter(Boolean);
|
|
44
|
-
if (!leadingSegment || !hasRepoCandidate(leadingSegment, ctx.allFilePaths)) {
|
|
44
|
+
if (!leadingSegment || !hasRepoCandidate(leadingSegment, ctx.allFilePaths, ctx.fromFile)) {
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
// Multi-segment absolute resolve: try exact paths first, then
|
|
49
|
-
//
|
|
50
|
-
// `
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
48
|
+
// Multi-segment absolute resolve: try exact paths first, then ancestor
|
|
49
|
+
// walk (mirrors the single-segment ancestor walk in
|
|
50
|
+
// `resolvePythonImportInternal`), then a suffix match in nested repos.
|
|
51
|
+
// Using direct `Set.has` + `endsWith` instead of `suffixResolve`'s shared
|
|
52
|
+
// helper because that helper requires a pre-built `SuffixIndex` to
|
|
53
|
+
// disambiguate ties — without one it falls back to an O(files) scan that
|
|
54
|
+
// silently picks the wrong file when the last segment collides across
|
|
55
|
+
// directories (e.g. `accounts.models` matching `billing/models.py` when
|
|
56
|
+
// both files exist).
|
|
57
|
+
return resolveAbsoluteFromFiles(pathLike, ctx.allFilePaths, ctx.fromFile);
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
60
|
* Resolve `package/sub/module` style paths (already dot-flattened) to a
|
|
59
|
-
* concrete file in `allFilePaths`. Tries the exact path first, then
|
|
60
|
-
* `
|
|
61
|
+
* concrete file in `allFilePaths`. Tries the exact path first, then walks
|
|
62
|
+
* ancestors of `fromFile` looking for `<ancestor>/<pathLike>.py` (or
|
|
63
|
+
* `__init__.py`), then falls back to a suffix match for nested layouts.
|
|
61
64
|
* Returns the original (un-normalized) path from the set.
|
|
65
|
+
*
|
|
66
|
+
* Precedence order:
|
|
67
|
+
* 1. Workspace-root direct hit (`<pathLike>.py`, `<pathLike>/__init__.py`).
|
|
68
|
+
* 2. Closest-ancestor match walking up from the importer's directory.
|
|
69
|
+
* 3. Suffix fallback (first match).
|
|
70
|
+
*
|
|
71
|
+
* Root wins over ancestor by construction — if both `services/sync.py` and
|
|
72
|
+
* `backend/services/sync.py` exist, `backend/routers/cron.py`'s
|
|
73
|
+
* `from services.sync import X` resolves to the root file. This mirrors
|
|
74
|
+
* Python's `sys.path` semantics where the project root is searched first.
|
|
75
|
+
*
|
|
76
|
+
* The ancestor walk mirrors the single-segment behavior in
|
|
77
|
+
* `resolvePythonImportInternal`. For `from services.sync import X` in
|
|
78
|
+
* `backend/routers/cron.py`, walk up: `backend/routers/services/sync.py` →
|
|
79
|
+
* `backend/services/sync.py` ✓.
|
|
62
80
|
*/
|
|
63
|
-
function resolveAbsoluteFromFiles(pathLike, allFilePaths) {
|
|
81
|
+
function resolveAbsoluteFromFiles(pathLike, allFilePaths, fromFile) {
|
|
64
82
|
const directFile = `${pathLike}.py`;
|
|
65
83
|
const directPkg = `${pathLike}/__init__.py`;
|
|
84
|
+
// Direct hit at workspace root.
|
|
85
|
+
if (allFilePaths.has(directFile))
|
|
86
|
+
return directFile;
|
|
87
|
+
if (allFilePaths.has(directPkg))
|
|
88
|
+
return directPkg;
|
|
89
|
+
// Ancestor walk — match the single-segment resolver's behavior at
|
|
90
|
+
// multi-segment granularity. Closest match wins. Stop at `i > 0` because
|
|
91
|
+
// `i === 0` would re-check the workspace-root candidates already covered
|
|
92
|
+
// by the direct check above.
|
|
93
|
+
const importerDir = fromFile.replace(/\\/g, '/').split('/').slice(0, -1).join('/');
|
|
94
|
+
if (importerDir) {
|
|
95
|
+
const dirParts = importerDir.split('/').filter(Boolean);
|
|
96
|
+
for (let i = dirParts.length; i > 0; i--) {
|
|
97
|
+
const ancestor = dirParts.slice(0, i).join('/');
|
|
98
|
+
const prefix = `${ancestor}/`;
|
|
99
|
+
const candidateFile = `${prefix}${directFile}`;
|
|
100
|
+
const candidatePkg = `${prefix}${directPkg}`;
|
|
101
|
+
if (allFilePaths.has(candidateFile))
|
|
102
|
+
return candidateFile;
|
|
103
|
+
if (allFilePaths.has(candidatePkg))
|
|
104
|
+
return candidatePkg;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Existing suffix-match fallback (preserved for monorepo/nested-repo
|
|
108
|
+
// layouts that don't share a directory ancestor with the importer).
|
|
66
109
|
const suffixFile = `/${directFile}`;
|
|
67
110
|
const suffixPkg = `/${directPkg}`;
|
|
68
111
|
let suffixMatch = null;
|
|
69
112
|
for (const raw of allFilePaths) {
|
|
70
113
|
const f = raw.replace(/\\/g, '/');
|
|
71
|
-
if (f === directFile || f === directPkg)
|
|
72
|
-
return raw;
|
|
73
114
|
if (suffixMatch === null && (f.endsWith(suffixFile) || f.endsWith(suffixPkg))) {
|
|
74
115
|
suffixMatch = raw;
|
|
75
116
|
}
|
|
@@ -77,23 +118,51 @@ function resolveAbsoluteFromFiles(pathLike, allFilePaths) {
|
|
|
77
118
|
return suffixMatch;
|
|
78
119
|
}
|
|
79
120
|
/**
|
|
80
|
-
* Does the repo contain a module/package named `leadingSegment`
|
|
81
|
-
*
|
|
82
|
-
* dotted imports (e.g. `django.apps` matching a local `accounts/apps.py`).
|
|
121
|
+
* Does the repo contain a module/package named `leadingSegment` somewhere
|
|
122
|
+
* the importer can plausibly reach?
|
|
83
123
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
124
|
+
* Used to guard against false-positive suffix matches on external dotted
|
|
125
|
+
* imports (e.g. `django.apps` matching a local `accounts/apps.py`).
|
|
126
|
+
*
|
|
127
|
+
* Checks, in order:
|
|
128
|
+
* 1. `SEGMENT.py` root file or `SEGMENT/__init__.py` regular package.
|
|
129
|
+
* 2. Any `SEGMENT/...py` file at the workspace root (namespace package).
|
|
130
|
+
* 3. Any `<importer-ancestor>/SEGMENT/...py` file (nested namespace
|
|
131
|
+
* package the importer could reach via an ancestor walk, e.g.
|
|
132
|
+
* `backend/services/sync.py` from `backend/routers/cron.py`).
|
|
133
|
+
*
|
|
134
|
+
* The nested case is bounded to the importer's own ancestors so a
|
|
135
|
+
* vendored copy of an external package (e.g. `vendor/django/urls.py`)
|
|
136
|
+
* does not gate-pass external imports like `from django.urls import path`
|
|
137
|
+
* issued from `app/main.py`. Files inside the vendored tree itself
|
|
138
|
+
* (importer under `vendor/django/...`) still resolve correctly because
|
|
139
|
+
* the ancestor walk includes their own parents.
|
|
86
140
|
*/
|
|
87
|
-
function hasRepoCandidate(leadingSegment, allFilePaths) {
|
|
141
|
+
function hasRepoCandidate(leadingSegment, allFilePaths, fromFile) {
|
|
88
142
|
const prefix = `${leadingSegment}/`;
|
|
89
143
|
const rootFile = `${leadingSegment}.py`;
|
|
90
144
|
const initFile = `${leadingSegment}/__init__.py`;
|
|
145
|
+
// Build importer-ancestor prefixes: for `backend/routers/cron.py`,
|
|
146
|
+
// produces `["backend/routers/services/", "backend/services/"]` for
|
|
147
|
+
// segment `services` (closest first, root excluded — covered above).
|
|
148
|
+
const importerDir = fromFile.replace(/\\/g, '/').split('/').slice(0, -1).join('/');
|
|
149
|
+
const dirParts = importerDir ? importerDir.split('/').filter(Boolean) : [];
|
|
150
|
+
const ancestorPrefixes = [];
|
|
151
|
+
for (let i = dirParts.length; i > 0; i--) {
|
|
152
|
+
ancestorPrefixes.push(`${dirParts.slice(0, i).join('/')}/${leadingSegment}/`);
|
|
153
|
+
}
|
|
91
154
|
for (const raw of allFilePaths) {
|
|
92
155
|
const f = raw.replace(/\\/g, '/');
|
|
93
156
|
if (f === rootFile || f === initFile)
|
|
94
157
|
return true;
|
|
95
158
|
if (f.startsWith(prefix) && f.endsWith('.py'))
|
|
96
159
|
return true;
|
|
160
|
+
if (f.endsWith('.py')) {
|
|
161
|
+
for (const ap of ancestorPrefixes) {
|
|
162
|
+
if (f.startsWith(ap))
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
97
166
|
}
|
|
98
167
|
return false;
|
|
99
168
|
}
|
package/package.json
CHANGED