Skip to content

diagnostic #178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 11, 2025
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/emmylua_code_analysis/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ smol_str.workspace = true
serde_with.workspace = true
include_dir.workspace = true
emmylua_codestyle.workspace = true
itertools.workspace = true

[package.metadata.i18n]
available-locales = ["en", "zh_CN", "zh_HK"]
Expand Down
9 changes: 9 additions & 0 deletions crates/emmylua_code_analysis/locales/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,12 @@ Cannot use `...` outside a vararg function.:
en: 'Redefined local variable `%{name}`'
zh_CN: '重定义局部变量 `%{name}`'
zh_HK: '重定義局部變量 `%{name}`'
'Missing required fields in type `%{typ}`: %{fields}':
en: 'Missing required fields in type `%{typ}`: %{fields}'
zh_CN: '缺少类型 `%{typ}` 的必要字段:%{fields}'
zh_HK: '缺少類型 `%{typ}` 的必要字段:%{fields}'
'Fields cannot be injected into the reference of `%{class}` for `%{field}`. ':
en: 'Fields cannot be injected into the reference of `%{class}` for `%{field}`. '
zh_CN: '不能在 `%{class}` 的引用中注入字段 `%{field}` 。'
zh_HK: '不能在 `%{class}` 的引用中注入字段 `%{field}` 。'

33 changes: 33 additions & 0 deletions crates/emmylua_code_analysis/src/db_index/type/type_decl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,39 @@ impl LuaTypeDeclId {

&just_name
}

pub fn collect_super_types(&self, db: &DbIndex, collected_types: &mut Vec<LuaType>) {
// 必须广度优先
let mut queue = Vec::new();
queue.push(self.clone());

while let Some(current_id) = queue.pop() {
let super_types = db.get_type_index().get_super_types(&current_id);
if let Some(super_types) = super_types {
for super_type in super_types {
match &super_type {
LuaType::Ref(super_type_id) => {
if !collected_types.contains(&super_type) {
collected_types.push(super_type.clone());
queue.push(super_type_id.clone());
}
}
_ => {
if !collected_types.contains(&super_type) {
collected_types.push(super_type.clone());
}
}
}
}
}
}
}

pub fn collect_super_types_with_self(&self, db: &DbIndex, typ: LuaType) -> Vec<LuaType> {
let mut collected_types: Vec<LuaType> = vec![typ];
self.collect_super_types(db, &mut collected_types);
collected_types
}
}

impl Serialize for LuaTypeDeclId {
Expand Down
149 changes: 149 additions & 0 deletions crates/emmylua_code_analysis/src/diagnostic/checker/inject_field.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use std::collections::{HashMap, HashSet};

use emmylua_parser::{LuaAssignStat, LuaAstNode, LuaIndexExpr, LuaIndexKey, LuaVarExpr};

use crate::{DiagnosticCode, LuaMemberOwner, LuaType, SemanticModel};

use super::{get_lint_type_name, DiagnosticContext};

pub const CODES: &[DiagnosticCode] = &[DiagnosticCode::InjectField];

pub fn check(context: &mut DiagnosticContext, semantic_model: &SemanticModel) -> Option<()> {
let root = semantic_model.get_root().clone();
let mut type_cache = HashMap::new();
for stat in root.descendants::<LuaAssignStat>() {
let (vars, _) = stat.get_var_and_expr_list();
for var in vars.iter() {
if let LuaVarExpr::IndexExpr(index_expr) = var {
check_inject_field(context, semantic_model, index_expr, &mut type_cache);
}
}
}
Some(())
}

fn check_inject_field(
context: &mut DiagnosticContext,
semantic_model: &SemanticModel,
index_expr: &LuaIndexExpr,
type_cache: &mut HashMap<LuaType, (HashSet<String>, HashSet<LuaType>)>,
) -> Option<()> {
let db = context.db;
let prefix_expr = index_expr.get_prefix_expr()?;
let typ = semantic_model.infer_expr(prefix_expr)?;
let index_key = index_expr.get_index_key()?;
let index_name = index_key.get_path_part();

let (field_names, index_access_keys) = match &typ {
LuaType::Ref(type_decl_id) => type_cache.entry(typ.clone()).or_insert_with(|| {
let types = type_decl_id.collect_super_types_with_self(context.db, typ.clone());
get_all_field_info(context, &types).unwrap_or_default()
}),
LuaType::Generic(generic_type) => {
let type_decl_id = generic_type.get_base_type_id();
type_cache.entry(typ.clone()).or_insert_with(|| {
let types = type_decl_id.collect_super_types_with_self(context.db, typ.clone());
get_all_field_info(context, &types).unwrap_or_default()
})
}
LuaType::Def(_) => {
return Some(());
}
_ => type_cache
.entry(typ.clone())
.or_insert_with(|| get_all_field_info(context, &vec![typ.clone()]).unwrap_or_default()),
};

if !field_names.contains(&index_name) {
let mut need_diagnostic = true;
if !index_access_keys.is_empty() {
let index_type = match &index_key {
LuaIndexKey::Name(_) => LuaType::String,
LuaIndexKey::String(_) => LuaType::String,
LuaIndexKey::Integer(_) => LuaType::Integer,
LuaIndexKey::Expr(key_expr) => semantic_model
.infer_expr(key_expr.clone())
.unwrap_or(LuaType::Any),
LuaIndexKey::Idx(_) => LuaType::Integer,
};
for index_access_key in index_access_keys.iter() {
if semantic_model
.type_check(&index_access_key, &index_type)
.is_ok()
{
need_diagnostic = false;
break;
}
}
}

if need_diagnostic {
context.add_diagnostic(
DiagnosticCode::InjectField,
index_key.get_range()?,
t!(
"Fields cannot be injected into the reference of `%{class}` for `%{field}`. ",
class = get_lint_type_name(&db, &typ),
field = index_name,
)
.to_string(),
None,
);
}
}

Some(())
}

fn get_all_field_info(
context: &mut DiagnosticContext,
types: &Vec<LuaType>,
) -> Option<(HashSet<String>, HashSet<LuaType>)> {
let member_index = context.db.get_member_index();
let mut field_names: HashSet<String> = HashSet::new();
let mut index_access_keys: HashSet<LuaType> = HashSet::new();

for cur_type in types {
let type_decl_id = match cur_type {
LuaType::Ref(type_decl_id) => type_decl_id.clone(),
LuaType::Generic(generic_type) => generic_type.get_base_type_id().clone(),
// 处理 ---@class test: { a: number }
LuaType::Object(object_type) => {
let fields = object_type.get_fields();
for (key, _) in fields {
let name = key.to_path();
if name.is_empty() {
continue;
}
field_names.insert(name);
}

for (key, _) in object_type.get_index_access() {
index_access_keys.insert(key.clone());
}
continue;
}
// 处理 ---@class test: table<string, boolean>
LuaType::TableGeneric(table_type) => {
if let Some(key_type) = table_type.get(0) {
index_access_keys.insert(key_type.clone());
}
continue;
}
_ => continue,
};
if let Some(member_map) =
member_index.get_member_map(&LuaMemberOwner::Type(type_decl_id.clone()))
{
for (key, _) in member_map {
let name = key.to_path();
if name.is_empty() {
continue;
}
field_names.insert(name);
}
}
}

Some((field_names, index_access_keys))
}
159 changes: 159 additions & 0 deletions crates/emmylua_code_analysis/src/diagnostic/checker/missing_fields.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use std::collections::{HashMap, HashSet};

use emmylua_parser::{LuaAstNode, LuaTableExpr};

use crate::{DiagnosticCode, LuaMemberOwner, LuaType, LuaTypeDeclId, SemanticModel};

use super::{get_lint_type_name, DiagnosticContext};
use itertools::Itertools;

pub const CODES: &[DiagnosticCode] = &[DiagnosticCode::MissingFields];

pub fn check(context: &mut DiagnosticContext, semantic_model: &SemanticModel) -> Option<()> {
let root = semantic_model.get_root().clone();

let mut type_cache = HashMap::new();
for expr in root.descendants::<LuaTableExpr>() {
check_table_expr(context, semantic_model, &expr, &mut type_cache);
}
Some(())
}

fn check_table_expr(
context: &mut DiagnosticContext,
semantic_model: &SemanticModel,
expr: &LuaTableExpr,
type_cache: &mut HashMap<LuaType, HashSet<String>>,
) -> Option<()> {
let db = context.db;
let table_type = semantic_model.infer_table_should_be(expr.clone())?;

let current_fields = expr
.get_fields()
.filter_map(|field| field.get_field_key().map(|key| key.get_path_part()))
.collect();

let required_fields = match &table_type {
LuaType::Ref(type_decl_id) => type_cache.entry(table_type.clone()).or_insert_with(|| {
let types = type_decl_id.collect_super_types_with_self(context.db, table_type.clone());
get_required_fields(context, &types).unwrap_or_default()
}),
LuaType::Generic(generic_type) => {
let type_decl_id = generic_type.get_base_type_id();
type_cache.entry(table_type.clone()).or_insert_with(|| {
let types =
type_decl_id.collect_super_types_with_self(context.db, table_type.clone());
get_required_fields(context, &types).unwrap_or_default()
})
}
LuaType::Object(_) => type_cache.entry(table_type.clone()).or_insert_with(|| {
get_required_fields(context, &vec![table_type.clone()]).unwrap_or_default()
}),
_ => return Some(()),
};

let missing_fields = required_fields
.difference(&current_fields)
.map(|s| format!("`{}`", s))
.sorted()
.join(", ");

if !missing_fields.is_empty() {
context.add_diagnostic(
DiagnosticCode::MissingFields,
expr.get_range(),
t!(
"Missing required fields in type `%{typ}`: %{fields}",
typ = get_lint_type_name(&db, &table_type),
fields = missing_fields
)
.to_string(),
None,
);
}

Some(())
}

fn get_required_fields(
context: &mut DiagnosticContext,
// types 应为广度优先, 子类型会先于父类型被遍历, 而子类型的优先级高于父类型
types: &Vec<LuaType>,
) -> Option<HashSet<String>> {
let member_index = context.db.get_member_index();
let mut required_fields: HashSet<String> = HashSet::new();

let mut optional_type = HashSet::new();
for super_type in types {
match super_type {
LuaType::Ref(type_decl_id) => process_type_decl_id(
member_index,
&mut required_fields,
&mut optional_type,
type_decl_id.clone(),
),
LuaType::Generic(generic_type) => process_type_decl_id(
member_index,
&mut required_fields,
&mut optional_type,
generic_type.get_base_type_id().clone(),
),
// 处理 ---@class test: { a: number }
LuaType::Object(object_type) => {
let fields = object_type.get_fields();
for (key, decl_type) in fields {
let name = key.to_path();
record_required_fields(
&mut required_fields,
&mut optional_type,
name,
decl_type.clone(),
);
}
continue;
}
_ => continue,
};
}

fn process_type_decl_id(
member_index: &crate::LuaMemberIndex,
required_fields: &mut HashSet<String>,
optional_type: &mut HashSet<String>,
type_decl_id: LuaTypeDeclId,
) {
if let Some(member_map) = member_index.get_member_map(&LuaMemberOwner::Type(type_decl_id)) {
for (key, member_id) in member_map {
let Some(member) = member_index.get_member(&member_id) else {
continue;
};
let name = key.to_path();
let decl_type = member.get_decl_type();
record_required_fields(required_fields, optional_type, name, decl_type);
}
}
}

Some(required_fields)
}

fn record_required_fields(
required_fields: &mut HashSet<String>,
optional_type: &mut HashSet<String>,
name: String,
decl_type: LuaType,
) {
if name.is_empty() {
return;
}

if decl_type.is_optional() {
optional_type.insert(name);
return;
}
if optional_type.contains(&name) {
return;
}

required_fields.insert(name);
}
Loading