@@ -140,6 +140,44 @@ fn unregister_watches(watcher: &mut RecommendedWatcher, watch_paths: &[(PathBuf,
140140 }
141141}
142142
143+ fn carry_forward_compile_warnings ( previous : & BuildCommandState , next : & mut BuildCommandState ) {
144+ for ( module_name, next_module) in next. build_state . modules . iter_mut ( ) {
145+ let Some ( previous_module) = previous. build_state . modules . get ( module_name) else {
146+ continue ;
147+ } ;
148+ if previous_module. package_name != next_module. package_name {
149+ continue ;
150+ }
151+
152+ match ( & previous_module. source_type , & mut next_module. source_type ) {
153+ ( SourceType :: SourceFile ( previous_source) , SourceType :: SourceFile ( next_source) ) => {
154+ if previous_source. implementation . path == next_source. implementation . path {
155+ next_source. implementation . compile_warnings =
156+ previous_source. implementation . compile_warnings . clone ( ) ;
157+
158+ if next_source. implementation . compile_warnings . is_some ( ) {
159+ next_source. implementation . compile_state =
160+ previous_source. implementation . compile_state . clone ( ) ;
161+ }
162+ }
163+
164+ if let ( Some ( previous_interface) , Some ( next_interface) ) =
165+ ( & previous_source. interface , & mut next_source. interface )
166+ && previous_interface. path == next_interface. path
167+ {
168+ next_interface. compile_warnings = previous_interface. compile_warnings . clone ( ) ;
169+
170+ if next_interface. compile_warnings . is_some ( ) {
171+ next_interface. compile_state = previous_interface. compile_state . clone ( ) ;
172+ }
173+ }
174+ }
175+ ( SourceType :: MlMap ( _) , SourceType :: MlMap ( _) ) => ( ) ,
176+ _ => ( ) ,
177+ }
178+ }
179+ }
180+
143181struct AsyncWatchArgs < ' a > {
144182 watcher : & ' a mut RecommendedWatcher ,
145183 current_watch_paths : Vec < ( PathBuf , RecursiveMode ) > ,
@@ -374,7 +412,7 @@ async fn async_watch(
374412 }
375413 CompileType :: Full => {
376414 let timing_total = Instant :: now ( ) ;
377- build_state = build:: initialize_build (
415+ let mut next_build_state = build:: initialize_build (
378416 None ,
379417 filter,
380418 show_progress,
@@ -384,6 +422,12 @@ async fn async_watch(
384422 )
385423 . expect ( "Could not initialize build" ) ;
386424
425+ // Full rebuilds can be triggered by editor atomic saves that surface as rename events.
426+ // Preserve warning state for unchanged modules so their warnings are re-emitted after the
427+ // fresh build state replaces the previous one.
428+ carry_forward_compile_warnings ( & build_state, & mut next_build_state) ;
429+ build_state = next_build_state;
430+
387431 // Re-register watches based on the new build state
388432 unregister_watches ( watcher, & current_watch_paths) ;
389433 current_watch_paths = compute_watch_paths ( & build_state, path) ;
@@ -475,3 +519,197 @@ pub fn start(
475519 . await
476520 } )
477521}
522+
523+ #[ cfg( test) ]
524+ mod tests {
525+ use super :: * ;
526+ use crate :: build:: build_types:: {
527+ CompileState , CompilerInfo , Implementation , Interface , Module , ParseState , SourceFile , SourceType ,
528+ } ;
529+ use crate :: build:: packages:: { Namespace , Package } ;
530+ use crate :: config;
531+ use crate :: project_context:: ProjectContext ;
532+ use ahash:: { AHashMap , AHashSet } ;
533+ use std:: path:: PathBuf ;
534+ use std:: sync:: RwLock ;
535+ use std:: time:: SystemTime ;
536+
537+ fn test_project_context ( root : & str ) -> ProjectContext {
538+ let root_path = PathBuf :: from ( root) ;
539+ let config = config:: tests:: create_config ( config:: tests:: CreateConfigArgs {
540+ name : "test-root" . to_string ( ) ,
541+ bs_deps : vec ! [ ] ,
542+ build_dev_deps : vec ! [ ] ,
543+ allowed_dependents : None ,
544+ path : root_path. clone ( ) ,
545+ } ) ;
546+
547+ ProjectContext {
548+ current_config : config,
549+ monorepo_context : None ,
550+ node_modules_exist_cache : RwLock :: new ( AHashMap :: new ( ) ) ,
551+ packages_cache : RwLock :: new ( AHashMap :: new ( ) ) ,
552+ }
553+ }
554+
555+ fn test_package ( name : & str , path : & str ) -> Package {
556+ let package_path = PathBuf :: from ( path) ;
557+ Package {
558+ name : name. to_string ( ) ,
559+ config : config:: tests:: create_config ( config:: tests:: CreateConfigArgs {
560+ name : name. to_string ( ) ,
561+ bs_deps : vec ! [ ] ,
562+ build_dev_deps : vec ! [ ] ,
563+ allowed_dependents : None ,
564+ path : package_path. clone ( ) ,
565+ } ) ,
566+ source_folders : AHashSet :: new ( ) ,
567+ source_files : None ,
568+ namespace : Namespace :: NoNamespace ,
569+ modules : None ,
570+ path : package_path,
571+ dirs : None ,
572+ is_local_dep : true ,
573+ is_root : true ,
574+ }
575+ }
576+
577+ fn test_build_state ( module_name : & str , module : Module ) -> BuildCommandState {
578+ let root = "/tmp/rewatch-warning-carry-forward" ;
579+ let package = test_package ( "test-package" , root) ;
580+ let mut packages = AHashMap :: new ( ) ;
581+ packages. insert ( package. name . clone ( ) , package) ;
582+
583+ let compiler = CompilerInfo {
584+ bsc_path : PathBuf :: from ( "/tmp/bsc" ) ,
585+ bsc_hash : blake3:: hash ( b"test-bsc" ) ,
586+ runtime_path : PathBuf :: from ( "/tmp/runtime" ) ,
587+ } ;
588+
589+ let mut build_state = BuildCommandState :: new (
590+ PathBuf :: from ( root) ,
591+ test_project_context ( root) ,
592+ packages,
593+ compiler,
594+ None ,
595+ ) ;
596+ build_state. insert_module ( module_name, module) ;
597+ build_state
598+ }
599+
600+ fn test_module (
601+ implementation_path : & str ,
602+ implementation_warning : Option < & str > ,
603+ interface_path : Option < & str > ,
604+ interface_warning : Option < & str > ,
605+ ) -> Module {
606+ let implementation_compile_state = if implementation_warning. is_some ( ) {
607+ CompileState :: Warning
608+ } else {
609+ CompileState :: Success
610+ } ;
611+ let interface_compile_state = if interface_warning. is_some ( ) {
612+ CompileState :: Warning
613+ } else {
614+ CompileState :: Success
615+ } ;
616+
617+ Module {
618+ source_type : SourceType :: SourceFile ( SourceFile {
619+ implementation : Implementation {
620+ path : PathBuf :: from ( implementation_path) ,
621+ parse_state : ParseState :: Success ,
622+ compile_state : implementation_compile_state,
623+ last_modified : SystemTime :: UNIX_EPOCH ,
624+ parse_dirty : false ,
625+ compile_warnings : implementation_warning. map ( str:: to_string) ,
626+ } ,
627+ interface : interface_path. map ( |interface_path| Interface {
628+ path : PathBuf :: from ( interface_path) ,
629+ parse_state : ParseState :: Success ,
630+ compile_state : interface_compile_state,
631+ last_modified : SystemTime :: UNIX_EPOCH ,
632+ parse_dirty : false ,
633+ compile_warnings : interface_warning. map ( str:: to_string) ,
634+ } ) ,
635+ } ) ,
636+ deps : AHashSet :: new ( ) ,
637+ dependents : AHashSet :: new ( ) ,
638+ package_name : "test-package" . to_string ( ) ,
639+ compile_dirty : false ,
640+ last_compiled_cmi : None ,
641+ last_compiled_cmt : None ,
642+ deps_dirty : false ,
643+ is_type_dev : false ,
644+ }
645+ }
646+
647+ #[ test]
648+ fn carries_forward_implementation_warnings_for_matching_module_paths ( ) {
649+ let previous = test_build_state (
650+ "ModuleA" ,
651+ test_module ( "src/ModuleA.res" , Some ( "warning: impl" ) , None , None ) ,
652+ ) ;
653+ let mut next = test_build_state ( "ModuleA" , test_module ( "src/ModuleA.res" , None , None , None ) ) ;
654+
655+ carry_forward_compile_warnings ( & previous, & mut next) ;
656+
657+ let module = next. get_module ( "ModuleA" ) . expect ( "module should exist" ) ;
658+ let SourceType :: SourceFile ( source_file) = & module. source_type else {
659+ panic ! ( "expected source file module" ) ;
660+ } ;
661+
662+ assert_eq ! (
663+ source_file. implementation. compile_warnings. as_deref( ) ,
664+ Some ( "warning: impl" )
665+ ) ;
666+ assert_eq ! ( source_file. implementation. compile_state, CompileState :: Warning ) ;
667+ }
668+
669+ #[ test]
670+ fn does_not_carry_forward_warnings_when_module_paths_change ( ) {
671+ let previous = test_build_state (
672+ "ModuleA" ,
673+ test_module ( "src/ModuleA.res" , Some ( "warning: impl" ) , None , None ) ,
674+ ) ;
675+ let mut next = test_build_state ( "ModuleA" , test_module ( "src/ModuleARenamed.res" , None , None , None ) ) ;
676+
677+ carry_forward_compile_warnings ( & previous, & mut next) ;
678+
679+ let module = next. get_module ( "ModuleA" ) . expect ( "module should exist" ) ;
680+ let SourceType :: SourceFile ( source_file) = & module. source_type else {
681+ panic ! ( "expected source file module" ) ;
682+ } ;
683+
684+ assert_eq ! ( source_file. implementation. compile_warnings, None ) ;
685+ assert_eq ! ( source_file. implementation. compile_state, CompileState :: Success ) ;
686+ }
687+
688+ #[ test]
689+ fn carries_forward_interface_warnings_for_matching_interface_paths ( ) {
690+ let previous = test_build_state (
691+ "ModuleA" ,
692+ test_module (
693+ "src/ModuleA.res" ,
694+ None ,
695+ Some ( "src/ModuleA.resi" ) ,
696+ Some ( "warning: interface" ) ,
697+ ) ,
698+ ) ;
699+ let mut next = test_build_state (
700+ "ModuleA" ,
701+ test_module ( "src/ModuleA.res" , None , Some ( "src/ModuleA.resi" ) , None ) ,
702+ ) ;
703+
704+ carry_forward_compile_warnings ( & previous, & mut next) ;
705+
706+ let module = next. get_module ( "ModuleA" ) . expect ( "module should exist" ) ;
707+ let SourceType :: SourceFile ( source_file) = & module. source_type else {
708+ panic ! ( "expected source file module" ) ;
709+ } ;
710+ let interface = source_file. interface . as_ref ( ) . expect ( "interface should exist" ) ;
711+
712+ assert_eq ! ( interface. compile_warnings. as_deref( ) , Some ( "warning: interface" ) ) ;
713+ assert_eq ! ( interface. compile_state, CompileState :: Warning ) ;
714+ }
715+ }
0 commit comments