@@ -298,7 +298,7 @@ fn walk_node(
298298 file,
299299 module_path,
300300 parent_id,
301- NodeKind :: Struct ,
301+ NodeKind :: Class ,
302302 result,
303303 ) ;
304304 }
@@ -1177,7 +1177,7 @@ fn extract_calls_from_text(
11771177 result. edges . push ( Edge {
11781178 source : caller_id. to_string ( ) ,
11791179 target : target_id,
1180- kind : EdgeKind :: Calls ,
1180+ kind : EdgeKind :: Reads ,
11811181 confidence : 0.5 ,
11821182 direction : None ,
11831183 operation : Some ( prefix) ,
@@ -1777,7 +1777,7 @@ fn extract_nav_prefix(node: tree_sitter::Node, source: &[u8]) -> Option<String>
17771777}
17781778
17791779/// Recursively scan for `call_expression` and `navigation_expression` nodes,
1780- /// emitting Calls edges for function calls and TypeRef edges for property accesses.
1780+ /// emitting Calls edges for function calls and Reads edges for property accesses.
17811781fn extract_calls (
17821782 node : tree_sitter:: Node ,
17831783 source : & [ u8 ] ,
@@ -1806,8 +1806,8 @@ fn extract_calls(
18061806 }
18071807
18081808 // Property access: `foo.bar` generates a navigation_expression.
1809- // Emit a Calls edge so that property reads appear in the graph
1810- // and impact analysis can trace through them .
1809+ // Emit a Reads edge so dependency queries can trace through the access
1810+ // without polluting call lists with field-like symbols .
18111811 // Skip if the parent is a call_expression (already handled above as the callee name).
18121812 if node. kind ( ) == "navigation_expression"
18131813 && !matches ! ( node. parent( ) . map( |p| p. kind( ) ) , Some ( "call_expression" ) )
@@ -1830,7 +1830,7 @@ fn extract_calls(
18301830 result. edges . push ( Edge {
18311831 source : caller_id. to_string ( ) ,
18321832 target : target_id,
1833- kind : EdgeKind :: Calls ,
1833+ kind : EdgeKind :: Reads ,
18341834 confidence : 0.6 ,
18351835 direction : None ,
18361836 operation : prefix,
@@ -1971,6 +1971,10 @@ fn dependency_read_name(
19711971 return None ;
19721972 }
19731973
1974+ if is_syntactic_argument_label ( node, source) {
1975+ return None ;
1976+ }
1977+
19741978 let name = node. utf8_text ( source) . ok ( ) ?. trim ( ) . to_string ( ) ;
19751979 if name. is_empty ( ) || name == "_" || uppercase_identifier ( & name) {
19761980 return None ;
@@ -1989,6 +1993,48 @@ fn dependency_read_name(
19891993 Some ( name)
19901994}
19911995
1996+ fn is_syntactic_argument_label ( node : tree_sitter:: Node , source : & [ u8 ] ) -> bool {
1997+ let mut cursor = node. parent ( ) ;
1998+ let mut inside_call_syntax = false ;
1999+ while let Some ( parent) = cursor {
2000+ if matches ! (
2001+ parent. kind( ) ,
2002+ "value_argument"
2003+ | "value_arguments"
2004+ | "call_suffix"
2005+ | "call_expression"
2006+ | "navigation_suffix"
2007+ | "navigation_expression"
2008+ ) {
2009+ inside_call_syntax = true ;
2010+ break ;
2011+ }
2012+ if matches ! (
2013+ parent. kind( ) ,
2014+ "function_declaration"
2015+ | "protocol_function_declaration"
2016+ | "init_declaration"
2017+ | "closure_parameter"
2018+ | "parameter"
2019+ ) {
2020+ break ;
2021+ }
2022+ cursor = parent. parent ( ) ;
2023+ }
2024+
2025+ if !inside_call_syntax {
2026+ return false ;
2027+ }
2028+
2029+ let remaining = & source[ node. end_byte ( ) ..] ;
2030+ let offset = remaining
2031+ . iter ( )
2032+ . position ( |b| !b. is_ascii_whitespace ( ) )
2033+ . unwrap_or ( remaining. len ( ) ) ;
2034+
2035+ remaining. get ( offset) . copied ( ) == Some ( b':' )
2036+ }
2037+
19922038fn emit_dependency_read (
19932039 node : tree_sitter:: Node ,
19942040 source : & [ u8 ] ,
@@ -3711,7 +3757,7 @@ mod tests {
37113757 fn extracts_class ( ) {
37123758 let result = extract ( "public class AppDelegate { }" ) ;
37133759 let node = find_node ( & result, "AppDelegate" ) ;
3714- assert_eq ! ( node. kind, NodeKind :: Struct ) ; // classes map to Struct in our model
3760+ assert_eq ! ( node. kind, NodeKind :: Class ) ;
37153761 assert_eq ! ( node. visibility, Visibility :: Public ) ;
37163762 }
37173763
@@ -3882,6 +3928,31 @@ mod tests {
38823928 assert_eq ! ( call_edge. provenance[ 0 ] . symbol_id, "test.swift::launch" ) ;
38833929 }
38843930
3931+ #[ test]
3932+ fn property_accesses_become_reads_not_calls ( ) {
3933+ let result = extract (
3934+ r#"
3935+ struct Model { let value: Int }
3936+ func launch(model: Model) {
3937+ let current = model.value
3938+ }
3939+ "# ,
3940+ ) ;
3941+
3942+ assert ! ( has_edge(
3943+ & result,
3944+ "test.swift::launch" ,
3945+ "test.swift::value" ,
3946+ EdgeKind :: Reads ,
3947+ ) ) ;
3948+ assert ! ( !has_edge(
3949+ & result,
3950+ "test.swift::launch" ,
3951+ "test.swift::value" ,
3952+ EdgeKind :: Calls ,
3953+ ) ) ;
3954+ }
3955+
38853956 #[ test]
38863957 fn extracts_condition_on_call_inside_if ( ) {
38873958 let result = extract (
@@ -4368,6 +4439,65 @@ mod tests {
43684439 assert ! ( has_read( "test.swift::RoomPage::minimizeGameRoom" ) ) ;
43694440 }
43704441
4442+ #[ test]
4443+ fn skips_argument_labels_when_enriching_property_reads ( ) {
4444+ let result = extract_with_localization (
4445+ r#"
4446+ import SwiftUI
4447+
4448+ struct GeneralButtonType {
4449+ let type: Int
4450+ }
4451+
4452+ struct AudioButton {
4453+ let label: String
4454+ }
4455+
4456+ struct LoginPage: View {
4457+ @State private var isNextValid = false
4458+
4459+ private var loginButton: some View {
4460+ Button {
4461+ } label: {
4462+ Text("Login")
4463+ }
4464+ .buttonStyle(.app(type: .yellowH112))
4465+ .disabled(isNextValid.negated)
4466+ }
4467+
4468+ var body: some View { loginButton }
4469+ }
4470+ "# ,
4471+ ) ;
4472+ let graph = grapha_core:: merge ( vec ! [ result] ) ;
4473+
4474+ let read_targets: Vec < _ > = graph
4475+ . edges
4476+ . iter ( )
4477+ . filter ( |edge| {
4478+ edge. source == "test.swift::LoginPage::loginButton" && edge. kind == EdgeKind :: Reads
4479+ } )
4480+ . map ( |edge| edge. target . as_str ( ) )
4481+ . collect ( ) ;
4482+
4483+ assert ! (
4484+ read_targets. contains( & "test.swift::LoginPage::isNextValid" ) ,
4485+ "real property reads should still be preserved"
4486+ ) ;
4487+ assert ! (
4488+ read_targets
4489+ . iter( )
4490+ . all( |target| !target. ends_with( "::label" ) ) ,
4491+ "trailing closure labels should not become dependency reads: {read_targets:?}"
4492+ ) ;
4493+ assert ! (
4494+ read_targets
4495+ . iter( )
4496+ . all( |target| !target. ends_with( "::type" ) ) ,
4497+ "named argument labels should not become dependency reads: {read_targets:?}"
4498+ ) ;
4499+ }
4500+
43714501 #[ test]
43724502 fn marks_swiftui_dynamic_properties_as_invalidation_sources ( ) {
43734503 let result = extract_with_localization (
0 commit comments