@@ -73,7 +73,7 @@ fn repository_dogfood_enables_every_preview_rule_without_changing_defaults() {
7373 let dogfood = Config :: parse_toml ( & std:: fs:: read_to_string ( path) . unwrap ( ) ) . unwrap ( ) ;
7474 let registry = crate :: detectors:: manifest:: rule_registry ( ) ;
7575
76- assert_eq ! ( registry. len( ) , 33 ) ;
76+ assert_eq ! ( registry. len( ) , 34 ) ;
7777 assert_eq ! ( dogfood. rules. enabled. len( ) , registry. len( ) ) ;
7878 assert ! (
7979 registry. iter( ) . all( |rule| {
@@ -86,6 +86,108 @@ fn repository_dogfood_enables_every_preview_rule_without_changing_defaults() {
8686 assert ! ( Config :: defaults( ) . rules. enabled. is_empty( ) ) ;
8787}
8888
89+ #[ test]
90+ fn low_module_cohesion_thresholds_follow_presets_and_overrides ( ) {
91+ for ( preset, functions, percent) in [
92+ ( "strict" , 16 , 40 ) ,
93+ ( "balanced" , 20 , 50 ) ,
94+ ( "relaxed" , 30 , 60 ) ,
95+ ] {
96+ let config =
97+ Config :: parse_toml ( & format ! ( "version = 2\n [codebase]\n preset = \" {preset}\" \n " ) )
98+ . unwrap ( ) ;
99+ let resolved = crate :: scan:: config:: effective_scan_config_with (
100+ & crate :: execution:: EffectiveConfig :: default ( ) ,
101+ Some ( & config. engine ) ,
102+ )
103+ . unwrap ( ) ;
104+ assert_eq ! ( resolved. args. min_module_functions, functions) ;
105+ assert_eq ! ( resolved. args. min_clustered_function_percent, percent) ;
106+ }
107+
108+ let config = Config :: parse_toml (
109+ "version = 2\n [codebase]\n preset = \" strict\" \n min-module-functions = 18\n min-clustered-function-percent = 75\n " ,
110+ )
111+ . unwrap ( ) ;
112+ let resolved = crate :: scan:: config:: effective_scan_config_with (
113+ & crate :: execution:: EffectiveConfig :: default ( ) ,
114+ Some ( & config. engine ) ,
115+ )
116+ . unwrap ( ) ;
117+ assert_eq ! ( resolved. args. min_module_functions, 18 ) ;
118+ assert_eq ! ( resolved. args. min_clustered_function_percent, 75 ) ;
119+ }
120+
121+ #[ test]
122+ fn low_module_cohesion_config_rejects_invalid_boundaries ( ) {
123+ for ( setting, expected) in [
124+ ( "min-module-functions = 0" , "positive integer" ) ,
125+ ( "min-clustered-function-percent = -1" , "between 0 and 100" ) ,
126+ ( "min-clustered-function-percent = 101" , "between 0 and 100" ) ,
127+ ] {
128+ let error = Config :: parse_toml ( & format ! ( "version = 2\n [codebase]\n {setting}\n " ) )
129+ . unwrap_err ( )
130+ . to_string ( ) ;
131+ assert ! ( error. contains( expected) , "{error}" ) ;
132+ }
133+ Config :: parse_toml (
134+ "version = 2\n [codebase]\n min-module-functions = 1\n min-clustered-function-percent = 0\n " ,
135+ )
136+ . unwrap ( ) ;
137+ }
138+
139+ #[ test]
140+ fn low_module_cohesion_report_is_schema_stable_and_python_is_not_applicable ( ) {
141+ let root = std:: env:: temp_dir ( ) . join ( format ! (
142+ "reforge-low-module-cohesion-{}" ,
143+ std:: process:: id( )
144+ ) ) ;
145+ let _ = std:: fs:: remove_dir_all ( & root) ;
146+ std:: fs:: create_dir_all ( & root) . unwrap ( ) ;
147+ std:: fs:: write (
148+ root. join ( "monolith.js" ) ,
149+ "function renderPage(){renderHeader();renderBody();}\n function renderHeader(){}\n function renderBody(){}\n function diffPage(){diffHeader();diffBody();}\n function diffHeader(){}\n function diffBody(){}\n " ,
150+ )
151+ . unwrap ( ) ;
152+ let config = Config :: parse_toml (
153+ "version = 2\n [rules]\n enable = [\" reforge.codebase.low_module_cohesion\" ]\n [codebase]\n min-module-functions = 6\n min-clustered-function-percent = 100\n churn = \" off\" \n " ,
154+ )
155+ . unwrap ( ) ;
156+ let analyze_once = || {
157+ analyze ( & AnalyzeOptions {
158+ root : root. clone ( ) ,
159+ config : config. clone ( ) ,
160+ reproducible : true ,
161+ metrics_output : None ,
162+ flow_ir_output : None ,
163+ } )
164+ . unwrap ( )
165+ } ;
166+ let first = analyze_once ( ) ;
167+ let second = analyze_once ( ) ;
168+ first. validate ( ) . unwrap ( ) ;
169+ let serialized = serde_json:: to_vec ( & first) . unwrap ( ) ;
170+ let round_trip: Report = serde_json:: from_slice ( & serialized) . unwrap ( ) ;
171+ round_trip. validate ( ) . unwrap ( ) ;
172+ assert_eq ! ( serialized, serde_json:: to_vec( & second) . unwrap( ) ) ;
173+ assert_eq ! ( first. issues. len( ) , 1 ) ;
174+ assert_eq ! ( first. issues[ 0 ] . id, round_trip. issues[ 0 ] . id) ;
175+ assert_eq ! (
176+ first. issues[ 0 ] . evidence[ 0 ] . id,
177+ round_trip. issues[ 0 ] . evidence[ 0 ] . id
178+ ) ;
179+
180+ std:: fs:: remove_file ( root. join ( "monolith.js" ) ) . unwrap ( ) ;
181+ std:: fs:: write ( root. join ( "monolith.py" ) , "def render_page():\n pass\n " ) . unwrap ( ) ;
182+ let python = analyze_once ( ) ;
183+ assert ! ( python. issues. is_empty( ) ) ;
184+ assert_eq ! (
185+ python. coverage[ "codebase" ] . rules[ "reforge.codebase.low_module_cohesion" ] . status,
186+ CoverageStatus :: NotApplicable
187+ ) ;
188+ std:: fs:: remove_dir_all ( root) . unwrap ( ) ;
189+ }
190+
89191fn options ( root : & Path , config : & Path , enabled : BTreeSet < Analysis > ) -> AnalyzeOptions {
90192 let mut config = Config :: parse_toml ( & std:: fs:: read_to_string ( config) . unwrap ( ) ) . unwrap ( ) ;
91193 config. set_enabled ( enabled) . unwrap ( ) ;
0 commit comments