use luaur_analysis::enums::solver_mode::SolverMode;
use luaur_analysis::functions::freeze::freeze;
use luaur_analysis::functions::register_builtin_globals::register_builtin_globals;
use luaur_analysis::functions::to_string_error::to_string_type_error;
use luaur_analysis::functions::unfreeze::unfreeze;
use luaur_analysis::records::config_resolver::ConfigResolver;
use luaur_analysis::records::file_resolver::{FileResolver, FileResolverVtable};
use luaur_analysis::records::frontend::Frontend;
use luaur_analysis::records::frontend_options::FrontendOptions;
use luaur_analysis::records::module_info::ModuleInfo;
use luaur_analysis::records::source_code::SourceCode;
use luaur_analysis::records::type_check_limits::TypeCheckLimits;
use luaur_analysis::type_aliases::module_name_file_resolver::ModuleName;
use luaur_ast::records::ast_expr::AstExpr;
use luaur_config::records::config::Config;
use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypeDiagnostic {
pub line: u32,
pub column: u32,
pub end_line: u32,
pub end_column: u32,
pub message: String,
pub in_definitions: bool,
}
impl fmt::Display for TypeDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.in_definitions {
write!(f, "(host definitions) ")?;
}
write!(f, "{}:{}: {}", self.line, self.column, self.message)
}
}
const MAIN_MODULE: &str = "main";
#[repr(C)]
struct CheckFileResolver {
base: FileResolver,
source: String,
}
unsafe fn check_file_resolver_read_source_thunk(
this: *mut FileResolver,
name: &ModuleName,
) -> Option<SourceCode> {
let this = this as *const CheckFileResolver;
if name != MAIN_MODULE {
return None;
}
let source = unsafe { (*this).source.clone() };
Some(SourceCode {
source,
r#type: SourceCode::Module,
})
}
unsafe fn check_file_resolver_resolve_module_thunk(
_this: *mut FileResolver,
_context: *const ModuleInfo,
_expr: *mut AstExpr,
_limits: &TypeCheckLimits,
) -> Option<ModuleInfo> {
None
}
unsafe fn check_file_resolver_get_human_readable_module_name_thunk(
_this: *const FileResolver,
name: &ModuleName,
) -> String {
name.clone()
}
unsafe fn check_file_resolver_get_environment_for_module_thunk(
_this: *const FileResolver,
_name: &ModuleName,
) -> Option<String> {
None
}
impl CheckFileResolver {
fn new(source: &str) -> Self {
let vtable = FileResolverVtable {
read_source: check_file_resolver_read_source_thunk,
resolve_module: check_file_resolver_resolve_module_thunk,
get_human_readable_module_name:
check_file_resolver_get_human_readable_module_name_thunk,
get_environment_for_module: check_file_resolver_get_environment_for_module_thunk,
};
CheckFileResolver {
base: FileResolver {
vtable,
require_suggester: None,
},
source: source.to_string(),
}
}
}
#[repr(C)]
struct CheckConfigResolver {
base: ConfigResolver,
default_config: Config,
}
unsafe fn check_config_resolver_get_config_thunk(
this: *const ConfigResolver,
_name: *const ModuleName,
_limits: *const TypeCheckLimits,
) -> *const Config {
let this = this as *const CheckConfigResolver;
unsafe { &(*this).default_config as *const Config }
}
impl CheckConfigResolver {
fn new() -> Self {
CheckConfigResolver {
base: ConfigResolver {
get_config: Some(check_config_resolver_get_config_thunk),
},
default_config: Config::default(),
}
}
}
const HOST_DEFINITIONS_PACKAGE: &str = "@host";
fn run_check(source: &str, definitions: Option<&str>) -> Vec<TypeDiagnostic> {
let mut diagnostics = Vec::new();
let mut file_resolver = CheckFileResolver::new(source);
let mut config_resolver = CheckConfigResolver::new();
let options = FrontendOptions::default();
let mut frontend = Frontend::frontend_file_resolver_config_resolver_frontend_options(
&mut file_resolver.base,
&mut config_resolver.base,
&options,
);
unsafe {
frontend.wire_self_pointers();
}
frontend.set_luau_solver_mode(SolverMode::Old);
let frontend_ptr = &mut frontend as *mut Frontend;
unsafe {
unfreeze((*frontend_ptr).globals.global_types_mut());
register_builtin_globals(&mut *frontend_ptr, &mut (*frontend_ptr).globals, false);
freeze((*frontend_ptr).globals.global_types_mut());
}
if let Some(defs) = definitions {
if !defs.is_empty() {
unsafe {
unfreeze((*frontend_ptr).globals.global_types_mut());
let target_scope = (*frontend_ptr).globals.global_scope();
let result = (*frontend_ptr).load_definition_file(
&mut (*frontend_ptr).globals,
target_scope,
defs,
String::from(HOST_DEFINITIONS_PACKAGE),
false,
false,
);
freeze((*frontend_ptr).globals.global_types_mut());
if !result.success {
for err in &result.parse_result.errors {
let begin = err.get_location().begin;
let end = err.get_location().end;
diagnostics.push(TypeDiagnostic {
line: begin.line + 1,
column: begin.column + 1,
end_line: end.line + 1,
end_column: end.column + 1,
message: err.get_message().to_string(),
in_definitions: true,
});
}
if let Some(module) = &result.module {
for err in &module.errors {
let begin = err.location.begin;
let end = err.location.end;
diagnostics.push(TypeDiagnostic {
line: begin.line + 1,
column: begin.column + 1,
end_line: end.line + 1,
end_column: end.column + 1,
message: to_string_type_error(err),
in_definitions: true,
});
}
}
if diagnostics.is_empty() {
diagnostics.push(TypeDiagnostic {
line: 1,
column: 1,
end_line: 1,
end_column: 1,
message: "failed to load".to_string(),
in_definitions: true,
});
}
return diagnostics;
}
}
}
}
let check_result =
frontend.check_module_name_optional_frontend_options(&MAIN_MODULE.to_string(), None);
for err in &check_result.errors {
let begin = err.location.begin;
let end = err.location.end;
diagnostics.push(TypeDiagnostic {
line: begin.line + 1,
column: begin.column + 1,
end_line: end.line + 1,
end_column: end.column + 1,
message: to_string_type_error(err),
in_definitions: false,
});
}
diagnostics
}
pub fn check(source: &str) -> core::result::Result<(), Vec<TypeDiagnostic>> {
check_inner(source, None)
}
pub fn check_with_definitions(
source: &str,
definitions: &str,
) -> core::result::Result<(), Vec<TypeDiagnostic>> {
check_inner(source, Some(definitions))
}
fn check_inner(
source: &str,
definitions: Option<&str>,
) -> core::result::Result<(), Vec<TypeDiagnostic>> {
let owned = source.to_string();
let owned_defs = definitions.map(|d| d.to_string());
let diagnostics =
match std::panic::catch_unwind(move || run_check(&owned, owned_defs.as_deref())) {
Ok(diagnostics) => diagnostics,
Err(payload) => vec![TypeDiagnostic {
line: 1,
column: 1,
end_line: 1,
end_column: 1,
message: panic_message(&payload),
in_definitions: false,
}],
};
if diagnostics.is_empty() {
Ok(())
} else {
Err(diagnostics)
}
}
fn panic_message(payload: &(dyn core::any::Any + Send)) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"unknown error".to_string()
}
}