Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/cachecov/coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ var versionCoverage = map[int][]string{
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
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
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
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
}

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

// extractorCache holds per-extractor facts keyed by a content hash of the files
// the extractor depends on. It is loaded from disk at the start of a snapshot and
Expand Down
5 changes: 5 additions & 0 deletions internal/engine/testdata/golden/swift_sample.facts.jsonl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{"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"}]}
{"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"}]}
{"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"}]}
{"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"}]}
Expand All @@ -10,6 +11,9 @@
{"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"}]}
{"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"}]}
{"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"}]}
{"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"}]}
{"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"}]}
{"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"}]}
{"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"}]}
{"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"}]}
{"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"}]}
Expand All @@ -29,5 +33,6 @@
{"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"}]}
{"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"}]}
{"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"}]}
{"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"}]}
{"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"}]}
{"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"}]}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Foundation

// AliasTarget is reached only through the AliasName typealias, so the alias must
// credit it or the dead-code detector reports it as an unreferenced orphan
// (GAP-SW-09, cache.go v102). handleTypeAlias folds the aliased type in as an
// instantiation edge on the alias fact.
class AliasTarget {
func perform() {}
}

typealias AliasName = AliasTarget

func useAlias() {
let t = AliasName()
t.perform()
}
18 changes: 15 additions & 3 deletions internal/extractors/swiftextractor/swift_ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,20 @@ func (w *astWalker) handleTypeAlias(node *sitter.Node) {
return
}
modifiers := findChildByKind(node, "modifiers")
relations := []facts.Relation{
{Kind: facts.RelDeclares, Target: w.dir},
}
// A `typealias Foo = Bar` is a genuine reference to Bar, so fold the aliased
// type in as an instantiation edge: a type reached only through its alias name
// (the idiomatic `typealias FooViewModel = FooEditorState`) would otherwise have
// no incoming edge and be mis-reported as an unreferenced orphan (GAP-SW-09).
// Mirrors handleInit's type guard — skip system types and function/tuple/
// optional RHS shapes, which yield no simple resolvable type name.
if valueNode := node.ChildByFieldName("value"); valueNode != nil {
if target := simpleTypeName(valueNode, w.src); target != "" && !isSystemType(target) && isTypeName(target) {
relations = append(relations, facts.Relation{Kind: facts.RelInstantiates, Target: target})
}
}
w.out = append(w.out, facts.Fact{
Kind: facts.KindSymbol,
Name: w.dir + "." + w.qualify(name),
Expand All @@ -1026,9 +1040,7 @@ func (w *astWalker) handleTypeAlias(node *sitter.Node) {
"exported": !isPrivateAccess(nodeText(modifiers, w.src)),
"language": "swift",
},
Relations: []facts.Relation{
{Kind: facts.RelDeclares, Target: w.dir},
},
Relations: relations,
})
}

Expand Down
47 changes: 47 additions & 0 deletions internal/extractors/swiftextractor/swift_typealias_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package swiftextractor

import (
"testing"

"github.com/enola-labs/enola/internal/facts"
)

// TestTypeAlias_EmitsReferenceEdgeToUnderlyingType pins GAP-SW-09: a type reached
// only through a `typealias` name must still credit the underlying declaration, or
// the dead-code detector reports it as an unreferenced orphan. handleTypeAlias
// folds the aliased type in as an instantiation edge on the alias fact.
func TestTypeAlias_EmitsReferenceEdgeToUnderlyingType(t *testing.T) {
ff := extractAST(t, `
class AliasTarget {
func perform() {}
}
typealias AliasName = AliasTarget
`, false)

f, ok := findFact(ff, "pkg.AliasName")
if !ok {
t.Fatal("expected fact for pkg.AliasName")
}
if !hasRelation(f, facts.RelInstantiates, "AliasTarget") {
t.Errorf("expected typealias to reference underlying type AliasTarget; relations=%v", f.Relations)
}
}

// TestTypeAlias_FunctionTypeRHSEmitsNoEdge guards the fold: a function-type,
// tuple, or otherwise unresolvable RHS yields no simple type name, so the alias
// fact must carry no spurious reference edge.
func TestTypeAlias_FunctionTypeRHSEmitsNoEdge(t *testing.T) {
ff := extractAST(t, `
typealias Handler = (Int) -> Void
`, false)

f, ok := findFact(ff, "pkg.Handler")
if !ok {
t.Fatal("expected fact for pkg.Handler")
}
for _, r := range f.Relations {
if r.Kind == facts.RelInstantiates || r.Kind == facts.RelCalls {
t.Errorf("did not expect a reference edge for a function-type alias; got %v", r)
}
}
}
Loading