Skip to content

Commit 6ef8e54

Browse files
committed
attribute add diagnostic
1 parent fdbff61 commit 6ef8e54

File tree

6 files changed

+286
-9
lines changed

6 files changed

+286
-9
lines changed

crates/emmylua_code_analysis/src/compilation/analyzer/doc/attribute_tags.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub fn analyze_tag_attribute_use(
2828
let attribute_uses = attribute_use.get_attribute_uses();
2929
for attribute_use in attribute_uses {
3030
let mut params = Vec::new();
31-
if let Some(attribute_call_arg_list) = attribute_use.get_attribute_call_arg_list() {
31+
if let Some(attribute_call_arg_list) = attribute_use.get_arg_list() {
3232
for arg in attribute_call_arg_list.get_args() {
3333
let arg_type = infer_attribute_arg_type(arg);
3434
params.push(arg_type);
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
use std::collections::HashSet;
2+
3+
use crate::{
4+
DiagnosticCode, LuaType, SemanticModel, TypeCheckFailReason, TypeCheckResult,
5+
diagnostic::checker::{generic::infer_doc_type::infer_doc_type, humanize_lint_type},
6+
};
7+
use emmylua_parser::{
8+
LuaAstNode, LuaDocAttributeUse, LuaDocTagAttributeUse, LuaDocType, LuaExpr, LuaLiteralExpr,
9+
};
10+
use rowan::TextRange;
11+
12+
use super::{Checker, DiagnosticContext};
13+
14+
pub struct AttributeCheckChecker;
15+
16+
impl Checker for AttributeCheckChecker {
17+
const CODES: &[DiagnosticCode] = &[
18+
DiagnosticCode::AttributeParamTypeMismatch,
19+
DiagnosticCode::AttributeMissingParameter,
20+
DiagnosticCode::AttributeRedundantParameter,
21+
];
22+
23+
fn check(context: &mut DiagnosticContext, semantic_model: &SemanticModel) {
24+
let root = semantic_model.get_root().clone();
25+
for tag_use in root.descendants::<LuaDocTagAttributeUse>() {
26+
for attribute_use in tag_use.get_attribute_uses() {
27+
check_attribute_use(context, semantic_model, &attribute_use);
28+
}
29+
}
30+
}
31+
}
32+
33+
fn check_attribute_use(
34+
context: &mut DiagnosticContext,
35+
semantic_model: &SemanticModel,
36+
attribute_use: &LuaDocAttributeUse,
37+
) -> Option<()> {
38+
let attribute_type =
39+
infer_doc_type(semantic_model, &LuaDocType::Name(attribute_use.get_type()?));
40+
let LuaType::Ref(type_id) = attribute_type else {
41+
return None;
42+
};
43+
let type_decl = semantic_model
44+
.get_db()
45+
.get_type_index()
46+
.get_type_decl(&type_id)?;
47+
if !type_decl.is_attribute() {
48+
return None;
49+
}
50+
let LuaType::DocAttribute(attr_def) = type_decl.get_attribute_type()? else {
51+
return None;
52+
};
53+
54+
let def_params = attr_def.get_params();
55+
let args = match attribute_use.get_arg_list() {
56+
Some(arg_list) => arg_list.get_args().collect::<Vec<_>>(),
57+
None => vec![],
58+
};
59+
check_param_count(context, &def_params, &attribute_use, &args);
60+
check_param(context, semantic_model, &def_params, args);
61+
62+
Some(())
63+
}
64+
65+
/// 检查参数数量是否匹配
66+
fn check_param_count(
67+
context: &mut DiagnosticContext,
68+
def_params: &[(String, Option<LuaType>)],
69+
attribute_use: &LuaDocAttributeUse,
70+
args: &Vec<LuaLiteralExpr>,
71+
) -> Option<()> {
72+
let call_args_count = args.len();
73+
// 调用参数少于定义参数, 需要考虑可空参数
74+
if call_args_count < def_params.len() {
75+
for def_param in def_params[call_args_count..].iter() {
76+
if def_param.0 == "..." {
77+
break;
78+
}
79+
if def_param.1.as_ref().is_some_and(|typ| is_nullable(typ)) {
80+
continue;
81+
}
82+
context.add_diagnostic(
83+
DiagnosticCode::AttributeMissingParameter,
84+
match args.last() {
85+
Some(arg) => arg.get_range(),
86+
None => attribute_use.get_range(),
87+
},
88+
t!(
89+
"expected %{num} parameters but found %{found_num}",
90+
num = def_params.len(),
91+
found_num = call_args_count
92+
)
93+
.to_string(),
94+
None,
95+
);
96+
}
97+
}
98+
// 调用参数多于定义参数, 需要考虑可变参数
99+
else if call_args_count > def_params.len() {
100+
// 参数定义中最后一个参数是 `...`
101+
if def_params.last().is_some_and(|(name, typ)| {
102+
name == "..." || typ.as_ref().is_some_and(|typ| typ.is_variadic())
103+
}) {
104+
return Some(());
105+
}
106+
for arg in args[def_params.len()..].iter() {
107+
context.add_diagnostic(
108+
DiagnosticCode::AttributeRedundantParameter,
109+
arg.get_range(),
110+
t!(
111+
"expected %{num} parameters but found %{found_num}",
112+
num = def_params.len(),
113+
found_num = call_args_count
114+
)
115+
.to_string(),
116+
None,
117+
);
118+
}
119+
}
120+
121+
Some(())
122+
}
123+
124+
/// 检查参数是否匹配
125+
fn check_param(
126+
context: &mut DiagnosticContext,
127+
semantic_model: &SemanticModel,
128+
def_params: &[(String, Option<LuaType>)],
129+
args: Vec<LuaLiteralExpr>,
130+
) -> Option<()> {
131+
let mut call_arg_types = Vec::new();
132+
for arg in &args {
133+
let arg_type = semantic_model
134+
.infer_expr(LuaExpr::LiteralExpr(arg.clone()))
135+
.ok()?;
136+
call_arg_types.push(arg_type);
137+
}
138+
139+
for (idx, param) in def_params.iter().enumerate() {
140+
if param.0 == "..." {
141+
if call_arg_types.len() < idx {
142+
break;
143+
}
144+
if let Some(variadic_type) = param.1.clone() {
145+
for arg_type in call_arg_types[idx..].iter() {
146+
let result = semantic_model.type_check_detail(&variadic_type, arg_type);
147+
if result.is_err() {
148+
add_type_check_diagnostic(
149+
context,
150+
semantic_model,
151+
args.get(idx)?.get_range(),
152+
&variadic_type,
153+
arg_type,
154+
result,
155+
);
156+
}
157+
}
158+
}
159+
break;
160+
}
161+
if let Some(param_type) = param.1.clone() {
162+
let arg_type = call_arg_types.get(idx).unwrap_or(&LuaType::Any);
163+
let result = semantic_model.type_check_detail(&param_type, arg_type);
164+
if result.is_err() {
165+
add_type_check_diagnostic(
166+
context,
167+
semantic_model,
168+
args.get(idx)?.get_range(),
169+
&param_type,
170+
arg_type,
171+
result,
172+
);
173+
}
174+
}
175+
}
176+
Some(())
177+
}
178+
179+
fn add_type_check_diagnostic(
180+
context: &mut DiagnosticContext,
181+
semantic_model: &SemanticModel,
182+
range: TextRange,
183+
param_type: &LuaType,
184+
expr_type: &LuaType,
185+
result: TypeCheckResult,
186+
) {
187+
let db = semantic_model.get_db();
188+
match result {
189+
Ok(_) => (),
190+
Err(reason) => {
191+
let reason_message = match reason {
192+
TypeCheckFailReason::TypeNotMatchWithReason(reason) => reason,
193+
TypeCheckFailReason::TypeNotMatch | TypeCheckFailReason::DonotCheck => {
194+
"".to_string()
195+
}
196+
TypeCheckFailReason::TypeRecursion => "type recursion".to_string(),
197+
};
198+
context.add_diagnostic(
199+
DiagnosticCode::AttributeParamTypeMismatch,
200+
range,
201+
t!(
202+
"expected `%{source}` but found `%{found}`. %{reason}",
203+
source = humanize_lint_type(db, param_type),
204+
found = humanize_lint_type(db, expr_type),
205+
reason = reason_message
206+
)
207+
.to_string(),
208+
None,
209+
);
210+
}
211+
}
212+
}
213+
214+
fn is_nullable(typ: &LuaType) -> bool {
215+
let mut stack: Vec<LuaType> = Vec::new();
216+
stack.push(typ.clone());
217+
let mut visited = HashSet::new();
218+
while let Some(typ) = stack.pop() {
219+
if visited.contains(&typ) {
220+
continue;
221+
}
222+
visited.insert(typ.clone());
223+
match typ {
224+
LuaType::Any | LuaType::Unknown | LuaType::Nil => return true,
225+
LuaType::Union(u) => {
226+
for t in u.into_vec() {
227+
stack.push(t);
228+
}
229+
}
230+
_ => {}
231+
}
232+
}
233+
false
234+
}

crates/emmylua_code_analysis/src/diagnostic/checker/generic/infer_doc_type.rs

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
use std::sync::Arc;
22

33
use emmylua_parser::{
4-
LuaAstNode, LuaDocBinaryType, LuaDocDescriptionOwner, LuaDocFuncType, LuaDocGenericType,
5-
LuaDocMultiLineUnionType, LuaDocObjectFieldKey, LuaDocObjectType, LuaDocStrTplType, LuaDocType,
6-
LuaDocUnaryType, LuaDocVariadicType, LuaLiteralToken, LuaSyntaxKind, LuaTypeBinaryOperator,
7-
LuaTypeUnaryOperator,
4+
LuaAstNode, LuaDocAttributeType, LuaDocBinaryType, LuaDocDescriptionOwner, LuaDocFuncType,
5+
LuaDocGenericType, LuaDocMultiLineUnionType, LuaDocObjectFieldKey, LuaDocObjectType,
6+
LuaDocStrTplType, LuaDocType, LuaDocUnaryType, LuaDocVariadicType, LuaLiteralToken,
7+
LuaSyntaxKind, LuaTypeBinaryOperator, LuaTypeUnaryOperator,
88
};
99
use rowan::TextRange;
1010
use smol_str::SmolStr;
1111

1212
use crate::{
1313
AsyncState, InFiled, LuaAliasCallKind, LuaAliasCallType, LuaArrayLen, LuaArrayType,
14-
LuaFunctionType, LuaGenericType, LuaIndexAccessKey, LuaIntersectionType, LuaMultiLineUnion,
15-
LuaObjectType, LuaStringTplType, LuaTupleStatus, LuaTupleType, LuaType, LuaTypeDeclId,
16-
SemanticModel, TypeOps, VariadicType,
14+
LuaAttributeType, LuaFunctionType, LuaGenericType, LuaIndexAccessKey, LuaIntersectionType,
15+
LuaMultiLineUnion, LuaObjectType, LuaStringTplType, LuaTupleStatus, LuaTupleType, LuaType,
16+
LuaTypeDeclId, SemanticModel, TypeOps, VariadicType,
1717
};
1818

1919
pub fn infer_doc_type(semantic_model: &SemanticModel, node: &LuaDocType) -> LuaType {
@@ -108,6 +108,9 @@ pub fn infer_doc_type(semantic_model: &SemanticModel, node: &LuaDocType) -> LuaT
108108
LuaDocType::MultiLineUnion(multi_union) => {
109109
return infer_multi_line_union_type(semantic_model, multi_union);
110110
}
111+
LuaDocType::Attribute(attribute_type) => {
112+
return infer_attribute_type(semantic_model, attribute_type);
113+
}
111114
_ => {}
112115
}
113116
LuaType::Unknown
@@ -563,3 +566,35 @@ fn infer_multi_line_union_type(
563566

564567
LuaType::MultiLineUnion(LuaMultiLineUnion::new(union_members).into())
565568
}
569+
570+
fn infer_attribute_type(
571+
semantic_model: &SemanticModel,
572+
attribute_type: &LuaDocAttributeType,
573+
) -> LuaType {
574+
let mut params_result = Vec::new();
575+
for param in attribute_type.get_params() {
576+
let name = if let Some(param) = param.get_name_token() {
577+
param.get_name_text().to_string()
578+
} else if param.is_dots() {
579+
"...".to_string()
580+
} else {
581+
continue;
582+
};
583+
584+
let nullable = param.is_nullable();
585+
586+
let type_ref = if let Some(type_ref) = param.get_type() {
587+
let mut typ = infer_doc_type(semantic_model, &type_ref);
588+
if nullable && !typ.is_nullable() {
589+
typ = TypeOps::Union.apply(semantic_model.get_db(), &typ, &LuaType::Nil);
590+
}
591+
Some(typ)
592+
} else {
593+
None
594+
};
595+
596+
params_result.push((name, type_ref));
597+
}
598+
599+
LuaType::DocAttribute(LuaAttributeType::new(params_result).into())
600+
}

crates/emmylua_code_analysis/src/diagnostic/checker/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod access_invisible;
22
mod analyze_error;
33
mod assign_type_mismatch;
4+
mod attribute_check;
45
mod await_in_sync;
56
mod cast_type_mismatch;
67
mod check_field;
@@ -110,6 +111,7 @@ pub fn check_file(context: &mut DiagnosticContext, semantic_model: &SemanticMode
110111
run_check::<require_module_visibility::RequireModuleVisibilityChecker>(context, semantic_model);
111112
run_check::<unknown_doc_tag::UnknownDocTag>(context, semantic_model);
112113
run_check::<enum_value_mismatch::EnumValueMismatchChecker>(context, semantic_model);
114+
run_check::<attribute_check::AttributeCheckChecker>(context, semantic_model);
113115

114116
run_check::<code_style::non_literal_expressions_in_assert::NonLiteralExpressionsInAssertChecker>(
115117
context,

crates/emmylua_code_analysis/src/diagnostic/lua_diagnostic_code.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ pub enum DiagnosticCode {
105105
PreferredLocalAlias,
106106
/// readonly
107107
ReadOnly,
108+
/// attribute-param-type-mismatch
109+
AttributeParamTypeMismatch,
110+
/// attribute-missing-parameter
111+
AttributeMissingParameter,
112+
/// attribute-redundant-parameter
113+
AttributeRedundantParameter,
108114

109115
#[serde(other)]
110116
None,

crates/emmylua_parser/src/syntax/node/doc/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ impl LuaDocAttributeUse {
522522
self.child()
523523
}
524524

525-
pub fn get_attribute_call_arg_list(&self) -> Option<LuaDocAttributeCallArgList> {
525+
pub fn get_arg_list(&self) -> Option<LuaDocAttributeCallArgList> {
526526
self.child()
527527
}
528528
}

0 commit comments

Comments
 (0)