Skip to content

Commit 3f9989c

Browse files
committed
swift: credit aliased type through typealias edge (v102)
handleTypeAlias now folds the RHS type in as a RelInstantiates edge, so a type reached only through its alias name is no longer reported as dead code. Guarded like handleInit; cacheVersion v102 + golden fixture.
1 parent fe6e105 commit 3f9989c

6 files changed

Lines changed: 92 additions & 4 deletions

File tree

internal/cachecov/coverage_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ var versionCoverage = map[int][]string{
133133
99: {"TestKtComplexity_CallsInScalingLoop_ConstantExcluded", "TestKtComplexity_CallsInScalingLoop_InfiniteLoopCallsRetained", "TestKtComplexity_CallsInScalingLoop_ScalingRetained", "TestKtComplexity_CallsInScalingLoop_MixedLoops", "TestKtComplexity_CallsInScalingLoop_AbsentWithoutLoopCalls", "TestExtract_CallsInScalingLoop_InfiniteLoopCallsRetained", "TestExtract_CallsInScalingLoop_PresentButEmptyWhenAllBounded", "TestExtract_CallsInScalingLoop_AbsentWithoutLoopCalls", "TestPyComplexity_CallsInScalingLoop_InfiniteLoopCallsRetained", "TestPyComplexity_CallsInScalingLoop_PresentButEmptyWhenAllBounded", "TestPyComplexity_CallsInScalingLoop_AbsentWithoutLoopCalls", "TestTsComplexity_CallsInScalingLoop_InfiniteLoopCallsRetained", "TestTsComplexity_CallsInScalingLoop_PresentButEmptyWhenAllBounded", "TestTsComplexity_CallsInScalingLoop_AbsentWithoutLoopCalls", "TestGolden"}, // calls_in_scaling_loop = repeated (not merely scaling) loops, emitted even when empty
134134
100: {"TestExtractTestRefs_InPackageCallResolvesToProductionSymbol", "TestExtractTestRefs_ExternalTestPackageResolvesThroughImport", "TestExtractTestRefs_SkipsBuiltinsAndEmitsNoSymbols", "TestExtractTestRefs_IgnoresFilesItDoesNotOwn", "TestExtractTestRefs_ReferenceFreeFileYieldsNoFact", "TestDefaultTestGlobsCoverGoAndStayIgnored", "TestGolden"}, // Go test_ref facts: both gates, so a function called only from its _test.go is no longer high-confidence dead
135135
101: {"TestGoHTTPClient_PackageConstAbsoluteBaseURL_TaggedExternalWithHost", "TestGoHTTPClient_PackageScopedConstFromSiblingFile_TaggedExternal", "TestGoHTTPClient_StructFieldAbsoluteBaseURL_TaggedExternalNoHostWhenAmbiguous", "TestGoHTTPClient_ConfigInjectedBaseURL_NotTaggedExternal", "TestGoHTTPClient_RelativeLiteralBaseURL_NotTaggedExternal", "TestGoHTTPClient_MixedAbsoluteAndRelativeBindings_NotTaggedExternal", "TestGoHTTPClient_RouteNameUnchangedByExternalTagging", "TestComputeLinks_ExternalClientStillMatchesLoadedServer", "TestGolden"}, // Go base-URL host recovery: third-party client calls tagged external, hardcoded internal host still resolves
136+
102: {"TestTypeAlias_EmitsReferenceEdgeToUnderlyingType", "TestTypeAlias_FunctionTypeRHSEmitsNoEdge", "TestGolden"}, // Swift typealias folds the aliased type in as a RelInstantiates edge, so a type reached only through its alias name is not mis-reported as dead code
136137
}
137138

138139
func TestCacheVersionCoverage(t *testing.T) {

internal/engine/cache.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,14 @@ import (
308308
// snapshots must re-extract. (The paired linker change — matching a client route
309309
// before bucketing it external, so a hardcoded internal host still resolves — runs
310310
// post-extraction and needs no cache bump; it is covered by TestGolden here.)
311-
const cacheVersion = "v101"
311+
// v102: the Swift extractor folds a typealias's aliased type into the alias fact
312+
// as a RelInstantiates edge. `typealias FooViewModel = FooEditorState` used to
313+
// leave FooEditorState with no incoming edge, so a type reached only through its
314+
// alias name was mis-reported as unreferenced dead code (GAP-SW-09). handleTypeAlias
315+
// now emits the edge (guarded like handleInit — system types and function/tuple/
316+
// optional RHS shapes, which have no simple type name, emit nothing). Swift symbol
317+
// facts gain a relation, so cached Swift snapshots must re-extract.
318+
const cacheVersion = "v102"
312319

313320
// extractorCache holds per-extractor facts keyed by a content hash of the files
314321
// the extractor depends on. It is loaded from disk at the start of a snapshot and

internal/engine/testdata/golden/swift_sample.facts.jsonl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{"kind":"dependency","name":"Sources/CoreKit -\u003e Foundation","file":"Sources/CoreKit/Aliasing.swift","line":1,"repo":"swift_sample","props":{"language":"swift","source":"stdlib"},"relations":[{"kind":"imports","target":"Foundation"}]}
12
{"kind":"dependency","name":"Sources/CoreKit -\u003e Foundation","file":"Sources/CoreKit/Endpoints.swift","line":1,"repo":"swift_sample","props":{"language":"swift","source":"stdlib"},"relations":[{"kind":"imports","target":"Foundation"}]}
23
{"kind":"dependency","name":"Sources/CoreKit -\u003e Foundation","file":"Sources/CoreKit/Inheritance.swift","line":1,"repo":"swift_sample","props":{"language":"swift","source":"stdlib"},"relations":[{"kind":"imports","target":"Foundation"}]}
34
{"kind":"dependency","name":"Sources/CoreKit -\u003e Foundation","file":"Sources/CoreKit/Networking.swift","line":1,"repo":"swift_sample","props":{"language":"swift","source":"stdlib"},"relations":[{"kind":"imports","target":"Foundation"}]}
@@ -10,6 +11,9 @@
1011
{"kind":"route","name":"feed/items","file":"Sources/CoreKit/Networking.swift","line":17,"repo":"swift_sample","props":{"api":"Networking","framework":"urlsession","language":"swift","method":"GET","role":"client","source":"urlsession"},"relations":[{"kind":"declares","target":"Sources/CoreKit"}]}
1112
{"kind":"symbol","name":"Sources/CoreKit.APIEndpoint","file":"Sources/CoreKit/Endpoints.swift","line":12,"repo":"swift_sample","props":{"exported":true,"language":"swift","symbol_kind":"interface"},"relations":[{"kind":"declares","target":"Sources/CoreKit"}]}
1213
{"kind":"symbol","name":"Sources/CoreKit.APIEndpoint.urlPrefixComponent","file":"Sources/CoreKit/Endpoints.swift","line":19,"repo":"swift_sample","props":{"cyclomatic":1,"exported":true,"language":"swift","symbol_kind":"variable"},"relations":[{"kind":"declares","target":"Sources/CoreKit"}]}
14+
{"kind":"symbol","name":"Sources/CoreKit.AliasName","file":"Sources/CoreKit/Aliasing.swift","line":11,"repo":"swift_sample","props":{"exported":true,"language":"swift","symbol_kind":"type"},"relations":[{"kind":"declares","target":"Sources/CoreKit"},{"kind":"instantiates","target":"Sources/CoreKit.AliasTarget"}]}
15+
{"kind":"symbol","name":"Sources/CoreKit.AliasTarget","file":"Sources/CoreKit/Aliasing.swift","line":7,"repo":"swift_sample","props":{"exported":true,"language":"swift","signature":"func perform()","symbol_kind":"class"},"relations":[{"kind":"declares","target":"Sources/CoreKit"}]}
16+
{"kind":"symbol","name":"Sources/CoreKit.AliasTarget.perform","file":"Sources/CoreKit/Aliasing.swift","line":8,"repo":"swift_sample","props":{"cyclomatic":1,"exported":true,"language":"swift","receiver":"AliasTarget","symbol_kind":"method"},"relations":[{"kind":"declares","target":"Sources/CoreKit"}]}
1317
{"kind":"symbol","name":"Sources/CoreKit.DataModel","file":"Sources/CoreKit/Inheritance.swift","line":4,"repo":"swift_sample","props":{"exported":true,"language":"swift","signature":"func runRequest()","symbol_kind":"class"},"relations":[{"kind":"declares","target":"Sources/CoreKit"}]}
1418
{"kind":"symbol","name":"Sources/CoreKit.DataModel.runRequest","file":"Sources/CoreKit/Inheritance.swift","line":5,"repo":"swift_sample","props":{"cyclomatic":1,"exported":true,"language":"swift","receiver":"DataModel","symbol_kind":"method"},"relations":[{"kind":"declares","target":"Sources/CoreKit"}]}
1519
{"kind":"symbol","name":"Sources/CoreKit.FeedService","file":"Sources/CoreKit/Networking.swift","line":8,"repo":"swift_sample","props":{"exported":true,"final":true,"language":"swift","signature":"let baseURL: URL\nfunc fetchFeed() async throws -\u003e Data\nfunc loadFeed() async throws -\u003e Data","symbol_kind":"class"},"relations":[{"kind":"declares","target":"Sources/CoreKit"}]}
@@ -29,5 +33,6 @@
2933
{"kind":"symbol","name":"Sources/CoreKit.TreeWalker.walk","file":"Sources/CoreKit/Recursion.swift","line":10,"repo":"swift_sample","props":{"cyclomatic":2,"exported":true,"language":"swift","receiver":"TreeWalker","recursive_self":true,"symbol_kind":"method"},"relations":[{"kind":"calls","target":"Sources/CoreKit.TreeWalker.walk"},{"kind":"declares","target":"Sources/CoreKit"}]}
3034
{"kind":"symbol","name":"Sources/CoreKit.UserModel","file":"Sources/CoreKit/Inheritance.swift","line":13,"repo":"swift_sample","props":{"exported":true,"final":true,"language":"swift","signature":"func refresh()","symbol_kind":"class"},"relations":[{"kind":"declares","target":"Sources/CoreKit"},{"kind":"implements","target":"DataModel"}]}
3135
{"kind":"symbol","name":"Sources/CoreKit.UserModel.refresh","file":"Sources/CoreKit/Inheritance.swift","line":14,"repo":"swift_sample","props":{"cyclomatic":1,"exported":true,"language":"swift","receiver":"UserModel","symbol_kind":"method"},"relations":[{"kind":"calls","target":"Sources/CoreKit.DataModel.runRequest"},{"kind":"declares","target":"Sources/CoreKit"}]}
36+
{"kind":"symbol","name":"Sources/CoreKit.useAlias","file":"Sources/CoreKit/Aliasing.swift","line":13,"repo":"swift_sample","props":{"cyclomatic":1,"exported":true,"language":"swift","symbol_kind":"function"},"relations":[{"kind":"calls","target":"Sources/CoreKit.AliasTarget.perform"},{"kind":"declares","target":"Sources/CoreKit"},{"kind":"instantiates","target":"AliasName"}]}
3237
{"kind":"symbol","name":"Tests/CoreKitTests.CoreKitTests","file":"Tests/CoreKitTests/CoreKitTests.swift","line":7,"repo":"swift_sample","props":{"exported":true,"final":true,"language":"swift","signature":"func testNodeHasNoParentByDefault()","symbol_kind":"class"},"relations":[{"kind":"declares","target":"Tests/CoreKitTests"},{"kind":"implements","target":"XCTestCase"}]}
3338
{"kind":"symbol","name":"Tests/CoreKitTests.CoreKitTests.testNodeHasNoParentByDefault","file":"Tests/CoreKitTests/CoreKitTests.swift","line":8,"repo":"swift_sample","props":{"cyclomatic":1,"exported":true,"language":"swift","receiver":"CoreKitTests","symbol_kind":"method"},"relations":[{"kind":"declares","target":"Tests/CoreKitTests"},{"kind":"instantiates","target":"Sources/CoreKit.Node"},{"kind":"instantiates","target":"XCTAssertNil"}]}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Foundation
2+
3+
// AliasTarget is reached only through the AliasName typealias, so the alias must
4+
// credit it or the dead-code detector reports it as an unreferenced orphan
5+
// (GAP-SW-09, cache.go v102). handleTypeAlias folds the aliased type in as an
6+
// instantiation edge on the alias fact.
7+
class AliasTarget {
8+
func perform() {}
9+
}
10+
11+
typealias AliasName = AliasTarget
12+
13+
func useAlias() {
14+
let t = AliasName()
15+
t.perform()
16+
}

internal/extractors/swiftextractor/swift_ast.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,20 @@ func (w *astWalker) handleTypeAlias(node *sitter.Node) {
10161016
return
10171017
}
10181018
modifiers := findChildByKind(node, "modifiers")
1019+
relations := []facts.Relation{
1020+
{Kind: facts.RelDeclares, Target: w.dir},
1021+
}
1022+
// A `typealias Foo = Bar` is a genuine reference to Bar, so fold the aliased
1023+
// type in as an instantiation edge: a type reached only through its alias name
1024+
// (the idiomatic `typealias FooViewModel = FooEditorState`) would otherwise have
1025+
// no incoming edge and be mis-reported as an unreferenced orphan (GAP-SW-09).
1026+
// Mirrors handleInit's type guard — skip system types and function/tuple/
1027+
// optional RHS shapes, which yield no simple resolvable type name.
1028+
if valueNode := node.ChildByFieldName("value"); valueNode != nil {
1029+
if target := simpleTypeName(valueNode, w.src); target != "" && !isSystemType(target) && isTypeName(target) {
1030+
relations = append(relations, facts.Relation{Kind: facts.RelInstantiates, Target: target})
1031+
}
1032+
}
10191033
w.out = append(w.out, facts.Fact{
10201034
Kind: facts.KindSymbol,
10211035
Name: w.dir + "." + w.qualify(name),
@@ -1026,9 +1040,7 @@ func (w *astWalker) handleTypeAlias(node *sitter.Node) {
10261040
"exported": !isPrivateAccess(nodeText(modifiers, w.src)),
10271041
"language": "swift",
10281042
},
1029-
Relations: []facts.Relation{
1030-
{Kind: facts.RelDeclares, Target: w.dir},
1031-
},
1043+
Relations: relations,
10321044
})
10331045
}
10341046

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package swiftextractor
2+
3+
import (
4+
"testing"
5+
6+
"github.com/enola-labs/enola/internal/facts"
7+
)
8+
9+
// TestTypeAlias_EmitsReferenceEdgeToUnderlyingType pins GAP-SW-09: a type reached
10+
// only through a `typealias` name must still credit the underlying declaration, or
11+
// the dead-code detector reports it as an unreferenced orphan. handleTypeAlias
12+
// folds the aliased type in as an instantiation edge on the alias fact.
13+
func TestTypeAlias_EmitsReferenceEdgeToUnderlyingType(t *testing.T) {
14+
ff := extractAST(t, `
15+
class AliasTarget {
16+
func perform() {}
17+
}
18+
typealias AliasName = AliasTarget
19+
`, false)
20+
21+
f, ok := findFact(ff, "pkg.AliasName")
22+
if !ok {
23+
t.Fatal("expected fact for pkg.AliasName")
24+
}
25+
if !hasRelation(f, facts.RelInstantiates, "AliasTarget") {
26+
t.Errorf("expected typealias to reference underlying type AliasTarget; relations=%v", f.Relations)
27+
}
28+
}
29+
30+
// TestTypeAlias_FunctionTypeRHSEmitsNoEdge guards the fold: a function-type,
31+
// tuple, or otherwise unresolvable RHS yields no simple type name, so the alias
32+
// fact must carry no spurious reference edge.
33+
func TestTypeAlias_FunctionTypeRHSEmitsNoEdge(t *testing.T) {
34+
ff := extractAST(t, `
35+
typealias Handler = (Int) -> Void
36+
`, false)
37+
38+
f, ok := findFact(ff, "pkg.Handler")
39+
if !ok {
40+
t.Fatal("expected fact for pkg.Handler")
41+
}
42+
for _, r := range f.Relations {
43+
if r.Kind == facts.RelInstantiates || r.Kind == facts.RelCalls {
44+
t.Errorf("did not expect a reference edge for a function-type alias; got %v", r)
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)