@@ -523,6 +523,92 @@ fn elixir_def_arity(def_call_node: tree_sitter::Node) -> Option<i64> {
523523 }
524524}
525525
526+ /// Dart (C3, Tier B audit): tree-sitter-dart has no dedicated call-expression
527+ /// node kind — a call like `a.b.c(x)` and a call-less field access `a.b.c`
528+ /// parse as the SAME flat `member_access` node shape: `[primary, selector,
529+ /// selector, ...]`, where each `selector` wraps either a dotted-name
530+ /// (`unconditional_assignable_selector`/`conditional_assignable_selector`,
531+ /// i.e. `.foo`/`?.foo`) or an `argument_part` (the call parens). Recognizing
532+ /// a call is therefore structural — "does the LAST selector wrap an
533+ /// `argument_part`?" — not expressible via the generic `call_node_types`/
534+ /// `call_function_field` field-lookup `walk_calls` uses for every other
535+ /// language, so it's handled as its own language-gated branch there instead
536+ /// (same precedent as `go_receiver_type`/`elixir_def_arity` for other
537+ /// grammar shapes the generic path can't cover). Verified against a real
538+ /// parse of tree-sitter-dart 0.0.4 (bare `print(x)`, `this.foo(x)`,
539+ /// `g.greet()`, `Greeter("world")`, chained `a.b.c()`, and the no-call
540+ /// `a.b.c` field access — see `test_dart_real_grammar_symbols_and_calls_are_
541+ /// accurate`), not guessed.
542+ ///
543+ /// Returns `(receiver, callee, arg_count)` when `node` (a `member_access`)
544+ /// is really a call site; `None` when it's a call-less field/index-access
545+ /// chain, or a shape this first pass doesn't cover (a call on a
546+ /// parenthesized/function-literal primary, or an `index_selector` — e.g.
547+ /// `list[0]` — in the middle of the chain) — deliberately bails rather than
548+ /// guessing wrong, same risk tolerance `split_receiver_callee` already
549+ /// applies for every other language.
550+ fn dart_call_from_member_access (
551+ node : tree_sitter:: Node ,
552+ source : & str ,
553+ ) -> Option < ( Option < String > , String , Option < i64 > ) > {
554+ let mut cursor = node. walk ( ) ;
555+ let named: Vec < tree_sitter:: Node > = node. named_children ( & mut cursor) . collect ( ) ;
556+ if named. len ( ) < 2 {
557+ return None ;
558+ }
559+ let last = named[ named. len ( ) - 1 ] ;
560+ if last. kind ( ) != "selector" {
561+ return None ;
562+ }
563+ let argument_part = last. named_child ( 0 ) ?;
564+ if argument_part. kind ( ) != "argument_part" {
565+ return None ; // last selector isn't a call — plain field/index access chain
566+ }
567+ let arg_count = count_arguments_node ( argument_part) ;
568+
569+ // Dotted-name selector (`.foo`/`?.foo`) -> the identifier's own text, or
570+ // `None` for an `index_selector` (`[i]`) — a shape this pass skips.
571+ fn dotted_name ( sel : tree_sitter:: Node , source : & str ) -> Option < String > {
572+ let inner = sel. named_child ( 0 ) ?;
573+ match inner. kind ( ) {
574+ "unconditional_assignable_selector" | "conditional_assignable_selector" => {
575+ let ident = inner. named_child ( 0 ) ?;
576+ ( ident. kind ( ) == "identifier" ) . then ( || source[ ident. byte_range ( ) ] . to_string ( ) )
577+ }
578+ _ => None ,
579+ }
580+ }
581+
582+ // Everything before the call selector: primary + zero or more dotted
583+ // selectors. The callee is the last dotted segment (or the primary
584+ // itself for a bare call); the receiver, if any, is the segment right
585+ // before that — the same "receiver = last segment" convention
586+ // `split_receiver_callee` already uses for every other language's
587+ // chained access (e.g. `a.b.c()` gets receiver "b", not the full "a.b").
588+ let pre = & named[ ..named. len ( ) - 1 ] ;
589+ if pre. len ( ) == 1 {
590+ let prim = pre[ 0 ] ;
591+ if !matches ! ( prim. kind( ) , "identifier" | "this" | "super" ) {
592+ return None ;
593+ }
594+ return Some ( ( None , source[ prim. byte_range ( ) ] . to_string ( ) , arg_count) ) ;
595+ }
596+
597+ let callee_sel = pre[ pre. len ( ) - 1 ] ;
598+ if callee_sel. kind ( ) != "selector" {
599+ return None ;
600+ }
601+ let callee = dotted_name ( callee_sel, source) ?;
602+
603+ let receiver_node = pre[ pre. len ( ) - 2 ] ;
604+ let receiver = if receiver_node. kind ( ) == "selector" {
605+ dotted_name ( receiver_node, source)
606+ } else {
607+ Some ( source[ receiver_node. byte_range ( ) ] . to_string ( ) )
608+ } ;
609+ Some ( ( receiver, callee, arg_count) )
610+ }
611+
526612/// Walks backward through contiguous same-kind comment siblings immediately
527613/// preceding `node` — no blank-line gap between any two, nor between the
528614/// last one and `node` itself — and joins them in source order. Line-
@@ -1416,6 +1502,7 @@ fn walk_calls(
14161502 node : tree_sitter:: Node ,
14171503 source : & str ,
14181504 consts : & crate :: indexer:: lang_constants:: LangConstants ,
1505+ language : & str ,
14191506 enclosing : Option < ( String , usize ) > ,
14201507 enclosing_class : Option < String > ,
14211508 out : & mut Vec < RawCall > ,
@@ -1444,7 +1531,33 @@ fn walk_calls(
14441531 enclosing_class. clone ( )
14451532 } ;
14461533
1447- if consts. call_node_types . contains ( & node. kind ( ) )
1534+ // Dart (C3, Tier B audit): no dedicated call-expression node kind exists
1535+ // in this grammar at all (see `dart_call_from_member_access`'s doc
1536+ // comment) — handled as its own language-gated branch rather than
1537+ // through `call_node_types`/`call_function_field` below, which assumes a
1538+ // call node names its callee via one fixed field or first-child
1539+ // position. `call_node_types` stays empty for Dart in `lang_constants.rs`
1540+ // on purpose, so the generic branch below can never also fire for a
1541+ // `member_access` node — the two paths are mutually exclusive by
1542+ // construction, not just by this `else`.
1543+ if language == "dart"
1544+ && node. kind ( ) == "member_access"
1545+ && let Some ( ( enc_name, enc_line) ) = & current
1546+ && let Some ( ( receiver, callee, arg_count) ) = dart_call_from_member_access ( node, source)
1547+ {
1548+ out. push ( RawCall {
1549+ enclosing_name : enc_name. clone ( ) ,
1550+ enclosing_line : * enc_line,
1551+ enclosing_class : child_class. clone ( ) ,
1552+ callee,
1553+ receiver_is_type_path : receiver. as_deref ( ) . is_some_and ( is_type_like) ,
1554+ receiver,
1555+ module_hint : None ,
1556+ looks_option_or_result_chained : looks_option_or_result_chained ( node, source) ,
1557+ line : node. start_position ( ) . row + 1 ,
1558+ arg_count,
1559+ } ) ;
1560+ } else if consts. call_node_types . contains ( & node. kind ( ) )
14481561 && !is_definition_macro_call ( node, source, consts)
14491562 && let Some ( ( enc_name, enc_line) ) = & current
14501563 && let field_name = consts
@@ -1562,6 +1675,7 @@ fn walk_calls(
15621675 child,
15631676 source,
15641677 consts,
1678+ language,
15651679 current. clone ( ) ,
15661680 child_class. clone ( ) ,
15671681 out,
@@ -1599,7 +1713,15 @@ pub fn extract_calls_from_tree(
15991713 return Vec :: new ( ) ;
16001714 } ;
16011715 let mut out = Vec :: new ( ) ;
1602- walk_calls ( tree. root_node ( ) , source, & consts, None , None , & mut out) ;
1716+ walk_calls (
1717+ tree. root_node ( ) ,
1718+ source,
1719+ & consts,
1720+ language,
1721+ None ,
1722+ None ,
1723+ & mut out,
1724+ ) ;
16031725 out
16041726}
16051727
@@ -4448,16 +4570,16 @@ class Foo {
44484570
44494571 #[ test]
44504572 #[ cfg( feature = "lang-dart" ) ]
4451- fn test_dart_real_grammar_symbols_are_accurate ( ) {
4573+ fn test_dart_real_grammar_symbols_and_calls_are_accurate ( ) {
44524574 // tree-sitter-dart 0.0.4 (an older, hand-written grammar — see
44534575 // lang_constants.rs's Dart entry) has no dedicated call-expression
4454- // node kind at all (calls are a generic member_access/selector/
4455- // argument_part postfix chain shared with plain field access) —
4456- // call-graph extraction is a documented scope cut, not an oversight.
4457- // This test locks that gap in place (asserts calls stay empty) so a
4458- // future accidental `call_node_types` entry that doesn't actually
4459- // work correctly gets caught instead of silently shipping partial
4460- // call edges .
4576+ // node kind at all — a call and a call-less field access share the
4577+ // same `member_access`/`selector`/` argument_part` postfix- chain
4578+ // shape. Call extraction (C3, Tier B audit) is handled by a
4579+ // dedicated `dart_call_from_member_access` branch in `walk_calls`,
4580+ // not the generic `call_node_types` mechanism — see that function's
4581+ // doc comment for why, and its own scratch-verified real-grammar
4582+ // shapes .
44614583 let code = "class Greeter {\n String name;\n \n Greeter(this.name);\n \n String greet() {\n print(\" hello\" );\n return this.formatGreeting(name);\n }\n \n String formatGreeting(String n) {\n return \" Hello, \" + n;\n }\n }\n \n abstract class Named {\n String getName();\n }\n \n void main() {\n var g = Greeter(\" world\" );\n g.greet();\n }\n " ;
44624584 let symbols = extract_symbols ( code, "dart" , "a.dart" ) . unwrap ( ) ;
44634585 let names = shallow_names ( & symbols) ;
@@ -4503,12 +4625,49 @@ class Foo {
45034625 ) ;
45044626
45054627 let calls = extract_calls ( code, "dart" , "a.dart" ) . unwrap ( ) ;
4506- assert ! (
4507- calls. is_empty( ) ,
4508- "Dart call-graph extraction is a documented scope cut (no clean \
4509- call-expression node in this grammar) — see this test's doc comment; \
4510- calls: {calls:?}"
4628+ assert_eq ! (
4629+ calls. len( ) ,
4630+ 4 ,
4631+ "expected print(\" hello\" ), this.formatGreeting(name), Greeter(\" world\" ), \
4632+ g.greet() — the constructor DECLARATION `Greeter(this.name);` must NOT \
4633+ itself count as a 5th call; calls: {calls:?}"
4634+ ) ;
4635+
4636+ let print_call = calls
4637+ . iter ( )
4638+ . find ( |c| c. callee == "print" )
4639+ . expect ( "bare print(\" hello\" ) call should be found" ) ;
4640+ assert_eq ! ( print_call. enclosing_name, "greet" ) ;
4641+ assert_eq ! ( print_call. receiver, None ) ;
4642+ assert_eq ! ( print_call. arg_count, Some ( 1 ) ) ;
4643+
4644+ let format_greeting_call = calls
4645+ . iter ( )
4646+ . find ( |c| c. callee == "formatGreeting" )
4647+ . expect ( "this.formatGreeting(name) call should be found" ) ;
4648+ assert_eq ! ( format_greeting_call. enclosing_name, "greet" ) ;
4649+ assert_eq ! (
4650+ format_greeting_call. receiver. as_deref( ) ,
4651+ Some ( "this" ) ,
4652+ "receiver should be \" this\" "
45114653 ) ;
4654+ assert_eq ! ( format_greeting_call. arg_count, Some ( 1 ) ) ;
4655+
4656+ let greeter_call = calls
4657+ . iter ( )
4658+ . find ( |c| c. callee == "Greeter" )
4659+ . expect ( "Greeter(\" world\" ) constructor call should be found" ) ;
4660+ assert_eq ! ( greeter_call. enclosing_name, "main" ) ;
4661+ assert_eq ! ( greeter_call. receiver, None ) ;
4662+ assert_eq ! ( greeter_call. arg_count, Some ( 1 ) ) ;
4663+
4664+ let greet_call = calls
4665+ . iter ( )
4666+ . find ( |c| c. callee == "greet" )
4667+ . expect ( "g.greet() call should be found" ) ;
4668+ assert_eq ! ( greet_call. enclosing_name, "main" ) ;
4669+ assert_eq ! ( greet_call. receiver. as_deref( ) , Some ( "g" ) ) ;
4670+ assert_eq ! ( greet_call. arg_count, Some ( 0 ) ) ;
45124671 }
45134672
45144673 #[ test]
0 commit comments