use std::collections::HashSet;
use emmylua_parser::{LuaAst, LuaAstNode, LuaCallExpr, LuaIndexExpr, LuaVarExpr};
use crate::{
DiagnosticCode, LuaSemanticDeclId, LuaType, ModuleInfo, SemanticDeclLevel, SemanticModel,
parse_require_module_info,
};
use super::{Checker, DiagnosticContext, check_field, humanize_lint_type};
pub struct CheckExportChecker;
impl Checker for CheckExportChecker {
const CODES: &[DiagnosticCode] = &[DiagnosticCode::InjectField, DiagnosticCode::UndefinedField];
fn check(context: &mut DiagnosticContext, semantic_model: &SemanticModel) {
let root = semantic_model.get_root().clone();
let mut checked_index_expr = HashSet::new();
for node in root.descendants::<LuaAst>() {
match node {
LuaAst::LuaAssignStat(assign) => {
let (vars, _) = assign.get_var_and_expr_list();
for var in vars.iter() {
if let LuaVarExpr::IndexExpr(index_expr) = var {
checked_index_expr.insert(index_expr.syntax().clone());
check_export_index_expr(
context,
semantic_model,
index_expr,
DiagnosticCode::InjectField,
);
}
}
}
LuaAst::LuaIndexExpr(index_expr) => {
if checked_index_expr.contains(index_expr.syntax()) {
continue;
}
check_export_index_expr(
context,
semantic_model,
&index_expr,
DiagnosticCode::UndefinedField,
);
}
_ => {}
}
}
}
}
fn check_export_index_expr(
context: &mut DiagnosticContext,
semantic_model: &SemanticModel,
index_expr: &LuaIndexExpr,
code: DiagnosticCode,
) -> Option<()> {
let db = context.db;
let prefix_expr = index_expr.get_prefix_expr()?;
let prefix_info = semantic_model.get_semantic_info(prefix_expr.syntax().clone().into())?;
let prefix_typ = prefix_info.typ.clone();
let LuaType::TableConst(table_const) = &prefix_typ else {
return Some(());
};
let index_key = index_expr.get_index_key()?;
if let Some(module_info) = check_require_table_const_with_export(semantic_model, index_expr) {
if code == DiagnosticCode::InjectField {
if let Some(info) = semantic_model.get_semantic_info(index_expr.syntax().clone().into())
&& is_cross_file_member_from_imported_export_table_const(
module_info,
info.semantic_decl,
)
{
let index_name = index_key.get_path_part();
context.add_diagnostic(
DiagnosticCode::InjectField,
index_key.get_range()?,
t!(
"Fields cannot be injected into the reference of `%{class}` for `%{field}`. ",
class = humanize_lint_type(db, &prefix_typ),
field = index_name,
)
.to_string(),
None,
);
return Some(());
}
}
if check_field::is_valid_member(semantic_model, &prefix_typ, index_expr, &index_key, code)
.is_some()
{
return Some(());
}
let index_name = index_key.get_path_part();
match code {
DiagnosticCode::InjectField => {
context.add_diagnostic(
DiagnosticCode::InjectField,
index_key.get_range()?,
t!(
"Fields cannot be injected into the reference of `%{class}` for `%{field}`. ",
class = humanize_lint_type(db, &prefix_typ),
field = index_name,
)
.to_string(),
None,
);
}
DiagnosticCode::UndefinedField => {
context.add_diagnostic(
DiagnosticCode::UndefinedField,
index_key.get_range()?,
t!("Undefined field `%{field}`. ", field = index_name,).to_string(),
None,
);
}
_ => {}
}
return Some(());
}
if code != DiagnosticCode::UndefinedField && table_const.file_id != semantic_model.get_file_id()
{
return Some(());
}
let Some(LuaSemanticDeclId::LuaDecl(decl_id)) = prefix_info.semantic_decl else {
return Some(());
};
let decl = semantic_model
.get_db()
.get_decl_index()
.get_decl(&decl_id)?;
if !decl.is_local() {
return Some(());
}
let property = semantic_model
.get_db()
.get_property_index()
.get_property(&decl_id.into())?;
if property.export().is_none() {
return Some(());
}
if check_field::is_valid_member(semantic_model, &prefix_typ, index_expr, &index_key, code)
.is_some()
{
return Some(());
}
let index_name = index_key.get_path_part();
context.add_diagnostic(
DiagnosticCode::UndefinedField,
index_key.get_range()?,
t!("Undefined field `%{field}`. ", field = index_name,).to_string(),
None,
);
Some(())
}
fn check_require_table_const_with_export<'a>(
semantic_model: &'a SemanticModel,
index_expr: &LuaIndexExpr,
) -> Option<&'a ModuleInfo> {
let prefix_expr = index_expr.get_prefix_expr()?;
if let Some(call_expr) = LuaCallExpr::cast(prefix_expr.syntax().clone()) {
let module_info = parse_require_expr_module_info(semantic_model, &call_expr)?;
if module_info.is_export(semantic_model.get_db()) {
return Some(module_info);
}
}
let semantic_decl_id = semantic_model.find_decl(
prefix_expr.syntax().clone().into(),
SemanticDeclLevel::NoTrace,
)?;
let decl_id = match semantic_decl_id {
LuaSemanticDeclId::LuaDecl(decl_id) => decl_id,
_ => return None,
};
let decl = semantic_model
.get_db()
.get_decl_index()
.get_decl(&decl_id)?;
let module_info = parse_require_module_info(semantic_model, &decl)?;
if module_info.is_export(semantic_model.get_db()) {
return Some(module_info);
}
None
}
fn parse_require_expr_module_info<'a>(
semantic_model: &'a SemanticModel,
call_expr: &LuaCallExpr,
) -> Option<&'a ModuleInfo> {
let arg_list = call_expr.get_args_list()?;
let first_arg = arg_list.get_args().next()?;
let require_path_type = semantic_model.infer_expr(first_arg.clone()).ok()?;
let module_path: String = match &require_path_type {
LuaType::StringConst(module_path) => module_path.as_ref().to_string(),
_ => return None,
};
semantic_model
.get_db()
.get_module_index()
.find_module(&module_path)
}
fn is_cross_file_member_from_imported_export_table_const(
module_info: &ModuleInfo,
semantic_decl: Option<LuaSemanticDeclId>,
) -> bool {
if let Some(LuaSemanticDeclId::Member(member_id)) = semantic_decl
&& module_info.file_id != member_id.file_id
{
return true;
}
false
}