@@ -140,14 +140,22 @@ fn normalize_field(s: &str) -> String {
140140 result
141141}
142142
143+ /// An auto-lift rule tagged with its source paradigm's languages.
144+ /// Rules with empty `languages` (core rules) match any file.
145+ struct TaggedRule {
146+ rule : AutoLiftRule ,
147+ languages : Vec < String > ,
148+ }
149+
143150/// Engine that matches entities against TOML-defined auto-lift rules.
144151///
145152/// Rules are collected from paradigm definitions in priority order.
146153/// `core.toml` (priority 100) is always included; framework-specific rules
147154/// from detected paradigms are prepended (lower priority number = higher priority
148- /// = checked first).
155+ /// = checked first). Language-specific rules only match entities whose file
156+ /// extension belongs to the rule's source language.
149157pub struct AutoLiftEngine {
150- rules : Vec < AutoLiftRule > ,
158+ rules : Vec < TaggedRule > ,
151159}
152160
153161impl AutoLiftEngine {
@@ -163,17 +171,38 @@ impl AutoLiftEngine {
163171 let is_core = def. languages . is_empty ( ) ;
164172 let is_active = active_paradigm_names. iter ( ) . any ( |n| n == & def. name ) ;
165173 if is_core || is_active {
166- rules. extend ( def. auto_lift . iter ( ) . cloned ( ) ) ;
174+ for auto_rule in & def. auto_lift {
175+ rules. push ( TaggedRule {
176+ rule : auto_rule. clone ( ) ,
177+ languages : def. languages . clone ( ) ,
178+ } ) ;
179+ }
167180 }
168181 }
169182 Self { rules }
170183 }
171184
172185 /// Try to match an entity against auto-lift rules. First match wins.
186+ /// Language-specific rules are skipped when the entity's file extension
187+ /// doesn't belong to the rule's source language.
173188 pub fn try_lift ( & self , raw : & RawEntity ) -> Option < Vec < String > > {
174- for rule in & self . rules {
175- if matches_entity ( & rule. match_rule , raw, & raw . file) {
176- return Some ( Self :: expand_templates ( rule, raw) ) ;
189+ let file_lang = raw
190+ . file
191+ . extension ( )
192+ . and_then ( |e| e. to_str ( ) )
193+ . and_then ( Language :: from_extension) ;
194+
195+ for tagged in & self . rules {
196+ // Skip language-specific rules that don't match the entity's file language.
197+ // When the file language is unknown, language-specific rules are skipped
198+ // (only core rules with empty languages apply).
199+ if !tagged. languages . is_empty ( )
200+ && !file_lang. is_some_and ( |lang| tagged. languages . iter ( ) . any ( |l| l == lang. name ( ) ) )
201+ {
202+ continue ;
203+ }
204+ if matches_entity ( & tagged. rule . match_rule , raw, & raw . file) {
205+ return Some ( Self :: expand_templates ( & tagged. rule , raw) ) ;
177206 }
178207 }
179208 None
@@ -374,10 +403,14 @@ mod tests {
374403 use rpg_core:: graph:: EntityKind ;
375404
376405 fn make_raw ( name : & str , parent : Option < & str > , source : & str ) -> RawEntity {
406+ make_raw_file ( name, parent, source, "src/lib.rs" )
407+ }
408+
409+ fn make_raw_file ( name : & str , parent : Option < & str > , source : & str , file : & str ) -> RawEntity {
377410 RawEntity {
378411 name : name. to_string ( ) ,
379412 kind : EntityKind :: Method ,
380- file : std:: path:: PathBuf :: from ( "src/lib.rs" ) ,
413+ file : std:: path:: PathBuf :: from ( file ) ,
381414 line_start : 1 ,
382415 line_end : source. lines ( ) . count ( ) ,
383416 parent_class : parent. map ( |s| s. to_string ( ) ) ,
@@ -775,4 +808,72 @@ mod tests {
775808 vec![ "return string representation of user" ]
776809 ) ;
777810 }
811+
812+ // --- Language scoping tests ---
813+
814+ fn make_mixed_engine ( ) -> AutoLiftEngine {
815+ let defs = rpg_parser:: paradigms:: defs:: load_builtin_defs ( ) . unwrap_or_default ( ) ;
816+ // Activate both C and Go paradigms (simulates a mixed-language repo)
817+ AutoLiftEngine :: new ( & defs, & [ "c" . to_string ( ) , "go" . to_string ( ) ] )
818+ }
819+
820+ #[ test]
821+ fn test_engine_language_scoping_go_init ( ) {
822+ // Regression: Go `init()` in a .go file must match go.init ("initialize package"),
823+ // NOT c.init ("initialize init") — even though C paradigm is also active.
824+ let engine = make_mixed_engine ( ) ;
825+ let raw = make_raw_file ( "init" , None , "func init() { setup() }" , "cmd/main.go" ) ;
826+ let features = engine. try_lift ( & raw ) . unwrap ( ) ;
827+ assert_eq ! (
828+ features,
829+ vec![ "initialize package" ] ,
830+ "Go init() should match go.init, not c.init"
831+ ) ;
832+ }
833+
834+ #[ test]
835+ fn test_engine_language_scoping_c_init ( ) {
836+ // C `init` in a .c file should match c.init
837+ let engine = make_mixed_engine ( ) ;
838+ let raw = make_raw_file ( "init" , None , "void init() { setup(); }" , "src/module.c" ) ;
839+ let features = engine. try_lift ( & raw ) . unwrap ( ) ;
840+ assert_eq ! (
841+ features,
842+ vec![ "initialize init" ] ,
843+ "C init() should match c.init, not go.init"
844+ ) ;
845+ }
846+
847+ #[ test]
848+ fn test_engine_core_rules_match_any_language ( ) {
849+ // Core rules (languages = []) should match regardless of file extension
850+ let engine = make_mixed_engine ( ) ;
851+ let raw = make_raw_file (
852+ "getName" ,
853+ None ,
854+ "String getName() { return this.name; }" ,
855+ "src/User.java" ,
856+ ) ;
857+ let features = engine. try_lift ( & raw ) ;
858+ assert ! (
859+ features. is_some( ) ,
860+ "core camelCase getter should match .java files"
861+ ) ;
862+ assert_eq ! ( features. unwrap( ) , vec![ "return name" ] ) ;
863+ }
864+
865+ #[ test]
866+ fn test_engine_unknown_extension_skips_language_rules ( ) {
867+ // Language-specific rules should NOT match files with unknown extensions.
868+ // Only core rules (languages = []) should apply.
869+ let engine = make_mixed_engine ( ) ;
870+ let raw = make_raw_file ( "init" , None , "func init() { }" , "src/module.xyz" ) ;
871+ // go.init and c.init should both be skipped (unknown extension).
872+ // No core rule matches bare "init", so no auto-lift.
873+ let features = engine. try_lift ( & raw ) ;
874+ assert ! (
875+ features. is_none( ) ,
876+ "language-specific rules should not match unknown file extensions"
877+ ) ;
878+ }
778879}
0 commit comments