Description
In Swift, a class-body property initialized from a static member or a constructor call without a type annotation (var a = Service.shared, var c = Service()) never seeds the receiver type map, so every method call through that property (a.reset(), self.a.reset()) is dropped. The callee then shows 0 callers in fn-impact / query / context, and the role classifier labels it leaf rather than dead-unresolved, so nothing signals that calls were lost.
The same initializer on a function-local binding (let s = Service.shared; s.reset()) does resolve, so local-variable inference already handles this shape; the gap is specifically class-body property_declaration nodes, which only go through seedSwiftPropertyTypeMap and that function returns early when there is no type_annotation. The chained form Service.shared.reset() is dropped too.
This is the Swift analogue of #2474 (Dart: local-variable constructor-call typing not seeded), one level up: properties rather than locals. The var x = Type.shared singleton idiom is very common in iOS code, so in practice a large share of intra-app method calls vanish from the graph. Both engines behave the same.
Steps to Reproduce
Sources/Service.swift:
import Foundation
public final class Service {
public static let shared = Service()
public func reset() { }
}
Sources/Client.swift:
import Foundation
class Client {
var a = Service.shared // static-member initializer, no annotation
var b: Service = .shared // annotated
var c = Service() // constructor-call initializer, no annotation
var d: Service = Service() // annotated + constructor
func useA() { a.reset() }
func useASelf() { self.a.reset() }
func useB() { b.reset() }
func useC() { c.reset() }
func useD() { d.reset() }
func useDirect() { Service.shared.reset() }
func useLocalUnannotated() {
let s = Service.shared
s.reset()
}
func useLocalAnnotated() {
let s: Service = .shared
s.reset()
}
}
codegraph build . --no-incremental
codegraph fn-impact Service.reset
Expected Behavior
All eight use* methods listed as level-1 callers of Service.reset.
Actual Behavior
Function impact: o Service.reset -- Sources/Service.swift:5
-- Level 1 (4 functions):
^ o Client.useB Sources/Client.swift:11
^ o Client.useD Sources/Client.swift:13
^ o Client.useLocalUnannotated Sources/Client.swift:15
^ o Client.useLocalAnnotated Sources/Client.swift:19
Total: 4 functions transitively depend on Service.reset
| Call shape |
Receiver declaration |
Resolved |
a.reset() |
var a = Service.shared (property, unannotated) |
no |
self.a.reset() |
same |
no |
b.reset() |
var b: Service = .shared (property, annotated) |
yes |
c.reset() |
var c = Service() (property, unannotated ctor) |
no |
d.reset() |
var d: Service = Service() (property, annotated) |
yes |
Service.shared.reset() |
chained static member |
no |
let s = Service.shared; s.reset() |
local, unannotated |
yes |
let s: Service = .shared; s.reset() |
local, annotated |
yes |
Identical output with --engine wasm.
Codegraph Version
3.17.0
Node.js Version
v26.8.1
Operating System
macOS (arm64)
Parser Engine
Both — native and --engine wasm give identical results.
Additional Context
seedSwiftPropertyTypeMap (src/extractors/swift.ts) starts with findChild(node, 'type_annotation') and returns if absent, so an unannotated class-body property contributes nothing to ctx.typeMap. Since the local-binding path already infers let s = Service.shared and let s = Service(), reusing that inference for property_declaration nodes without an annotation (initializer is a navigation_expression whose head is a capitalized simple_identifier, or a call_expression on a capitalized identifier) would close the gap, mirroring the #2474 fix.
Independently of the fix, it would help if the role classifier could mark a callee as dead-unresolved when the graph holds unresolved method-call sites with a matching method name; today the 0-caller result and a genuinely uncalled method are indistinguishable from the graph alone.
Description
In Swift, a class-body property initialized from a static member or a constructor call without a type annotation (
var a = Service.shared,var c = Service()) never seeds the receiver type map, so every method call through that property (a.reset(),self.a.reset()) is dropped. The callee then shows 0 callers infn-impact/query/context, and the role classifier labels itleafrather thandead-unresolved, so nothing signals that calls were lost.The same initializer on a function-local binding (
let s = Service.shared; s.reset()) does resolve, so local-variable inference already handles this shape; the gap is specifically class-bodyproperty_declarationnodes, which only go throughseedSwiftPropertyTypeMapand that function returns early when there is notype_annotation. The chained formService.shared.reset()is dropped too.This is the Swift analogue of #2474 (Dart: local-variable constructor-call typing not seeded), one level up: properties rather than locals. The
var x = Type.sharedsingleton idiom is very common in iOS code, so in practice a large share of intra-app method calls vanish from the graph. Both engines behave the same.Steps to Reproduce
Sources/Service.swift:Sources/Client.swift:codegraph build . --no-incremental codegraph fn-impact Service.resetExpected Behavior
All eight
use*methods listed as level-1 callers ofService.reset.Actual Behavior
a.reset()var a = Service.shared(property, unannotated)self.a.reset()b.reset()var b: Service = .shared(property, annotated)c.reset()var c = Service()(property, unannotated ctor)d.reset()var d: Service = Service()(property, annotated)Service.shared.reset()let s = Service.shared; s.reset()let s: Service = .shared; s.reset()Identical output with
--engine wasm.Codegraph Version
3.17.0
Node.js Version
v26.8.1
Operating System
macOS (arm64)
Parser Engine
Both — native and
--engine wasmgive identical results.Additional Context
seedSwiftPropertyTypeMap(src/extractors/swift.ts) starts withfindChild(node, 'type_annotation')and returns if absent, so an unannotated class-body property contributes nothing toctx.typeMap. Since the local-binding path already inferslet s = Service.sharedandlet s = Service(), reusing that inference forproperty_declarationnodes without an annotation (initializer is anavigation_expressionwhose head is a capitalizedsimple_identifier, or acall_expressionon a capitalized identifier) would close the gap, mirroring the #2474 fix.Independently of the fix, it would help if the role classifier could mark a callee as
dead-unresolvedwhen the graph holds unresolved method-call sites with a matching method name; today the 0-caller result and a genuinely uncalled method are indistinguishable from the graph alone.