Skip to content

Commit 025f5c3

Browse files
committed
add diagnostic: EnumValueMismatch
1 parent ac7314c commit 025f5c3

7 files changed

Lines changed: 327 additions & 0 deletions

File tree

crates/emmylua_code_analysis/locales/lint.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,3 +268,8 @@ Cannot use `...` outside a vararg function.:
268268
"Unknown doc tag: `%{name}`":
269269
en: "Unknown doc tag: `%{name}`"
270270
# TODO: translate
271+
272+
"Value '%{value}' does not match any enum value. Expected one of: %{enum_values}":
273+
en: "Value '%{value}' does not match any enum value. Expected one of: %{enum_values}"
274+
zh_CN: "值 '%{value}' 与任何枚举值都不匹配。应为以下之一: %{enum_values}"
275+
zh_HK: "值 '%{value}' 與任何枚舉值都不匹配。應為以下之一: %{enum_values}"

crates/emmylua_code_analysis/resources/schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,11 @@
401401
"description": "require-module-not-visible",
402402
"type": "string",
403403
"const": "require-module-not-visible"
404+
},
405+
{
406+
"description": "enum-value-mismatch",
407+
"type": "string",
408+
"const": "enum-value-mismatch"
404409
}
405410
]
406411
},
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
use emmylua_parser::{BinaryOperator, LuaAst, LuaAstNode, LuaBinaryExpr, LuaExpr};
2+
3+
use crate::{
4+
diagnostic::checker::humanize_lint_type, DiagnosticCode, LuaMemberKey, LuaType, LuaTypeDeclId,
5+
SemanticModel,
6+
};
7+
8+
use super::{Checker, DiagnosticContext};
9+
10+
pub struct EnumValueMismatchChecker;
11+
12+
impl Checker for EnumValueMismatchChecker {
13+
const CODES: &[DiagnosticCode] = &[DiagnosticCode::EnumValueMismatch];
14+
15+
fn check(context: &mut DiagnosticContext, semantic_model: &SemanticModel) {
16+
let root = semantic_model.get_root().clone();
17+
for node in root.descendants::<LuaAst>() {
18+
let condition_expr = match node {
19+
LuaAst::LuaIfStat(if_stat) => if_stat.get_condition_expr(),
20+
LuaAst::LuaElseIfClauseStat(elseif_stat) => elseif_stat.get_condition_expr(),
21+
_ => None,
22+
};
23+
24+
if let Some(expr) = condition_expr {
25+
check_condition_expr(context, semantic_model, expr);
26+
}
27+
}
28+
}
29+
}
30+
31+
fn check_condition_expr(
32+
context: &mut DiagnosticContext,
33+
semantic_model: &SemanticModel,
34+
condition_expr: LuaExpr,
35+
) -> Option<()> {
36+
if let LuaExpr::BinaryExpr(binary_expr) = condition_expr {
37+
check_binary_expr(context, semantic_model, binary_expr);
38+
}
39+
Some(())
40+
}
41+
42+
fn check_binary_expr(
43+
context: &mut DiagnosticContext,
44+
semantic_model: &SemanticModel,
45+
binary_expr: LuaBinaryExpr,
46+
) -> Option<()> {
47+
let op_token = binary_expr.get_op_token()?;
48+
let operator = op_token.get_op();
49+
50+
if !matches!(operator, BinaryOperator::OpEq | BinaryOperator::OpNe) {
51+
return Some(());
52+
}
53+
54+
let (left_expr, right_expr) = binary_expr.get_exprs()?;
55+
let left_type = semantic_model.infer_expr(left_expr.clone()).ok()?;
56+
let right_type = semantic_model.infer_expr(right_expr.clone()).ok()?;
57+
58+
if check_enum_value_pair(context, &right_expr, &left_type, &right_type).is_some() {
59+
return Some(());
60+
}
61+
if check_enum_value_pair(context, &left_expr, &right_type, &left_type).is_some() {
62+
return Some(());
63+
}
64+
65+
Some(())
66+
}
67+
68+
fn check_enum_value_pair(
69+
context: &mut DiagnosticContext,
70+
value_expr: &LuaExpr,
71+
enum_type: &LuaType,
72+
value_type: &LuaType,
73+
) -> Option<()> {
74+
let enum_decl_id = match &enum_type {
75+
LuaType::Ref(id) | LuaType::Def(id) => id,
76+
_ => return None,
77+
};
78+
let type_decl = context.db.get_type_index().get_type_decl(enum_decl_id)?;
79+
if !type_decl.is_enum() {
80+
return None;
81+
}
82+
83+
let constant_type = get_constant_type(value_type)?;
84+
let enum_value_types = get_enum_value_types(context, enum_decl_id)?;
85+
86+
if !enum_value_types.contains(constant_type) {
87+
let constant_value_str = humanize_lint_type(context.db, constant_type);
88+
let enum_values_str: Vec<String> = enum_value_types
89+
.iter()
90+
.map(|typ| humanize_lint_type(context.db, typ))
91+
.collect();
92+
context.add_diagnostic(
93+
DiagnosticCode::EnumValueMismatch,
94+
value_expr.get_range(),
95+
t!(
96+
"Value '%{value}' does not match any enum value. Expected one of: %{enum_values}",
97+
value = constant_value_str,
98+
enum_values = enum_values_str.join(", ")
99+
)
100+
.to_string(),
101+
None,
102+
);
103+
}
104+
105+
Some(())
106+
}
107+
108+
fn get_enum_value_types(
109+
context: &DiagnosticContext,
110+
enum_decl_id: &LuaTypeDeclId,
111+
) -> Option<Vec<LuaType>> {
112+
let type_decl = context.db.get_type_index().get_type_decl(enum_decl_id)?;
113+
let mut values = Vec::new();
114+
let is_enum_key = type_decl.is_enum_key();
115+
116+
if let Some(members) = context
117+
.db
118+
.get_member_index()
119+
.get_members(&enum_decl_id.clone().into())
120+
{
121+
for member in members {
122+
if is_enum_key {
123+
let key = member.get_key();
124+
match key {
125+
LuaMemberKey::Name(name) => {
126+
values.push(LuaType::StringConst(name.clone().into()));
127+
}
128+
LuaMemberKey::Integer(i) => {
129+
values.push(LuaType::IntegerConst(*i));
130+
}
131+
LuaMemberKey::ExprType(typ) => {
132+
if let Some(value) = get_constant_type(typ) {
133+
values.push(value.clone());
134+
}
135+
}
136+
_ => {}
137+
}
138+
} else {
139+
if let Some(type_cache) = context
140+
.db
141+
.get_type_index()
142+
.get_type_cache(&member.get_id().into())
143+
{
144+
if let Some(value) = get_constant_type(type_cache.as_type()) {
145+
values.push(value.clone());
146+
}
147+
}
148+
}
149+
}
150+
}
151+
152+
Some(values)
153+
}
154+
155+
fn get_constant_type(typ: &LuaType) -> Option<&LuaType> {
156+
match typ {
157+
LuaType::StringConst(_)
158+
| LuaType::DocStringConst(_)
159+
| LuaType::IntegerConst(_)
160+
| LuaType::DocIntegerConst(_)
161+
| LuaType::FloatConst(_)
162+
| LuaType::BooleanConst(_)
163+
| LuaType::DocBooleanConst(_)
164+
| LuaType::TableConst(_) => Some(typ),
165+
_ => None,
166+
}
167+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod duplicate_field;
1515
mod duplicate_index;
1616
mod duplicate_require;
1717
mod duplicate_type;
18+
mod enum_value_mismatch;
1819
mod generic;
1920
mod incomplete_signature_doc;
2021
mod local_const_reassign;
@@ -102,6 +103,7 @@ pub fn check_file(context: &mut DiagnosticContext, semantic_model: &SemanticMode
102103
run_check::<cast_type_mismatch::CastTypeMismatchChecker>(context, semantic_model);
103104
run_check::<require_module_visibility::RequireModuleVisibilityChecker>(context, semantic_model);
104105
run_check::<unknown_doc_tag::UnknownDocTag>(context, semantic_model);
106+
run_check::<enum_value_mismatch::EnumValueMismatchChecker>(context, semantic_model);
105107

106108
run_check::<code_style::non_literal_expressions_in_assert::NonLiteralExpressionsInAssertChecker>(
107109
context,

crates/emmylua_code_analysis/src/diagnostic/lua_diagnostic_code.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ pub enum DiagnosticCode {
9999
CastTypeMismatch,
100100
/// require-module-not-visible
101101
RequireModuleNotVisible,
102+
/// enum-value-mismatch
103+
EnumValueMismatch,
102104

103105
#[serde(other)]
104106
None,
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#[cfg(test)]
2+
mod tests {
3+
use crate::{DiagnosticCode, VirtualWorkspace};
4+
5+
#[test]
6+
fn test_enum_value_mismatch_string() {
7+
let mut ws = VirtualWorkspace::new();
8+
9+
assert!(!ws.check_code_for(
10+
DiagnosticCode::EnumValueMismatch,
11+
r#"
12+
---@enum Status
13+
local Status = {
14+
PENDING = "pending",
15+
COMPLETED = "completed",
16+
FAILED = "failed"
17+
}
18+
19+
---@type Status
20+
local status
21+
22+
if status == "invalid" then
23+
end
24+
"#,
25+
));
26+
}
27+
28+
#[test]
29+
fn test_enum_value_mismatch_number() {
30+
let mut ws = VirtualWorkspace::new();
31+
32+
assert!(!ws.check_code_for(
33+
DiagnosticCode::EnumValueMismatch,
34+
r#"
35+
---@enum ErrorCode
36+
local ErrorCode = {
37+
SUCCESS = 0,
38+
NOT_FOUND = 404,
39+
SERVER_ERROR = 500
40+
}
41+
42+
---@type ErrorCode
43+
local code
44+
45+
if code == 999 then
46+
end
47+
"#,
48+
));
49+
}
50+
51+
#[test]
52+
fn test_enum_value_mismatch_elseif() {
53+
let mut ws = VirtualWorkspace::new();
54+
55+
assert!(!ws.check_code_for(
56+
DiagnosticCode::EnumValueMismatch,
57+
r#"
58+
---@enum State
59+
local State = {
60+
ACTIVE = "active",
61+
INACTIVE = "inactive"
62+
}
63+
64+
---@type State
65+
local state
66+
67+
if state == "active" then
68+
elseif state == "unknown" then
69+
end
70+
"#,
71+
));
72+
}
73+
74+
#[test]
75+
fn test_enum_value_mismatch_reverse_order() {
76+
let mut ws = VirtualWorkspace::new();
77+
78+
assert!(!ws.check_code_for(
79+
DiagnosticCode::EnumValueMismatch,
80+
r#"
81+
---@enum Color
82+
local Color = {
83+
RED = "red",
84+
GREEN = "green",
85+
BLUE = "blue"
86+
}
87+
88+
---@type Color
89+
local color
90+
91+
if "purple" == color then
92+
end
93+
"#,
94+
));
95+
}
96+
97+
#[test]
98+
fn test_enum_value_valid_cases() {
99+
let mut ws = VirtualWorkspace::new();
100+
101+
assert!(ws.check_code_for(
102+
DiagnosticCode::EnumValueMismatch,
103+
r#"
104+
---@enum Status
105+
local Status = {
106+
PENDING = "pending",
107+
COMPLETED = "completed"
108+
}
109+
110+
---@type Status
111+
local status
112+
113+
if status == "pending" then
114+
elseif status == "completed" then
115+
end
116+
"#,
117+
));
118+
}
119+
120+
#[test]
121+
fn test_enum_value_valid_exact_matches() {
122+
let mut ws = VirtualWorkspace::new();
123+
124+
assert!(ws.check_code_for(
125+
DiagnosticCode::EnumValueMismatch,
126+
r#"
127+
---@enum Numbers
128+
local Numbers = {
129+
ONE = 1,
130+
TWO = 2,
131+
THREE = 3
132+
}
133+
134+
---@type Numbers
135+
local num = Numbers.ONE
136+
137+
if num == 1 then
138+
end
139+
140+
if num == 2 then
141+
end
142+
"#,
143+
));
144+
}
145+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod disable_line_test;
88
mod duplicate_field_test;
99
mod duplicate_index_test;
1010
mod duplicate_require_test;
11+
mod enum_value_mismatch_test;
1112
mod generic_constraint_mismatch_test;
1213
mod incomplete_signature_doc_test;
1314
mod inject_field_test;

0 commit comments

Comments
 (0)