@@ -6,89 +6,137 @@ use crate::{config::lua_loader::load_lua_config, read_file_with_encoding};
66
77use 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+
92140fn 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+ }
0 commit comments