Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::compilation::analyzer::doc::tags::report_orphan_tag;
use emmylua_parser::{
LuaAst, LuaAstNode, LuaDocDescriptionOwner, LuaDocTagAsync, LuaDocTagDeprecated,
LuaDocTagExport, LuaDocTagNodiscard, LuaDocTagReadonly, LuaDocTagSource, LuaDocTagVersion,
LuaDocTagVisibility, LuaTableExpr,
LuaDocTagVisibility, LuaExpr,
};

pub fn analyze_visibility(
Expand Down Expand Up @@ -121,11 +121,27 @@ pub fn analyze_export(analyzer: &mut DocAnalyzer, tag: LuaDocTagExport) -> Optio
};
let owner_id = match owner {
LuaAst::LuaReturnStat(return_stat) => {
let return_table_expr = return_stat.child::<LuaTableExpr>()?;
LuaSemanticDeclId::LuaDecl(LuaDeclId::new(
analyzer.file_id,
return_table_expr.get_position(),
))
let expr = return_stat.child::<LuaExpr>()?;
match expr {
LuaExpr::NameExpr(name_expr) => {
let name = name_expr.get_name_text()?;
let tree = analyzer
.db
.get_decl_index()
.get_decl_tree(&analyzer.file_id)?;
let decl = tree.find_local_decl(&name, name_expr.get_position())?;

Some(LuaSemanticDeclId::LuaDecl(decl.get_id()))
}
LuaExpr::ClosureExpr(closure) => Some(LuaSemanticDeclId::Signature(
LuaSignatureId::from_closure(analyzer.file_id, &closure),
)),
LuaExpr::TableExpr(table_expr) => Some(LuaSemanticDeclId::LuaDecl(LuaDeclId::new(
analyzer.file_id,
table_expr.get_position(),
))),
_ => None,
}?
}
_ => get_owner_id_or_report(analyzer, &tag)?,
};
Expand All @@ -134,10 +150,10 @@ pub fn analyze_export(analyzer: &mut DocAnalyzer, tag: LuaDocTagExport) -> Optio
match scope_text.as_str() {
"namespace" => LuaExportScope::Namespace,
"global" => LuaExportScope::Global,
_ => LuaExportScope::Global, // 默认为 global
_ => LuaExportScope::Default,
}
} else {
LuaExportScope::Global // 没有参数时默认为 global
LuaExportScope::Default
};

let export = LuaExport {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,28 @@ mod test {
assert_eq!(e_ty, LuaType::Integer);
assert_eq!(f_ty, LuaType::Integer);
}

#[test]
fn test_keyof() {
let mut ws = VirtualWorkspace::new();

ws.def(
r#"
---@class SuiteHooks
---@field beforeAll string
---@field afterAll number

---@type SuiteHooks
local hooks = {}

---@type keyof SuiteHooks
local name = "beforeAll"

A = hooks[name]
"#,
);

let ty = ws.expr_ty("A");
assert_eq!(ws.humanize_type(ty), "(number|string)");
}
}
18 changes: 18 additions & 0 deletions crates/emmylua_code_analysis/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use regex::Regex;
use rowan::NodeCache;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::process::Command;

#[derive(Serialize, Deserialize, Debug, JsonSchema, Default, Clone)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -197,9 +198,26 @@ fn replace_placeholders(input: &str, workspace_folder: &str) -> String {
workspace_folder.to_string()
} else if let Some(env_name) = key.strip_prefix("env:") {
std::env::var(env_name).unwrap_or_default()
} else if key == "luarocks" {
get_luarocks_deploy_dir()
} else {
caps[0].to_string()
}
})
.to_string()
}

fn get_luarocks_deploy_dir() -> String {
Command::new("luarocks")
.args(["config", "deploy_lua_dir"])
.output()
.ok()
.and_then(|output| {
if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
None
}
})
.unwrap_or_default()
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ pub enum LuaDeprecated {

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LuaExportScope {
Default, // 默认声明, 会根据配置文件作不同的处理.
Global,
Namespace,
}
Expand Down
240 changes: 240 additions & 0 deletions crates/emmylua_code_analysis/src/diagnostic/checker/check_export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
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();

// `check_export` 仅需要处理 `TableConst, 其它类型由 `check_field` 负责.
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(());
};
// 必须为 local 声明
let decl = semantic_model
.get_db()
.get_decl_index()
.get_decl(&decl_id)?;
if !decl.is_local() {
return Some(());
}
// 且该声明标记了 `export`
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
}
Loading