mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-29 14:49:36 +08:00
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:
co-authored by
Claude Opus 5.5
parent
e9be68b61d
commit
7639c787e4
@@ -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']);
|
||||
|
||||
@@ -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[^{]*)?\{/;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user