Skip to content

Commit b737ad5

Browse files
authored
Rewatch: preserve warnings after atomic-save full rebuilds (#8358)
* test: cover warning persistence after atomic saves Add a rewatch integration test that simulates an editor-style atomic save by renaming B.res into place and verifies warnings from ModuleA.res remain in .compiler.log after the rebuild. * Preserve warnings across watch full rebuilds Atomic-save rename events force the watcher down the full rebuild path, which was recreating build state and dropping stored warnings for unchanged modules. Carry warning state forward into the new build state so unrelated module warnings continue to be re-emitted.\n\nAlso add unit tests for the warning carry-forward helper. * docs: add changelog
1 parent 4bf0cd3 commit b737ad5

4 files changed

Lines changed: 370 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#### :bug: Bug fix
2525

2626
- Fix partial application generalization for `...`. https://github.com/rescript-lang/rescript/pull/8343
27+
- Rewatch: preserve warnings after atomic-save full rebuilds. https://github.com/rescript-lang/rescript/pull/8358
2728

2829
- Preserve JSX prop locations across the AST0 translation layer, fixing `0:0` editor diagnostics in PPX-related flows. https://github.com/rescript-lang/rescript/pull/8350
2930

rewatch/src/watcher.rs

Lines changed: 239 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
143181
struct 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+
}

rewatch/tests/suite.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ fi
8686
# Watch tests
8787
./watch/01-watch-recompile.sh &&
8888
./watch/02-watch-warnings-persist.sh &&
89+
./watch/02-watch-warnings-persist-atomic-save.sh &&
8990
./watch/03-watch-new-file.sh &&
9091
./watch/04-watch-config-change.sh &&
9192
./watch/05-watch-ignores-non-source.sh &&

0 commit comments

Comments
 (0)