Skip to content

Commit 3712627

Browse files
committed
fix(config): preprocess paths per config file
Resolve relative workspace paths before merged runtime config loses the declaring file context. Keep the raw single-file config loader for edit commands, and cover parent library/package paths plus entry-local ignoreDir preprocessing.
1 parent 420edbc commit 3712627

5 files changed

Lines changed: 348 additions & 63 deletions

File tree

crates/emmylua_code_analysis/src/config/config_loader.rs

Lines changed: 249 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,89 +6,137 @@ use crate::{config::lua_loader::load_lua_config, read_file_with_encoding};
66

77
use super::{Emmyrc, flatten_config::FlattenConfigObject};
88

9-
pub fn load_configs_raw(config_files: Vec<PathBuf>, partial_emmyrcs: Option<Vec<Value>>) -> Value {
10-
let mut config_jsons = Vec::new();
9+
/// Load a config file into an Emmyrc-shaped JSON value without path preprocessing.
10+
///
11+
/// This keeps file-relative paths exactly as written in the source config. It is
12+
/// intended for callers that need to inspect or edit the raw config shape before
13+
/// writing it back to disk.
14+
pub fn load_config_json_unprocessed(config_file: PathBuf) -> Value {
15+
let Some(config_value) = load_config_file_value(&config_file) else {
16+
log::info!("No valid config file found.");
17+
return Value::Object(Default::default());
18+
};
19+
20+
FlattenConfigObject::parse(config_value).to_emmyrc()
21+
}
22+
23+
/// Load config files into a final [`Emmyrc`].
24+
///
25+
/// Unlike [`load_config_json_unprocessed`], this is the semantic runtime loader.
26+
/// Each file config is flattened and preprocessed relative to the directory that
27+
/// declared it before configs are merged. That has to happen pre-merge because
28+
/// relative settings such as `library`, `packages`, and per-entry ignores lose
29+
/// their originating config path once they have been folded into one JSON value.
30+
pub fn load_configs(config_files: Vec<PathBuf>, partial_emmyrcs: Option<Vec<Value>>) -> Emmyrc {
31+
// This starts as a placeholder; the first surviving config becomes the real merge base.
32+
let mut merged_emmyrc_json = Value::Object(Default::default());
33+
let mut has_config = false;
1134

1235
for config_file in config_files {
13-
log::info!("Loading config file: {:?}", config_file);
14-
let config_content = match read_file_with_encoding(&config_file, "utf-8") {
15-
Some(content) => content,
16-
None => {
36+
let Some(config_value) = load_config_file_value(&config_file) else {
37+
continue;
38+
};
39+
40+
// Preprocess while we still know which config file produced these values. After merge,
41+
// a relative `library` / `packages` / `ignoreDir` entry no longer carries its base dir.
42+
let config_emmyrc_json = FlattenConfigObject::parse(config_value).to_emmyrc();
43+
let mut emmyrc: Emmyrc = match serde_json::from_value(config_emmyrc_json) {
44+
Ok(emmyrc) => emmyrc,
45+
Err(err) => {
1746
log::error!(
18-
"Failed to read config file: {:?}, error: File not found or unreadable",
19-
config_file
47+
"Failed to parse config file as emmyrc {:?}: {:?}",
48+
config_file,
49+
err
2050
);
2151
continue;
2252
}
2353
};
2454

25-
let config_value = if config_file.extension().and_then(|s| s.to_str()) == Some("lua") {
26-
match load_lua_config(&config_content) {
27-
Ok(value) => value,
28-
Err(e) => {
29-
log::error!(
30-
"Failed to parse lua config file: {:?}, error: {:?}",
31-
&config_file,
32-
e
33-
);
34-
continue;
55+
if let Some(config_root) = config_file.parent() {
56+
emmyrc.pre_process_emmyrc(config_root);
57+
}
58+
59+
match serde_json::to_value(emmyrc) {
60+
Ok(config_emmyrc_json) => {
61+
if has_config {
62+
merge_values(&mut merged_emmyrc_json, config_emmyrc_json);
63+
} else {
64+
merged_emmyrc_json = config_emmyrc_json;
65+
has_config = true;
3566
}
3667
}
37-
} else {
38-
match serde_json::from_str(&config_content) {
39-
Ok(json) => json,
40-
Err(e) => {
41-
log::error!(
42-
"Failed to parse config file: {:?}, error: {:?}",
43-
&config_file,
44-
e
45-
);
46-
continue;
47-
}
68+
Err(err) => {
69+
log::error!(
70+
"Failed to serialize pre-processed config {:?}: {:?}",
71+
config_file,
72+
err
73+
);
4874
}
4975
};
50-
51-
config_jsons.push(config_value);
5276
}
5377

5478
if let Some(partial_emmyrcs) = partial_emmyrcs {
79+
// Partial configs are late overlays from the client, so they win over file-backed config.
5580
for partial_emmyrc in partial_emmyrcs {
56-
config_jsons.push(partial_emmyrc);
81+
if has_config {
82+
merge_values(&mut merged_emmyrc_json, partial_emmyrc);
83+
} else {
84+
merged_emmyrc_json = partial_emmyrc;
85+
has_config = true;
86+
}
5787
}
5888
}
5989

60-
if config_jsons.is_empty() {
90+
if !has_config {
6191
log::info!("No valid config file found.");
62-
Value::Object(Default::default())
63-
} else if config_jsons.len() == 1 {
64-
let first_config = config_jsons.into_iter().next().unwrap_or_else(|| {
65-
log::error!("No valid config file found.");
66-
Value::Object(Default::default())
67-
});
68-
69-
let flatten_config = FlattenConfigObject::parse(first_config);
70-
flatten_config.to_emmyrc()
71-
} else {
72-
let merge_config =
73-
config_jsons
74-
.into_iter()
75-
.fold(Value::Object(Default::default()), |mut acc, item| {
76-
merge_values(&mut acc, item);
77-
acc
78-
});
79-
let flatten_config = FlattenConfigObject::parse(merge_config.clone());
80-
flatten_config.to_emmyrc()
8192
}
82-
}
8393

84-
pub fn load_configs(config_files: Vec<PathBuf>, partial_emmyrcs: Option<Vec<Value>>) -> Emmyrc {
85-
let emmyrc_json_value = load_configs_raw(config_files, partial_emmyrcs);
86-
serde_json::from_value(emmyrc_json_value).unwrap_or_else(|err| {
94+
serde_json::from_value(merged_emmyrc_json).unwrap_or_else(|err| {
8795
log::error!("Failed to parse config: error: {:?}", err);
8896
Emmyrc::default()
8997
})
9098
}
9199

100+
fn load_config_file_value(config_file: &PathBuf) -> Option<Value> {
101+
log::info!("Loading config file: {:?}", config_file);
102+
let config_content = match read_file_with_encoding(config_file, "utf-8") {
103+
Some(content) => content,
104+
None => {
105+
log::error!(
106+
"Failed to read config file: {:?}, error: File not found or unreadable",
107+
config_file
108+
);
109+
return None;
110+
}
111+
};
112+
113+
if config_file.extension().and_then(|s| s.to_str()) == Some("lua") {
114+
match load_lua_config(&config_content) {
115+
Ok(value) => Some(value),
116+
Err(err) => {
117+
log::error!(
118+
"Failed to parse lua config file: {:?}, error: {:?}",
119+
config_file,
120+
err
121+
);
122+
None
123+
}
124+
}
125+
} else {
126+
match serde_json::from_str(&config_content) {
127+
Ok(json) => Some(json),
128+
Err(err) => {
129+
log::error!(
130+
"Failed to parse config file: {:?}, error: {:?}",
131+
config_file,
132+
err
133+
);
134+
None
135+
}
136+
}
137+
}
138+
}
139+
92140
fn merge_values(base: &mut Value, overlay: Value) {
93141
match (base, overlay) {
94142
(Value::Object(base_map), Value::Object(overlay_map)) => {
@@ -116,3 +164,148 @@ fn merge_values(base: &mut Value, overlay: Value) {
116164
}
117165
}
118166
}
167+
168+
#[cfg(test)]
169+
mod tests {
170+
use std::{
171+
fs,
172+
path::{Path, PathBuf},
173+
sync::atomic::{AtomicU64, Ordering},
174+
time::{SystemTime, UNIX_EPOCH},
175+
};
176+
177+
use super::{Emmyrc, load_config_json_unprocessed, load_configs};
178+
179+
static TEST_CONFIG_COUNTER: AtomicU64 = AtomicU64::new(0);
180+
181+
struct TestConfigRoot {
182+
root: PathBuf,
183+
}
184+
185+
impl TestConfigRoot {
186+
fn new() -> Self {
187+
let unique = SystemTime::now()
188+
.duration_since(UNIX_EPOCH)
189+
.unwrap()
190+
.as_nanos();
191+
let counter = TEST_CONFIG_COUNTER.fetch_add(1, Ordering::Relaxed);
192+
let root = std::env::temp_dir().join(format!(
193+
"emmylua-config-loader-{}-{}-{}",
194+
std::process::id(),
195+
unique,
196+
counter,
197+
));
198+
fs::create_dir_all(&root).unwrap();
199+
Self { root }
200+
}
201+
202+
fn write_file(&self, relative_path: &str, contents: &str) -> PathBuf {
203+
let path = self.root.join(relative_path);
204+
fs::create_dir_all(path.parent().unwrap()).unwrap();
205+
fs::write(&path, contents).unwrap();
206+
path
207+
}
208+
209+
fn path(&self, relative_path: &str) -> PathBuf {
210+
self.root.join(relative_path)
211+
}
212+
}
213+
214+
impl Drop for TestConfigRoot {
215+
fn drop(&mut self) {
216+
let _ = fs::remove_dir_all(&self.root);
217+
}
218+
}
219+
220+
fn to_string(path: &Path) -> String {
221+
path.to_string_lossy().to_string()
222+
}
223+
224+
#[test]
225+
fn merged_configs_should_resolve_relative_workspace_paths_from_declaring_config_file() {
226+
let workspace = TestConfigRoot::new();
227+
let shared_config = workspace.write_file(
228+
"shared/config/.luarc.json",
229+
r#"{
230+
"workspace": {
231+
"library": ["../shared-lib"]
232+
}
233+
}"#,
234+
);
235+
let workspace_config = workspace.write_file(
236+
"project/.luarc.json",
237+
r#"{
238+
"workspace": {
239+
"library": ["./vendor"]
240+
}
241+
}"#,
242+
);
243+
let workspace_root = workspace.path("project");
244+
let expected = vec![
245+
to_string(&workspace.path("shared/shared-lib")),
246+
to_string(&workspace.path("project/vendor")),
247+
];
248+
249+
let mut emmyrc = load_configs(vec![shared_config, workspace_config], None);
250+
emmyrc.pre_process_emmyrc(&workspace_root);
251+
252+
let actual = emmyrc
253+
.workspace
254+
.library
255+
.iter()
256+
.map(|item| item.get_path().clone())
257+
.collect::<Vec<_>>();
258+
259+
assert_eq!(actual, expected);
260+
}
261+
262+
#[test]
263+
fn raw_config_loader_keeps_relative_workspace_paths_as_authored() {
264+
let workspace = TestConfigRoot::new();
265+
let config = workspace.write_file(
266+
"project/.luarc.json",
267+
r#"{
268+
"workspace": {
269+
"library": ["../shared-lib"]
270+
}
271+
}"#,
272+
);
273+
274+
let emmyrc_value = load_config_json_unprocessed(config);
275+
let emmyrc: Emmyrc = serde_json::from_value(emmyrc_value).unwrap();
276+
277+
let actual = emmyrc
278+
.workspace
279+
.library
280+
.iter()
281+
.map(|item| item.get_path().clone())
282+
.collect::<Vec<_>>();
283+
284+
assert_eq!(actual, vec!["../shared-lib".to_string()]);
285+
}
286+
287+
#[test]
288+
fn invalid_config_does_not_override_earlier_valid_settings() {
289+
let workspace = TestConfigRoot::new();
290+
let valid_config = workspace.write_file(
291+
"project/.luarc.json",
292+
r#"{
293+
"diagnostics": {
294+
"enable": false
295+
}
296+
}"#,
297+
);
298+
let invalid_config = workspace.write_file(
299+
"project/invalid.luarc.json",
300+
r#"{
301+
"diagnostics": {
302+
"enable": "oops"
303+
}
304+
}"#,
305+
);
306+
307+
let emmyrc = load_configs(vec![valid_config, invalid_config], None);
308+
309+
assert!(!emmyrc.diagnostics.enable);
310+
}
311+
}

crates/emmylua_code_analysis/src/config/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ mod pre_process;
66

77
use std::{collections::HashMap, path::Path};
88

9-
pub use config_loader::{load_configs, load_configs_raw};
9+
pub use config_loader::{load_config_json_unprocessed, load_configs};
1010
pub use configs::{
1111
DiagnosticSeveritySetting, DocSyntax, EmmyLibraryConfig, EmmyLibraryItem, EmmyrcCodeAction,
1212
EmmyrcCodeLens, EmmyrcCompletion, EmmyrcDiagnostic, EmmyrcDoc, EmmyrcDocumentColor,

0 commit comments

Comments
 (0)