Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ef7c6c1
Expand tuple types with rest parameters in generic instantiation
xuhuanzy Sep 30, 2025
a0c6436
AST: Added `extends` `infer` keywords and implemented conditional types.
xuhuanzy Oct 1, 2025
ce4fc69
测试时`check_code_for`仅执行目标代码的诊断
xuhuanzy Oct 1, 2025
dea80df
analyzer conditional type
xuhuanzy Oct 2, 2025
716b864
conditional type evaluate
xuhuanzy Oct 2, 2025
897c3e6
update conditional type evaluate
xuhuanzy Oct 3, 2025
5e9d9e2
impl `TypeMapped` ast
xuhuanzy Oct 3, 2025
3a59f06
impl analyzer `LuaMappedType`
xuhuanzy Oct 4, 2025
968c10b
impl instantiate `LuaMappedType`
xuhuanzy Oct 4, 2025
5bcbab3
update `TypeMapped` ast
xuhuanzy Oct 4, 2025
17b50e7
Impl built-in generic `Partial`
xuhuanzy Oct 4, 2025
7f1f4c9
refactor generic: Variable arguments should be inferred as tuples.
xuhuanzy Oct 5, 2025
ee42e4a
移除 `...: T` 默认解析为 Tuple
xuhuanzy Oct 7, 2025
fadf61b
调整测试
xuhuanzy Oct 8, 2025
12b047f
impl extends keyword `new`
xuhuanzy Oct 9, 2025
dc051ab
移除泛型实例化过程中可变类型自动转换元组
xuhuanzy Oct 10, 2025
2512a70
Merge remote-tracking branch 'EmmyLuaLs/main' into Generics
xuhuanzy Oct 10, 2025
37e8d9e
Merge remote-tracking branch 'EmmyLuaLs/main' into Generics
xuhuanzy Oct 10, 2025
8791210
fix generic_index range
xuhuanzy Oct 10, 2025
a8fb57e
Merge remote-tracking branch 'EmmyLuaLs/main' into Generics
xuhuanzy Oct 11, 2025
34d8afe
remove GenericParam field `is_variadic`
xuhuanzy Oct 12, 2025
a6ffe9c
调整泛型规范
xuhuanzy Oct 12, 2025
10a4548
update semantic_tokens
xuhuanzy Oct 13, 2025
a164173
impl call_generic
xuhuanzy Oct 13, 2025
26ef82e
Merge remote-tracking branch 'EmmyLuaLs/main' into Generics
xuhuanzy Oct 15, 2025
3b3904d
Merge remote-tracking branch 'EmmyLuaLs/main' into Generics
xuhuanzy Oct 20, 2025
d6bba23
Merge remote-tracking branch 'EmmyLuaLs/main' into Generics
xuhuanzy Oct 24, 2025
a406fc6
添加一些基础泛型定义
xuhuanzy Oct 24, 2025
ccb4e94
Update type checking allows conditional types to match strings.
xuhuanzy Oct 28, 2025
b9b8ffc
Merge remote-tracking branch 'EmmyLuaLs/main' into Generics
xuhuanzy Nov 2, 2025
46d778d
update test
xuhuanzy Nov 2, 2025
beddf44
Merge remote-tracking branch 'EmmyLuaLs/main' into Generics
xuhuanzy Nov 7, 2025
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
9 changes: 9 additions & 0 deletions crates/emmylua_code_analysis/resources/std/builtin.lua
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,15 @@

---@alias Language<T: string> string

--- Get the parameters of a function as a tuple
---@alias Parameters<T extends function> T extends (fun(...: infer P): any) and P or never

--- Get the parameters of a constructor as a tuple
---@alias ConstructorParameters<T> T extends new (fun(...: infer P): any) and P or never

--- Make all properties in T optional
---@alias Partial<T> { [P in keyof T]?: T[P]; }

--- attribute

--- Deprecated. Receives an optional message parameter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ impl FileGenericIndex {
is_func: bool,
) {
let params_id = self.generic_params.len();
// 由于我们允许 infer 推断出一个虚拟泛型, 因此需要计算已声明的泛型数量确定其位置
let start = self.get_start(&ranges).unwrap_or(0);
self.generic_params
.push(TagGenericParams::new(params, is_func));
.push(TagGenericParams::new(params, is_func, start));
let params_id = GenericParamId::new(params_id);
let root_node_ids: Vec<_> = self.root_node_ids.clone();
for range in ranges {
Expand All @@ -53,6 +55,17 @@ impl FileGenericIndex {
}
}

fn get_start(&self, ranges: &[TextRange]) -> Option<usize> {
let params_ids = self.find_generic_params(ranges.first()?.start())?;
let mut start = 0;
for params_id in params_ids.iter() {
if let Some(params) = self.generic_params.get(*params_id) {
start += params.params.len();
}
}
Some(start)
}

fn try_add_range_to_effect_node(
&mut self,
range: TextRange,
Expand Down Expand Up @@ -95,17 +108,17 @@ impl FileGenericIndex {

/// Find generic parameter by position and name.
/// return (GenericTplId, is_variadic)
pub fn find_generic(&self, position: TextSize, name: &str) -> Option<(GenericTplId, bool)> {
pub fn find_generic(&self, position: TextSize, name: &str) -> Option<GenericTplId> {
let params_ids = self.find_generic_params(position)?;

for params_id in params_ids.iter().rev() {
if let Some(params) = self.generic_params.get(*params_id)
&& let Some((id, is_variadic)) = params.params.get(name)
&& let Some(id) = params.params.get(name)
{
if params.is_func {
return Some((GenericTplId::Func(*id as u32), *is_variadic));
return Some(GenericTplId::Func(*id as u32));
} else {
return Some((GenericTplId::Type(*id as u32), *is_variadic));
return Some(GenericTplId::Type(*id as u32));
}
}
}
Expand Down Expand Up @@ -150,8 +163,8 @@ impl FileGenericIndex {
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
struct GenericParamId {
id: usize,
pub struct GenericParamId {
pub id: usize,
}

impl GenericParamId {
Expand Down Expand Up @@ -180,15 +193,15 @@ impl GenericEffectId {

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TagGenericParams {
params: HashMap<String, (usize, bool)>, // bool: is_variadic
params: HashMap<String, usize>,
is_func: bool,
}

impl TagGenericParams {
pub fn new(generic_params: Vec<GenericParam>, is_func: bool) -> Self {
pub fn new(generic_params: Vec<GenericParam>, is_func: bool, start: usize) -> Self {
let mut params = HashMap::new();
for (i, param) in generic_params.into_iter().enumerate() {
params.insert(param.name.to_string(), (i, param.is_variadic));
params.insert(param.name.to_string(), start + i);
}
Self { params, is_func }
}
Expand Down
142 changes: 130 additions & 12 deletions crates/emmylua_code_analysis/src/compilation/analyzer/doc/infer_type.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
use std::sync::Arc;

use emmylua_parser::{
LuaAst, LuaAstNode, LuaDocAttributeType, LuaDocBinaryType, LuaDocDescriptionOwner,
LuaDocFuncType, LuaDocGenericType, LuaDocMultiLineUnionType, LuaDocObjectFieldKey,
LuaDocObjectType, LuaDocStrTplType, LuaDocType, LuaDocUnaryType, LuaDocVariadicType,
LuaLiteralToken, LuaSyntaxKind, LuaTypeBinaryOperator, LuaTypeUnaryOperator, LuaVarExpr,
LuaAst, LuaAstNode, LuaDocAttributeType, LuaDocBinaryType, LuaDocConditionalType,
LuaDocDescriptionOwner, LuaDocFuncType, LuaDocGenericDecl, LuaDocGenericType,
LuaDocIndexAccessType, LuaDocInferType, LuaDocMappedType, LuaDocMultiLineUnionType,
LuaDocObjectFieldKey, LuaDocObjectType, LuaDocStrTplType, LuaDocType, LuaDocUnaryType,
LuaDocVariadicType, LuaLiteralToken, LuaSyntaxKind, LuaTypeBinaryOperator,
LuaTypeUnaryOperator, LuaVarExpr,
};
use internment::ArcIntern;
use rowan::TextRange;
use smol_str::SmolStr;

use crate::{
AsyncState, DiagnosticCode, GenericTpl, InFiled, LuaAliasCallKind, LuaArrayLen, LuaArrayType,
LuaAttributeType, LuaMultiLineUnion, LuaTupleStatus, LuaTypeDeclId, TypeOps, VariadicType,
AsyncState, DiagnosticCode, GenericParam, GenericTpl, InFiled, LuaAliasCallKind, LuaArrayLen,
LuaArrayType, LuaAttributeType, LuaMultiLineUnion, LuaTupleStatus, LuaTypeDeclId, TypeOps,
VariadicType,
db_index::{
AnalyzeError, LuaAliasCallType, LuaFunctionType, LuaGenericType, LuaIndexAccessKey,
LuaIntersectionType, LuaObjectType, LuaStringTplType, LuaTupleType, LuaType,
AnalyzeError, LuaAliasCallType, LuaConditionalType, LuaFunctionType, LuaGenericType,
LuaIndexAccessKey, LuaIntersectionType, LuaMappedType, LuaObjectType, LuaStringTplType,
LuaTupleType, LuaType,
},
};

Expand Down Expand Up @@ -111,7 +116,20 @@ pub fn infer_type(analyzer: &mut DocAnalyzer, node: LuaDocType) -> LuaType {
LuaDocType::Attribute(attribute_type) => {
return infer_attribute_type(analyzer, attribute_type);
}
_ => {} // LuaDocType::Conditional(lua_doc_conditional_type) => todo!(),
LuaDocType::Conditional(cond_type) => {
return infer_conditional_type(analyzer, cond_type);
}
LuaDocType::Infer(infer_type) => {
if let Some(name) = infer_type.get_generic_decl_name_text() {
return LuaType::ConditionalInfer(ArcIntern::new(SmolStr::new(&name)));
}
}
LuaDocType::Mapped(mapped_type) => {
return infer_mapped_type(analyzer, mapped_type).unwrap_or(LuaType::Unknown);
}
LuaDocType::IndexAccess(index_access) => {
return infer_index_access_type(analyzer, index_access);
}
}
LuaType::Unknown
}
Expand All @@ -125,6 +143,7 @@ fn infer_buildin_or_ref_type(
let position = range.start();
match name {
"unknown" => LuaType::Unknown,
"never" => LuaType::Never,
"nil" | "void" => LuaType::Nil,
"any" => LuaType::Any,
"userdata" => LuaType::Userdata,
Expand All @@ -145,12 +164,10 @@ fn infer_buildin_or_ref_type(
LuaType::Table
}
_ => {
if let Some((tpl_id, is_variadic)) = analyzer.generic_index.find_generic(position, name)
{
if let Some(tpl_id) = analyzer.generic_index.find_generic(position, name) {
return LuaType::TplRef(Arc::new(GenericTpl::new(
tpl_id,
SmolStr::new(name).into(),
is_variadic,
)));
}

Expand Down Expand Up @@ -672,3 +689,104 @@ fn infer_attribute_type(

LuaType::DocAttribute(LuaAttributeType::new(params_result).into())
}

fn infer_conditional_type(
analyzer: &mut DocAnalyzer,
cond_type: &LuaDocConditionalType,
) -> LuaType {
if let Some((condition, when_true, when_false)) = cond_type.get_types() {
// 收集条件中的所有 infer 声明
let infer_params = collect_cond_infer_params(&condition);
if !infer_params.is_empty() {
// 条件表达式中 infer 声明的类型参数只允许在`true`分支中使用
let true_range = when_true.get_range();
analyzer
.generic_index
.add_generic_scope(vec![true_range], infer_params.clone(), false);
}

// 处理条件和分支类型
let condition_type = infer_type(analyzer, condition);
let true_type = infer_type(analyzer, when_true);
let false_type = infer_type(analyzer, when_false);

return LuaConditionalType::new(
condition_type,
true_type,
false_type,
infer_params,
cond_type.has_new().unwrap_or(false),
)
.into();
}

LuaType::Unknown
}

/// 收集条件类型中的条件表达式中所有 infer 声明
fn collect_cond_infer_params(doc_type: &LuaDocType) -> Vec<GenericParam> {
let mut params = Vec::new();
let doc_infer_types = doc_type.descendants::<LuaDocInferType>();
for infer_type in doc_infer_types {
if let Some(name) = infer_type.get_generic_decl_name_text() {
params.push(GenericParam::new(SmolStr::new(&name), None, None));
}
}
params
}

fn infer_mapped_type(
analyzer: &mut DocAnalyzer,
mapped_type: &LuaDocMappedType,
) -> Option<LuaType> {
// [P in K]
let mapped_key = mapped_type.get_key()?;
let generic_decl = mapped_key.child::<LuaDocGenericDecl>()?;
let name_token = generic_decl.get_name_token()?;
let name = name_token.get_name_text();
let constraint = generic_decl
.get_type()
.map(|constraint| infer_type(analyzer, constraint));
let param = GenericParam::new(SmolStr::new(name), constraint, None);

analyzer.generic_index.add_generic_scope(
vec![mapped_type.get_range()],
vec![param.clone()],
false,
);
let position = mapped_type.get_range().start();
let id = analyzer.generic_index.find_generic(position, name)?;

let doc_type = mapped_type.get_value_type()?;
let value_type = infer_type(analyzer, doc_type);

Some(LuaType::Mapped(
LuaMappedType::new(
(id, param),
value_type,
mapped_type.is_readonly(),
mapped_type.is_optional(),
)
.into(),
))
}

fn infer_index_access_type(
analyzer: &mut DocAnalyzer,
index_access: &LuaDocIndexAccessType,
) -> LuaType {
let mut types_iter = index_access.children::<LuaDocType>();
let Some(source_doc) = types_iter.next() else {
return LuaType::Unknown;
};
let Some(key_doc) = types_iter.next() else {
return LuaType::Unknown;
};

let source_type = infer_type(analyzer, source_doc);
let key_type = infer_type(analyzer, key_doc);

LuaType::Call(
LuaAliasCallType::new(LuaAliasCallKind::Index, vec![source_type, key_type]).into(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ use crate::{
use emmylua_parser::{LuaAstNode, LuaComment, LuaSyntaxNode};
use file_generic_index::FileGenericIndex;
use tags::get_owner_id;

pub struct DocAnalysisPipeline;

impl AnalysisPipeline for DocAnalysisPipeline {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub fn analyze_class(analyzer: &mut DocAnalyzer, tag: LuaDocTagClass) -> Option<
.get_type_index_mut()
.add_generic_params(class_decl_id.clone(), generic_params.clone());

add_generic_index(analyzer, generic_params);
add_generic_index(analyzer, generic_params, &tag);
}

if let Some(supers) = tag.get_supers() {
Expand Down Expand Up @@ -209,17 +209,19 @@ fn get_generic_params(
.get_type()
.map(|type_ref| infer_type(analyzer, type_ref));

let is_variadic = param.is_variadic();
params_result.push(GenericParam::new(name, type_ref, is_variadic, None));
params_result.push(GenericParam::new(name, type_ref, None));
}

params_result
}

fn add_generic_index(analyzer: &mut DocAnalyzer, generic_params: Vec<GenericParam>) {
fn add_generic_index(
analyzer: &mut DocAnalyzer,
generic_params: Vec<GenericParam>,
tag: &LuaDocTagClass,
) {
let mut ranges = Vec::new();
let range = analyzer.comment.get_range();
ranges.push(range);
ranges.push(tag.get_effective_range());
if let Some(comment_owner) = analyzer.comment.get_owner() {
let range = comment_owner.get_range();
ranges.push(range);
Expand Down Expand Up @@ -348,7 +350,6 @@ pub fn analyze_func_generic(analyzer: &mut DocAnalyzer, tag: LuaDocTagGeneric) -
params_result.push(GenericParam::new(
SmolStr::new(name.as_str()),
type_ref.clone(),
false,
None,
));
param_info.push(Arc::new(LuaGenericParamInfo::new(name, type_ref, None)));
Expand Down
Loading