@@ -2672,6 +2672,288 @@ mod tests {
26722672 }
26732673 }
26742674
2675+ // Cross-file same-type member: a self-form call whose member is
2676+ // defined in another file of the SAME type (rust split impl
2677+ // blocks) resolves on the named-ClassMember match — caller and
2678+ // member both declare membership in Widget — behind exactly-one
2679+ // same-language discipline and the same-tree constraint. The
2680+ // Other.setup decoy is filtered by the named match. Production
2681+ // lanes only, and the indexed path is PRE-REGISTERED in settings:
2682+ // rust module identity is path-derived and the List lane has no
2683+ // walk root, so its strip base comes from the registered indexed
2684+ // paths — exactly the shape production incremental runs have
2685+ // (invariant: bare test contexts without registered paths
2686+ // degenerate to module None and the arm correctly fails closed).
2687+ #[ test]
2688+ fn rust_split_impl_self_call_resolves_on_named_member ( ) {
2689+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
2690+ let src = dir. path ( ) . join ( "src" ) ;
2691+ std:: fs:: create_dir_all ( & src) . unwrap ( ) ;
2692+ std:: fs:: write (
2693+ src. join ( "widget.rs" ) ,
2694+ "pub struct Widget {}\n \n impl Widget {\n pub fn setup(&self) {\n }\n }\n " ,
2695+ )
2696+ . unwrap ( ) ;
2697+ let consumer = "use crate::widget::Widget;\n \n impl Widget {\n pub fn make(&self) {\n self.setup();\n }\n }\n " ;
2698+ std:: fs:: write ( src. join ( "consumer.rs" ) , consumer) . unwrap ( ) ;
2699+ std:: fs:: write (
2700+ src. join ( "other.rs" ) ,
2701+ "pub struct Other {}\n \n impl Other {\n pub fn setup(&self) {\n }\n }\n " ,
2702+ )
2703+ . unwrap ( ) ;
2704+
2705+ let mut settings = Settings {
2706+ index_path : dir. path ( ) . join ( "index" ) ,
2707+ workspace_root : None ,
2708+ ..Default :: default ( )
2709+ } ;
2710+ settings
2711+ . add_indexed_path ( src. clone ( ) )
2712+ . expect ( "register indexed path" ) ;
2713+ let mut facade = IndexFacade :: new ( std:: sync:: Arc :: new ( settings) ) . unwrap ( ) ;
2714+
2715+ let assert_resolves = |facade : & IndexFacade , leg : & str | {
2716+ let makes = facade. find_symbols_by_name ( "make" , None ) ;
2717+ assert_eq ! ( makes. len( ) , 1 , "one make symbol expected ({leg})" ) ;
2718+ let callees = facade. get_called_functions ( makes[ 0 ] . id ) ;
2719+ let picked: Vec < String > = callees
2720+ . iter ( )
2721+ . map ( |s| {
2722+ format ! (
2723+ "{}@{}" ,
2724+ s. name,
2725+ facade. get_file_path( s. file_id) . unwrap_or_default( )
2726+ )
2727+ } )
2728+ . collect ( ) ;
2729+ assert ! (
2730+ callees. iter( ) . any( |s| s. name. as_ref( ) == "setup"
2731+ && facade
2732+ . get_file_path( s. file_id)
2733+ . unwrap_or_default( )
2734+ . ends_with( "widget.rs" ) ) ,
2735+ "split-impl self call must resolve to the same type's \
2736+ member on the named-ClassMember witness ({leg}), \
2737+ got: {picked:?}"
2738+ ) ;
2739+ } ;
2740+
2741+ facade. index_directory ( & src, true ) . unwrap ( ) ;
2742+ assert_resolves ( & facade, "fresh" ) ;
2743+
2744+ std:: fs:: write ( src. join ( "consumer.rs" ) , format ! ( "{consumer}\n // touched\n " ) ) . unwrap ( ) ;
2745+ std:: fs:: File :: options ( )
2746+ . write ( true )
2747+ . open ( src. join ( "consumer.rs" ) )
2748+ . unwrap ( )
2749+ . set_modified ( std:: time:: SystemTime :: now ( ) + std:: time:: Duration :: from_secs ( 5 ) )
2750+ . unwrap ( ) ;
2751+ facade. index_directory ( & src, false ) . unwrap ( ) ;
2752+ assert_resolves ( & facade, "seeded incremental" ) ;
2753+ }
2754+
2755+ // Same-file name claimant vetoes the cross-file borrow: when the
2756+ // caller's own file holds ANY member named like the call (under
2757+ // whatever class), the tree-wide named match must not borrow a
2758+ // same-named-class copy from another file. Witnessed leak:
2759+ // three.js minified twin bundles — independent minification
2760+ // scrambles class names, so the caller's class name matches the
2761+ // TWIN bundle's copy while its own bundle's copy sits same-file
2762+ // under a different name. The row still resolves through the
2763+ // local tier to the same-file claimant.
2764+ #[ test]
2765+ fn same_file_claimant_vetoes_cross_file_named_borrow ( ) {
2766+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
2767+ let src = dir. path ( ) . join ( "src" ) ;
2768+ // Shared parent dir: both modules root at `lib`, so the (b)
2769+ // same-tree constraint admits the twin — the veto is the only
2770+ // discipline left between the caller and the wrong copy.
2771+ let lib = src. join ( "lib" ) ;
2772+ std:: fs:: create_dir_all ( & lib) . unwrap ( ) ;
2773+ std:: fs:: write (
2774+ lib. join ( "appA.js" ) ,
2775+ "class Painter {\n parse() {\n this.createNodeFromType();\n }\n }\n \n class Registry {\n createNodeFromType() {\n }\n }\n " ,
2776+ )
2777+ . unwrap ( ) ;
2778+ std:: fs:: write (
2779+ lib. join ( "appB.js" ) ,
2780+ "class Painter {\n createNodeFromType() {\n }\n }\n " ,
2781+ )
2782+ . unwrap ( ) ;
2783+
2784+ let mut settings = Settings {
2785+ index_path : dir. path ( ) . join ( "index" ) ,
2786+ workspace_root : None ,
2787+ ..Default :: default ( )
2788+ } ;
2789+ settings
2790+ . add_indexed_path ( src. clone ( ) )
2791+ . expect ( "register indexed path" ) ;
2792+ let mut facade = IndexFacade :: new ( std:: sync:: Arc :: new ( settings) ) . unwrap ( ) ;
2793+ facade. index_directory ( & src, true ) . unwrap ( ) ;
2794+
2795+ let parses = facade. find_symbols_by_name ( "parse" , None ) ;
2796+ assert_eq ! ( parses. len( ) , 1 , "one parse symbol expected" ) ;
2797+ let callees = facade. get_called_functions ( parses[ 0 ] . id ) ;
2798+ let picked: Vec < String > = callees
2799+ . iter ( )
2800+ . map ( |s| {
2801+ format ! (
2802+ "{}@{}" ,
2803+ s. name,
2804+ facade. get_file_path( s. file_id) . unwrap_or_default( )
2805+ )
2806+ } )
2807+ . collect ( ) ;
2808+ assert ! (
2809+ !callees
2810+ . iter( )
2811+ . any( |s| s. name. as_ref( ) == "createNodeFromType"
2812+ && facade
2813+ . get_file_path( s. file_id)
2814+ . unwrap_or_default( )
2815+ . ends_with( "appB.js" ) ) ,
2816+ "cross-file borrow past a same-file claimant is a wrong-copy \
2817+ pick, got: {picked:?}"
2818+ ) ;
2819+ }
2820+
2821+ // Language gate on the split-type premise: php declares one class
2822+ // per file, so a same-named class in another file is a DIFFERENT
2823+ // class — the named match must not borrow its member (witnessed:
2824+ // laravel Schema\Grammars\SqlServerGrammar callers borrowing
2825+ // Query\Grammars\SqlServerGrammar's wrapTable — namespace twins,
2826+ // no inheritance relation). The arm runs only where the language
2827+ // has split-type syntax (rust impl blocks, cpp out-of-line,
2828+ // csharp partial).
2829+ #[ test]
2830+ fn php_namespace_twin_class_member_stays_closed ( ) {
2831+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
2832+ let src = dir. path ( ) . join ( "src" ) ;
2833+ let a = src. join ( "schema" ) ;
2834+ let b = src. join ( "query" ) ;
2835+ std:: fs:: create_dir_all ( & a) . unwrap ( ) ;
2836+ std:: fs:: create_dir_all ( & b) . unwrap ( ) ;
2837+ std:: fs:: write (
2838+ a. join ( "Widget.php" ) ,
2839+ "<?php\n namespace App\\ Schema;\n \n class Widget {\n public function make() {\n $this->setup(1);\n }\n }\n " ,
2840+ )
2841+ . unwrap ( ) ;
2842+ std:: fs:: write (
2843+ b. join ( "Widget.php" ) ,
2844+ "<?php\n namespace App\\ Query;\n \n class Widget {\n public function setup($x) {\n }\n }\n " ,
2845+ )
2846+ . unwrap ( ) ;
2847+
2848+ let mut settings = Settings {
2849+ index_path : dir. path ( ) . join ( "index" ) ,
2850+ workspace_root : None ,
2851+ ..Default :: default ( )
2852+ } ;
2853+ settings
2854+ . add_indexed_path ( src. clone ( ) )
2855+ . expect ( "register indexed path" ) ;
2856+ let mut facade = IndexFacade :: new ( std:: sync:: Arc :: new ( settings) ) . unwrap ( ) ;
2857+ facade. index_directory ( & src, true ) . unwrap ( ) ;
2858+
2859+ let makes = facade. find_symbols_by_name ( "make" , None ) ;
2860+ assert_eq ! ( makes. len( ) , 1 , "one make symbol expected" ) ;
2861+ let callees = facade. get_called_functions ( makes[ 0 ] . id ) ;
2862+ assert ! (
2863+ !callees. iter( ) . any( |s| s. name. as_ref( ) == "setup" ) ,
2864+ "a namespace twin's member is another class's member, got: {callees:?}"
2865+ ) ;
2866+ }
2867+
2868+ // Duplicate type copies: two same-named types in one tree, both
2869+ // declaring the member — the named match cannot pick a copy, so
2870+ // exactly-one discipline fails closed.
2871+ #[ test]
2872+ fn rust_split_impl_duplicate_type_copies_fail_closed ( ) {
2873+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
2874+ let src = dir. path ( ) . join ( "src" ) ;
2875+ std:: fs:: create_dir_all ( & src) . unwrap ( ) ;
2876+ std:: fs:: write (
2877+ src. join ( "widget.rs" ) ,
2878+ "pub struct Widget {}\n \n impl Widget {\n pub fn setup(&self) {\n }\n }\n " ,
2879+ )
2880+ . unwrap ( ) ;
2881+ std:: fs:: write (
2882+ src. join ( "twin.rs" ) ,
2883+ "pub struct Widget {}\n \n impl Widget {\n pub fn setup(&self) {\n }\n }\n " ,
2884+ )
2885+ . unwrap ( ) ;
2886+ std:: fs:: write (
2887+ src. join ( "consumer.rs" ) ,
2888+ "use crate::widget::Widget;\n \n impl Widget {\n pub fn make(&self) {\n self.setup();\n }\n }\n " ,
2889+ )
2890+ . unwrap ( ) ;
2891+
2892+ let mut settings = Settings {
2893+ index_path : dir. path ( ) . join ( "index" ) ,
2894+ workspace_root : None ,
2895+ ..Default :: default ( )
2896+ } ;
2897+ settings
2898+ . add_indexed_path ( src. clone ( ) )
2899+ . expect ( "register indexed path" ) ;
2900+ let mut facade = IndexFacade :: new ( std:: sync:: Arc :: new ( settings) ) . unwrap ( ) ;
2901+ facade. index_directory ( & src, true ) . unwrap ( ) ;
2902+
2903+ let makes = facade. find_symbols_by_name ( "make" , None ) ;
2904+ assert_eq ! ( makes. len( ) , 1 , "one make symbol expected" ) ;
2905+ let callees = facade. get_called_functions ( makes[ 0 ] . id ) ;
2906+ assert ! (
2907+ !callees. iter( ) . any( |s| s. name. as_ref( ) == "setup" ) ,
2908+ "two named claimants cannot license a copy pick, got: {callees:?}"
2909+ ) ;
2910+ }
2911+
2912+ // Cross-tree block, the (b) discipline: a single global claimant
2913+ // in ANOTHER tree (different module root) is not a candidate — the
2914+ // caller's own same-named class lacking the member must not borrow
2915+ // it across trees.
2916+ #[ test]
2917+ fn python_cross_tree_single_claimant_stays_closed ( ) {
2918+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
2919+ let src = dir. path ( ) . join ( "src" ) ;
2920+ let pkg_a = src. join ( "pkg_a" ) ;
2921+ let pkg_b = src. join ( "pkg_b" ) ;
2922+ std:: fs:: create_dir_all ( & pkg_a) . unwrap ( ) ;
2923+ std:: fs:: create_dir_all ( & pkg_b) . unwrap ( ) ;
2924+ std:: fs:: write ( pkg_a. join ( "__init__.py" ) , "" ) . unwrap ( ) ;
2925+ std:: fs:: write ( pkg_b. join ( "__init__.py" ) , "" ) . unwrap ( ) ;
2926+ std:: fs:: write (
2927+ pkg_a. join ( "widget.py" ) ,
2928+ "class Widget:\n def setup(self):\n pass\n " ,
2929+ )
2930+ . unwrap ( ) ;
2931+ std:: fs:: write (
2932+ pkg_b. join ( "consumer.py" ) ,
2933+ "class Widget:\n def make(self):\n self.setup()\n " ,
2934+ )
2935+ . unwrap ( ) ;
2936+
2937+ let mut settings = Settings {
2938+ index_path : dir. path ( ) . join ( "index" ) ,
2939+ workspace_root : None ,
2940+ ..Default :: default ( )
2941+ } ;
2942+ settings
2943+ . add_indexed_path ( src. clone ( ) )
2944+ . expect ( "register indexed path" ) ;
2945+ let mut facade = IndexFacade :: new ( std:: sync:: Arc :: new ( settings) ) . unwrap ( ) ;
2946+ facade. index_directory ( & src, true ) . unwrap ( ) ;
2947+
2948+ let makes = facade. find_symbols_by_name ( "make" , None ) ;
2949+ assert_eq ! ( makes. len( ) , 1 , "one make symbol expected" ) ;
2950+ let callees = facade. get_called_functions ( makes[ 0 ] . id ) ;
2951+ assert ! (
2952+ !callees. iter( ) . any( |s| s. name. as_ref( ) == "setup" ) ,
2953+ "a cross-tree claimant must not be borrowed, got: {callees:?}"
2954+ ) ;
2955+ }
2956+
26752957 // Own-scope exemption: a bare call to the caller's own non-public
26762958 // member keeps its same-file evidence — the gate fires only on
26772959 // cross-file picks. The cross-file public decoy guards that the
0 commit comments