rspack_plugin_case_sensitive/
lib.rs1use cow_utils::CowUtils;
4use itertools::Itertools;
5use rspack_collections::{Identifier, IdentifierSet};
6use rspack_core::{Compilation, CompilationSeal, CompilerEmit, Logger, ModuleGraph, Plugin};
7use rspack_error::{Diagnostic, Result};
8use rspack_hook::{plugin, plugin_hook};
9use rustc_hash::{FxBuildHasher, FxHashMap as HashMap, FxHashSet as HashSet};
10
11#[plugin]
12#[derive(Debug, Default)]
13pub struct CaseSensitivePlugin;
14
15impl CaseSensitivePlugin {
16 pub fn create_sensitive_modules_warning(
17 &self,
18 modules: Vec<Identifier>,
19 graph: &ModuleGraph,
20 ) -> String {
21 let mut message =
22 String::from("There are multiple modules with names that only differ in casing.\n");
23
24 for m in modules {
25 if let Some(boxed_m) = graph.module_by_identifier(&m) {
26 message.push_str(" - ");
27 message.push_str(&m);
28 message.push('\n');
29 graph
30 .get_incoming_connections(&boxed_m.identifier())
31 .for_each(|c| {
32 if let Some(original_identifier) = c.original_module_identifier {
33 message.push_str(" - used by ");
34 message.push_str(&original_identifier);
35 message.push('\n');
36 }
37 });
38 }
39 }
40
41 message
42 }
43
44 pub fn create_sensitive_assets_warning(&self, filenames: &HashSet<String>) -> String {
45 let filenames_str = filenames.iter().map(|f| format!(" - {f}")).join("\n");
46 format!(
47 r#"Prevent writing to file that only differs in casing or query string from already written file.
48This will lead to a race-condition and corrupted files on case-insensitive file systems.
49{}"#,
50 filenames_str
51 )
52 }
53}
54
55#[plugin_hook(CompilationSeal for CaseSensitivePlugin)]
56async fn seal(&self, compilation: &mut Compilation) -> Result<()> {
57 let logger = compilation.get_logger(self.name());
58 let start = logger.time("check case sensitive modules");
59 let mut diagnostics: Vec<Diagnostic> = vec![];
60 let module_graph = compilation.get_module_graph();
61 let all_modules = module_graph.modules();
62 let mut not_conflect: HashMap<String, Identifier> =
63 HashMap::with_capacity_and_hasher(all_modules.len(), FxBuildHasher);
64 let mut conflict: HashMap<String, IdentifierSet> = HashMap::default();
65
66 for module in all_modules.values() {
67 if let Some(normal_module) = module.as_normal_module()
69 && normal_module
70 .resource_resolved_data()
71 .encoded_content()
72 .is_some()
73 {
74 continue;
75 }
76
77 let identifier = module.identifier();
78 let lower_identifier = identifier.cow_to_ascii_lowercase();
79 if let Some(prev_identifier) = not_conflect.remove(lower_identifier.as_ref()) {
80 conflict.insert(
81 lower_identifier.into_owned(),
82 IdentifierSet::from_iter([prev_identifier, identifier]),
83 );
84 } else if let Some(set) = conflict.get_mut(lower_identifier.as_ref()) {
85 set.insert(identifier);
86 } else {
87 not_conflect.insert(lower_identifier.into_owned(), identifier);
88 }
89 }
90
91 let mut case_map_vec = conflict.into_iter().collect::<Vec<_>>();
93 case_map_vec.sort_unstable_by(|a, b| a.0.cmp(&b.0));
94
95 for (_, set) in case_map_vec {
96 let mut case_modules = set.iter().copied().collect::<Vec<_>>();
97 case_modules.sort_unstable();
98 diagnostics.push(Diagnostic::warn(
99 "Sensitive Warn".to_string(),
100 self.create_sensitive_modules_warning(case_modules, compilation.get_module_graph()),
101 ));
102 }
103
104 compilation.extend_diagnostics(diagnostics);
105 logger.time_end(start);
106 Ok(())
107}
108
109#[plugin_hook(CompilerEmit for CaseSensitivePlugin)]
110async fn emit(&self, compilation: &mut Compilation) -> Result<()> {
111 let mut diagnostics: Vec<Diagnostic> = vec![];
112
113 let mut case_map: HashMap<String, HashSet<String>> = HashMap::default();
117 for filename in compilation.assets().keys() {
118 let (target_file, _query) = filename.split_once('?').unwrap_or((filename, ""));
119 let lower_key = cow_utils::CowUtils::cow_to_lowercase(target_file);
120 case_map
121 .entry(lower_key.to_string())
122 .or_default()
123 .insert(target_file.to_string());
124 }
125
126 for (_lower_key, filenames) in case_map.iter() {
128 if filenames.len() > 1 {
130 diagnostics.push(Diagnostic::warn(
131 "Sensitive Warn".to_string(),
132 self.create_sensitive_assets_warning(filenames),
133 ));
134 }
135 }
136
137 compilation.extend_diagnostics(diagnostics);
138 Ok(())
139}
140
141impl Plugin for CaseSensitivePlugin {
145 fn name(&self) -> &'static str {
146 "rspack.CaseSensitivePlugin"
147 }
148
149 fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
150 ctx.compilation_hooks.seal.tap(seal::new(self));
151 ctx.compiler_hooks.emit.tap(emit::new(self));
152 Ok(())
153 }
154}