fix(swift): what a SwiftUI app's viewer showed that was not so (#2109)

Three things IceCubesApp showed in `codegraph ui`:

Steps labelled calls that leave the index by other stacks' names:
`rawValue.data(using: .utf8)` network (25 boxes), Foundation's `Timer`
telemetry, `Calendar` and a SiriKit `intent` device, the app's own
`Notifications` endpoint enum device (expo-notifications' row), and
`viewModel.votes.firstIndex(of:)` a database read (the `\w*Model` row).
Now: a Swift `.data/.upload/.download/.bytes` is network only on a
`*session` receiver; Timer/Calendar/intent are no effect in Swift; a
`viewModel` is not a table (any language); a keychain receiver is
storage ahead of Swift's generic `x.delete` database row. A call whose
receiver is a type the Swift project declares — not an `extension X {}`,
which declares nothing (read through the site reader) — no longer
matches the library-name rows; the model and reply rows, written for
project types, still do.

"Files that run something" listed every view with a `#Preview { … }`:
the macro sits at a file's top level, so its constructions were
module-level edges. Entry points now subtract calls inside a Swift
file's top-level previews (trailing closures included — WidgetKit's
`} timeline: {`), drop a file left with none, and re-rank.

Every `struct X: View` had a one-line `component` twin, and `@main`
apps, view controllers and UIView subclasses a `class` twin, from the
SwiftUI/UIKit resolvers' regex extract(): 188 nodes on IceCubesApp, 184
on mastodon-ios, zero edges into any of them — dead ends in search and
the symbol view. The two extract()s are removed.

IceCubesApp's effect sweep is now URLSession-only network, no
telemetry, no database, keychain storage; its entry files drop from
eleven preview files to its one real script. SwiftPackageIndex's Fluent
database and reply steps are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-29 22:05:38 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent e9be68b61d
commit 7639c787e4
9 changed files with 334 additions and 134 deletions
+5 -110
View File
@@ -8,6 +8,10 @@ import { Node } from '../../types';
import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
import { stripCommentsForRegex } from '../strip-comments';
// No extract(): a SwiftUI view is its own struct node, and a UIKit controller
// its class. A one-line `component`/`class` twin per `struct X: View` (and per
// `@main` app, view controller and UIView subclass) carried no edges and
// showed up in search and the symbol view as a dead-end duplicate.
export const swiftUIResolver: FrameworkResolver = {
name: 'swiftui',
languages: ['swift'],
@@ -76,61 +80,6 @@ export const swiftUIResolver: FrameworkResolver = {
return null;
},
extract(filePath, content) {
if (!filePath.endsWith('.swift')) return { nodes: [], references: [] };
const nodes: Node[] = [];
const now = Date.now();
const safe = stripCommentsForRegex(content, 'swift');
// Extract SwiftUI View structs
// struct ContentView: View { ... }
const viewPattern = /struct\s+(\w+)\s*:\s*(?:\w+\s*,\s*)*View/g;
let match: RegExpExecArray | null;
while ((match = viewPattern.exec(safe)) !== null) {
const [, viewName] = match;
const line = safe.slice(0, match.index).split('\n').length;
nodes.push({
id: `view:${filePath}:${viewName}:${line}`,
kind: 'component',
name: viewName!,
qualifiedName: `${filePath}::${viewName}`,
filePath,
startLine: line,
endLine: line,
startColumn: 0,
endColumn: match[0].length,
language: 'swift',
updatedAt: now,
});
}
// Extract @main App entry point
const appPattern = /@main\s+struct\s+(\w+)\s*:\s*App/g;
while ((match = appPattern.exec(safe)) !== null) {
const [, appName] = match;
const line = safe.slice(0, match.index).split('\n').length;
nodes.push({
id: `app:${filePath}:${appName}:${line}`,
kind: 'class',
name: appName!,
qualifiedName: `${filePath}::${appName}`,
filePath,
startLine: line,
endLine: line,
startColumn: 0,
endColumn: match[0].length,
language: 'swift',
updatedAt: now,
});
}
return { nodes, references: [] };
},
};
export const uikitResolver: FrameworkResolver = {
@@ -210,60 +159,6 @@ export const uikitResolver: FrameworkResolver = {
return null;
},
extract(filePath, content) {
if (!filePath.endsWith('.swift')) return { nodes: [], references: [] };
const nodes: Node[] = [];
const now = Date.now();
const safe = stripCommentsForRegex(content, 'swift');
// Extract UIViewController subclasses
const vcPattern = /class\s+(\w+)\s*:\s*(?:\w+\s*,\s*)*UIViewController/g;
let match: RegExpExecArray | null;
while ((match = vcPattern.exec(safe)) !== null) {
const [, vcName] = match;
const line = safe.slice(0, match.index).split('\n').length;
nodes.push({
id: `viewcontroller:${filePath}:${vcName}:${line}`,
kind: 'class',
name: vcName!,
qualifiedName: `${filePath}::${vcName}`,
filePath,
startLine: line,
endLine: line,
startColumn: 0,
endColumn: match[0].length,
language: 'swift',
updatedAt: now,
});
}
// Extract UIView subclasses
const viewPattern = /class\s+(\w+)\s*:\s*(?:\w+\s*,\s*)*UIView[^C]/g;
while ((match = viewPattern.exec(safe)) !== null) {
const [, viewName] = match;
const line = safe.slice(0, match.index).split('\n').length;
nodes.push({
id: `uiview:${filePath}:${viewName}:${line}`,
kind: 'class',
name: viewName!,
qualifiedName: `${filePath}::${viewName}`,
filePath,
startLine: line,
endLine: line,
startColumn: 0,
endColumn: match[0].length,
language: 'swift',
updatedAt: now,
});
}
return { nodes, references: [] };
},
};
export const vaporResolver: FrameworkResolver = {
@@ -547,7 +442,7 @@ function resolveVaporHandler(typePath: string | null, method: string, ref: Unres
const SWIFT_TYPE_KINDS = new Set(['class', 'struct', 'enum', 'protocol']);
const VIEW_KINDS = new Set(['struct', 'component']);
const VIEW_KINDS = new Set(['struct']);
const CLASS_KINDS = new Set(['class']);
const MODEL_KINDS = new Set(['struct', 'class']);
const PROTOCOL_KINDS = new Set(['protocol']);
+13 -11
View File
@@ -64,21 +64,23 @@ export function isSwiftExtension(node: Node, context: ResolutionContext): boolea
const hit = memo.get(node.id);
if (hit !== undefined) return hit;
const lines = linesOf(node.filePath, context);
let extension = false;
if (lines) {
const head = [
(lines[node.startLine - 1] ?? '').slice(node.startColumn),
...lines.slice(node.startLine, Math.min(node.endLine, node.startLine + 4)),
]
.join(' ')
// An attribute's string argument (`message: "use class Foo"`) is not a keyword.
.replace(/"(?:[^"\\\n]|\\.)*"/g, '""');
extension = DECLARATION_KEYWORD.exec(head)?.[1] === 'extension';
}
const extension = !!lines && declaresExtension(lines, node);
memo.set(node.id, extension);
return extension;
}
/** Does the class_declaration at `node`'s position open with `extension`? From the file's lines. */
export function declaresExtension(lines: readonly string[], node: Pick<Node, 'startLine' | 'startColumn' | 'endLine'>): boolean {
const head = [
(lines[node.startLine - 1] ?? '').slice(node.startColumn),
...lines.slice(node.startLine, Math.min(node.endLine, node.startLine + 4)),
]
.join(' ')
// An attribute's string argument (`message: "use class Foo"`) is not a keyword.
.replace(/"(?:[^"\\\n]|\\.)*"/g, '""');
return DECLARATION_KEYWORD.exec(head)?.[1] === 'extension';
}
/** A declaration's inheritance clause: `struct HomeView: View, Sendable {` → `View, Sendable`. */
const INHERITANCE_CLAUSE = /\b(?:class|struct|enum|actor|protocol|extension)\s+[A-Za-z_][\w.]*\s*(?:<[^{}]*?>)?\s*:\s*([^{]*?)\s*(?:\bwhere\b[^{]*)?\{/;