Description
In Swift, a type whose first inheritance clause is a protocol (struct S: P, class C: P, final class C: P, enum E: P) gets no heritage edge at all. The Swift extractor treats the first inheritance_specifier as extends and the rest as implements; when that first clause resolves to a protocol (interface node), the extends edge is silently dropped instead of being emitted as implements. Only the class C: Superclass, P shape records an implements edge.
Since Swift structs and enums can never have a superclass, and most protocol conformances in practice are declared without one, this drops the majority of conformances. Downstream, implementations <Protocol>, interfaces <Type>, and CHA dispatch through protocol-typed receivers all come back empty for these types, so fn-impact on a conformer's method reports "No callers found" even when the method is invoked through the protocol.
Conformance declared via an extension (extension S: P {}) is also not recorded, and additionally creates a second S node of kind class alongside the struct node.
Both engines behave the same (native and --engine wasm).
Steps to Reproduce
Sources/Heritage.swift:
import Foundation
public protocol Runnable { func run() }
public class Base { public func run() { } }
struct S1: Runnable { func run() { } } // struct, protocol first
public struct S2: Runnable { public func run() { } } // public struct, protocol first
class C1: Runnable { func run() { } } // class, protocol first
final class C2: Runnable { func run() { } } // final class, protocol first
public final class C3: Runnable { public func run() { } } // public final class, protocol first
enum E1: Runnable { case a; func run() { } } // enum, protocol first
struct S3 { func run() { } }
extension S3: Runnable { } // conformance via extension
public class C4: Base { } // superclass only
class C5: Base, Runnable { } // superclass first, then protocol
class Caller {
let r: Runnable
init(r: Runnable) { self.r = r }
func go() { r.run() } // protocol-typed receiver
}
codegraph build . --no-incremental
sqlite3 .codegraph/graph.db "select e.kind||' '||s.name||' -> '||t.name||' ('||t.kind||')' \
from edges e join nodes s on s.id=e.source_id join nodes t on t.id=e.target_id \
where e.kind in ('extends','implements') order by s.line"
codegraph implementations Runnable
codegraph interfaces S1
codegraph fn-impact S1.run
codegraph fn-impact Runnable.run
Expected Behavior
Nine heritage edges: implements for S1, S2, C1, C2, C3, E1, S3 (via extension) and C5 → Runnable, plus extends for C4 and C5 → Base. implementations Runnable lists all eight conformers. interfaces S1 lists Runnable.
Actual Behavior
Only three edges are stored:
extends C4 -> Base (class)
extends C5 -> Base (class)
implements C5 -> Runnable (interface)
| Declaration shape |
Edge stored |
struct S1: Runnable |
none |
public struct S2: Runnable |
none |
class C1: Runnable |
none |
final class C2: Runnable |
none |
public final class C3: Runnable |
none |
enum E1: Runnable |
none |
extension S3: Runnable |
none (+ duplicate S3 node of kind class) |
public class C4: Base |
extends ✓ |
class C5: Base, Runnable |
extends ✓ + implements ✓ |
$ codegraph implementations Runnable
I Runnable Sources/Heritage.swift:3
Implementors (1):
* C5 Sources/Heritage.swift:16
$ codegraph interfaces S1
- S1 Sources/Heritage.swift:6
(no interfaces/traits found)
$ codegraph fn-impact S1.run
No callers found.
$ codegraph fn-impact Runnable.run
^ o Caller.go Sources/Heritage.swift:21
Total: 1 functions transitively depend on Runnable.run
The protocol-typed call r.run() does resolve to Runnable.run, so the receiver side works; it is only the missing conformance edges that stop CHA from fanning out to S1…E1.
Codegraph Version
3.17.0 (tree-sitter-swift 0.7.1 as shipped)
Node.js Version
v26.8.1
Operating System
macOS (arm64)
Parser Engine
Both — native and --engine wasm give identical results.
Additional Context
The parse shape is not the problem. Dumping the tree with the shipped tree-sitter-swift.wasm shows every variant as class_declaration > … > inheritance_specifier > user_type > type_identifier, exactly what collectSwiftInheritance in src/extractors/swift.ts walks:
class_declaration @6: struct type_identifier(S1) : inheritance_specifier(Runnable) class_body
class_declaration @15: modifiers class type_identifier(C4) : inheritance_specifier(Base) class_body
class_declaration @16: class type_identifier(C5) : inheritance_specifier(Base) , inheritance_specifier(Runnable) class_body
So the extractor sees the clause and emits { name, extends: 'Runnable' } per its "first = extends, rest = implements" comment, and the edge builder then drops an extends edge whose target is an interface node. Two possible fixes, either would do:
- In the edge builder: when an
extends target resolves to an interface/protocol node, store it as implements instead of discarding it.
- In the extractor: for
struct/enum declarations every clause is a protocol, so emit implements for all of them; for class declarations, only the first clause can be a superclass, and it is still a protocol whenever the name resolves to one.
Related, possibly intended, but it compounds the effect on UIKit-style codebases: an extends edge whose target has no node in the graph (a framework superclass such as UITableViewController) is dropped silently as well, so a class that subclasses a framework type and conforms to a delegate protocol can end up with no heritage edges at all. In a ~340-file Swift codebase with vendored dependencies, the build recorded 6 implements edges in total.
Description
In Swift, a type whose first inheritance clause is a protocol (
struct S: P,class C: P,final class C: P,enum E: P) gets no heritage edge at all. The Swift extractor treats the firstinheritance_specifierasextendsand the rest asimplements; when that first clause resolves to a protocol (interfacenode), theextendsedge is silently dropped instead of being emitted asimplements. Only theclass C: Superclass, Pshape records animplementsedge.Since Swift structs and enums can never have a superclass, and most protocol conformances in practice are declared without one, this drops the majority of conformances. Downstream,
implementations <Protocol>,interfaces <Type>, and CHA dispatch through protocol-typed receivers all come back empty for these types, sofn-impacton a conformer's method reports "No callers found" even when the method is invoked through the protocol.Conformance declared via an extension (
extension S: P {}) is also not recorded, and additionally creates a secondSnode of kindclassalongside thestructnode.Both engines behave the same (native and
--engine wasm).Steps to Reproduce
Sources/Heritage.swift:Expected Behavior
Nine heritage edges:
implementsfor S1, S2, C1, C2, C3, E1, S3 (via extension) and C5 →Runnable, plusextendsfor C4 and C5 →Base.implementations Runnablelists all eight conformers.interfaces S1listsRunnable.Actual Behavior
Only three edges are stored:
struct S1: Runnablepublic struct S2: Runnableclass C1: Runnablefinal class C2: Runnablepublic final class C3: Runnableenum E1: Runnableextension S3: RunnableS3node of kindclass)public class C4: Baseextends✓class C5: Base, Runnableextends✓ +implements✓The protocol-typed call
r.run()does resolve toRunnable.run, so the receiver side works; it is only the missing conformance edges that stop CHA from fanning out to S1…E1.Codegraph Version
3.17.0 (tree-sitter-swift 0.7.1 as shipped)
Node.js Version
v26.8.1
Operating System
macOS (arm64)
Parser Engine
Both — native and
--engine wasmgive identical results.Additional Context
The parse shape is not the problem. Dumping the tree with the shipped
tree-sitter-swift.wasmshows every variant asclass_declaration > … > inheritance_specifier > user_type > type_identifier, exactly whatcollectSwiftInheritanceinsrc/extractors/swift.tswalks:So the extractor sees the clause and emits
{ name, extends: 'Runnable' }per its "first = extends, rest = implements" comment, and the edge builder then drops anextendsedge whose target is aninterfacenode. Two possible fixes, either would do:extendstarget resolves to aninterface/protocol node, store it asimplementsinstead of discarding it.struct/enumdeclarations every clause is a protocol, so emitimplementsfor all of them; forclassdeclarations, only the first clause can be a superclass, and it is still a protocol whenever the name resolves to one.Related, possibly intended, but it compounds the effect on UIKit-style codebases: an
extendsedge whose target has no node in the graph (a framework superclass such asUITableViewController) is dropped silently as well, so a class that subclasses a framework type and conforms to a delegate protocol can end up with no heritage edges at all. In a ~340-file Swift codebase with vendored dependencies, the build recorded 6implementsedges in total.