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
40 changes: 15 additions & 25 deletions crates/emmylua_code_analysis/src/check/checker/attribute_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,29 +274,19 @@ fn add_type_check_diagnostic(
arg_type: &LuaType,
param_type: &LuaType,
) {
match check_type_detail(semantic_model, arg_type, param_type) {
Ok(()) => {}
Err(reason) => {
let reason_message = match reason {
TypeCheckFailReason::TypeNotMatchWithReason(reason) => reason,
TypeCheckFailReason::TypeNotMatch => String::new(),
TypeCheckFailReason::TypeRecursion => "type recursion".to_string(),
};
let reason = if reason_message.is_empty() {
String::new()
} else {
format!(" {}", reason_message)
};
context.add_diagnostic(
DiagnosticCode::AttributeParamTypeMismatch,
range,
t!(
"expected `%{expected}` but found `%{found}`.%{reason}",
expected = humanize_type(semantic_model, param_type),
found = humanize_type(semantic_model, arg_type),
reason = reason
),
);
}
}
let reason = match check_type_detail(semantic_model, arg_type, param_type) {
Err(TypeCheckFailReason::TypeNotMatchWithReason(reason)) => format!(" {reason}"),
Err(TypeCheckFailReason::TypeNotMatch) => String::new(),
_ => return,
};
context.add_diagnostic(
DiagnosticCode::AttributeParamTypeMismatch,
range,
t!(
"expected `%{expected}` but found `%{found}`.%{reason}",
expected = humanize_type(semantic_model, param_type),
found = humanize_type(semantic_model, arg_type),
reason = reason
),
);
}
8 changes: 5 additions & 3 deletions crates/emmylua_code_analysis/src/check/checker/check_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use emmylua_parser::{
use crate::DiagnosticCode;
use crate::semantic_db::def::{SemanticId, TypeDef, TypeDefKind};
use crate::semantic_model::SemanticModel;
use crate::semantic_model::type_eval::expand_alias_branches;
use crate::{LuaMemberKey, LuaType, LuaTypeDeclId, LuaUnionType};

use super::assign_type_mismatch::signature_matches_key;
Expand Down Expand Up @@ -485,7 +486,7 @@ fn check_against_type(
// MissingFields: required (non-nullable) members are missing.
let mut missing: Vec<_> = members
.iter()
.filter(|(key, ty)| !field_keys.contains(*key) && !is_optional(ty))
.filter(|(key, ty)| !field_keys.contains(*key) && !is_optional(semantic_model, ty))
.map(|(key, _)| key.to_path())
.collect();
missing.sort_unstable();
Expand Down Expand Up @@ -577,8 +578,9 @@ fn find_type_def(semantic_model: &SemanticModel<'_>, type_def_id: &SemanticId) -
}

/// Whether the field is optional (nullable / unknown).
fn is_optional(ty: &LuaType) -> bool {
match ty {
fn is_optional(semantic_model: &SemanticModel<'_>, ty: &LuaType) -> bool {
let ty = expand_alias_branches(semantic_model, ty).unwrap_or_else(|| ty.clone());
match &ty {
LuaType::Any | LuaType::Unknown | LuaType::Nil => true,
LuaType::Union(union) => union.into_vec().iter().any(|t| matches!(t, LuaType::Nil)),
_ => false,
Expand Down
18 changes: 11 additions & 7 deletions crates/emmylua_code_analysis/src/check/checker/duplicate_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::FileId;
use crate::LuaType;
use crate::semantic_db::def::{Member, SemanticId};
use crate::semantic_model::SemanticModel;
use crate::semantic_model::type_eval::expand_alias_head;

use super::{CheckContext, Checker};

Expand Down Expand Up @@ -163,7 +164,7 @@ fn required_module_file(
semantic_model.module_file_of(&module_name)
}

/// Whether a runtime member is a function/closure definition.
/// Whether a member resolves to a function or has a closure value.
fn is_function_like(semantic_model: &SemanticModel<'_>, member: &Member) -> bool {
if let Some(value_syntax) = member.value_syntax
&& let Some(tree) = semantic_model.syntax_tree()
Expand All @@ -172,10 +173,13 @@ fn is_function_like(semantic_model: &SemanticModel<'_>, member: &Member) -> bool
{
return true;
}
semantic_model.type_of_member(&member.id).is_some_and(|ty| {
matches!(
ty,
LuaType::DocFunction(_) | LuaType::Signature(_) | LuaType::Function
)
})
semantic_model
.type_of_member(&member.id)
.and_then(|ty| expand_alias_head(semantic_model, &ty))
.is_some_and(|ty| {
matches!(
ty,
LuaType::DocFunction(_) | LuaType::Signature(_) | LuaType::Function
)
})
}
19 changes: 19 additions & 0 deletions crates/emmylua_code_analysis/src/check/checker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,25 @@ pub fn check_file(semantic_model: &SemanticModel<'_>, config: Arc<CheckConfig>)
run_checked::<check_field::CheckFieldChecker>(&mut context, semantic_model);
run_checked::<discard_returns::DiscardReturnsChecker>(&mut context, semantic_model);
run_checked::<global_non_module::GlobalInNonModuleChecker>(&mut context, semantic_model);
let mut ranges: Vec<_> = semantic_model
.type_evaluation_errors()
.into_iter()
.map(|origin| {
origin
.and_then(|id| semantic_model.type_def_of(&id))
.filter(|def| def.file_id == semantic_model.file_id())
.map_or(TextRange::empty(0.into()), |def| def.name_range)
})
.collect();
ranges.sort_unstable_by_key(|range| (range.start(), range.end()));
ranges.dedup();
for range in ranges {
context.add_diagnostic(
DiagnosticCode::TypeComplexity,
range,
t!("Type evaluation is too complex or possibly infinite."),
);
}
context.into_diagnostics()
}

Expand Down
2 changes: 2 additions & 0 deletions crates/emmylua_code_analysis/src/check/diagnostic_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub enum DiagnosticCode {
DocSyntaxError,
/// Type not found
TypeNotFound,
/// Type evaluation exceeded its resource budget
TypeComplexity,
/// Missing return statement
MissingReturn,
/// Param Type not match
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1906,6 +1906,9 @@ return t
---@class B
---@class C
---@class D
---@alias Pair A | B
---@alias TooWide A | B | C | D
---@alias Identity<T> T

---@param a A | B | C
---@return boolean
Expand All @@ -1914,21 +1917,23 @@ return t
end
"#,
);
assert!(!ws.has_no_diagnostic(
DiagnosticCode::AssignTypeMismatch,
r#"
---@type fun(a: A | B | C | D): boolean
local tmp = condition
"#,
));

assert!(ws.has_no_diagnostic(
DiagnosticCode::AssignTypeMismatch,
r#"
---@type fun(a: A | B): boolean
local tmp = condition
"#,
));
for (parameter, accepted) in [
("A | B | C | D", false),
("TooWide", false),
("Identity<A | B | C | D>", false),
("A | B", true),
("Pair", true),
("Identity<A | B>", true),
] {
assert_eq!(
ws.has_no_diagnostic(
DiagnosticCode::AssignTypeMismatch,
&format!("---@type fun(a: {parameter}): boolean\nlocal tmp = condition"),
),
accepted,
"{parameter}"
);
}
}

#[test]
Expand Down
50 changes: 38 additions & 12 deletions crates/emmylua_code_analysis/src/check/test/duplicate_field_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,44 @@ mod test {
"#
));

assert!(ws.has_no_diagnostic_in_namespace(
DiagnosticCode::DuplicateDocField,
r#"
---@class Test
---@field a fun()
---@field a fun()
local Test = {}

function Test.a()
end
"#
));
for (first, second, overloads) in [
(
"fun(value: string): string",
"fun(value: integer): integer",
true,
),
("Callback<string>", "Callback<integer>", true),
(
"Identity<Callback<string>>",
"Identity<Callback<integer>>",
true,
),
("Choose<string>", "Choose<'fixed'>", true),
("Identity<string>", "Identity<integer>", false),
("Callback<string>", "Identity<integer>", false),
] {
assert_eq!(
ws.has_no_diagnostic_in_namespace(
DiagnosticCode::DuplicateDocField,
&format!(
r#"
---@alias Callback<T> fun(value: T): T
---@alias Identity<T> T
---@alias Choose<T> T extends string and Callback<T> or integer
---@class Test
---@field a {first}
---@field a {second}
local Test = {{}}

function Test.a()
end
"#
)
),
overloads,
"field types: {first}, {second}"
);
}

assert!(!ws.has_no_diagnostic_in_namespace(
DiagnosticCode::DuplicateSetField,
Expand Down
40 changes: 32 additions & 8 deletions crates/emmylua_code_analysis/src/check/test/missing_fields_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,22 +340,46 @@ foo({})
}

#[test]
fn test_multiline_union_nil_field_is_optional() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::MissingFields,
r#"
fn test_alias_field_optionality() {
for (field_type, optional) in [
("PersonAge", true),
("Maybe<integer>", true),
("Wrapped<integer>", true),
("Identity<unknown>", true),
("Optional<string>", true),
("Identity<integer>", false),
("Optional<integer>", false),
("Identity<Node>", false),
] {
let mut ws = VirtualWorkspace::new();
assert_eq!(
ws.has_no_diagnostic(
DiagnosticCode::MissingFields,
&format!(
r#"
---@alias PersonAge
--- | integer
--- | nil
---@alias Maybe<T> T | nil
---@alias Wrapped<T> Maybe<T> | T
---@alias Identity<T> T
---@alias Optional<T> T extends string and nil or T

---@class Node
---@field next Node

---@class Person
---@field name string
---@field age PersonAge
---@field age {field_type}

---@type Person
local person = { name = "123" }
local person = {{ name = "123" }}
"#
));
)
),
optional,
"field type: {field_type}"
);
}
}
}
1 change: 1 addition & 0 deletions crates/emmylua_code_analysis/src/check/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ mod param_type_check_test;
mod readonly_test;
mod redefined_local_test;
mod redundant_parameter_test;
mod reentry_test;
mod require_module_visibility_test;
mod return_type_mismatch_test;
mod syntax_error_test;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1680,23 +1680,39 @@ mod test {
#[test]
fn test_index_access_with_keyof_alias() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::ParamTypeMismatch,
ws.def(
r#"
---@class A
---@field one 1

---@class B
---@field one string
---@alias Key 'one'
---@alias KeyofA keyof A
---@type A[KeyofA]
local tmp

---@param v 1
local function test(v)
end

test(tmp)
"#,
));
---@alias KeyofB keyof B
"#,
);
for indexed in [
"A[KeyofA]",
"A[Key]",
"A[KeyofB]",
"RawGet<A, Key>",
"RawGet<A, keyof B>",
"[1, string][0]",
"RawGet<[1, string], 0>",
] {
for (parameter, accepted) in [("1", true), ("string", false)] {
assert_eq!(
ws.has_no_diagnostic(
DiagnosticCode::ParamTypeMismatch,
&format!(
"---@type {indexed}\nlocal value\n---@param v {parameter}\nlocal function accept(v) end\naccept(value)"
),
),
accepted,
"{indexed} -> {parameter}"
);
}
}
}

#[test]
Expand Down
Loading
Loading