Skip to content

[Bug]: Swift: protocol-first conformance (struct/class/enum X: P) records no heritage edge — first clause is treated as extends and dropped when the target is a protocol #2660

Description

@rplotkin

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions