Skip to main content

luaur_analyze_cli/methods/
cli_config_resolver_read_config_rec.rs

1use crate::records::cli_config_resolver::CliConfigResolver;
2use crate::records::luau_config_interrupt_info::LuauConfigInterruptInfo;
3use alloc::format;
4use alloc::rc::Rc;
5use alloc::string::{String, ToString};
6use core::ffi::c_int;
7use core::ffi::c_void;
8use luaur_analysis::records::time_limit_error::TimeLimitError;
9use luaur_analysis::records::type_check_limits::TypeCheckLimits;
10use luaur_analysis::records::user_cancel_error::UserCancelError;
11use luaur_cli_lib::functions::get_parent_path::get_parent_path;
12use luaur_cli_lib::functions::is_file::is_file;
13use luaur_cli_lib::functions::join_paths_file_utils_alt_b::join_paths_string_view_string_view;
14use luaur_cli_lib::functions::read_file::read_file;
15use luaur_common::functions::get_clock::get_clock;
16use luaur_config::functions::extract_luau_config::extract_luau_config;
17use luaur_config::functions::parse_config::parse_config;
18use luaur_config::records::alias_options::AliasOptions;
19use luaur_config::records::config::Config;
20use luaur_config::records::config_options::ConfigOptions;
21use luaur_config::records::interrupt_callbacks::InterruptCallbacks;
22use luaur_vm::functions::lua_getthreaddata::lua_getthreaddata;
23use luaur_vm::functions::lua_setthreaddata::lua_setthreaddata;
24use luaur_vm::type_aliases::lua_state::lua_State;
25
26// Luau::kConfigName / Luau::kLuauConfigName (Config.h / LuauConfig.h).
27const K_CONFIG_NAME: &str = ".luaurc";
28const K_LUAU_CONFIG_NAME: &str = ".config.luau";
29
30/// The Lua interrupt callback used while evaluating a `.config.luau` file.
31///
32/// Mirrors the C++ lambda:
33/// ```cpp
34/// callbacks.interruptCallback = [](lua_State* L, int gc) {
35///     LuauConfigInterruptInfo* info = static_cast<LuauConfigInterruptInfo*>(lua_getthreaddata(L));
36///     if (info->limits.finishTime && getClock() > *info->limits.finishTime)
37///         throw TimeLimitError{info->module};
38///     if (info->limits.cancellationToken && info->limits.cancellationToken->requested())
39///         throw UserCancelError{info->module};
40/// };
41/// ```
42/// The `throw` becomes an unwinding panic carrying the typed error, which propagates
43/// across the `extern "C-unwind"` boundary just as the C++ exception does.
44pub(crate) unsafe extern "C-unwind" fn luau_config_interrupt(l: *mut lua_State, _gc: c_int) {
45    let info = lua_getthreaddata(l) as *const LuauConfigInterruptInfo;
46    if info.is_null() {
47        return;
48    }
49    let info = &*info;
50
51    if let Some(finish_time) = info.limits.finishTime() {
52        if get_clock() > finish_time {
53            std::panic::panic_any(TimeLimitError::time_limit_error_time_limit_error(
54                &info.module,
55            ));
56        }
57    }
58    if let Some(token) = info.limits.cancellationToken() {
59        if token.requested() {
60            std::panic::panic_any(UserCancelError::new(info.module.clone()));
61        }
62    }
63}
64
65impl CliConfigResolver {
66    /// C++ `const Config& readConfigRec(const std::string& path, const TypeCheckLimits& limits) const`
67    /// (`CLI/src/Analyze.cpp:252-320`).
68    pub fn read_config_rec(&mut self, path: &str, limits: &TypeCheckLimits) -> &Config {
69        // auto it = configCache.find(path); if (it != configCache.end()) return it->second;
70        if self.config_cache.contains_key(path) {
71            return &self.config_cache[path];
72        }
73
74        // std::optional<std::string> parent = getParentPath(path);
75        // Config result = parent ? readConfigRec(*parent, limits) : defaultConfig;
76        let mut result: Config = match get_parent_path(path) {
77            Some(parent) => self.read_config_rec(&parent, limits).clone(),
78            None => self.default_config.clone(),
79        };
80
81        // std::optional<std::string> configPath = joinPaths(path, kConfigName);
82        // if (!isFile(*configPath)) configPath = std::nullopt;
83        let config_path_candidate = join_paths_string_view_string_view(path, K_CONFIG_NAME);
84        let config_path: Option<String> = if is_file(&config_path_candidate) {
85            Some(config_path_candidate)
86        } else {
87            None
88        };
89
90        // std::optional<std::string> luauConfigPath = joinPaths(path, kLuauConfigName);
91        // if (!isFile(*luauConfigPath)) luauConfigPath = std::nullopt;
92        let luau_config_path_candidate =
93            join_paths_string_view_string_view(path, K_LUAU_CONFIG_NAME);
94        let luau_config_path: Option<String> = if is_file(&luau_config_path_candidate) {
95            Some(luau_config_path_candidate)
96        } else {
97            None
98        };
99
100        if config_path.is_some() && luau_config_path.is_some() {
101            // configErrors.emplace_back(*configPath, "Both ... files exist");
102            let ambiguous_error = format!(
103                "Both {} and {} files exist",
104                K_CONFIG_NAME, K_LUAU_CONFIG_NAME
105            );
106            self.config_errors
107                .push((config_path.clone().unwrap(), ambiguous_error));
108        } else if let Some(config_path) = config_path.as_ref() {
109            // if (std::optional<std::string> contents = readFile(*configPath))
110            if let Some(contents) = read_file(config_path) {
111                let alias_opts = AliasOptions {
112                    config_location: Some(config_path.clone()),
113                    overwrite_aliases: true,
114                };
115
116                let opts = ConfigOptions {
117                    compat: false,
118                    alias_options: Some(alias_opts),
119                };
120
121                // std::optional<std::string> error = parseConfig(*contents, result, opts);
122                if let Some(error) = parse_config(&contents, &mut result, &opts) {
123                    self.config_errors.push((config_path.clone(), error));
124                }
125            }
126        } else if let Some(luau_config_path) = luau_config_path.as_ref() {
127            // if (std::optional<std::string> contents = readFile(*luauConfigPath))
128            if let Some(contents) = read_file(luau_config_path) {
129                // C++ sets `aliasOpts.configLocation = *configPath;` here, but in this
130                // branch `configPath` is `std::nullopt` (dereferencing it is UB upstream);
131                // faithfully carry the (absent) value through.
132                let alias_opts = AliasOptions {
133                    config_location: config_path.clone(),
134                    overwrite_aliases: true,
135                };
136
137                // The interrupt info lives on the stack for the duration of the
138                // synchronous extractLuauConfig call (mirroring the C++ stack local
139                // whose address is stored via lua_setthreaddata).
140                let mut info = LuauConfigInterruptInfo {
141                    limits: limits.clone(),
142                    module: luau_config_path.clone(),
143                };
144                let info_ptr: *mut LuauConfigInterruptInfo = &mut info;
145
146                let callbacks = InterruptCallbacks {
147                    init_callback: Some(Rc::new(move |l: *mut lua_State| unsafe {
148                        lua_setthreaddata(l, info_ptr as *mut c_void);
149                    })),
150                    interrupt_callback: Some(luau_config_interrupt),
151                };
152
153                // std::optional<std::string> error = extractLuauConfig(*contents, result, aliasOpts, callbacks);
154                if let Some(error) =
155                    extract_luau_config(&contents, &mut result, Some(alias_opts), callbacks)
156                {
157                    self.config_errors.push((luau_config_path.clone(), error));
158                }
159            }
160        }
161
162        // return configCache[path] = result;
163        self.config_cache.insert(path.to_string(), result);
164        &self.config_cache[path]
165    }
166}