gitnexus 1.6.13-rc.21 → 1.6.13-rc.22
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/ingestion/import-resolvers/configs/swift.d.ts +12 -9
- package/dist/core/ingestion/import-resolvers/configs/swift.js +35 -27
- package/dist/core/ingestion/language-config.d.ts +32 -0
- package/dist/core/ingestion/language-config.js +809 -7
- package/dist/core/ingestion/languages/swift/implicit-imports.d.ts +8 -5
- package/dist/core/ingestion/languages/swift/implicit-imports.js +14 -13
- package/dist/core/ingestion/languages/swift/import-decomposer.d.ts +9 -13
- package/dist/core/ingestion/languages/swift/import-decomposer.js +213 -23
- package/dist/core/ingestion/languages/swift/import-target.d.ts +9 -21
- package/dist/core/ingestion/languages/swift/import-target.js +140 -34
- package/dist/core/ingestion/languages/swift/interpret.js +11 -7
- package/dist/core/ingestion/languages/swift/scope-resolver.d.ts +8 -7
- package/dist/core/ingestion/languages/swift/scope-resolver.js +19 -12
- package/dist/core/ingestion/languages/swift/sdk-modules.d.ts +9 -0
- package/dist/core/ingestion/languages/swift/sdk-modules.js +28 -0
- package/dist/core/ingestion/languages/swift/target-grouping.d.ts +13 -16
- package/dist/core/ingestion/languages/swift/target-grouping.js +23 -20
- package/dist/core/ingestion/languages/swift/target-siblings.d.ts +7 -4
- package/dist/core/ingestion/languages/swift/target-siblings.js +7 -4
- package/package.json +1 -1
|
@@ -14,12 +14,15 @@
|
|
|
14
14
|
* Module identity: Swift has no in-source `package X` marker. Module
|
|
15
15
|
* membership is the SPM target *subtree* (`Sources/<Target>/…`), threaded
|
|
16
16
|
* in via the SPM target map (`resolutionConfig` → `coerceSwiftTargets`)
|
|
17
|
-
* and grouped by `groupSwiftFilesBySpmTarget
|
|
18
|
-
* `groupSwiftFilesByTarget
|
|
17
|
+
* and grouped by `groupSwiftFilesBySpmTarget`. The helper preserves the
|
|
18
|
+
* legacy `groupSwiftFilesByTarget` bucketing contract for ordinary layouts
|
|
19
|
+
* while intentionally fixing #2931's repeated-prefix edge case. With no
|
|
20
|
+
* target map (no Package.swift and no scanned `Sources/*`) the map is null
|
|
19
21
|
* and all files form one `__default__` module (single-Xcode-project
|
|
20
|
-
* assumption).
|
|
21
|
-
*
|
|
22
|
-
*
|
|
22
|
+
* assumption). An inferred folder map must keep sibling folders isolated.
|
|
23
|
+
* Every pair of distinct `.swift`
|
|
24
|
+
* files in the same module gets a directed IMPORTS edge in both directions
|
|
25
|
+
* (whole-module visibility is symmetric).
|
|
23
26
|
*
|
|
24
27
|
* Node identity + edge construction mirror the generic `emitImportEdges`
|
|
25
28
|
* convention (`graph-bridge/imports-to-edges.ts`): `generateId('File', path)`
|
|
@@ -14,12 +14,15 @@
|
|
|
14
14
|
* Module identity: Swift has no in-source `package X` marker. Module
|
|
15
15
|
* membership is the SPM target *subtree* (`Sources/<Target>/…`), threaded
|
|
16
16
|
* in via the SPM target map (`resolutionConfig` → `coerceSwiftTargets`)
|
|
17
|
-
* and grouped by `groupSwiftFilesBySpmTarget
|
|
18
|
-
* `groupSwiftFilesByTarget
|
|
17
|
+
* and grouped by `groupSwiftFilesBySpmTarget`. The helper preserves the
|
|
18
|
+
* legacy `groupSwiftFilesByTarget` bucketing contract for ordinary layouts
|
|
19
|
+
* while intentionally fixing #2931's repeated-prefix edge case. With no
|
|
20
|
+
* target map (no Package.swift and no scanned `Sources/*`) the map is null
|
|
19
21
|
* and all files form one `__default__` module (single-Xcode-project
|
|
20
|
-
* assumption).
|
|
21
|
-
*
|
|
22
|
-
*
|
|
22
|
+
* assumption). An inferred folder map must keep sibling folders isolated.
|
|
23
|
+
* Every pair of distinct `.swift`
|
|
24
|
+
* files in the same module gets a directed IMPORTS edge in both directions
|
|
25
|
+
* (whole-module visibility is symmetric).
|
|
23
26
|
*
|
|
24
27
|
* Node identity + edge construction mirror the generic `emitImportEdges`
|
|
25
28
|
* convention (`graph-bridge/imports-to-edges.ts`): `generateId('File', path)`
|
|
@@ -31,22 +34,20 @@
|
|
|
31
34
|
import { generateId } from '../../../../lib/utils.js';
|
|
32
35
|
import { coerceSwiftTargets, groupSwiftFilesBySpmTarget } from './target-grouping.js';
|
|
33
36
|
export function emitSwiftImplicitImportEdges(graph, parsedFiles, _nodeLookup, resolutionConfig) {
|
|
34
|
-
// Group files by SPM target subtree (the module). No-source-dir → all
|
|
35
|
-
// files in one `__default__` bucket.
|
|
36
37
|
const targets = coerceSwiftTargets(resolutionConfig);
|
|
37
38
|
const filesByTarget = groupSwiftFilesBySpmTarget(parsedFiles, (parsed) => parsed.filePath, targets);
|
|
38
39
|
for (const [, group] of filesByTarget) {
|
|
39
40
|
if (group.length < 2)
|
|
40
|
-
continue;
|
|
41
|
+
continue;
|
|
41
42
|
for (const source of group) {
|
|
42
|
-
for (const
|
|
43
|
-
if (source.filePath ===
|
|
44
|
-
continue;
|
|
45
|
-
const dedupKey = `${source.filePath}->${
|
|
43
|
+
for (const dest of group) {
|
|
44
|
+
if (source.filePath === dest.filePath)
|
|
45
|
+
continue;
|
|
46
|
+
const dedupKey = `${source.filePath}->${dest.filePath}`;
|
|
46
47
|
graph.addRelationship({
|
|
47
48
|
id: generateId('IMPORTS', dedupKey),
|
|
48
49
|
sourceId: generateId('File', source.filePath),
|
|
49
|
-
targetId: generateId('File',
|
|
50
|
+
targetId: generateId('File', dest.filePath),
|
|
50
51
|
type: 'IMPORTS',
|
|
51
52
|
confidence: 1.0,
|
|
52
53
|
reason: 'swift-scope: implicit module visibility',
|
|
@@ -4,20 +4,16 @@
|
|
|
4
4
|
* `@import.name` / `@import.testable` that `interpretSwiftImport`
|
|
5
5
|
* consumes.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* import Foundation → kind=namespace, source=Foundation
|
|
8
|
+
* import Foo.Bar → kind=namespace, source=Foo
|
|
9
|
+
* import struct Foo.Bar → kind=named, source=Foo, name=Bar
|
|
10
|
+
* @testable import MyApp → kind=namespace, source=MyApp, testable=1
|
|
11
|
+
* @_exported import Foo → kind=reexport, source=Foo, name=Foo
|
|
12
|
+
* @_exported import struct Foo.Bar → kind=reexport, source=Foo, name=Bar
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @testable import MyApp → kind=namespace, source=MyApp, testable=1
|
|
16
|
-
*
|
|
17
|
-
* Verified against tree-sitter-swift 0.7.1:
|
|
18
|
-
* (import_declaration
|
|
19
|
-
* (modifiers (attribute (user_type (type_identifier))))? ; @testable / @_exported
|
|
20
|
-
* (identifier (simple_identifier)+)) ; one per dotted segment
|
|
14
|
+
* Import-kind (`struct`/`class`/…) is not a named tree-sitter child
|
|
15
|
+
* (hidden `_import_kind` in 0.7.1). Read it from the statement text.
|
|
16
|
+
* `@_exported` / `@testable` live on `modifiers`.
|
|
21
17
|
*/
|
|
22
18
|
import type { CaptureMatch } from '../../../../_shared/index.js';
|
|
23
19
|
import { type SyntaxNode } from '../../utils/ast-helpers.js';
|
|
@@ -4,22 +4,19 @@
|
|
|
4
4
|
* `@import.name` / `@import.testable` that `interpretSwiftImport`
|
|
5
5
|
* consumes.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* import Foundation → kind=namespace, source=Foundation
|
|
8
|
+
* import Foo.Bar → kind=namespace, source=Foo
|
|
9
|
+
* import struct Foo.Bar → kind=named, source=Foo, name=Bar
|
|
10
|
+
* @testable import MyApp → kind=namespace, source=MyApp, testable=1
|
|
11
|
+
* @_exported import Foo → kind=reexport, source=Foo, name=Foo
|
|
12
|
+
* @_exported import struct Foo.Bar → kind=reexport, source=Foo, name=Bar
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @testable import MyApp → kind=namespace, source=MyApp, testable=1
|
|
16
|
-
*
|
|
17
|
-
* Verified against tree-sitter-swift 0.7.1:
|
|
18
|
-
* (import_declaration
|
|
19
|
-
* (modifiers (attribute (user_type (type_identifier))))? ; @testable / @_exported
|
|
20
|
-
* (identifier (simple_identifier)+)) ; one per dotted segment
|
|
14
|
+
* Import-kind (`struct`/`class`/…) is not a named tree-sitter child
|
|
15
|
+
* (hidden `_import_kind` in 0.7.1). Read it from the statement text.
|
|
16
|
+
* `@_exported` / `@testable` live on `modifiers`.
|
|
21
17
|
*/
|
|
22
18
|
import { nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
|
|
19
|
+
const IMPORT_KIND_TOKEN_RE = /^(struct|class|enum|protocol|func|let|var|typealias)\b/;
|
|
23
20
|
export function splitSwiftImport(stmtNode) {
|
|
24
21
|
if (stmtNode.type !== 'import_declaration')
|
|
25
22
|
return null;
|
|
@@ -30,15 +27,17 @@ export function splitSwiftImport(stmtNode) {
|
|
|
30
27
|
}
|
|
31
28
|
function parseSwiftImport(node) {
|
|
32
29
|
let testable = false;
|
|
30
|
+
let exported = false;
|
|
33
31
|
let identifierNode = null;
|
|
34
32
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
35
33
|
const child = node.namedChild(i);
|
|
36
34
|
if (child === null)
|
|
37
35
|
continue;
|
|
38
36
|
if (child.type === 'modifiers') {
|
|
39
|
-
|
|
40
|
-
if (/\btestable\b/.test(child.text))
|
|
37
|
+
if (swiftModifiersHaveAttribute(child.text, 'testable'))
|
|
41
38
|
testable = true;
|
|
39
|
+
if (swiftModifiersHaveAttribute(child.text, '_exported'))
|
|
40
|
+
exported = true;
|
|
42
41
|
}
|
|
43
42
|
else if (child.type === 'identifier') {
|
|
44
43
|
identifierNode = child;
|
|
@@ -46,8 +45,7 @@ function parseSwiftImport(node) {
|
|
|
46
45
|
}
|
|
47
46
|
if (identifierNode === null)
|
|
48
47
|
return null;
|
|
49
|
-
|
|
50
|
-
// dotted segment. The SPM target is the FIRST segment.
|
|
48
|
+
const importKind = importKindFromClause(node, identifierNode);
|
|
51
49
|
const segments = [];
|
|
52
50
|
for (let i = 0; i < identifierNode.namedChildCount; i++) {
|
|
53
51
|
const seg = identifierNode.namedChild(i);
|
|
@@ -55,27 +53,219 @@ function parseSwiftImport(node) {
|
|
|
55
53
|
segments.push(seg.text);
|
|
56
54
|
}
|
|
57
55
|
if (segments.length === 0) {
|
|
58
|
-
// Fall back to the raw identifier text (e.g. a grammar shape we didn't
|
|
59
|
-
// anticipate). Split on `.` to recover the target segment.
|
|
60
56
|
const raw = identifierNode.text.trim();
|
|
61
57
|
if (raw === '')
|
|
62
58
|
return null;
|
|
63
|
-
|
|
64
|
-
return { source: parts[0], fullPath: raw, testable, atNode: node };
|
|
59
|
+
segments.push(...raw.split('.'));
|
|
65
60
|
}
|
|
66
61
|
return {
|
|
67
62
|
source: segments[0],
|
|
63
|
+
memberName: segments.length > 1 ? segments[segments.length - 1] : segments[0],
|
|
68
64
|
fullPath: segments.join('.'),
|
|
69
65
|
testable,
|
|
66
|
+
exported,
|
|
67
|
+
importKind,
|
|
70
68
|
atNode: node,
|
|
71
69
|
};
|
|
72
70
|
}
|
|
71
|
+
function isSwiftIdentCont(ch) {
|
|
72
|
+
return ch !== undefined && /[A-Za-z0-9_]/.test(ch);
|
|
73
|
+
}
|
|
74
|
+
/** First `word` outside comments and strings. Linear scan. */
|
|
75
|
+
function indexOfBareWord(text, word) {
|
|
76
|
+
let inString = null;
|
|
77
|
+
let escape = false;
|
|
78
|
+
let inLineComment = false;
|
|
79
|
+
let blockCommentDepth = 0;
|
|
80
|
+
for (let i = 0; i < text.length; i++) {
|
|
81
|
+
const ch = text[i];
|
|
82
|
+
const next = text[i + 1];
|
|
83
|
+
if (inLineComment) {
|
|
84
|
+
if (ch === '\n')
|
|
85
|
+
inLineComment = false;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (blockCommentDepth > 0) {
|
|
89
|
+
if (ch === '*' && next === '/') {
|
|
90
|
+
blockCommentDepth--;
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
else if (ch === '/' && next === '*') {
|
|
94
|
+
blockCommentDepth++;
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (inString !== null) {
|
|
100
|
+
if (escape) {
|
|
101
|
+
escape = false;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (ch === '\\') {
|
|
105
|
+
escape = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (ch === inString)
|
|
109
|
+
inString = null;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (ch === '"' || ch === "'") {
|
|
113
|
+
inString = ch;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (ch === '/' && next === '/') {
|
|
117
|
+
inLineComment = true;
|
|
118
|
+
i++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (ch === '/' && next === '*') {
|
|
122
|
+
blockCommentDepth = 1;
|
|
123
|
+
i++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (text.startsWith(word, i) &&
|
|
127
|
+
!isSwiftIdentCont(text[i + word.length]) &&
|
|
128
|
+
(i === 0 || !isSwiftIdentCont(text[i - 1]))) {
|
|
129
|
+
return i;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return -1;
|
|
133
|
+
}
|
|
134
|
+
/** `@name` token in modifier text — not `_exported` / `testable` inside a string. */
|
|
135
|
+
function swiftModifiersHaveAttribute(text, name) {
|
|
136
|
+
let inString = null;
|
|
137
|
+
let escape = false;
|
|
138
|
+
let inLineComment = false;
|
|
139
|
+
let blockCommentDepth = 0;
|
|
140
|
+
for (let i = 0; i < text.length; i++) {
|
|
141
|
+
const ch = text[i];
|
|
142
|
+
const next = text[i + 1];
|
|
143
|
+
if (inLineComment) {
|
|
144
|
+
if (ch === '\n')
|
|
145
|
+
inLineComment = false;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (blockCommentDepth > 0) {
|
|
149
|
+
if (ch === '*' && next === '/') {
|
|
150
|
+
blockCommentDepth--;
|
|
151
|
+
i++;
|
|
152
|
+
}
|
|
153
|
+
else if (ch === '/' && next === '*') {
|
|
154
|
+
blockCommentDepth++;
|
|
155
|
+
i++;
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (inString !== null) {
|
|
160
|
+
if (escape) {
|
|
161
|
+
escape = false;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (ch === '\\') {
|
|
165
|
+
escape = true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (ch === inString)
|
|
169
|
+
inString = null;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (ch === '"' || ch === "'") {
|
|
173
|
+
inString = ch;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (ch === '/' && next === '/') {
|
|
177
|
+
inLineComment = true;
|
|
178
|
+
i++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (ch === '/' && next === '*') {
|
|
182
|
+
blockCommentDepth = 1;
|
|
183
|
+
i++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (ch !== '@')
|
|
187
|
+
continue;
|
|
188
|
+
let j = i + 1;
|
|
189
|
+
while (j < text.length && /\s/.test(text[j]))
|
|
190
|
+
j++;
|
|
191
|
+
if (text.startsWith(name, j) && !isSwiftIdentCont(text[j + name.length]))
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
/** Kind token from the import clause only — skip `@available(..., message: "import struct")`. */
|
|
197
|
+
function importKindFromClause(node, identifierNode) {
|
|
198
|
+
const identRel = identifierNode.startIndex - node.startIndex;
|
|
199
|
+
const before = identRel >= 0 ? node.text.slice(0, identRel) : node.text;
|
|
200
|
+
const importAt = indexOfBareWord(before, 'import');
|
|
201
|
+
const clause = importAt === -1 ? before : before.slice(importAt);
|
|
202
|
+
return kindAfterImportKeyword(clause);
|
|
203
|
+
}
|
|
204
|
+
/** After `import`, skip whitespace and comments, then read a kind token. Linear: no nested-quantifier backtracking. */
|
|
205
|
+
function kindAfterImportKeyword(clause) {
|
|
206
|
+
const start = indexOfBareWord(clause, 'import');
|
|
207
|
+
if (start === -1)
|
|
208
|
+
return null;
|
|
209
|
+
let i = start + 'import'.length;
|
|
210
|
+
let skipped = false;
|
|
211
|
+
while (i < clause.length) {
|
|
212
|
+
const ch = clause[i];
|
|
213
|
+
const next = clause[i + 1];
|
|
214
|
+
if (/\s/.test(ch)) {
|
|
215
|
+
skipped = true;
|
|
216
|
+
i += 1;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (ch === '/' && next === '/') {
|
|
220
|
+
const nl = clause.indexOf('\n', i + 2);
|
|
221
|
+
if (nl === -1)
|
|
222
|
+
return null;
|
|
223
|
+
skipped = true;
|
|
224
|
+
i = nl + 1;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (ch === '/' && next === '*') {
|
|
228
|
+
let depth = 1;
|
|
229
|
+
i += 2;
|
|
230
|
+
while (i < clause.length && depth > 0) {
|
|
231
|
+
if (clause[i] === '/' && clause[i + 1] === '*') {
|
|
232
|
+
depth++;
|
|
233
|
+
i += 2;
|
|
234
|
+
}
|
|
235
|
+
else if (clause[i] === '*' && clause[i + 1] === '/') {
|
|
236
|
+
depth--;
|
|
237
|
+
i += 2;
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
i++;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (depth !== 0)
|
|
244
|
+
return null;
|
|
245
|
+
skipped = true;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
if (!skipped)
|
|
251
|
+
return null;
|
|
252
|
+
return IMPORT_KIND_TOKEN_RE.exec(clause.slice(i))?.[1] ?? null;
|
|
253
|
+
}
|
|
254
|
+
function bindingKind(spec) {
|
|
255
|
+
if (spec.exported)
|
|
256
|
+
return 'reexport';
|
|
257
|
+
if (spec.importKind !== null && spec.fullPath.includes('.'))
|
|
258
|
+
return 'named';
|
|
259
|
+
return 'namespace';
|
|
260
|
+
}
|
|
73
261
|
function buildImportMatch(stmtNode, spec) {
|
|
262
|
+
const kind = bindingKind(spec);
|
|
263
|
+
const nameText = kind === 'namespace' ? spec.fullPath : spec.memberName;
|
|
74
264
|
const m = {
|
|
75
265
|
'@import.statement': nodeToCapture('@import.statement', stmtNode),
|
|
76
|
-
'@import.kind': syntheticCapture('@import.kind', spec.atNode,
|
|
266
|
+
'@import.kind': syntheticCapture('@import.kind', spec.atNode, kind),
|
|
77
267
|
'@import.source': syntheticCapture('@import.source', spec.atNode, spec.source),
|
|
78
|
-
'@import.name': syntheticCapture('@import.name', spec.atNode,
|
|
268
|
+
'@import.name': syntheticCapture('@import.name', spec.atNode, nameText),
|
|
79
269
|
};
|
|
80
270
|
if (spec.testable) {
|
|
81
271
|
m['@import.testable'] = syntheticCapture('@import.testable', spec.atNode, '1');
|
|
@@ -1,33 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `resolveImportTarget` adapter for the Swift `ScopeResolver`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* convention `Sources/<Module>/*.swift` (and the common
|
|
9
|
-
* `<Module>/*.swift` layout). This needs no manifest parsing.
|
|
4
|
+
* A Package.swift declaration map (`origin: 'package.swift'`) resolves
|
|
5
|
+
* only declared target names. Otherwise refuse well-known SDK module
|
|
6
|
+
* names and fall back to the memoized directory-segment index so local
|
|
7
|
+
* folder modules still resolve without a manifest.
|
|
10
8
|
*
|
|
11
|
-
* Same-module
|
|
12
|
-
*
|
|
13
|
-
* by `populateSwiftTargetSiblings` (see `target-siblings.ts`). This
|
|
14
|
-
* adapter only resolves EXPLICIT `import` statements (cross-module).
|
|
15
|
-
*
|
|
16
|
-
* Returns all matching files (one ImportEdge per file, like Go's
|
|
17
|
-
* package resolver) so every exported symbol in the module materializes
|
|
18
|
-
* a binding. Returns `null` for external frameworks (Foundation, UIKit,
|
|
19
|
-
* …) that have no in-repo directory.
|
|
20
|
-
*
|
|
21
|
-
* Performance: the directory→files grouping is memoized on the stable
|
|
22
|
-
* `allFilePaths` Set identity (the same Set is threaded to every import
|
|
23
|
-
* in a run), so it is built once per run — NOT once per import. Mirrors
|
|
24
|
-
* Python's `getPythonFileIndex` WeakMap pattern (PR #1918).
|
|
9
|
+
* Same-module visibility without `import` is `populateSwiftTargetSiblings`.
|
|
10
|
+
* This adapter only resolves EXPLICIT cross-module `import`s.
|
|
25
11
|
*/
|
|
26
|
-
import type { ParsedImport, WorkspaceIndex } from '../../../../_shared/index.js';
|
|
12
|
+
import type { ParsedFile, ParsedImport, WorkspaceIndex } from '../../../../_shared/index.js';
|
|
27
13
|
export interface SwiftResolveContext {
|
|
28
14
|
readonly fromFile: string;
|
|
29
15
|
/** `ReadonlySet` so the orchestrator's stable run-level set flows
|
|
30
16
|
* straight through to the memoized index key. */
|
|
31
17
|
readonly allFilePaths: ReadonlySet<string>;
|
|
18
|
+
readonly resolutionConfig?: unknown;
|
|
19
|
+
readonly parsedFiles?: readonly ParsedFile[];
|
|
32
20
|
}
|
|
33
21
|
export declare function resolveSwiftImportTarget(parsedImport: ParsedImport, workspaceIndex: WorkspaceIndex): string | readonly string[] | null;
|
|
@@ -1,40 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `resolveImportTarget` adapter for the Swift `ScopeResolver`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* convention `Sources/<Module>/*.swift` (and the common
|
|
9
|
-
* `<Module>/*.swift` layout). This needs no manifest parsing.
|
|
4
|
+
* A Package.swift declaration map (`origin: 'package.swift'`) resolves
|
|
5
|
+
* only declared target names. Otherwise refuse well-known SDK module
|
|
6
|
+
* names and fall back to the memoized directory-segment index so local
|
|
7
|
+
* folder modules still resolve without a manifest.
|
|
10
8
|
*
|
|
11
|
-
* Same-module
|
|
12
|
-
*
|
|
13
|
-
* by `populateSwiftTargetSiblings` (see `target-siblings.ts`). This
|
|
14
|
-
* adapter only resolves EXPLICIT `import` statements (cross-module).
|
|
15
|
-
*
|
|
16
|
-
* Returns all matching files (one ImportEdge per file, like Go's
|
|
17
|
-
* package resolver) so every exported symbol in the module materializes
|
|
18
|
-
* a binding. Returns `null` for external frameworks (Foundation, UIKit,
|
|
19
|
-
* …) that have no in-repo directory.
|
|
20
|
-
*
|
|
21
|
-
* Performance: the directory→files grouping is memoized on the stable
|
|
22
|
-
* `allFilePaths` Set identity (the same Set is threaded to every import
|
|
23
|
-
* in a run), so it is built once per run — NOT once per import. Mirrors
|
|
24
|
-
* Python's `getPythonFileIndex` WeakMap pattern (PR #1918).
|
|
9
|
+
* Same-module visibility without `import` is `populateSwiftTargetSiblings`.
|
|
10
|
+
* This adapter only resolves EXPLICIT cross-module `import`s.
|
|
25
11
|
*/
|
|
26
12
|
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
|
13
|
+
import { coerceDeclaredSwiftTargets, swiftDeclaredTargetPrefix } from '../../language-config.js';
|
|
14
|
+
import { isSwiftSdkModule } from './sdk-modules.js';
|
|
27
15
|
const getSwiftModuleIndex = perFileSet((allFilePaths) => {
|
|
28
16
|
const byModule = new Map();
|
|
29
17
|
for (const raw of allFilePaths) {
|
|
30
18
|
const norm = raw.replace(/\\/g, '/');
|
|
31
19
|
if (!norm.endsWith('.swift'))
|
|
32
20
|
continue;
|
|
33
|
-
// Each interior directory segment is a candidate module name. A file
|
|
34
|
-
// `Sources/Models/User.swift` is attributed to module `Sources` and
|
|
35
|
-
// module `Models`; an `import Models` then resolves to it.
|
|
36
21
|
const segments = norm.split('/');
|
|
37
|
-
// Drop the filename (last segment); the rest are directory segments.
|
|
38
22
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
39
23
|
const seg = segments[i];
|
|
40
24
|
if (seg === '')
|
|
@@ -49,9 +33,61 @@ const getSwiftModuleIndex = perFileSet((allFilePaths) => {
|
|
|
49
33
|
}
|
|
50
34
|
return { byModule };
|
|
51
35
|
});
|
|
52
|
-
|
|
36
|
+
const SWIFT_DECLARED_INDEX = new WeakMap();
|
|
37
|
+
function getDeclaredFilesByName(allFilePaths, declared) {
|
|
38
|
+
const hit = SWIFT_DECLARED_INDEX.get(allFilePaths);
|
|
39
|
+
if (hit !== undefined && hit.declared === declared)
|
|
40
|
+
return hit.byName;
|
|
41
|
+
const dirs = [...declared.entries()].map(([name, dir]) => ({
|
|
42
|
+
name,
|
|
43
|
+
prefix: swiftDeclaredTargetPrefix(dir),
|
|
44
|
+
}));
|
|
45
|
+
const byName = new Map();
|
|
46
|
+
for (const { name } of dirs)
|
|
47
|
+
byName.set(name, []);
|
|
48
|
+
for (const raw of allFilePaths) {
|
|
49
|
+
const norm = raw.replace(/\\/g, '/');
|
|
50
|
+
if (!norm.endsWith('.swift'))
|
|
51
|
+
continue;
|
|
52
|
+
for (const { name, prefix } of dirs) {
|
|
53
|
+
if (!norm.startsWith(prefix) && !norm.includes(`/${prefix}`))
|
|
54
|
+
continue;
|
|
55
|
+
const bucket = byName.get(name);
|
|
56
|
+
if (bucket !== undefined)
|
|
57
|
+
bucket.push(raw);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const index = { declared, byName };
|
|
61
|
+
SWIFT_DECLARED_INDEX.set(allFilePaths, index);
|
|
62
|
+
return byName;
|
|
63
|
+
}
|
|
64
|
+
const getSwiftReexportFlag = perFileSet((parsedFiles) => {
|
|
65
|
+
for (const parsed of parsedFiles) {
|
|
66
|
+
for (const imp of parsed.parsedImports) {
|
|
67
|
+
if (imp.kind === 'reexport')
|
|
68
|
+
return { hasReexport: true };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { hasReexport: false };
|
|
72
|
+
});
|
|
73
|
+
const getSwiftParsedByPath = perFileSet((parsedFiles) => {
|
|
74
|
+
const byPath = new Map();
|
|
75
|
+
for (const parsed of parsedFiles) {
|
|
76
|
+
byPath.set(parsed.filePath, parsed);
|
|
77
|
+
}
|
|
78
|
+
return byPath;
|
|
79
|
+
});
|
|
80
|
+
function excludeImporter(files, fromFile) {
|
|
81
|
+
return files.filter((f) => f !== fromFile);
|
|
82
|
+
}
|
|
83
|
+
function firstSwiftModuleSegment(targetRaw) {
|
|
84
|
+
if (targetRaw === '')
|
|
85
|
+
return null;
|
|
86
|
+
const moduleName = targetRaw.split('.')[0];
|
|
87
|
+
return moduleName === '' ? null : moduleName;
|
|
88
|
+
}
|
|
89
|
+
function narrowContext(workspaceIndex) {
|
|
53
90
|
const ctx = workspaceIndex;
|
|
54
|
-
// Duck-type the set (PR #1918 P2: don't `instanceof Set`).
|
|
55
91
|
const allFilePaths = ctx?.allFilePaths;
|
|
56
92
|
if (ctx === undefined ||
|
|
57
93
|
typeof ctx.fromFile !== 'string' ||
|
|
@@ -59,18 +95,88 @@ export function resolveSwiftImportTarget(parsedImport, workspaceIndex) {
|
|
|
59
95
|
typeof allFilePaths?.[Symbol.iterator] !== 'function') {
|
|
60
96
|
return null;
|
|
61
97
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const moduleName = targetRaw.split('.')[0];
|
|
98
|
+
return ctx;
|
|
99
|
+
}
|
|
100
|
+
/** Module files only — no @_exported closure. Null means external / unknown. */
|
|
101
|
+
function resolveSwiftModuleFiles(moduleName, ctx) {
|
|
67
102
|
if (moduleName === '')
|
|
68
103
|
return null;
|
|
104
|
+
const declared = coerceDeclaredSwiftTargets(ctx.resolutionConfig);
|
|
105
|
+
if (declared !== null) {
|
|
106
|
+
if (!declared.has(moduleName))
|
|
107
|
+
return null;
|
|
108
|
+
const files = getDeclaredFilesByName(ctx.allFilePaths, declared).get(moduleName);
|
|
109
|
+
if (files === undefined)
|
|
110
|
+
return null;
|
|
111
|
+
const out = excludeImporter(files, ctx.fromFile);
|
|
112
|
+
return out.length > 0 ? out : null;
|
|
113
|
+
}
|
|
114
|
+
if (isSwiftSdkModule(moduleName))
|
|
115
|
+
return null;
|
|
69
116
|
const index = getSwiftModuleIndex(ctx.allFilePaths);
|
|
70
117
|
const files = index.byModule.get(moduleName);
|
|
71
118
|
if (files === undefined || files.length === 0)
|
|
72
|
-
return null;
|
|
73
|
-
|
|
74
|
-
const out = files.filter((f) => f !== ctx.fromFile);
|
|
119
|
+
return null;
|
|
120
|
+
const out = excludeImporter(files, ctx.fromFile);
|
|
75
121
|
return out.length > 0 ? out : null;
|
|
76
122
|
}
|
|
123
|
+
function expandSwiftReexportFiles(seed, ctx) {
|
|
124
|
+
const parsedFiles = ctx.parsedFiles;
|
|
125
|
+
if (parsedFiles === undefined || parsedFiles.length === 0)
|
|
126
|
+
return [...seed];
|
|
127
|
+
if (!getSwiftReexportFlag(parsedFiles).hasReexport)
|
|
128
|
+
return [...seed];
|
|
129
|
+
const byPath = getSwiftParsedByPath(parsedFiles);
|
|
130
|
+
const seenModules = new Set();
|
|
131
|
+
const out = new Set(seed);
|
|
132
|
+
const queue = [...seed];
|
|
133
|
+
let head = 0;
|
|
134
|
+
while (head < queue.length) {
|
|
135
|
+
const file = queue[head++];
|
|
136
|
+
const parsed = byPath.get(file);
|
|
137
|
+
if (parsed === undefined)
|
|
138
|
+
continue;
|
|
139
|
+
for (const imp of parsed.parsedImports) {
|
|
140
|
+
if (imp.kind !== 'reexport')
|
|
141
|
+
continue;
|
|
142
|
+
const targetRaw = imp.targetRaw;
|
|
143
|
+
if (targetRaw === null)
|
|
144
|
+
continue;
|
|
145
|
+
const moduleName = firstSwiftModuleSegment(targetRaw);
|
|
146
|
+
if (moduleName === null || seenModules.has(moduleName))
|
|
147
|
+
continue;
|
|
148
|
+
// `@_exported import struct Models.User` re-exports User, not Models.
|
|
149
|
+
// File-level resolve cannot attribute a member to a file, so skip
|
|
150
|
+
// the whole-module enqueue rather than painting every Models file.
|
|
151
|
+
if (imp.importedName !== moduleName)
|
|
152
|
+
continue;
|
|
153
|
+
seenModules.add(moduleName);
|
|
154
|
+
const more = resolveSwiftModuleFiles(moduleName, ctx);
|
|
155
|
+
if (more === null)
|
|
156
|
+
continue;
|
|
157
|
+
for (const next of more) {
|
|
158
|
+
if (out.has(next))
|
|
159
|
+
continue;
|
|
160
|
+
out.add(next);
|
|
161
|
+
queue.push(next);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return [...out];
|
|
166
|
+
}
|
|
167
|
+
export function resolveSwiftImportTarget(parsedImport, workspaceIndex) {
|
|
168
|
+
const ctx = narrowContext(workspaceIndex);
|
|
169
|
+
if (ctx === null)
|
|
170
|
+
return null;
|
|
171
|
+
const targetRaw = parsedImport.targetRaw;
|
|
172
|
+
if (targetRaw === null)
|
|
173
|
+
return null;
|
|
174
|
+
const moduleName = firstSwiftModuleSegment(targetRaw);
|
|
175
|
+
if (moduleName === null)
|
|
176
|
+
return null;
|
|
177
|
+
const files = resolveSwiftModuleFiles(moduleName, ctx);
|
|
178
|
+
if (files === null)
|
|
179
|
+
return null;
|
|
180
|
+
const expanded = expandSwiftReexportFiles(files, ctx);
|
|
181
|
+
return expanded.length > 0 ? expanded : null;
|
|
182
|
+
}
|