@@ -1115,3 +1115,151 @@ mod csharp_signature {
11151115 ) ;
11161116 }
11171117}
1118+
1119+ // ─── Structural diff tests (AstDiffer through extract_symbols pipeline) ──────
1120+
1121+ #[ cfg( any(
1122+ feature = "lang-rust" ,
1123+ feature = "lang-typescript" ,
1124+ feature = "lang-python"
1125+ ) ) ]
1126+ use commitbee:: domain:: diff:: { ChangeDetail , SymbolDiff } ;
1127+
1128+ /// Extract symbols AND diffs by providing both old (HEAD) and new (staged) source.
1129+ /// Creates a synthetic Modified `FileChange` with a hunk covering all lines.
1130+ #[ cfg( any(
1131+ feature = "lang-rust" ,
1132+ feature = "lang-typescript" ,
1133+ feature = "lang-python"
1134+ ) ) ]
1135+ fn extract_diffs_from_sources ( old_source : & str , new_source : & str , ext : & str ) -> Vec < SymbolDiff > {
1136+ use commitbee:: domain:: ChangeStatus ;
1137+
1138+ let path = PathBuf :: from ( format ! ( "test.{ext}" ) ) ;
1139+ let new_line_count = new_source. lines ( ) . count ( ) ;
1140+ let old_line_count = old_source. lines ( ) . count ( ) ;
1141+ let diff_lines: String = new_source. lines ( ) . map ( |l| format ! ( "+{l}\n " ) ) . collect ( ) ;
1142+ let change = FileChange {
1143+ path : path. clone ( ) ,
1144+ status : ChangeStatus :: Modified ,
1145+ diff : Arc :: from ( format ! (
1146+ "@@ -1,{old_line_count} +1,{new_line_count} @@\n {diff_lines}"
1147+ ) ) ,
1148+ additions : new_line_count,
1149+ deletions : old_line_count,
1150+ category : FileCategory :: Source ,
1151+ is_binary : false ,
1152+ old_path : None ,
1153+ rename_similarity : None ,
1154+ } ;
1155+
1156+ let staged_map = HashMap :: from ( [ ( path. clone ( ) , new_source. to_string ( ) ) ] ) ;
1157+ let head_map = HashMap :: from ( [ ( path, old_source. to_string ( ) ) ] ) ;
1158+ let analyzer = AnalyzerService :: new ( ) . expect ( "AnalyzerService::new() should succeed" ) ;
1159+ let ( _, diffs) = analyzer. extract_symbols ( & [ change] , & staged_map, & head_map) ;
1160+ diffs
1161+ }
1162+
1163+ #[ cfg( feature = "lang-rust" ) ]
1164+ mod rust_structural_diffs {
1165+ use super :: * ;
1166+
1167+ #[ test]
1168+ fn rust_detect_added_parameter ( ) {
1169+ let old = "pub fn process(items: Vec<Item>) -> bool {\n true\n }\n " ;
1170+ let new = "pub fn process(items: Vec<Item>, strict: bool) -> bool {\n true\n }\n " ;
1171+ let diffs = extract_diffs_from_sources ( old, new, "rs" ) ;
1172+ assert ! ( !diffs. is_empty( ) , "should produce at least one diff" ) ;
1173+ let d = & diffs[ 0 ] ;
1174+ assert_eq ! ( d. name, "process" ) ;
1175+ assert ! (
1176+ d. changes
1177+ . iter( )
1178+ . any( |c| matches!( c, ChangeDetail :: ParamAdded ( p) if p. contains( "strict" ) ) ) ,
1179+ "should detect added param: {:?}" ,
1180+ d. changes
1181+ ) ;
1182+ }
1183+
1184+ #[ test]
1185+ fn rust_detect_return_type_change ( ) {
1186+ let old = "fn validate(input: &str) -> bool {\n true\n }\n " ;
1187+ let new = "fn validate(input: &str) -> Result<()> {\n Ok(())\n }\n " ;
1188+ let diffs = extract_diffs_from_sources ( old, new, "rs" ) ;
1189+ assert ! ( !diffs. is_empty( ) , "should produce diffs" ) ;
1190+ assert ! (
1191+ diffs[ 0 ]
1192+ . changes
1193+ . iter( )
1194+ . any( |c| matches!( c, ChangeDetail :: ReturnTypeChanged { .. } ) ) ,
1195+ "should detect return type change: {:?}" ,
1196+ diffs[ 0 ] . changes
1197+ ) ;
1198+ }
1199+
1200+ #[ test]
1201+ fn rust_detect_visibility_change ( ) {
1202+ let old = "fn internal() {\n // body\n }\n " ;
1203+ let new = "pub fn internal() {\n // body\n }\n " ;
1204+ let diffs = extract_diffs_from_sources ( old, new, "rs" ) ;
1205+ assert ! ( !diffs. is_empty( ) , "should produce diffs" ) ;
1206+ assert ! (
1207+ diffs[ 0 ]
1208+ . changes
1209+ . iter( )
1210+ . any( |c| matches!( c, ChangeDetail :: VisibilityChanged { .. } ) ) ,
1211+ "should detect visibility change: {:?}" ,
1212+ diffs[ 0 ] . changes
1213+ ) ;
1214+ }
1215+
1216+ #[ test]
1217+ fn rust_whitespace_only_body_is_unchanged ( ) {
1218+ let old = "fn foo() {\n let x = 1;\n }\n " ;
1219+ let new = "fn foo() {\n let x = 1;\n }\n " ;
1220+ let diffs = extract_diffs_from_sources ( old, new, "rs" ) ;
1221+ // Whitespace-only change should either produce no diff or BodyUnchanged
1222+ if !diffs. is_empty ( ) {
1223+ assert ! (
1224+ diffs[ 0 ]
1225+ . changes
1226+ . iter( )
1227+ . any( |c| matches!( c, ChangeDetail :: BodyUnchanged ) ) ,
1228+ "whitespace-only body should be BodyUnchanged: {:?}" ,
1229+ diffs[ 0 ] . changes
1230+ ) ;
1231+ }
1232+ }
1233+ }
1234+
1235+ #[ cfg( feature = "lang-python" ) ]
1236+ mod python_structural_diffs {
1237+ use super :: * ;
1238+
1239+ #[ test]
1240+ fn python_detect_added_parameter ( ) {
1241+ let old = "def process(items):\n return items\n " ;
1242+ let new = "def process(items, strict=False):\n return items\n " ;
1243+ let diffs = extract_diffs_from_sources ( old, new, "py" ) ;
1244+ // Python param extraction may or may not work depending on tree-sitter grammar
1245+ // Just verify no panic and check if diffs are produced
1246+ if !diffs. is_empty ( ) {
1247+ assert_eq ! ( diffs[ 0 ] . name, "process" ) ;
1248+ }
1249+ }
1250+ }
1251+
1252+ #[ cfg( feature = "lang-typescript" ) ]
1253+ mod typescript_structural_diffs {
1254+ use super :: * ;
1255+
1256+ #[ test]
1257+ fn typescript_detect_return_type_change ( ) {
1258+ let old = "function validate(input: string): boolean {\n return true;\n }\n " ;
1259+ let new = "function validate(input: string): Promise<boolean> {\n return true;\n }\n " ;
1260+ let diffs = extract_diffs_from_sources ( old, new, "ts" ) ;
1261+ if !diffs. is_empty ( ) {
1262+ assert_eq ! ( diffs[ 0 ] . name, "validate" ) ;
1263+ }
1264+ }
1265+ }
0 commit comments