Skip to content

Commit cba0680

Browse files
committed
diagnostic add DuplicateDocField, DuplicateSetField, DuplicateIndex
1 parent 7aa70dc commit cba0680

File tree

12 files changed

+724
-21
lines changed

12 files changed

+724
-21
lines changed

crates/emmylua_code_analysis/locales/lint.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,4 +233,12 @@ Cannot use `...` outside a vararg function.:
233233
en: "Duplicate class constructor '%{name}'. constructor must have only one."
234234
zh_CN: "类有重复的 (constructor) 定义 '%{name}'。(constructor) 必须只有一个。"
235235
zh_HK: "類有重複的 (constructor) 定義 '%{name}'。(constructor) 必須只有一個。"
236+
"Duplicate field `%{name}`.":
237+
en: "Duplicate field `%{name}`."
238+
zh_CN: "重复定义的字段 `%{name}`."
239+
zh_HK: "重複定義的字段 `%{name}`."
240+
"Duplicate index `%{name}`.":
241+
en: "Duplicate index `%{name}`."
242+
zh_CN: "重复定义的索引 `%{name}`."
243+
zh_HK: "重複定義的索引 `%{name}`."
236244

crates/emmylua_code_analysis/resources/schema.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,20 @@
474474
"enum": [
475475
"unnecessary-if"
476476
]
477+
},
478+
{
479+
"description": "duplicate-set-field",
480+
"type": "string",
481+
"enum": [
482+
"duplicate-set-field"
483+
]
484+
},
485+
{
486+
"description": "duplicate-index",
487+
"type": "string",
488+
"enum": [
489+
"duplicate-index"
490+
]
477491
}
478492
]
479493
},
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use std::collections::HashMap;
2+
3+
use emmylua_parser::{LuaAstNode, LuaDocTag, LuaDocTagClass};
4+
5+
use crate::{DiagnosticCode, LuaMember, LuaMemberFeature, LuaMemberKey, LuaType, SemanticModel};
6+
7+
use super::{Checker, DiagnosticContext};
8+
9+
pub struct DuplicateFieldChecker;
10+
11+
impl Checker for DuplicateFieldChecker {
12+
const CODES: &[DiagnosticCode] = &[
13+
DiagnosticCode::DuplicateDocField,
14+
DiagnosticCode::DuplicateSetField,
15+
];
16+
17+
fn check(context: &mut DiagnosticContext, semantic_model: &SemanticModel) {
18+
let root = semantic_model.get_root().clone();
19+
20+
for tag in root.descendants::<LuaDocTag>() {
21+
match tag {
22+
LuaDocTag::Class(class_tag) => {
23+
check_class_duplicate_field(context, semantic_model, class_tag);
24+
}
25+
_ => {}
26+
}
27+
}
28+
}
29+
}
30+
31+
struct DiagnosticMemberInfo<'a> {
32+
typ: LuaType,
33+
feature: LuaMemberFeature,
34+
member: &'a LuaMember,
35+
}
36+
37+
fn check_class_duplicate_field(
38+
context: &mut DiagnosticContext,
39+
semantic_model: &SemanticModel,
40+
class_tag: LuaDocTagClass,
41+
) -> Option<()> {
42+
let file_id = context.file_id;
43+
let name_token = class_tag.get_name_token()?;
44+
let name = name_token.get_name_text();
45+
let type_decl = context
46+
.get_db()
47+
.get_type_index()
48+
.find_type_decl(file_id, name)?;
49+
50+
let members = semantic_model
51+
.get_db()
52+
.get_member_index()
53+
.get_members(&type_decl.get_id().into())?;
54+
55+
let mut member_map: HashMap<&LuaMemberKey, Vec<&LuaMember>> = HashMap::new();
56+
57+
for member in members.iter() {
58+
// 过滤掉 meta 定义的 signature
59+
match member.get_feature() {
60+
LuaMemberFeature::MetaMethodDecl => {
61+
continue;
62+
}
63+
_ => {}
64+
}
65+
66+
member_map
67+
.entry(member.get_key())
68+
.or_insert_with(Vec::new)
69+
.push(*member);
70+
}
71+
72+
for (key, members) in member_map.iter() {
73+
if members.len() < 2 {
74+
continue;
75+
}
76+
77+
let mut member_infos = Vec::new();
78+
for member in members.iter() {
79+
let typ = semantic_model.get_type(member.get_id().into());
80+
let feature = member.get_feature();
81+
member_infos.push(DiagnosticMemberInfo {
82+
typ,
83+
feature,
84+
member,
85+
});
86+
}
87+
88+
// 1. 检查 signature
89+
let signatures = member_infos
90+
.iter()
91+
.filter(|info| matches!(info.typ, LuaType::Signature(_)));
92+
if signatures.clone().count() > 1 {
93+
for signature in signatures {
94+
if signature.member.get_file_id() != file_id {
95+
continue;
96+
}
97+
context.add_diagnostic(
98+
DiagnosticCode::DuplicateSetField,
99+
signature.member.get_range(),
100+
t!("Duplicate field `%{name}`.", name = key.to_path()).to_string(),
101+
None,
102+
);
103+
}
104+
}
105+
106+
// 2. 检查 ---@field 成员
107+
let field_decls = member_infos
108+
.iter()
109+
.filter(|info| info.feature.is_field_decl())
110+
.collect::<Vec<_>>();
111+
// 如果 field_decls 数量大于1,则进一步检查
112+
if field_decls.len() > 1 {
113+
// 检查是否所有 field_decls 都是 DocFunction
114+
let all_doc_functions = field_decls
115+
.iter()
116+
.all(|info| matches!(info.typ, LuaType::DocFunction(_)));
117+
118+
// 如果不全是 DocFunction,则报错
119+
if !all_doc_functions {
120+
for field_decl in &field_decls {
121+
if field_decl.member.get_file_id() == file_id {
122+
context.add_diagnostic(
123+
DiagnosticCode::DuplicateDocField,
124+
// TODO: 范围缩小到名称而不是整个 ---@field
125+
field_decl.member.get_range(),
126+
t!("Duplicate field `%{name}`.", name = key.to_path()).to_string(),
127+
None,
128+
);
129+
}
130+
}
131+
}
132+
}
133+
}
134+
135+
Some(())
136+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use std::collections::HashMap;
2+
3+
use emmylua_parser::{LuaAstNode, LuaIndexKey, LuaTableExpr};
4+
5+
use crate::{DiagnosticCode, SemanticModel};
6+
7+
use super::{Checker, DiagnosticContext};
8+
9+
pub struct DuplicateIndexChecker;
10+
11+
impl Checker for DuplicateIndexChecker {
12+
const CODES: &[DiagnosticCode] = &[DiagnosticCode::DuplicateIndex];
13+
14+
fn check(context: &mut DiagnosticContext, semantic_model: &SemanticModel) {
15+
let root = semantic_model.get_root().clone();
16+
for table in root.descendants::<LuaTableExpr>() {
17+
check_table_duplicate_index(context, semantic_model, table);
18+
}
19+
}
20+
}
21+
22+
fn check_table_duplicate_index(
23+
context: &mut DiagnosticContext,
24+
_: &SemanticModel,
25+
table: LuaTableExpr,
26+
) -> Option<()> {
27+
let fields = table.get_fields();
28+
let mut index_map: HashMap<String, Vec<LuaIndexKey>> = HashMap::new();
29+
30+
for field in fields {
31+
let key = field.get_field_key();
32+
if let Some(key) = key {
33+
index_map
34+
.entry(key.get_path_part().to_string())
35+
.or_insert_with(Vec::new)
36+
.push(key);
37+
}
38+
}
39+
40+
for (name, keys) in index_map {
41+
if keys.len() > 1 {
42+
for key in keys {
43+
let range = if let Some(range) = key.get_range() {
44+
range
45+
} else {
46+
continue;
47+
};
48+
context.add_diagnostic(
49+
DiagnosticCode::DuplicateIndex,
50+
range,
51+
t!("Duplicate index `%{name}`.", name = name).to_string(),
52+
None,
53+
);
54+
}
55+
}
56+
}
57+
58+
Some(())
59+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ mod code_style;
1010
mod code_style_check;
1111
mod deprecated;
1212
mod discard_returns;
13+
mod duplicate_field;
14+
mod duplicate_index;
1315
mod duplicate_require;
1416
mod duplicate_type;
1517
mod incomplete_signature_doc;
@@ -87,6 +89,8 @@ pub fn check_file(context: &mut DiagnosticContext, semantic_model: &SemanticMode
8789
run_check::<check_return_count::CheckReturnCount>(context, semantic_model);
8890
run_check::<unbalanced_assignments::UnbalancedAssignmentsChecker>(context, semantic_model);
8991
run_check::<check_param_count::CheckParamCountChecker>(context, semantic_model);
92+
run_check::<duplicate_field::DuplicateFieldChecker>(context, semantic_model);
93+
run_check::<duplicate_index::DuplicateIndexChecker>(context, semantic_model);
9094

9195
run_check::<code_style::non_literal_expressions_in_assert::NonLiteralExpressionsInAssertChecker>(
9296
context,

crates/emmylua_code_analysis/src/diagnostic/lua_diagnostic_code.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ pub enum DiagnosticCode {
9292
UnnecessaryAssert,
9393
/// unnecessary-if
9494
UnnecessaryIf,
95+
/// duplicate-set-field
96+
DuplicateSetField,
97+
/// duplicate-index
98+
DuplicateIndex,
9599

96100
#[serde(skip)]
97101
All,

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

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#[cfg(test)]
2+
mod test {
3+
use crate::{DiagnosticCode, VirtualWorkspace};
4+
5+
#[test]
6+
fn test_duplicate_field() {
7+
let mut ws = VirtualWorkspace::new();
8+
9+
assert!(!ws.check_code_for_namespace(
10+
DiagnosticCode::DuplicateDocField,
11+
r#"
12+
---@class Test
13+
---@field name string
14+
---@field name string
15+
local Test = {}
16+
17+
Test.name = 1
18+
"#
19+
));
20+
21+
assert!(ws.check_code_for_namespace(
22+
DiagnosticCode::DuplicateDocField,
23+
r#"
24+
---@class Test
25+
---@field name string
26+
---@field age number
27+
local Test = {}
28+
"#
29+
));
30+
31+
assert!(!ws.check_code_for_namespace(
32+
DiagnosticCode::DuplicateDocField,
33+
r#"
34+
---@class Test
35+
---@field name string
36+
---@field name number
37+
local Test = {}
38+
"#
39+
));
40+
41+
assert!(ws.check_code_for_namespace(
42+
DiagnosticCode::DuplicateDocField,
43+
r#"
44+
---@class Test1
45+
---@field name string
46+
47+
---@class Test2
48+
---@field name string
49+
"#
50+
));
51+
}
52+
53+
#[test]
54+
fn test_duplicate_function_1() {
55+
let mut ws = VirtualWorkspace::new();
56+
57+
assert!(ws.check_code_for_namespace(
58+
DiagnosticCode::DuplicateDocField,
59+
r#"
60+
---@class Test
61+
---@field a fun()
62+
local Test = {}
63+
64+
function Test.a()
65+
end
66+
"#
67+
));
68+
69+
assert!(ws.check_code_for_namespace(
70+
DiagnosticCode::DuplicateDocField,
71+
r#"
72+
---@class Test
73+
---@field a fun()
74+
---@field a fun()
75+
local Test = {}
76+
77+
function Test.a()
78+
end
79+
"#
80+
));
81+
82+
assert!(!ws.check_code_for_namespace(
83+
DiagnosticCode::DuplicateSetField,
84+
r#"
85+
---@class Test
86+
---@field a fun()
87+
local Test = {}
88+
89+
function Test.a()
90+
end
91+
92+
function Test.a()
93+
end
94+
"#
95+
));
96+
}
97+
}

0 commit comments

Comments
 (0)