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.
@@ -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` — replicating legacy
18
- * `groupSwiftFilesByTarget`. With no scanned source dir the map is null
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). Every pair of distinct `.swift` files in the same module
21
- * gets a directed IMPORTS edge in both directions (whole-module
22
- * visibility is symmetric).
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` — replicating legacy
18
- * `groupSwiftFilesByTarget`. With no scanned source dir the map is null
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). Every pair of distinct `.swift` files in the same module
21
- * gets a directed IMPORTS edge in both directions (whole-module
22
- * visibility is symmetric).
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; // no siblings to import
41
+ continue;
41
42
  for (const source of group) {
42
- for (const target of group) {
43
- if (source.filePath === target.filePath)
44
- continue; // no self-import
45
- const dedupKey = `${source.filePath}->${target.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', target.filePath),
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
- * Swift imports are whole-module (no named members), so this is 1:1 —
8
- * one `import` produces exactly one import. The split layer exposes the
9
- * module name and the `@testable` flag without pushing raw-text parsing
10
- * into `interpret.ts`.
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
- * import Foundation → kind=namespace, source=Foundation
13
- * import Foo.Bar → kind=namespace, source=Foo (SPM target),
14
- * name=Foo.Bar (full path, for reference)
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
- * Swift imports are whole-module (no named members), so this is 1:1 —
8
- * one `import` produces exactly one import. The split layer exposes the
9
- * module name and the `@testable` flag without pushing raw-text parsing
10
- * into `interpret.ts`.
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
- * import Foundation → kind=namespace, source=Foundation
13
- * import Foo.Bar → kind=namespace, source=Foo (SPM target),
14
- * name=Foo.Bar (full path, for reference)
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
- // Any attribute whose text mentions `testable` flips the flag.
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
- // The module path is one or more simple_identifier children, one per
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
- const parts = raw.split('.');
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, 'namespace'),
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, spec.fullPath),
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
- * Swift's `import ModuleName` brings in a whole SPM target / framework
5
- * module. The scope-resolution contract passes only `allFilePaths` (no
6
- * `SwiftPackageConfig`), so we resolve a module name to the `.swift`
7
- * files under a directory segment named after the module — the SPM
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 (intra-target) visibility — the bulk of Swift cross-file
12
- * resolution, which needs NO `import` statement — is handled separately
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
- * Swift's `import ModuleName` brings in a whole SPM target / framework
5
- * module. The scope-resolution contract passes only `allFilePaths` (no
6
- * `SwiftPackageConfig`), so we resolve a module name to the `.swift`
7
- * files under a directory segment named after the module — the SPM
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 (intra-target) visibility — the bulk of Swift cross-file
12
- * resolution, which needs NO `import` statement — is handled separately
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
- export function resolveSwiftImportTarget(parsedImport, workspaceIndex) {
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
- // Swift import target is the SPM module name (first dotted segment).
63
- const targetRaw = parsedImport.targetRaw;
64
- if (targetRaw === null || targetRaw === '')
65
- return null;
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; // external framework
73
- // Exclude the importer itself (a file under `Foo/` importing `Foo`).
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
+ }