diff --git a/CHANGELOG.md b/CHANGELOG.md index a6cff0383..cd1cad3bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,57 @@ *All notable changes to the EmmyLua Analyzer Rust project will be documented in this file.* --- +## [0.17.0] - Unreleased + +### 🔧 Changed +- **Refactor IndexAliasName**: 删除原先的索引别名实现(`-- [IndexAliasName]`), 现在使用`---@[index_alias("name")]` +- **Refactor ClassDefaultCall**: 删除配置项`runtime.class_default_call`, 转为使用`---@[constructor("")]` + +### ✨ Added +- **Attribute**: 实现了新的特性`---@attribute`,用于定义附加元数据,内置多个特性: +```lua +--- Deprecated. Receives an optional message parameter. +---@attribute deprecated(message: string?) + +--- Language Server Performance Optimization Items. +--- +--- Receives a parameter, the options are: +--- - `check_table_field` - Skip the assign check for table fields. It is recommended to use this option for all large configuration tables. +---@attribute lsp_perf_optim(code: "check_table_field"|string) + +--- Index field alias, will be displayed in `hint` and `completion`. +--- +--- Receives a string parameter for the alias name. +---@attribute index_alias(name: string) + +--- This attribute must be applied to function parameters, and the function parameter's type must be a string template generic, +--- used to specify the default constructor of a class. +--- +--- Parameters: +--- - `name` - The name of the method as a constructor. +--- - `root_class` - Used to mark the root class, will implicitly inherit this class, such as `System.Object` in c#. Defaults to empty. +--- - `strip_self` - Whether the `self` parameter can be omitted when calling the constructor, defaults to `true` +--- - `return_self` - Whether the constructor is forced to return `self`, defaults to `true` +---@attribute constructor(name: string, root_class: string?, strip_self: boolean?, return_self: boolean?) + +--- Associates `getter` and `setter` methods with a field. Currently provides only definition navigation functionality, +--- and the target methods must reside within the same class. +--- +--- Parameters: +--- - convention: Naming convention, defaults to `camelCase`. Implicitly adds `get` and `set` prefixes. eg: `_age` -> `getAge`, `setAge`. +--- - getter: Getter method name. Takes precedence over `convention`. +--- - setter: Setter method name. Takes precedence over `convention`. +---@attribute field_accessor(convention: "camelCase"|"PascalCase"|"snake_case"|nil, getter: string?, setter: string?) +``` + +使用语法为 `---@[attribute_name_1(arg...), attribute_name_2(arg...), ...]`, 可以同时使用多个特性, 示例: +```lua +---@class A +---@[deprecated] # 如果特性可以省略参数, 则可以省略`()` +---@field b string # b 此时被标记为弃用 +---@[index_alias("b")] +---@field [1] string # 此时在提示和补全中会显示为 `b` +``` ## [0.16.0] - 2025-10-17 ### ✨ Added diff --git a/Cargo.lock b/Cargo.lock index 2f78e5e33..ef002f21c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -579,6 +579,7 @@ dependencies = [ "emmylua_parser", "encoding_rs", "flagset", + "googletest", "include_dir", "internment", "itertools 0.14.0", @@ -665,6 +666,7 @@ dependencies = [ "fern", "glob", "googletest", + "internment", "itertools 0.14.0", "log", "lsp-server", diff --git a/crates/emmylua_code_analysis/Cargo.toml b/crates/emmylua_code_analysis/Cargo.toml index 6a26d8839..89b9a8489 100644 --- a/crates/emmylua_code_analysis/Cargo.toml +++ b/crates/emmylua_code_analysis/Cargo.toml @@ -14,6 +14,8 @@ include = [ "src/**", "resources/**", ] +[dev-dependencies] +googletest.workspace = true # Inherit workspace lints configuration [lints] diff --git a/crates/emmylua_code_analysis/resources/schema.json b/crates/emmylua_code_analysis/resources/schema.json index 2c42c83d5..d9e953a0a 100644 --- a/crates/emmylua_code_analysis/resources/schema.json +++ b/crates/emmylua_code_analysis/resources/schema.json @@ -110,11 +110,6 @@ "runtime": { "$ref": "#/$defs/EmmyrcRuntime", "default": { - "classDefaultCall": { - "forceNonColon": false, - "forceReturnSelf": false, - "functionName": "" - }, "extensions": [], "frameworkVersions": [], "nonstandardSymbol": [], @@ -163,26 +158,6 @@ } }, "$defs": { - "ClassDefaultCall": { - "type": "object", - "properties": { - "forceNonColon": { - "description": "Mandatory non`:` definition. When `function_name` is not empty, it takes effect.", - "type": "boolean", - "default": true - }, - "forceReturnSelf": { - "description": "Force to return `self`.", - "type": "boolean", - "default": true - }, - "functionName": { - "description": "class default overload function. eg. \"__init\".", - "type": "string", - "default": "" - } - } - }, "DiagnosticCode": { "oneOf": [ { @@ -435,6 +410,21 @@ "description": "Global variable defined in non-module scope", "type": "string", "const": "global-in-non-module" + }, + { + "description": "attribute-param-type-mismatch", + "type": "string", + "const": "attribute-param-type-mismatch" + }, + { + "description": "attribute-missing-parameter", + "type": "string", + "const": "attribute-missing-parameter" + }, + { + "description": "attribute-redundant-parameter", + "type": "string", + "const": "attribute-redundant-parameter" } ] }, @@ -942,15 +932,6 @@ "EmmyrcRuntime": { "type": "object", "properties": { - "classDefaultCall": { - "description": "class default overload function.", - "$ref": "#/$defs/ClassDefaultCall", - "default": { - "forceNonColon": false, - "forceReturnSelf": false, - "functionName": "" - } - }, "extensions": { "description": "file Extensions. eg: .lua, .lua.txt", "type": "array", diff --git a/crates/emmylua_code_analysis/resources/std/builtin.lua b/crates/emmylua_code_analysis/resources/std/builtin.lua index 01ca3b5d5..3bb4ea17c 100644 --- a/crates/emmylua_code_analysis/resources/std/builtin.lua +++ b/crates/emmylua_code_analysis/resources/std/builtin.lua @@ -138,3 +138,38 @@ ---@alias TypeGuard boolean ---@alias Language string + +--- attribute + +--- Deprecated. Receives an optional message parameter. +---@attribute deprecated(message: string?) + +--- Language Server Performance Optimization Items. +--- +--- Receives a parameter, the options are: +--- - `check_table_field` - Skip the assign check for table fields. It is recommended to use this option for all large configuration tables. +---@attribute lsp_perf_optim(code: "check_table_field"|string) + +--- Index field alias, will be displayed in `hint` and `completion`. +--- +--- Receives a string parameter for the alias name. +---@attribute index_alias(name: string) + +--- This attribute must be applied to function parameters, and the function parameter's type must be a string template generic, +--- used to specify the default constructor of a class. +--- +--- Parameters: +--- - `name` - The name of the method as a constructor. +--- - `root_class` - Used to mark the root class, will implicitly inherit this class, such as `System.Object` in c#. Defaults to empty. +--- - `strip_self` - Whether the `self` parameter can be omitted when calling the constructor, defaults to `true` +--- - `return_self` - Whether the constructor is forced to return `self`, defaults to `true` +---@attribute constructor(name: string, root_class: string?, strip_self: boolean?, return_self: boolean?) + +--- Associates `getter` and `setter` methods with a field. Currently provides only definition navigation functionality, +--- and the target methods must reside within the same class. +--- +--- Parameters: +--- - convention: Naming convention, defaults to `camelCase`. Implicitly adds `get` and `set` prefixes. eg: `_age` -> `getAge`, `setAge`. +--- - getter: Getter method name. Takes precedence over `convention`. +--- - setter: Setter method name. Takes precedence over `convention`. +---@attribute field_accessor(convention: "camelCase"|"PascalCase"|"snake_case"|nil, getter: string?, setter: string?) diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/decl/docs.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/decl/docs.rs index 09257c380..7951b6490 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/decl/docs.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/decl/docs.rs @@ -1,13 +1,14 @@ use emmylua_parser::{ - LuaAstNode, LuaAstToken, LuaComment, LuaDocAttribute, LuaDocTag, LuaDocTagAlias, + LuaAstNode, LuaAstToken, LuaComment, LuaDocTag, LuaDocTagAlias, LuaDocTagAttribute, LuaDocTagClass, LuaDocTagEnum, LuaDocTagMeta, LuaDocTagNamespace, LuaDocTagUsing, + LuaDocTypeFlag, }; use flagset::FlagSet; use rowan::TextRange; use crate::{ LuaTypeDecl, LuaTypeDeclId, - db_index::{LuaDeclTypeKind, LuaTypeAttribute}, + db_index::{LuaDeclTypeKind, LuaTypeFlag}, }; use super::DeclAnalyzer; @@ -16,39 +17,39 @@ pub fn analyze_doc_tag_class(analyzer: &mut DeclAnalyzer, class: LuaDocTagClass) let name_token = class.get_name_token()?; let name = name_token.get_name_text().to_string(); let range = name_token.syntax().text_range(); - let attrib = get_attrib_value(analyzer, class.get_attrib()); + let type_flag = get_type_flag_value(analyzer, class.get_type_flag()); - add_type_decl(analyzer, &name, range, LuaDeclTypeKind::Class, attrib); + add_type_decl(analyzer, &name, range, LuaDeclTypeKind::Class, type_flag); Some(()) } -fn get_attrib_value( +fn get_type_flag_value( analyzer: &mut DeclAnalyzer, - attrib: Option, -) -> FlagSet { - let mut attr: FlagSet = if analyzer.is_meta { - LuaTypeAttribute::Meta.into() + flag: Option, +) -> FlagSet { + let mut attr: FlagSet = if analyzer.is_meta { + LuaTypeFlag::Meta.into() } else { - LuaTypeAttribute::None.into() + LuaTypeFlag::None.into() }; - if let Some(attrib) = attrib { - for token in attrib.get_attrib_tokens() { + if let Some(flag) = flag { + for token in flag.get_attrib_tokens() { match token.get_name_text() { "partial" => { - attr |= LuaTypeAttribute::Partial; + attr |= LuaTypeFlag::Partial; } "key" => { - attr |= LuaTypeAttribute::Key; + attr |= LuaTypeFlag::Key; } // "global" => { // attr |= LuaTypeAttribute::Global; // } "exact" => { - attr |= LuaTypeAttribute::Exact; + attr |= LuaTypeFlag::Exact; } "constructor" => { - attr |= LuaTypeAttribute::Constructor; + attr |= LuaTypeFlag::Constructor; } _ => {} } @@ -62,9 +63,9 @@ pub fn analyze_doc_tag_enum(analyzer: &mut DeclAnalyzer, enum_: LuaDocTagEnum) - let name_token = enum_.get_name_token()?; let name = name_token.get_name_text().to_string(); let range = name_token.syntax().text_range(); - let attrib = get_attrib_value(analyzer, enum_.get_attrib()); + let flag = get_type_flag_value(analyzer, enum_.get_type_flag()); - add_type_decl(analyzer, &name, range, LuaDeclTypeKind::Enum, attrib); + add_type_decl(analyzer, &name, range, LuaDeclTypeKind::Enum, flag); Some(()) } @@ -78,7 +79,25 @@ pub fn analyze_doc_tag_alias(analyzer: &mut DeclAnalyzer, alias: LuaDocTagAlias) &name, range, LuaDeclTypeKind::Alias, - LuaTypeAttribute::None.into(), + LuaTypeFlag::None.into(), + ); + Some(()) +} + +pub fn analyze_doc_tag_attribute( + analyzer: &mut DeclAnalyzer, + attribute: LuaDocTagAttribute, +) -> Option<()> { + let name_token = attribute.get_name_token()?; + let name = name_token.get_name_text().to_string(); + let range = name_token.syntax().text_range(); + + add_type_decl( + analyzer, + &name, + range, + LuaDeclTypeKind::Attribute, + LuaTypeFlag::None.into(), ); Some(()) } @@ -166,7 +185,7 @@ fn add_type_decl( name: &str, range: TextRange, kind: LuaDeclTypeKind, - attrib: FlagSet, + flag: FlagSet, ) { let file_id = analyzer.get_file_id(); let type_index = analyzer.db.get_type_index_mut(); @@ -180,6 +199,6 @@ fn add_type_decl( let simple_name = id.get_simple_name(); type_index.add_type_decl( file_id, - LuaTypeDecl::new(file_id, range, simple_name.to_string(), kind, attrib, id), + LuaTypeDecl::new(file_id, range, simple_name.to_string(), kind, flag, id), ); } diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/decl/mod.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/decl/mod.rs index 841770e63..859111678 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/decl/mod.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/decl/mod.rs @@ -107,6 +107,9 @@ fn walk_node_enter(analyzer: &mut DeclAnalyzer, node: LuaAst) { LuaAst::LuaDocTagAlias(doc_tag) => { docs::analyze_doc_tag_alias(analyzer, doc_tag); } + LuaAst::LuaDocTagAttribute(doc_tag) => { + docs::analyze_doc_tag_attribute(analyzer, doc_tag); + } LuaAst::LuaDocTagNamespace(doc_tag) => { docs::analyze_doc_tag_namespace(analyzer, doc_tag); } diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/attribute_tags.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/attribute_tags.rs new file mode 100644 index 000000000..c02591689 --- /dev/null +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/attribute_tags.rs @@ -0,0 +1,214 @@ +use emmylua_parser::{ + LuaAst, LuaAstNode, LuaDocTagAttributeUse, LuaDocType, LuaExpr, LuaKind, LuaLiteralExpr, + LuaLiteralToken, LuaSyntaxKind, LuaSyntaxNode, LuaTokenKind, +}; +use smol_str::SmolStr; + +use crate::{ + LuaAttributeUse, LuaSemanticDeclId, LuaType, + compilation::analyzer::doc::{ + DocAnalyzer, + infer_type::infer_type, + tags::{get_owner_id, report_orphan_tag}, + }, +}; + +pub fn analyze_tag_attribute_use( + analyzer: &mut DocAnalyzer, + tag_use: LuaDocTagAttributeUse, +) -> Option<()> { + let owner = attribute_use_get_owner(analyzer, &tag_use); + let owner_id = match get_owner_id(analyzer, owner.clone(), true) { + Some(id) => id, + None => { + report_orphan_tag(analyzer, &tag_use); + return None; + } + }; + + if let Some(owner) = owner { + match (owner, &owner_id) { + (LuaAst::LuaDocTagParam(_), LuaSemanticDeclId::Signature(_)) => { + return Some(()); + } + (LuaAst::LuaDocTagReturn(_), LuaSemanticDeclId::Signature(_)) => { + return Some(()); + } + _ => {} + } + } + + let attribute_uses = infer_attribute_uses(analyzer, tag_use)?; + for attribute_use in attribute_uses { + analyzer.db.get_property_index_mut().add_attribute_use( + analyzer.file_id, + owner_id.clone(), + attribute_use, + ); + } + Some(()) +} + +pub fn infer_attribute_uses( + analyzer: &mut DocAnalyzer, + tag_use: LuaDocTagAttributeUse, +) -> Option> { + let attribute_uses = tag_use.get_attribute_uses(); + let mut result = Vec::new(); + for attribute_use in attribute_uses { + let attribute_type = infer_type(analyzer, LuaDocType::Name(attribute_use.get_type()?)); + if let LuaType::Ref(type_id) = attribute_type { + let arg_types: Vec = attribute_use + .get_arg_list() + .map(|arg_list| arg_list.get_args().map(infer_attribute_arg_type).collect()) + .unwrap_or_default(); + let param_names = analyzer + .db + .get_type_index() + .get_type_decl(&type_id) + .and_then(|decl| decl.get_attribute_type()) + .and_then(|typ| match typ { + LuaType::DocAttribute(attr_type) => Some( + attr_type + .get_params() + .iter() + .map(|(name, _)| name.clone()) + .collect::>(), + ), + _ => None, + }) + .unwrap_or_default(); + + let mut params = Vec::new(); + for (idx, arg_type) in arg_types.into_iter().enumerate() { + let param_name = param_names + .get(idx) + .cloned() + .or_else(|| { + param_names.last().and_then(|last| { + if last == "..." { + Some(last.clone()) + } else { + None + } + }) + }) + .unwrap_or_default(); + params.push((param_name, Some(arg_type))); + } + + result.push(LuaAttributeUse::new(type_id, params)); + } + } + Some(result) +} + +fn infer_attribute_arg_type(expr: LuaLiteralExpr) -> LuaType { + if let Some(literal_token) = expr.get_literal() { + match literal_token { + LuaLiteralToken::String(str_token) => { + return LuaType::DocStringConst(SmolStr::new(str_token.get_value()).into()); + } + LuaLiteralToken::Number(number_token) => { + if number_token.is_int() { + return LuaType::DocIntegerConst(number_token.get_int_value()); + } else { + return LuaType::Number; + } + } + LuaLiteralToken::Bool(bool_token) => { + return LuaType::DocBooleanConst(bool_token.is_true()); + } + LuaLiteralToken::Nil(_) => return LuaType::Nil, + LuaLiteralToken::Dots(_) => return LuaType::Any, + LuaLiteralToken::Question(_) => return LuaType::Nil, + } + } + LuaType::Unknown +} + +/// 寻找特性的所有者 +fn attribute_use_get_owner( + analyzer: &mut DocAnalyzer, + attribute_use: &LuaDocTagAttributeUse, +) -> Option { + if let Some(attached_node) = attribute_find_doc(&attribute_use.syntax()) { + return LuaAst::cast(attached_node); + } + analyzer.comment.get_owner() +} + +fn attribute_find_doc(comment: &LuaSyntaxNode) -> Option { + let mut next_sibling = comment.next_sibling_or_token(); + loop { + next_sibling.as_ref()?; + if let Some(sibling) = &next_sibling { + match sibling.kind() { + LuaKind::Syntax( + LuaSyntaxKind::DocTagField + | LuaSyntaxKind::DocTagParam + | LuaSyntaxKind::DocTagReturn, + ) => { + if let Some(node) = sibling.as_node() { + return Some(node.clone()); + } + } + LuaKind::Syntax(LuaSyntaxKind::Comment) => { + return None; + } + LuaKind::Syntax(LuaSyntaxKind::Block) => { + return None; + } + _ => { + if LuaExpr::can_cast(sibling.kind().into()) { + return None; + } + } + } + next_sibling = sibling.next_sibling_or_token(); + } + } +} + +fn find_up_attribute( + comment: &LuaSyntaxNode, + result: &mut Vec, + stop_by_continue: bool, +) -> Option<()> { + let mut next_sibling = comment.prev_sibling_or_token(); + loop { + next_sibling.as_ref()?; + if let Some(sibling) = &next_sibling { + match sibling.kind() { + LuaKind::Syntax(LuaSyntaxKind::DocTagAttributeUse) => { + if let Some(node) = sibling.as_node() { + if let Some(node) = LuaDocTagAttributeUse::cast(node.clone()) { + result.push(node); + } + } + } + // 某些情况下我们需要以 --- 为分割中断寻找 + LuaKind::Token(LuaTokenKind::TkDocContinue) => { + if stop_by_continue { + return None; + } + } + LuaKind::Syntax(LuaSyntaxKind::DocDescription) => {} + LuaKind::Syntax(_) => { + return None; + } + _ => {} + } + next_sibling = sibling.prev_sibling_or_token(); + } + } +} + +pub fn find_attach_attribute(ast: LuaAst) -> Option> { + if let LuaAst::LuaDocTagParam(param) = ast { + let mut result = Vec::new(); + find_up_attribute(param.syntax(), &mut result, true); + return Some(result); + } + None +} diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/field_or_operator_def_tags.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/field_or_operator_def_tags.rs index cda01472b..8ddc86fb7 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/field_or_operator_def_tags.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/field_or_operator_def_tags.rs @@ -199,7 +199,7 @@ pub fn analyze_operator(analyzer: &mut DocAnalyzer, tag: LuaDocTagOperator) -> O } fn get_visibility_from_field_attrib(tag: &LuaDocTagField) -> Option { - if let Some(attrib) = tag.get_attrib() { + if let Some(attrib) = tag.get_type_flag() { for token in attrib.get_attrib_tokens() { let visibility = VisibilityKind::to_visibility_kind(token.get_name_text()); if visibility.is_some() { diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/infer_type.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/infer_type.rs index 4d23843f5..7b7efbc46 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/infer_type.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/infer_type.rs @@ -1,17 +1,17 @@ use std::sync::Arc; use emmylua_parser::{ - LuaAst, LuaAstNode, LuaDocBinaryType, LuaDocDescriptionOwner, LuaDocFuncType, - LuaDocGenericType, LuaDocMultiLineUnionType, LuaDocObjectFieldKey, LuaDocObjectType, - LuaDocStrTplType, LuaDocType, LuaDocUnaryType, LuaDocVariadicType, LuaLiteralToken, - LuaSyntaxKind, LuaTypeBinaryOperator, LuaTypeUnaryOperator, LuaVarExpr, + LuaAst, LuaAstNode, LuaDocAttributeType, LuaDocBinaryType, LuaDocDescriptionOwner, + LuaDocFuncType, LuaDocGenericType, LuaDocMultiLineUnionType, LuaDocObjectFieldKey, + LuaDocObjectType, LuaDocStrTplType, LuaDocType, LuaDocUnaryType, LuaDocVariadicType, + LuaLiteralToken, LuaSyntaxKind, LuaTypeBinaryOperator, LuaTypeUnaryOperator, LuaVarExpr, }; use rowan::TextRange; use smol_str::SmolStr; use crate::{ AsyncState, DiagnosticCode, GenericTpl, InFiled, LuaAliasCallKind, LuaArrayLen, LuaArrayType, - LuaMultiLineUnion, LuaTupleStatus, LuaTypeDeclId, TypeOps, VariadicType, + LuaAttributeType, LuaMultiLineUnion, LuaTupleStatus, LuaTypeDeclId, TypeOps, VariadicType, db_index::{ AnalyzeError, LuaAliasCallType, LuaFunctionType, LuaGenericType, LuaIndexAccessKey, LuaIntersectionType, LuaObjectType, LuaStringTplType, LuaTupleType, LuaType, @@ -108,6 +108,9 @@ pub fn infer_type(analyzer: &mut DocAnalyzer, node: LuaDocType) -> LuaType { LuaDocType::MultiLineUnion(multi_union) => { return infer_multi_line_union_type(analyzer, multi_union); } + LuaDocType::Attribute(attribute_type) => { + return infer_attribute_type(analyzer, attribute_type); + } _ => {} // LuaDocType::Conditional(lua_doc_conditional_type) => todo!(), } LuaType::Unknown @@ -637,3 +640,35 @@ fn infer_multi_line_union_type( LuaType::MultiLineUnion(LuaMultiLineUnion::new(union_members).into()) } + +fn infer_attribute_type( + analyzer: &mut DocAnalyzer, + attribute_type: &LuaDocAttributeType, +) -> LuaType { + let mut params_result = Vec::new(); + for param in attribute_type.get_params() { + let name = if let Some(param) = param.get_name_token() { + param.get_name_text().to_string() + } else if param.is_dots() { + "...".to_string() + } else { + continue; + }; + + let nullable = param.is_nullable(); + + let type_ref = if let Some(type_ref) = param.get_type() { + let mut typ = infer_type(analyzer, type_ref); + if nullable && !typ.is_nullable() { + typ = TypeOps::Union.apply(analyzer.db, &typ, &LuaType::Nil); + } + Some(typ) + } else { + None + }; + + params_result.push((name, type_ref)); + } + + LuaType::DocAttribute(LuaAttributeType::new(params_result).into()) +} diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/mod.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/mod.rs index 083790a9c..844748055 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/mod.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/mod.rs @@ -1,3 +1,4 @@ +mod attribute_tags; mod diagnostic_tags; mod field_or_operator_def_tags; mod file_generic_index; @@ -48,7 +49,7 @@ fn analyze_comment(analyzer: &mut DocAnalyzer) -> Option<()> { tags::analyze_tag(analyzer, tag); } - let owenr = get_owner_id(analyzer)?; + let owenr = get_owner_id(analyzer, None, false)?; let comment_description = preprocess_description( &comment.get_description()?.get_description_text(), Some(&owenr), diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/tags.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/tags.rs index 90a31f1b8..ea4719c45 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/tags.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/tags.rs @@ -4,7 +4,10 @@ use emmylua_parser::{ use crate::{ AnalyzeError, DiagnosticCode, LuaDeclId, - compilation::analyzer::doc::property_tags::analyze_readonly, + compilation::analyzer::doc::{ + attribute_tags::analyze_tag_attribute_use, property_tags::analyze_readonly, + type_def_tags::analyze_attribute, + }, db_index::{LuaMemberId, LuaSemanticDeclId, LuaSignatureId}, }; @@ -38,6 +41,9 @@ pub fn analyze_tag(analyzer: &mut DocAnalyzer, tag: LuaDocTag) -> Option<()> { LuaDocTag::Alias(alias) => { analyze_alias(analyzer, alias)?; } + LuaDocTag::Attribute(attribute) => { + analyze_attribute(analyzer, attribute)?; + } // ref LuaDocTag::Type(type_tag) => { @@ -111,6 +117,10 @@ pub fn analyze_tag(analyzer: &mut DocAnalyzer, tag: LuaDocTag) -> Option<()> { LuaDocTag::Readonly(readonly) => { analyze_readonly(analyzer, readonly)?; } + // 属性使用, 与 ---@tag 的语法不同 + LuaDocTag::AttributeUse(attribute_use) => { + analyze_tag_attribute_use(analyzer, attribute_use)?; + } _ => {} } @@ -152,11 +162,17 @@ pub fn find_owner_closure_or_report( } } -pub fn get_owner_id(analyzer: &mut DocAnalyzer) -> Option { - if let Some(current_type_id) = &analyzer.current_type_id { - return Some(LuaSemanticDeclId::TypeDecl(current_type_id.clone())); +pub fn get_owner_id( + analyzer: &mut DocAnalyzer, + owner: Option, + find_doc_field: bool, +) -> Option { + if !find_doc_field { + if let Some(current_type_id) = &analyzer.current_type_id { + return Some(LuaSemanticDeclId::TypeDecl(current_type_id.clone())); + } } - let owner = analyzer.comment.get_owner()?; + let owner = owner.or_else(|| analyzer.comment.get_owner())?; match owner { LuaAst::LuaAssignStat(assign) => { let first_var = assign.child::()?; @@ -197,6 +213,10 @@ pub fn get_owner_id(analyzer: &mut DocAnalyzer) -> Option { LuaAst::LuaClosureExpr(closure) => Some(LuaSemanticDeclId::Signature( LuaSignatureId::from_closure(analyzer.file_id, &closure), )), + LuaAst::LuaDocTagField(tag) => { + let member_id = LuaMemberId::new(tag.get_syntax_id(), analyzer.file_id); + Some(LuaSemanticDeclId::Member(member_id)) + } _ => { let closure = find_owner_closure(analyzer)?; Some(LuaSemanticDeclId::Signature(LuaSignatureId::from_closure( @@ -211,7 +231,7 @@ pub fn get_owner_id_or_report( analyzer: &mut DocAnalyzer, tag: &impl LuaAstNode, ) -> Option { - match get_owner_id(analyzer) { + match get_owner_id(analyzer, None, false) { Some(id) => Some(id), None => { report_orphan_tag(analyzer, tag); diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_def_tags.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_def_tags.rs index 504364c64..bed84ac31 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_def_tags.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_def_tags.rs @@ -1,8 +1,8 @@ use emmylua_parser::{ LuaAssignStat, LuaAst, LuaAstNode, LuaAstToken, LuaCommentOwner, LuaDocDescription, - LuaDocDescriptionOwner, LuaDocGenericDeclList, LuaDocTagAlias, LuaDocTagClass, LuaDocTagEnum, - LuaDocTagGeneric, LuaFuncStat, LuaLocalName, LuaLocalStat, LuaNameExpr, LuaSyntaxId, - LuaSyntaxKind, LuaTokenKind, LuaVarExpr, + LuaDocDescriptionOwner, LuaDocGenericDeclList, LuaDocTagAlias, LuaDocTagAttribute, + LuaDocTagClass, LuaDocTagEnum, LuaDocTagGeneric, LuaFuncStat, LuaLocalName, LuaLocalStat, + LuaNameExpr, LuaSyntaxId, LuaSyntaxKind, LuaTokenKind, LuaVarExpr, }; use rowan::TextRange; use smol_str::SmolStr; @@ -15,8 +15,11 @@ use crate::compilation::analyzer::doc::tags::report_orphan_tag; use crate::{ LuaTypeCache, LuaTypeDeclId, compilation::analyzer::common::bind_type, - db_index::{LuaDeclId, LuaMemberId, LuaSemanticDeclId, LuaSignatureId, LuaType}, + db_index::{ + LuaDeclId, LuaGenericParamInfo, LuaMemberId, LuaSemanticDeclId, LuaSignatureId, LuaType, + }, }; +use std::sync::Arc; pub fn analyze_class(analyzer: &mut DocAnalyzer, tag: LuaDocTagClass) -> Option<()> { let file_id = analyzer.file_id; @@ -174,6 +177,32 @@ pub fn analyze_alias(analyzer: &mut DocAnalyzer, tag: LuaDocTagAlias) -> Option< Some(()) } +/// 分析属性定义 +pub fn analyze_attribute(analyzer: &mut DocAnalyzer, tag: LuaDocTagAttribute) -> Option<()> { + let file_id = analyzer.file_id; + let name = tag.get_name_token()?.get_name_text().to_string(); + + let decl_id = { + let decl = analyzer + .db + .get_type_index() + .find_type_decl(file_id, &name)?; + if !decl.is_attribute() { + return None; + } + decl.get_id() + }; + let attribute_type = infer_type(analyzer, tag.get_type()?); + let attribute_decl = analyzer + .db + .get_type_index_mut() + .get_type_decl_mut(&decl_id)?; + attribute_decl.add_attribute_type(attribute_type); + + add_description_for_type_decl(analyzer, &decl_id, tag.get_descriptions()); + Some(()) +} + fn get_generic_params( analyzer: &mut DocAnalyzer, params: LuaDocGenericDeclList, @@ -185,13 +214,12 @@ fn get_generic_params( } else { continue; }; - let type_ref = param .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)); + params_result.push(GenericParam::new(name, type_ref, is_variadic, None)); } params_result @@ -322,7 +350,6 @@ pub fn analyze_func_generic(analyzer: &mut DocAnalyzer, tag: LuaDocTagGeneric) - } else { continue; }; - let type_ref = param .get_type() .map(|type_ref| infer_type(analyzer, type_ref)); @@ -331,8 +358,9 @@ pub fn analyze_func_generic(analyzer: &mut DocAnalyzer, tag: LuaDocTagGeneric) - SmolStr::new(name.as_str()), type_ref.clone(), false, + None, )); - param_info.push((name, type_ref)); + param_info.push(Arc::new(LuaGenericParamInfo::new(name, type_ref, None))); } } diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs index deb76cee5..583bd49dd 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs @@ -11,9 +11,6 @@ use super::{ preprocess_description, tags::{find_owner_closure, get_owner_id_or_report}, }; -use crate::compilation::analyzer::doc::tags::{ - find_owner_closure_or_report, get_owner_id, report_orphan_tag, -}; use crate::{ InFiled, InferFailReason, LuaOperatorMetaMethod, LuaTypeCache, LuaTypeOwner, OperatorFunction, SignatureReturnStatus, TypeOps, @@ -23,6 +20,13 @@ use crate::{ LuaSignatureId, LuaType, }, }; +use crate::{ + LuaAttributeUse, + compilation::analyzer::doc::{ + attribute_tags::{find_attach_attribute, infer_attribute_uses}, + tags::{find_owner_closure_or_report, get_owner_id, report_orphan_tag}, + }, +}; pub fn analyze_type(analyzer: &mut DocAnalyzer, tag: LuaDocTagType) -> Option<()> { let description = tag @@ -186,18 +190,31 @@ pub fn analyze_param(analyzer: &mut DocAnalyzer, tag: LuaDocTagParam) -> Option< // bind type ref to signature and param if let Some(closure) = find_owner_closure(analyzer) { let id = LuaSignatureId::from_closure(analyzer.file_id, &closure); + // 绑定`attribute`标记 + let attributes = + find_attach_attribute(LuaAst::LuaDocTagParam(tag)).and_then(|tag_attribute_uses| { + let result: Vec = tag_attribute_uses + .into_iter() + .filter_map(|tag_use| infer_attribute_uses(analyzer, tag_use)) + .flatten() + .collect(); + (!result.is_empty()).then_some(result) + }); + let signature = analyzer.db.get_signature_index_mut().get_or_create(id); let param_info = LuaDocParamInfo { name: name.clone(), type_ref: type_ref.clone(), nullable, description, + attributes, }; let idx = signature.find_param_idx(&name)?; signature.param_docs.insert(idx, param_info); } else if let Some(LuaAst::LuaForRangeStat(for_range)) = analyzer.comment.get_owner() { + // for in 支持 @param 语法 for it_name_token in for_range.get_var_name_list() { let it_name = it_name_token.get_name_text(); if it_name == name { @@ -224,7 +241,7 @@ pub fn analyze_return(analyzer: &mut DocAnalyzer, tag: LuaDocTagReturn) -> Optio if let Some(closure) = find_owner_closure_or_report(analyzer, &tag) { let signature_id = LuaSignatureId::from_closure(analyzer.file_id, &closure); - let returns = tag.get_type_and_name_list(); + let returns = tag.get_info_list(); for (doc_type, name_token) in returns { let name = name_token.map(|name| name.get_name_text().to_string()); @@ -233,6 +250,7 @@ pub fn analyze_return(analyzer: &mut DocAnalyzer, tag: LuaDocTagReturn) -> Optio name, type_ref, description: description.clone(), + attributes: None, }; let signature = analyzer @@ -247,7 +265,7 @@ pub fn analyze_return(analyzer: &mut DocAnalyzer, tag: LuaDocTagReturn) -> Optio } pub fn analyze_return_cast(analyzer: &mut DocAnalyzer, tag: LuaDocTagReturnCast) -> Option<()> { - if let Some(LuaSemanticDeclId::Signature(signature_id)) = get_owner_id(analyzer) { + if let Some(LuaSemanticDeclId::Signature(signature_id)) = get_owner_id(analyzer, None, false) { let name_token = tag.get_name_token()?; let name = name_token.get_name_text(); @@ -415,7 +433,7 @@ pub fn analyze_see(analyzer: &mut DocAnalyzer, tag: LuaDocTagSee) -> Option<()> } pub fn analyze_other(analyzer: &mut DocAnalyzer, other: LuaDocTagOther) -> Option<()> { - let owner = get_owner_id(analyzer)?; + let owner = get_owner_id(analyzer, None, false)?; let tag_name = other.get_tag_name()?; let description = if let Some(des) = other.get_description() { preprocess_description(&des.get_description_text(), None) diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/lua/call.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/lua/call.rs new file mode 100644 index 000000000..9fb6d3834 --- /dev/null +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/lua/call.rs @@ -0,0 +1,31 @@ +use emmylua_parser::LuaCallExpr; + +use crate::{ + InferFailReason, LuaType, + compilation::analyzer::{lua::LuaAnalyzer, unresolve::UnResolveConstructor}, +}; + +pub fn analyze_call(analyzer: &mut LuaAnalyzer, call_expr: LuaCallExpr) -> Option<()> { + let prefix_expr = call_expr.clone().get_prefix_expr()?; + if let Ok(expr_type) = analyzer.infer_expr(&prefix_expr) { + let LuaType::Signature(signature_id) = expr_type else { + return Some(()); + }; + let signature = analyzer.db.get_signature_index().get(&signature_id)?; + for (idx, param_info) in signature.param_docs.iter() { + if param_info.get_attribute_by_name("constructor").is_some() { + let unresolve = UnResolveConstructor { + file_id: analyzer.file_id, + call_expr: call_expr.clone(), + signature_id, + param_idx: *idx, + }; + analyzer + .context + .add_unresolve(unresolve.into(), InferFailReason::None); + return Some(()); + } + } + } + Some(()) +} diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/lua/closure.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/lua/closure.rs index e041fd822..d04ab89ca 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/lua/closure.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/lua/closure.rs @@ -151,6 +151,7 @@ fn analyze_return( type_ref: LuaType::Unknown, description: None, name: None, + attributes: None, }] } Err(reason) => { @@ -236,6 +237,7 @@ pub fn analyze_return_point( type_ref: return_type, description: None, name: None, + attributes: None, }]) } diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/lua/mod.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/lua/mod.rs index 6c90885f3..9c6fedd3a 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/lua/mod.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/lua/mod.rs @@ -1,3 +1,4 @@ +mod call; mod closure; mod for_range_stat; mod func_body; @@ -22,7 +23,7 @@ use stats::{ use crate::{ Emmyrc, FileId, InferFailReason, - compilation::analyzer::AnalysisPipeline, + compilation::analyzer::{AnalysisPipeline, lua::call::analyze_call}, db_index::{DbIndex, LuaType}, profile::Profile, semantic::infer_expr, @@ -81,6 +82,8 @@ fn analyze_node(analyzer: &mut LuaAnalyzer, node: LuaAst) { LuaAst::LuaCallExpr(call_expr) => { if call_expr.is_setmetatable() { analyze_setmetatable(analyzer, call_expr); + } else { + analyze_call(analyzer, call_expr); } } _ => {} @@ -107,6 +110,7 @@ impl LuaAnalyzer<'_> { } } + #[allow(unused)] pub fn get_emmyrc(&self) -> &Emmyrc { self.db.get_emmyrc() } diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/lua/stats.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/lua/stats.rs index 41d15c8eb..2720f9511 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/lua/stats.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/lua/stats.rs @@ -1,11 +1,10 @@ use emmylua_parser::{ - BinaryOperator, LuaAssignStat, LuaAstNode, LuaAstToken, LuaExpr, LuaFuncStat, LuaIndexExpr, + BinaryOperator, LuaAssignStat, LuaAstNode, LuaExpr, LuaFuncStat, LuaIndexExpr, LuaLocalFuncStat, LuaLocalStat, LuaTableField, LuaVarExpr, PathTrait, }; use crate::{ - InFiled, InferFailReason, LuaOperator, LuaOperatorMetaMethod, LuaOperatorOwner, LuaTypeCache, - LuaTypeOwner, OperatorFunction, + InFiled, InferFailReason, LuaTypeCache, LuaTypeOwner, compilation::analyzer::{ common::{add_member, bind_type}, unresolve::{UnResolveDecl, UnResolveMember}, @@ -415,8 +414,6 @@ pub fn analyze_func_stat(analyzer: &mut LuaAnalyzer, func_stat: LuaFuncStat) -> .get_type_index_mut() .bind_type(type_owner, LuaTypeCache::InferType(signature_type.clone())); - try_add_class_default_call(analyzer, func_name, signature_type); - Some(()) } @@ -503,53 +500,3 @@ fn special_assign_pattern( Some(()) } - -pub fn try_add_class_default_call( - analyzer: &mut LuaAnalyzer, - func_name: LuaVarExpr, - signature_type: LuaType, -) -> Option<()> { - let LuaType::Signature(signature_id) = signature_type else { - return None; - }; - - let default_name = &analyzer - .get_emmyrc() - .runtime - .class_default_call - .function_name; - - if default_name.is_empty() { - return None; - } - if let LuaVarExpr::IndexExpr(index_expr) = func_name { - let index_key = index_expr.get_index_key()?; - if index_key.get_path_part() == *default_name { - let prefix_expr = index_expr.get_prefix_expr()?; - if let Ok(prefix_type) = analyzer.infer_expr(&prefix_expr) - && let LuaType::Def(decl_id) = prefix_type - { - // 如果已经存在, 则不添加 - let call = analyzer.db.get_operator_index().get_operators( - &LuaOperatorOwner::Type(decl_id.clone()), - LuaOperatorMetaMethod::Call, - ); - if call.is_some() { - return None; - } - - let operator = LuaOperator::new( - decl_id.into(), - LuaOperatorMetaMethod::Call, - analyzer.file_id, - // 必须指向名称, 使用 index_expr 的完整范围不会跳转到函数上 - index_expr.get_name_token()?.syntax().text_range(), - OperatorFunction::DefaultCall(signature_id), - ); - analyzer.db.get_operator_index_mut().add_operator(operator); - } - } - } - - Some(()) -} diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/check_reason.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/check_reason.rs index a90176112..291ea9db5 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/check_reason.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/check_reason.rs @@ -18,6 +18,7 @@ pub fn check_reach_reason( match reason { InferFailReason::None | InferFailReason::FieldNotFound + | InferFailReason::UnResolveOperatorCall | InferFailReason::RecursiveInfer => Some(true), InferFailReason::UnResolveDeclType(decl_id) => { let decl = db.get_decl_index().get_decl(decl_id)?; @@ -60,6 +61,7 @@ pub fn resolve_as_any(db: &mut DbIndex, reason: &InferFailReason, loop_count: us match reason { InferFailReason::None | InferFailReason::FieldNotFound + | InferFailReason::UnResolveOperatorCall | InferFailReason::RecursiveInfer => { return Some(()); } @@ -97,6 +99,7 @@ pub fn resolve_as_any(db: &mut DbIndex, reason: &InferFailReason, loop_count: us name: None, type_ref: LuaType::Any, description: None, + attributes: None, }]; signature.resolve_return = SignatureReturnStatus::InferResolve; } diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/mod.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/mod.rs index 6b8b99dab..f6621fac9 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/mod.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/mod.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use crate::{ FileId, InferFailReason, LuaMemberFeature, LuaSemanticDeclId, - compilation::analyzer::AnalysisPipeline, + compilation::analyzer::{AnalysisPipeline, unresolve::resolve::try_resolve_constructor}, db_index::{DbIndex, LuaDeclId, LuaMemberId, LuaSignatureId}, profile::Profile, }; @@ -201,6 +201,9 @@ fn try_resolve( UnResolve::TableField(un_resolve_table_field) => { try_resolve_table_field(db, cache, un_resolve_table_field) } + UnResolve::ClassCtor(un_resolve_constructor) => { + try_resolve_constructor(db, cache, un_resolve_constructor) + } }; match resolve_result { @@ -213,6 +216,12 @@ fn try_resolve( retain_unresolve.push((unresolve, InferFailReason::FieldNotFound)); } } + Err(InferFailReason::UnResolveOperatorCall) => { + if !cache.get_config().analysis_phase.is_force() { + retain_unresolve + .push((unresolve, InferFailReason::UnResolveOperatorCall)); + } + } Err(reason) => { if reason != *check_reason { changed = true; @@ -254,6 +263,7 @@ pub enum UnResolve { ClosureParentParams(Box), ModuleRef(Box), TableField(Box), + ClassCtor(Box), } #[allow(dead_code)] @@ -276,6 +286,7 @@ impl UnResolve { } UnResolve::TableField(un_resolve_table_field) => Some(un_resolve_table_field.file_id), UnResolve::ModuleRef(_) => None, + UnResolve::ClassCtor(un_resolve_constructor) => Some(un_resolve_constructor.file_id), } } } @@ -422,3 +433,17 @@ impl From for UnResolve { UnResolve::TableField(Box::new(un_resolve_table_field)) } } + +#[derive(Debug)] +pub struct UnResolveConstructor { + pub file_id: FileId, + pub call_expr: LuaCallExpr, + pub signature_id: LuaSignatureId, + pub param_idx: usize, +} + +impl From for UnResolve { + fn from(un_resolve_constructor: UnResolveConstructor) -> Self { + UnResolve::ClassCtor(Box::new(un_resolve_constructor)) + } +} diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/resolve.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/resolve.rs index 975115cf1..9436b3f01 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/resolve.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/resolve.rs @@ -1,15 +1,20 @@ use std::ops::Deref; -use emmylua_parser::{LuaAstNode, LuaAstToken, LuaExpr, LuaLocalStat, LuaTableExpr}; +use emmylua_parser::{ + LuaAstNode, LuaAstToken, LuaCallExpr, LuaExpr, LuaIndexExpr, LuaLocalStat, LuaTableExpr, +}; use crate::{ - InFiled, InferFailReason, LuaDeclId, LuaMember, LuaMemberId, LuaMemberKey, LuaSemanticDeclId, - LuaTypeCache, SignatureReturnStatus, TypeOps, + InFiled, InferFailReason, LuaDeclId, LuaMember, LuaMemberId, LuaMemberInfo, LuaMemberKey, + LuaOperator, LuaOperatorMetaMethod, LuaOperatorOwner, LuaSemanticDeclId, LuaTypeCache, + LuaTypeDeclId, OperatorFunction, SignatureReturnStatus, TypeOps, compilation::analyzer::{ common::{add_member, bind_type}, lua::{analyze_return_point, infer_for_range_iter_expr_func}, + unresolve::UnResolveConstructor, }, db_index::{DbIndex, LuaMemberOwner, LuaType}, + find_members_with_key, semantic::{LuaInferCache, infer_expr}, }; @@ -246,3 +251,183 @@ pub fn try_resolve_module_ref( Ok(()) } + +pub fn try_resolve_constructor( + db: &mut DbIndex, + cache: &mut LuaInferCache, + unresolve_constructor: &mut UnResolveConstructor, +) -> ResolveResult { + let (param_type, target_signature_name, root_class, strip_self, return_self) = { + let signature = db + .get_signature_index() + .get(&unresolve_constructor.signature_id) + .ok_or(InferFailReason::None)?; + let param_info = signature + .get_param_info_by_id(unresolve_constructor.param_idx) + .ok_or(InferFailReason::None)?; + let constructor_use = param_info + .get_attribute_by_name("constructor") + .ok_or(InferFailReason::None)?; + + // 作为构造函数的方法名 + let target_signature_name = constructor_use + .get_param_by_name("name") + .and_then(|typ| match typ { + LuaType::DocStringConst(value) => Some(value.deref().clone()), + _ => None, + }) + .ok_or(InferFailReason::None)?; + // 作为构造函数的根类 + let root_class = + constructor_use + .get_param_by_name("root_class") + .and_then(|typ| match typ { + LuaType::DocStringConst(value) => Some(value.deref().clone()), + _ => None, + }); + // 是否可以省略self参数 + let strip_self = constructor_use + .get_param_by_name("strip_self") + .and_then(|typ| match typ { + LuaType::DocBooleanConst(value) => Some(*value), + _ => None, + }) + .unwrap_or(true); + // 是否返回self + let return_self = constructor_use + .get_param_by_name("return_self") + .and_then(|typ| match typ { + LuaType::DocBooleanConst(value) => Some(*value), + _ => None, + }) + .unwrap_or(true); + + Ok::<_, InferFailReason>(( + param_info.type_ref.clone(), + target_signature_name, + root_class, + strip_self, + return_self, + )) + }?; + + // 需要添加构造函数的目标类型 + let target_id = get_constructor_target_type( + db, + cache, + ¶m_type, + unresolve_constructor.call_expr.clone(), + unresolve_constructor.param_idx, + ) + .ok_or(InferFailReason::None)?; + + // 添加根类 + if let Some(root_class) = root_class { + let root_type_id = LuaTypeDeclId::new(&root_class); + if let Some(type_decl) = db.get_type_index().get_type_decl(&root_type_id) { + if type_decl.is_class() { + let root_type = LuaType::Ref(root_type_id.clone()); + db.get_type_index_mut().add_super_type( + target_id.clone(), + unresolve_constructor.file_id, + root_type, + ); + } + } + } + + // 添加构造函数 + let target_type = LuaType::Ref(target_id); + let member_key = LuaMemberKey::Name(target_signature_name); + let members = + find_members_with_key(db, &target_type, member_key, false).ok_or(InferFailReason::None)?; + let ctor_signature_member = members.first().ok_or(InferFailReason::None)?; + + set_signature_to_default_call(db, cache, ctor_signature_member, strip_self, return_self) + .ok_or(InferFailReason::None)?; + + Ok(()) +} + +fn set_signature_to_default_call( + db: &mut DbIndex, + cache: &mut LuaInferCache, + member_info: &LuaMemberInfo, + strip_self: bool, + return_self: bool, +) -> Option<()> { + let LuaType::Signature(signature_id) = member_info.typ else { + return None; + }; + let Some(LuaSemanticDeclId::Member(member_id)) = member_info.property_owner_id else { + return None; + }; + // 我们仍然需要再做一次判断确定是否来源于`Def`类型 + let root = db + .get_vfs() + .get_syntax_tree(&member_id.file_id)? + .get_red_root(); + let index_expr = LuaIndexExpr::cast(member_id.get_syntax_id().to_node_from_root(&root)?)?; + let prefix_expr = index_expr.get_prefix_expr()?; + let prefix_type = infer_expr(db, cache, prefix_expr.clone()).ok()?; + let LuaType::Def(decl_id) = prefix_type else { + return None; + }; + // 如果已经存在显式的`__call`定义, 则不添加 + let call = db.get_operator_index().get_operators( + &LuaOperatorOwner::Type(decl_id.clone()), + LuaOperatorMetaMethod::Call, + ); + if call.is_some() { + return None; + } + + let operator = LuaOperator::new( + decl_id.into(), + LuaOperatorMetaMethod::Call, + member_id.file_id, + // 必须指向名称, 使用 index_expr 的完整范围不会跳转到函数上 + index_expr.get_name_token()?.syntax().text_range(), + OperatorFunction::DefaultClassCtor { + id: signature_id, + strip_self, + return_self, + }, + ); + db.get_operator_index_mut().add_operator(operator); + Some(()) +} + +fn get_constructor_target_type( + db: &DbIndex, + cache: &mut LuaInferCache, + param_type: &LuaType, + call_expr: LuaCallExpr, + call_index: usize, +) -> Option { + if let LuaType::StrTplRef(str_tpl) = param_type { + let name = { + let arg_expr = call_expr + .get_args_list()? + .get_args() + .nth(call_index)? + .clone(); + let name = infer_expr(db, cache, arg_expr).ok()?; + match name { + LuaType::StringConst(s) => s.to_string(), + _ => return None, + } + }; + + let prefix = str_tpl.get_prefix(); + let suffix = str_tpl.get_suffix(); + let type_decl_id: LuaTypeDeclId = + LuaTypeDeclId::new(format!("{}{}{}", prefix, name, suffix).as_str()); + let type_decl = db.get_type_index().get_type_decl(&type_decl_id)?; + if type_decl.is_class() { + return Some(type_decl_id); + } + } + + None +} diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/resolve_closure.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/resolve_closure.rs index 9bc2d4d3f..7291eeb1c 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/resolve_closure.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/resolve_closure.rs @@ -94,6 +94,7 @@ pub fn try_resolve_call_closure_params( type_ref: type_ref.clone().unwrap_or(LuaType::Any), description: None, nullable: false, + attributes: None, }, ); } @@ -171,6 +172,7 @@ pub fn try_resolve_closure_return( name: None, type_ref: ret_type.clone(), description: None, + attributes: None, }); signature.resolve_return = SignatureReturnStatus::DocResolve; @@ -467,6 +469,7 @@ fn resolve_doc_function( type_ref: param.1.clone().unwrap_or(LuaType::Any), description: None, nullable: false, + attributes: None, }, ); } @@ -480,6 +483,7 @@ fn resolve_doc_function( name: None, type_ref: doc_func.get_ret().clone(), description: None, + attributes: None, }); } diff --git a/crates/emmylua_code_analysis/src/compilation/test/annotation_test.rs b/crates/emmylua_code_analysis/src/compilation/test/annotation_test.rs index 3dbb971ea..65c5c0cae 100644 --- a/crates/emmylua_code_analysis/src/compilation/test/annotation_test.rs +++ b/crates/emmylua_code_analysis/src/compilation/test/annotation_test.rs @@ -127,12 +127,17 @@ mod test { #[test] fn test_generic_type_extends() { - let mut ws = VirtualWorkspace::new(); - let mut emmyrc = ws.get_emmyrc(); - emmyrc.runtime.class_default_call.force_non_colon = true; - emmyrc.runtime.class_default_call.force_return_self = true; - emmyrc.runtime.class_default_call.function_name = "__init".to_string(); - ws.update_emmyrc(emmyrc); + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + ws.def( + r#" + ---@generic T + ---@[constructor("__init")] + ---@param name `T` + ---@return T + function meta(name) + end + "#, + ); ws.def( r#" ---@class State @@ -141,7 +146,7 @@ mod test { ---@class StateMachine ---@field aaa T ---@field new fun(self: self): self - StateMachine = {} + StateMachine = meta("StateMachine") ---@return self function StateMachine:abc() diff --git a/crates/emmylua_code_analysis/src/compilation/test/attribute_test.rs b/crates/emmylua_code_analysis/src/compilation/test/attribute_test.rs new file mode 100644 index 000000000..bd0b4e724 --- /dev/null +++ b/crates/emmylua_code_analysis/src/compilation/test/attribute_test.rs @@ -0,0 +1,44 @@ +#[cfg(test)] +mod test { + use crate::{DiagnosticCode, VirtualWorkspace}; + + #[test] + fn test_constructor() { + let mut ws = VirtualWorkspace::new(); + + ws.def_files(vec![ + ( + "init.lua", + r#" + A = meta("A") + "#, + ), + ( + "meta.lua", + r#" + ---@attribute constructor(name: string, root_class: string?, strip_self: boolean?, return_self: boolean?) + + ---@generic T + ---@[constructor("__init")] + ---@param name `T` + ---@return T + function meta(name) + end + "#, + ), + ]); + } + + #[test] + fn test_def_attribute() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + + ws.check_code_for( + DiagnosticCode::AssignTypeMismatch, + r#" + ---@[lsp_perf_optim("check_table_field")] + local config = {} + "#, + ); + } +} diff --git a/crates/emmylua_code_analysis/src/compilation/test/mod.rs b/crates/emmylua_code_analysis/src/compilation/test/mod.rs index 58d8faec8..188a8cbb6 100644 --- a/crates/emmylua_code_analysis/src/compilation/test/mod.rs +++ b/crates/emmylua_code_analysis/src/compilation/test/mod.rs @@ -1,6 +1,7 @@ mod and_or_test; mod annotation_test; mod array_test; +mod attribute_test; mod closure_generic; mod closure_param_infer_test; mod closure_return_test; diff --git a/crates/emmylua_code_analysis/src/compilation/test/overload_test.rs b/crates/emmylua_code_analysis/src/compilation/test/overload_test.rs index e91cc5719..961533a2c 100644 --- a/crates/emmylua_code_analysis/src/compilation/test/overload_test.rs +++ b/crates/emmylua_code_analysis/src/compilation/test/overload_test.rs @@ -1,6 +1,5 @@ #[cfg(test)] mod test { - use std::{ops::Deref, sync::Arc}; use crate::{DiagnosticCode, VirtualWorkspace}; @@ -29,20 +28,26 @@ mod test { } #[test] - fn test_class_default_call() { - let mut ws = VirtualWorkspace::new(); - let mut emmyrc = ws.analysis.emmyrc.deref().clone(); - emmyrc.runtime.class_default_call.function_name = "__init".to_string(); - emmyrc.runtime.class_default_call.force_non_colon = true; - emmyrc.runtime.class_default_call.force_return_self = true; - ws.analysis.update_config(Arc::new(emmyrc)); + fn test_class_default_constructor() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + ws.def( + r#" + + ---@generic T + ---@[constructor("__init")] + ---@param name `T` + ---@return T + function meta(name) + end + "#, + ); ws.def( r#" ---@class MyClass - local M = {} + local M = meta("MyClass") - function M:__init(a) + function M:__init() end A = M() diff --git a/crates/emmylua_code_analysis/src/config/configs/runtime.rs b/crates/emmylua_code_analysis/src/config/configs/runtime.rs index cd1d7970c..36b7beea1 100644 --- a/crates/emmylua_code_analysis/src/config/configs/runtime.rs +++ b/crates/emmylua_code_analysis/src/config/configs/runtime.rs @@ -24,9 +24,6 @@ pub struct EmmyrcRuntime { #[serde(default)] /// Require pattern. eg. "?.lua", "?/init.lua" pub require_pattern: Vec, - #[serde(default)] - /// class default overload function. - pub class_default_call: ClassDefaultCall, /// Non-standard symbols. #[serde(default)] pub nonstandard_symbol: Vec, @@ -75,20 +72,7 @@ impl EmmyrcLuaVersion { } } -#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone, Default)] -#[serde(rename_all = "camelCase")] -pub struct ClassDefaultCall { - #[serde(default)] - /// class default overload function. eg. "__init". - pub function_name: String, - #[serde(default = "default_true")] - /// Mandatory non`:` definition. When `function_name` is not empty, it takes effect. - pub force_non_colon: bool, - /// Force to return `self`. - #[serde(default = "default_true")] - pub force_return_self: bool, -} - +#[allow(unused)] fn default_true() -> bool { true } diff --git a/crates/emmylua_code_analysis/src/db_index/operators/lua_operator.rs b/crates/emmylua_code_analysis/src/db_index/operators/lua_operator.rs index b13f57930..ce6d4ede8 100644 --- a/crates/emmylua_code_analysis/src/db_index/operators/lua_operator.rs +++ b/crates/emmylua_code_analysis/src/db_index/operators/lua_operator.rs @@ -23,7 +23,11 @@ pub struct LuaOperator { pub enum OperatorFunction { Func(Arc), Signature(LuaSignatureId), - DefaultCall(LuaSignatureId), + DefaultClassCtor { + id: LuaSignatureId, + strip_self: bool, + return_self: bool, + }, } impl LuaOperator { @@ -73,7 +77,7 @@ impl LuaOperator { LuaType::Any } // 只有 .field 才有`operand`, call 不会有这个 - OperatorFunction::DefaultCall(_) => LuaType::Unknown, + OperatorFunction::DefaultClassCtor { .. } => LuaType::Unknown, } } @@ -95,17 +99,15 @@ impl LuaOperator { Ok(LuaType::Any) } - OperatorFunction::DefaultCall(signature_id) => { - let emmyrc = db.get_emmyrc(); - if emmyrc.runtime.class_default_call.force_return_self { + OperatorFunction::DefaultClassCtor { + id, return_self, .. + } => { + if *return_self { return Ok(LuaType::SelfInfer); } - if let Some(signature) = db.get_signature_index().get(signature_id) { - if signature.resolve_return == SignatureReturnStatus::UnResolve { - return Err(InferFailReason::UnResolveSignatureReturn(*signature_id)); - } - + let signature = db.get_signature_index().get(id); + if let Some(signature) = signature { let return_type = signature.return_docs.first(); if let Some(return_type) = return_type { return Ok(return_type.type_ref.clone()); @@ -121,17 +123,19 @@ impl LuaOperator { match &self.func { OperatorFunction::Func(func) => LuaType::DocFunction(func.clone()), OperatorFunction::Signature(signature) => LuaType::Signature(*signature), - OperatorFunction::DefaultCall(signature_id) => { - let emmyrc = db.get_emmyrc(); - - if let Some(signature) = db.get_signature_index().get(signature_id) { + OperatorFunction::DefaultClassCtor { + id, + strip_self, + return_self, + } => { + if let Some(signature) = db.get_signature_index().get(id) { let params = signature.get_type_params(); - let is_colon_define = if emmyrc.runtime.class_default_call.force_non_colon { + let is_colon_define = if *strip_self { false } else { signature.is_colon_define }; - let return_type = if emmyrc.runtime.class_default_call.force_return_self { + let return_type = if *return_self { LuaType::SelfInfer } else { signature.get_return_type() @@ -145,7 +149,7 @@ impl LuaOperator { return LuaType::DocFunction(Arc::new(func_type)); } - LuaType::Signature(*signature_id) + LuaType::Signature(*id) } } } diff --git a/crates/emmylua_code_analysis/src/db_index/property/mod.rs b/crates/emmylua_code_analysis/src/db_index/property/mod.rs index 02e8751b5..85eea4f93 100644 --- a/crates/emmylua_code_analysis/src/db_index/property/mod.rs +++ b/crates/emmylua_code_analysis/src/db_index/property/mod.rs @@ -9,6 +9,7 @@ use emmylua_parser::{LuaAstNode, LuaDocTagField, LuaDocType, LuaVersionCondition pub use property::LuaCommonProperty; pub use property::{LuaDeprecated, LuaExport, LuaExportScope, LuaPropertyId}; +pub use crate::db_index::property::property::LuaAttributeUse; use crate::{DbIndex, FileId, LuaMember, LuaSignatureId}; use super::{LuaSemanticDeclId, traits::LuaIndex}; @@ -234,6 +235,22 @@ impl LuaPropertyIndex { Some(()) } + pub fn add_attribute_use( + &mut self, + file_id: FileId, + owner_id: LuaSemanticDeclId, + attribute_use: LuaAttributeUse, + ) -> Option<()> { + let (property, _) = self.get_or_create_property(owner_id.clone())?; + property.add_attribute_use(attribute_use); + + self.in_filed_owner + .entry(file_id) + .or_default() + .insert(owner_id); + Some(()) + } + pub fn get_property(&self, owner_id: &LuaSemanticDeclId) -> Option<&LuaCommonProperty> { self.property_owners_map .get(owner_id) diff --git a/crates/emmylua_code_analysis/src/db_index/property/property.rs b/crates/emmylua_code_analysis/src/db_index/property/property.rs index 5489334f8..a97316e02 100644 --- a/crates/emmylua_code_analysis/src/db_index/property/property.rs +++ b/crates/emmylua_code_analysis/src/db_index/property/property.rs @@ -1,6 +1,11 @@ +use std::sync::Arc; + use emmylua_parser::{LuaVersionCondition, VisibilityKind}; -use crate::db_index::property::decl_feature::{DeclFeatureFlag, PropertyDeclFeature}; +use crate::{ + LuaType, LuaTypeDeclId, + db_index::property::decl_feature::{DeclFeatureFlag, PropertyDeclFeature}, +}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct LuaCommonProperty { @@ -12,6 +17,7 @@ pub struct LuaCommonProperty { pub tag_content: Option>, pub export: Option, pub decl_features: DeclFeatureFlag, + pub attribute_uses: Option>>, } impl Default for LuaCommonProperty { @@ -31,6 +37,7 @@ impl LuaCommonProperty { tag_content: None, export: None, decl_features: DeclFeatureFlag::new(), + attribute_uses: None, } } @@ -90,6 +97,26 @@ impl LuaCommonProperty { pub fn add_decl_feature(&mut self, feature: PropertyDeclFeature) { self.decl_features.add_feature(feature); } + + pub fn add_attribute_use(&mut self, attribute_use: LuaAttributeUse) { + Arc::make_mut( + self.attribute_uses + .get_or_insert_with(|| Arc::new(Vec::new())), + ) + .push(attribute_use); + } + + pub fn attribute_uses(&self) -> Option<&Arc>> { + self.attribute_uses.as_ref() + } + + pub fn find_attribute_use(&self, id: LuaTypeDeclId) -> Option<&LuaAttributeUse> { + self.attribute_uses.as_ref().and_then(|attribute_uses| { + attribute_uses + .iter() + .find(|attribute_use| attribute_use.id == id) + }) + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -144,3 +171,22 @@ impl LuaPropertyId { Self { id } } } + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct LuaAttributeUse { + pub id: LuaTypeDeclId, + pub args: Vec<(String, Option)>, +} + +impl LuaAttributeUse { + pub fn new(id: LuaTypeDeclId, args: Vec<(String, Option)>) -> Self { + Self { id, args } + } + + pub fn get_param_by_name(&self, name: &str) -> Option<&LuaType> { + self.args + .iter() + .find(|(n, _)| n == name) + .and_then(|(_, typ)| typ.as_ref()) + } +} diff --git a/crates/emmylua_code_analysis/src/db_index/signature/mod.rs b/crates/emmylua_code_analysis/src/db_index/signature/mod.rs index 223c9cbc3..7c496430b 100644 --- a/crates/emmylua_code_analysis/src/db_index/signature/mod.rs +++ b/crates/emmylua_code_analysis/src/db_index/signature/mod.rs @@ -6,8 +6,8 @@ use std::collections::{HashMap, HashSet}; pub use async_state::AsyncState; pub use signature::{ - LuaDocParamInfo, LuaDocReturnInfo, LuaNoDiscard, LuaSignature, LuaSignatureId, - SignatureReturnStatus, + LuaDocParamInfo, LuaDocReturnInfo, LuaGenericParamInfo, LuaNoDiscard, LuaSignature, + LuaSignatureId, SignatureReturnStatus, }; use crate::FileId; diff --git a/crates/emmylua_code_analysis/src/db_index/signature/signature.rs b/crates/emmylua_code_analysis/src/db_index/signature/signature.rs index 4371acc47..9519293a5 100644 --- a/crates/emmylua_code_analysis/src/db_index/signature/signature.rs +++ b/crates/emmylua_code_analysis/src/db_index/signature/signature.rs @@ -11,11 +11,11 @@ use crate::{ FileId, db_index::{LuaFunctionType, LuaType}, }; -use crate::{SemanticModel, VariadicType, first_param_may_not_self}; +use crate::{LuaAttributeUse, SemanticModel, VariadicType, first_param_may_not_self}; #[derive(Debug)] pub struct LuaSignature { - pub generic_params: Vec<(String, Option)>, + pub generic_params: Vec>, pub overloads: Vec>, pub param_docs: HashMap, pub params: Vec, @@ -180,6 +180,16 @@ pub struct LuaDocParamInfo { pub type_ref: LuaType, pub nullable: bool, pub description: Option, + pub attributes: Option>, +} + +impl LuaDocParamInfo { + pub fn get_attribute_by_name(&self, name: &str) -> Option<&LuaAttributeUse> { + self.attributes + .iter() + .flatten() + .find(|attr| attr.id.get_name() == name) + } } #[derive(Debug, Clone)] @@ -187,6 +197,7 @@ pub struct LuaDocReturnInfo { pub name: Option, pub type_ref: LuaType, pub description: Option, + pub attributes: Option>, } #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)] @@ -277,3 +288,24 @@ pub enum SignatureReturnStatus { DocResolve, InferResolve, } + +#[derive(Debug, Clone)] +pub struct LuaGenericParamInfo { + pub name: String, + pub type_constraint: Option, + pub attributes: Option>, +} + +impl LuaGenericParamInfo { + pub fn new( + name: String, + type_constraint: Option, + attributes: Option>, + ) -> Self { + Self { + name, + type_constraint, + attributes, + } + } +} diff --git a/crates/emmylua_code_analysis/src/db_index/type/generic_param.rs b/crates/emmylua_code_analysis/src/db_index/type/generic_param.rs index 153e5258d..a6282537c 100644 --- a/crates/emmylua_code_analysis/src/db_index/type/generic_param.rs +++ b/crates/emmylua_code_analysis/src/db_index/type/generic_param.rs @@ -1,20 +1,27 @@ use smol_str::SmolStr; -use crate::LuaType; +use crate::{LuaAttributeUse, LuaType}; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct GenericParam { pub name: SmolStr, pub type_constraint: Option, pub is_variadic: bool, + pub attributes: Option>, } impl GenericParam { - pub fn new(name: SmolStr, type_constraint: Option, is_variadic: bool) -> Self { + pub fn new( + name: SmolStr, + type_constraint: Option, + is_variadic: bool, + attributes: Option>, + ) -> Self { Self { name, type_constraint, is_variadic, + attributes, } } } diff --git a/crates/emmylua_code_analysis/src/db_index/type/humanize_type.rs b/crates/emmylua_code_analysis/src/db_index/type/humanize_type.rs index 33974104c..80aed9149 100644 --- a/crates/emmylua_code_analysis/src/db_index/type/humanize_type.rs +++ b/crates/emmylua_code_analysis/src/db_index/type/humanize_type.rs @@ -713,7 +713,7 @@ fn humanize_signature_type( let generics = signature .generic_params .iter() - .map(|(name, _)| name.to_string()) + .map(|generic_param| generic_param.name.to_string()) .collect::>() .join(", "); diff --git a/crates/emmylua_code_analysis/src/db_index/type/mod.rs b/crates/emmylua_code_analysis/src/db_index/type/mod.rs index 0dbea4606..ad941ac60 100644 --- a/crates/emmylua_code_analysis/src/db_index/type/mod.rs +++ b/crates/emmylua_code_analysis/src/db_index/type/mod.rs @@ -12,9 +12,7 @@ use crate::{DbIndex, FileId, InFiled}; pub use generic_param::GenericParam; pub use humanize_type::{RenderLevel, format_union_type, humanize_type}; use std::collections::{HashMap, HashSet}; -pub use type_decl::{ - LuaDeclLocation, LuaDeclTypeKind, LuaTypeAttribute, LuaTypeDecl, LuaTypeDeclId, -}; +pub use type_decl::{LuaDeclLocation, LuaDeclTypeKind, LuaTypeDecl, LuaTypeDeclId, LuaTypeFlag}; pub use type_ops::TypeOps; pub use type_owner::{LuaTypeCache, LuaTypeOwner}; pub use type_visit_trait::TypeVisitTrait; diff --git a/crates/emmylua_code_analysis/src/db_index/type/test.rs b/crates/emmylua_code_analysis/src/db_index/type/test.rs index 40667fa4c..e9df26636 100644 --- a/crates/emmylua_code_analysis/src/db_index/type/test.rs +++ b/crates/emmylua_code_analysis/src/db_index/type/test.rs @@ -4,7 +4,7 @@ mod test { use crate::db_index::traits::LuaIndex; use crate::db_index::r#type::LuaTypeIndex; - use crate::db_index::{LuaDeclTypeKind, LuaTypeAttribute}; + use crate::db_index::{LuaDeclTypeKind, LuaTypeFlag}; use crate::{FileId, LuaTypeDecl, LuaTypeDeclId}; fn create_type_index() -> LuaTypeIndex { @@ -26,7 +26,7 @@ mod test { TextRange::new(0.into(), 4.into()), "new_type".to_string(), LuaDeclTypeKind::Alias, - LuaTypeAttribute::Partial.into(), + LuaTypeFlag::Partial.into(), LuaTypeDeclId::new("test.new_type"), ), ); @@ -62,7 +62,7 @@ mod test { TextRange::new(0.into(), 4.into()), "new_type".to_string(), LuaDeclTypeKind::Alias, - LuaTypeAttribute::Partial.into(), + LuaTypeFlag::Partial.into(), LuaTypeDeclId::new("test.new_type"), ), ); @@ -92,7 +92,7 @@ mod test { TextRange::new(0.into(), 4.into()), "new_type".to_string(), LuaDeclTypeKind::Class, - LuaTypeAttribute::Partial.into(), + LuaTypeFlag::Partial.into(), LuaTypeDeclId::new("new_type"), ), ); @@ -110,7 +110,7 @@ mod test { TextRange::new(0.into(), 4.into()), "new_type".to_string(), LuaDeclTypeKind::Class, - LuaTypeAttribute::Partial.into(), + LuaTypeFlag::Partial.into(), LuaTypeDeclId::new(".new_type"), ), ); @@ -123,7 +123,7 @@ mod test { TextRange::new(0.into(), 4.into()), "new_type".to_string(), LuaDeclTypeKind::Class, - LuaTypeAttribute::Partial.into(), + LuaTypeFlag::Partial.into(), LuaTypeDeclId::new("new_type"), ), ); @@ -150,7 +150,7 @@ mod test { TextRange::new(0.into(), 4.into()), "new_type".to_string(), LuaDeclTypeKind::Class, - LuaTypeAttribute::Partial.into(), + LuaTypeFlag::Partial.into(), LuaTypeDeclId::new("test.new_type"), ), ); diff --git a/crates/emmylua_code_analysis/src/db_index/type/type_decl.rs b/crates/emmylua_code_analysis/src/db_index/type/type_decl.rs index 0c1d2323d..1140348e9 100644 --- a/crates/emmylua_code_analysis/src/db_index/type/type_decl.rs +++ b/crates/emmylua_code_analysis/src/db_index/type/type_decl.rs @@ -15,10 +15,11 @@ pub enum LuaDeclTypeKind { Class, Enum, Alias, + Attribute, } flags! { - pub enum LuaTypeAttribute: u8 { + pub enum LuaTypeFlag: u8 { None, Key, Partial, @@ -42,7 +43,7 @@ impl LuaTypeDecl { range: TextRange, name: String, kind: LuaDeclTypeKind, - attrib: FlagSet, + flag: FlagSet, id: LuaTypeDeclId, ) -> Self { Self { @@ -50,13 +51,14 @@ impl LuaTypeDecl { locations: vec![LuaDeclLocation { file_id, range, - attrib, + flag, }], id, extra: match kind { LuaDeclTypeKind::Enum => LuaTypeExtra::Enum { base: None }, LuaDeclTypeKind::Class => LuaTypeExtra::Class, LuaDeclTypeKind::Alias => LuaTypeExtra::Alias { origin: None }, + LuaDeclTypeKind::Attribute => LuaTypeExtra::Attribute { typ: None }, }, } } @@ -85,22 +87,26 @@ impl LuaTypeDecl { matches!(self.extra, LuaTypeExtra::Alias { .. }) } + pub fn is_attribute(&self) -> bool { + matches!(self.extra, LuaTypeExtra::Attribute { .. }) + } + pub fn is_exact(&self) -> bool { self.locations .iter() - .any(|l| l.attrib.contains(LuaTypeAttribute::Exact)) + .any(|l| l.flag.contains(LuaTypeFlag::Exact)) } pub fn is_partial(&self) -> bool { self.locations .iter() - .any(|l| l.attrib.contains(LuaTypeAttribute::Partial)) + .any(|l| l.flag.contains(LuaTypeFlag::Partial)) } pub fn is_enum_key(&self) -> bool { self.locations .iter() - .any(|l| l.attrib.contains(LuaTypeAttribute::Key)) + .any(|l| l.flag.contains(LuaTypeFlag::Key)) } pub fn get_id(&self) -> LuaTypeDeclId { @@ -166,6 +172,20 @@ impl LuaTypeDecl { } } + pub fn add_attribute_type(&mut self, attribute_type: LuaType) { + if let LuaTypeExtra::Attribute { typ } = &mut self.extra { + *typ = Some(attribute_type); + } + } + + pub fn get_attribute_type(&self) -> Option<&LuaType> { + if let LuaTypeExtra::Attribute { typ: Some(typ) } = &self.extra { + Some(typ) + } else { + None + } + } + pub fn merge_decl(&mut self, other: LuaTypeDecl) { self.locations.extend(other.locations); } @@ -301,7 +321,7 @@ impl<'de> Deserialize<'de> for LuaTypeDeclId { pub struct LuaDeclLocation { pub file_id: FileId, pub range: TextRange, - pub attrib: FlagSet, + pub flag: FlagSet, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] @@ -309,4 +329,5 @@ pub enum LuaTypeExtra { Enum { base: Option }, Class, Alias { origin: Option }, + Attribute { typ: Option }, } diff --git a/crates/emmylua_code_analysis/src/db_index/type/types.rs b/crates/emmylua_code_analysis/src/db_index/type/types.rs index 8fc5621a5..282dffb4b 100644 --- a/crates/emmylua_code_analysis/src/db_index/type/types.rs +++ b/crates/emmylua_code_analysis/src/db_index/type/types.rs @@ -64,6 +64,7 @@ pub enum LuaType { ConstTplRef(Arc), Language(ArcIntern), ModuleRef(FileId), + DocAttribute(Arc), } impl PartialEq for LuaType { @@ -114,6 +115,7 @@ impl PartialEq for LuaType { (LuaType::ConstTplRef(a), LuaType::ConstTplRef(b)) => a == b, (LuaType::Language(a), LuaType::Language(b)) => a == b, (LuaType::ModuleRef(a), LuaType::ModuleRef(b)) => a == b, + (LuaType::DocAttribute(a), LuaType::DocAttribute(b)) => a == b, _ => false, // 不同变体之间不相等 } } @@ -193,6 +195,7 @@ impl Hash for LuaType { LuaType::ConstTplRef(a) => (46, a).hash(state), LuaType::Language(a) => (47, a).hash(state), LuaType::ModuleRef(a) => (48, a).hash(state), + LuaType::DocAttribute(a) => (52, a).hash(state), } } } @@ -1463,3 +1466,31 @@ impl LuaArrayType { self.base.contain_tpl() } } + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct LuaAttributeType { + params: Vec<(String, Option)>, +} + +impl TypeVisitTrait for LuaAttributeType { + fn visit_type(&self, f: &mut F) + where + F: FnMut(&LuaType), + { + for (_, t) in &self.params { + if let Some(t) = t { + t.visit_type(f); + } + } + } +} + +impl LuaAttributeType { + pub fn new(params: Vec<(String, Option)>) -> Self { + Self { params } + } + + pub fn get_params(&self) -> &[(String, Option)] { + &self.params + } +} diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/assign_type_mismatch.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/assign_type_mismatch.rs index f8d888874..dbeb34e0f 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/assign_type_mismatch.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/assign_type_mismatch.rs @@ -2,14 +2,14 @@ use std::ops::Deref; use emmylua_parser::{ LuaAssignStat, LuaAst, LuaAstNode, LuaAstToken, LuaExpr, LuaIndexExpr, LuaLocalStat, - LuaNameExpr, LuaTableExpr, LuaVarExpr, + LuaNameExpr, LuaSyntaxNode, LuaSyntaxToken, LuaTableExpr, LuaVarExpr, }; -use rowan::TextRange; +use rowan::{NodeOrToken, TextRange}; use crate::{ DiagnosticCode, LuaDeclExtra, LuaDeclId, LuaMemberKey, LuaSemanticDeclId, LuaType, - SemanticDeclLevel, SemanticModel, TypeCheckFailReason, TypeCheckResult, VariadicType, - infer_index_expr, + LuaTypeDeclId, SemanticDeclLevel, SemanticModel, TypeCheckFailReason, TypeCheckResult, + VariadicType, infer_index_expr, }; use super::{Checker, DiagnosticContext, humanize_lint_type}; @@ -78,7 +78,7 @@ fn check_name_expr( rowan::NodeOrToken::Node(name_expr.syntax().clone()), SemanticDeclLevel::default(), )?; - let source_type = match semantic_decl { + let source_type = match semantic_decl.clone() { LuaSemanticDeclId::LuaDecl(decl_id) => { let decl = semantic_model .get_db() @@ -116,9 +116,9 @@ fn check_name_expr( check_table_expr( context, semantic_model, + rowan::NodeOrToken::Node(name_expr.syntax().clone()), &expr, source_type.as_ref(), - Some(&value_type), ); } @@ -152,9 +152,9 @@ fn check_index_expr( check_table_expr( context, semantic_model, + rowan::NodeOrToken::Node(index_expr.syntax().clone()), &expr, source_type.as_ref(), - Some(&value_type), ); } Some(()) @@ -195,31 +195,45 @@ fn check_local_stat( check_table_expr( context, semantic_model, + rowan::NodeOrToken::Node(var.syntax().clone()), expr, Some(&var_type), - Some(&value_type), ); } } Some(()) } +/// 检查整个表, 返回`true`表示诊断出异常. pub fn check_table_expr( context: &mut DiagnosticContext, semantic_model: &SemanticModel, + decl_node: NodeOrToken, table_expr: &LuaExpr, table_type: Option<&LuaType>, // 记录的类型 - _expr_type: Option<&LuaType>, // 实际表达式推导出的类型 ) -> Option { - // 需要进行一些过滤 - // if table_type == expr_type { - // return Some(false); - // } + // 检查是否附加了元数据以跳过诊断 + if let Some(semantic_decl) = semantic_model.find_decl(decl_node, SemanticDeclLevel::default()) { + if let Some(property) = semantic_model + .get_db() + .get_property_index() + .get_property(&semantic_decl) + { + if let Some(lsp_perf_optim) = + property.find_attribute_use(LuaTypeDeclId::new("lsp_perf_optim")) + { + if let Some(LuaType::DocStringConst(code)) = + lsp_perf_optim.get_param_by_name("code") + { + if code.as_ref() == "check_table_field" { + return Some(false); + } + }; + } + } + } + let table_type = table_type?; - // match table_type { - // LuaType::Def(_) => return Some(false), - // _ => {} - // } if let Some(table_expr) = LuaTableExpr::cast(table_expr.syntax().clone()) { return check_table_expr_content(context, semantic_model, table_type, &table_expr); } diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/attribute_check.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/attribute_check.rs new file mode 100644 index 000000000..415bfd255 --- /dev/null +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/attribute_check.rs @@ -0,0 +1,234 @@ +use std::collections::HashSet; + +use crate::{ + DiagnosticCode, LuaType, SemanticModel, TypeCheckFailReason, TypeCheckResult, + diagnostic::checker::{generic::infer_doc_type::infer_doc_type, humanize_lint_type}, +}; +use emmylua_parser::{ + LuaAstNode, LuaDocAttributeUse, LuaDocTagAttributeUse, LuaDocType, LuaExpr, LuaLiteralExpr, +}; +use rowan::TextRange; + +use super::{Checker, DiagnosticContext}; + +pub struct AttributeCheckChecker; + +impl Checker for AttributeCheckChecker { + const CODES: &[DiagnosticCode] = &[ + DiagnosticCode::AttributeParamTypeMismatch, + DiagnosticCode::AttributeMissingParameter, + DiagnosticCode::AttributeRedundantParameter, + ]; + + fn check(context: &mut DiagnosticContext, semantic_model: &SemanticModel) { + let root = semantic_model.get_root().clone(); + for tag_use in root.descendants::() { + for attribute_use in tag_use.get_attribute_uses() { + check_attribute_use(context, semantic_model, &attribute_use); + } + } + } +} + +fn check_attribute_use( + context: &mut DiagnosticContext, + semantic_model: &SemanticModel, + attribute_use: &LuaDocAttributeUse, +) -> Option<()> { + let attribute_type = + infer_doc_type(semantic_model, &LuaDocType::Name(attribute_use.get_type()?)); + let LuaType::Ref(type_id) = attribute_type else { + return None; + }; + let type_decl = semantic_model + .get_db() + .get_type_index() + .get_type_decl(&type_id)?; + if !type_decl.is_attribute() { + return None; + } + let LuaType::DocAttribute(attr_def) = type_decl.get_attribute_type()? else { + return None; + }; + + let def_params = attr_def.get_params(); + let args = match attribute_use.get_arg_list() { + Some(arg_list) => arg_list.get_args().collect::>(), + None => vec![], + }; + check_param_count(context, &def_params, &attribute_use, &args); + check_param(context, semantic_model, &def_params, args); + + Some(()) +} + +/// 检查参数数量是否匹配 +fn check_param_count( + context: &mut DiagnosticContext, + def_params: &[(String, Option)], + attribute_use: &LuaDocAttributeUse, + args: &[LuaLiteralExpr], +) -> Option<()> { + let call_args_count = args.len(); + // 调用参数少于定义参数, 需要考虑可空参数 + if call_args_count < def_params.len() { + for def_param in def_params[call_args_count..].iter() { + if def_param.0 == "..." { + break; + } + if def_param.1.as_ref().is_some_and(is_nullable) { + continue; + } + context.add_diagnostic( + DiagnosticCode::AttributeMissingParameter, + match args.last() { + Some(arg) => arg.get_range(), + None => attribute_use.get_range(), + }, + t!( + "expected %{num} parameters but found %{found_num}", + num = def_params.len(), + found_num = call_args_count + ) + .to_string(), + None, + ); + } + } + // 调用参数多于定义参数, 需要考虑可变参数 + else if call_args_count > def_params.len() { + // 参数定义中最后一个参数是 `...` + if def_params.last().is_some_and(|(name, typ)| { + name == "..." || typ.as_ref().is_some_and(|typ| typ.is_variadic()) + }) { + return Some(()); + } + for arg in args[def_params.len()..].iter() { + context.add_diagnostic( + DiagnosticCode::AttributeRedundantParameter, + arg.get_range(), + t!( + "expected %{num} parameters but found %{found_num}", + num = def_params.len(), + found_num = call_args_count + ) + .to_string(), + None, + ); + } + } + + Some(()) +} + +/// 检查参数是否匹配 +fn check_param( + context: &mut DiagnosticContext, + semantic_model: &SemanticModel, + def_params: &[(String, Option)], + args: Vec, +) -> Option<()> { + let mut call_arg_types = Vec::new(); + for arg in &args { + let arg_type = semantic_model + .infer_expr(LuaExpr::LiteralExpr(arg.clone())) + .ok()?; + call_arg_types.push(arg_type); + } + + for (idx, param) in def_params.iter().enumerate() { + if param.0 == "..." { + if call_arg_types.len() < idx { + break; + } + if let Some(variadic_type) = param.1.clone() { + for arg_type in call_arg_types[idx..].iter() { + let result = semantic_model.type_check_detail(&variadic_type, arg_type); + if result.is_err() { + add_type_check_diagnostic( + context, + semantic_model, + args.get(idx)?.get_range(), + &variadic_type, + arg_type, + result, + ); + } + } + } + break; + } + if let Some(param_type) = param.1.clone() { + let arg_type = call_arg_types.get(idx).unwrap_or(&LuaType::Any); + let result = semantic_model.type_check_detail(¶m_type, arg_type); + if result.is_err() { + add_type_check_diagnostic( + context, + semantic_model, + args.get(idx)?.get_range(), + ¶m_type, + arg_type, + result, + ); + } + } + } + Some(()) +} + +fn add_type_check_diagnostic( + context: &mut DiagnosticContext, + semantic_model: &SemanticModel, + range: TextRange, + param_type: &LuaType, + expr_type: &LuaType, + result: TypeCheckResult, +) { + let db = semantic_model.get_db(); + match result { + Ok(_) => (), + Err(reason) => { + let reason_message = match reason { + TypeCheckFailReason::TypeNotMatchWithReason(reason) => reason, + TypeCheckFailReason::TypeNotMatch | TypeCheckFailReason::DonotCheck => { + "".to_string() + } + TypeCheckFailReason::TypeRecursion => "type recursion".to_string(), + }; + context.add_diagnostic( + DiagnosticCode::AttributeParamTypeMismatch, + range, + t!( + "expected `%{source}` but found `%{found}`. %{reason}", + source = humanize_lint_type(db, param_type), + found = humanize_lint_type(db, expr_type), + reason = reason_message + ) + .to_string(), + None, + ); + } + } +} + +fn is_nullable(typ: &LuaType) -> bool { + let mut stack: Vec = Vec::new(); + stack.push(typ.clone()); + let mut visited = HashSet::new(); + while let Some(typ) = stack.pop() { + if visited.contains(&typ) { + continue; + } + visited.insert(typ.clone()); + match typ { + LuaType::Any | LuaType::Unknown | LuaType::Nil => return true, + LuaType::Union(u) => { + for t in u.into_vec() { + stack.push(t); + } + } + _ => {} + } + } + false +} diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/deprecated.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/deprecated.rs index 2c9ae365b..19d9f5063 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/deprecated.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/deprecated.rs @@ -1,8 +1,8 @@ use emmylua_parser::{LuaAst, LuaAstNode, LuaIndexExpr, LuaNameExpr}; use crate::{ - DiagnosticCode, LuaDeclId, LuaDeprecated, LuaMemberId, LuaSemanticDeclId, SemanticDeclLevel, - SemanticModel, + DiagnosticCode, LuaDeclId, LuaDeprecated, LuaMemberId, LuaSemanticDeclId, LuaType, + SemanticDeclLevel, SemanticModel, }; use super::{Checker, DiagnosticContext}; @@ -45,23 +45,12 @@ fn check_name_expr( return Some(()); } - let property = semantic_model - .get_db() - .get_property_index() - .get_property(&semantic_decl)?; - if let Some(deprecated) = property.deprecated() { - let deprecated_message = match deprecated { - LuaDeprecated::Deprecated => "deprecated".to_string(), - LuaDeprecated::DeprecatedWithMessage(message) => message.to_string(), - }; - - context.add_diagnostic( - DiagnosticCode::Deprecated, - name_expr.get_range(), - deprecated_message, - None, - ); - } + check_deprecated( + context, + semantic_model, + &semantic_decl, + name_expr.get_range(), + ); Some(()) } @@ -80,24 +69,43 @@ fn check_index_expr( { return Some(()); } + let index_name_range = index_expr.get_index_name_token()?.text_range(); + check_deprecated(context, semantic_model, &semantic_decl, index_name_range); + Some(()) +} + +fn check_deprecated( + context: &mut DiagnosticContext, + semantic_model: &SemanticModel, + semantic_decl: &LuaSemanticDeclId, + range: rowan::TextRange, +) { let property = semantic_model .get_db() .get_property_index() - .get_property(&semantic_decl)?; + .get_property(semantic_decl); + let Some(property) = property else { + return; + }; if let Some(deprecated) = property.deprecated() { let deprecated_message = match deprecated { LuaDeprecated::Deprecated => "deprecated".to_string(), LuaDeprecated::DeprecatedWithMessage(message) => message.to_string(), }; - let index_name_range = index_expr.get_index_name_token()?.text_range(); - - context.add_diagnostic( - DiagnosticCode::Deprecated, - index_name_range, - deprecated_message, - None, - ); + context.add_diagnostic(DiagnosticCode::Deprecated, range, deprecated_message, None); + } + // 检查特性 + if let Some(attribute_uses) = property.attribute_uses() { + for attribute_use in attribute_uses.iter() { + if attribute_use.id.get_name() == "deprecated" { + let deprecated_message = + match attribute_use.args.first().and_then(|(_, typ)| typ.as_ref()) { + Some(LuaType::DocStringConst(message)) => message.as_ref().to_string(), + _ => "deprecated".to_string(), + }; + context.add_diagnostic(DiagnosticCode::Deprecated, range, deprecated_message, None); + } + } } - Some(()) } diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/duplicate_type.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/duplicate_type.rs index 2cfc4f446..640c8bf62 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/duplicate_type.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/duplicate_type.rs @@ -2,7 +2,7 @@ use emmylua_parser::{ LuaAstNode, LuaAstToken, LuaDocTag, LuaDocTagAlias, LuaDocTagClass, LuaDocTagEnum, }; -use crate::{DiagnosticCode, LuaTypeAttribute, SemanticModel}; +use crate::{DiagnosticCode, LuaTypeFlag, SemanticModel}; use super::{Checker, DiagnosticContext}; @@ -45,13 +45,13 @@ fn check_duplicate_class(context: &mut DiagnosticContext, class_tag: LuaDocTagCl let mut partial_times = 0; let mut constructor_times = 0; for location in locations { - let attrib = location.attrib; - if attrib.contains(LuaTypeAttribute::Meta) { + let flag = location.flag; + if flag.contains(LuaTypeFlag::Meta) { continue; } - if attrib.contains(LuaTypeAttribute::Partial) { + if flag.contains(LuaTypeFlag::Partial) { partial_times += 1; - } else if attrib.contains(LuaTypeAttribute::Constructor) { + } else if flag.contains(LuaTypeFlag::Constructor) { constructor_times += 1; } else { type_times += 1; @@ -104,11 +104,11 @@ fn check_duplicate_enum(context: &mut DiagnosticContext, enum_tag: LuaDocTagEnum let mut type_times = 0; let mut partial_times = 0; for location in locations { - let attrib = location.attrib; - if attrib.contains(LuaTypeAttribute::Meta) { + let flag = location.flag; + if flag.contains(LuaTypeFlag::Meta) { continue; } - if attrib.contains(LuaTypeAttribute::Partial) { + if flag.contains(LuaTypeFlag::Partial) { partial_times += 1; } else { type_times += 1; @@ -148,8 +148,8 @@ fn check_duplicate_alias(context: &mut DiagnosticContext, alias_tag: LuaDocTagAl if locations.len() > 1 { let mut type_times = 0; for location in locations { - let attrib = location.attrib; - if !attrib.contains(LuaTypeAttribute::Meta) { + let flag = location.flag; + if !flag.contains(LuaTypeFlag::Meta) { type_times += 1; } } diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/generic/generic_constraint_mismatch.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/generic/generic_constraint_mismatch.rs index c04a0f1c1..b7dc0fae7 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/generic/generic_constraint_mismatch.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/generic/generic_constraint_mismatch.rs @@ -201,7 +201,11 @@ fn get_extend_type( signature: &LuaSignature, ) -> Option { match tpl_id { - GenericTplId::Func(tpl_id) => signature.generic_params.get(tpl_id as usize)?.1.clone(), + GenericTplId::Func(tpl_id) => signature + .generic_params + .get(tpl_id as usize)? + .type_constraint + .clone(), GenericTplId::Type(tpl_id) => { let prefix_expr = call_expr.get_prefix_expr()?; let semantic_decl = semantic_model.find_decl( @@ -391,10 +395,10 @@ fn try_instantiate_arg_type( .get_db() .get_signature_index() .get(&signature_id)?; - if let Some((_, param_type)) = + if let Some(generic_param) = signature.generic_params.get(tpl_id as usize) { - return param_type.clone(); + return generic_param.type_constraint.clone(); } } _ => return None, diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/generic/infer_doc_type.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/generic/infer_doc_type.rs index 94ef3d6f6..520d25647 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/generic/infer_doc_type.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/generic/infer_doc_type.rs @@ -1,19 +1,19 @@ use std::sync::Arc; use emmylua_parser::{ - LuaAstNode, LuaDocBinaryType, LuaDocDescriptionOwner, LuaDocFuncType, LuaDocGenericType, - LuaDocMultiLineUnionType, LuaDocObjectFieldKey, LuaDocObjectType, LuaDocStrTplType, LuaDocType, - LuaDocUnaryType, LuaDocVariadicType, LuaLiteralToken, LuaSyntaxKind, LuaTypeBinaryOperator, - LuaTypeUnaryOperator, + LuaAstNode, LuaDocAttributeType, LuaDocBinaryType, LuaDocDescriptionOwner, LuaDocFuncType, + LuaDocGenericType, LuaDocMultiLineUnionType, LuaDocObjectFieldKey, LuaDocObjectType, + LuaDocStrTplType, LuaDocType, LuaDocUnaryType, LuaDocVariadicType, LuaLiteralToken, + LuaSyntaxKind, LuaTypeBinaryOperator, LuaTypeUnaryOperator, }; use rowan::TextRange; use smol_str::SmolStr; use crate::{ AsyncState, InFiled, LuaAliasCallKind, LuaAliasCallType, LuaArrayLen, LuaArrayType, - LuaFunctionType, LuaGenericType, LuaIndexAccessKey, LuaIntersectionType, LuaMultiLineUnion, - LuaObjectType, LuaStringTplType, LuaTupleStatus, LuaTupleType, LuaType, LuaTypeDeclId, - SemanticModel, TypeOps, VariadicType, + LuaAttributeType, LuaFunctionType, LuaGenericType, LuaIndexAccessKey, LuaIntersectionType, + LuaMultiLineUnion, LuaObjectType, LuaStringTplType, LuaTupleStatus, LuaTupleType, LuaType, + LuaTypeDeclId, SemanticModel, TypeOps, VariadicType, }; pub fn infer_doc_type(semantic_model: &SemanticModel, node: &LuaDocType) -> LuaType { @@ -108,6 +108,9 @@ pub fn infer_doc_type(semantic_model: &SemanticModel, node: &LuaDocType) -> LuaT LuaDocType::MultiLineUnion(multi_union) => { return infer_multi_line_union_type(semantic_model, multi_union); } + LuaDocType::Attribute(attribute_type) => { + return infer_attribute_type(semantic_model, attribute_type); + } _ => {} } LuaType::Unknown @@ -563,3 +566,35 @@ fn infer_multi_line_union_type( LuaType::MultiLineUnion(LuaMultiLineUnion::new(union_members).into()) } + +fn infer_attribute_type( + semantic_model: &SemanticModel, + attribute_type: &LuaDocAttributeType, +) -> LuaType { + let mut params_result = Vec::new(); + for param in attribute_type.get_params() { + let name = if let Some(param) = param.get_name_token() { + param.get_name_text().to_string() + } else if param.is_dots() { + "...".to_string() + } else { + continue; + }; + + let nullable = param.is_nullable(); + + let type_ref = if let Some(type_ref) = param.get_type() { + let mut typ = infer_doc_type(semantic_model, &type_ref); + if nullable && !typ.is_nullable() { + typ = TypeOps::Union.apply(semantic_model.get_db(), &typ, &LuaType::Nil); + } + Some(typ) + } else { + None + }; + + params_result.push((name, type_ref)); + } + + LuaType::DocAttribute(LuaAttributeType::new(params_result).into()) +} diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/mod.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/mod.rs index 66b328cbf..449b28fdb 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/mod.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/mod.rs @@ -1,6 +1,7 @@ mod access_invisible; mod analyze_error; mod assign_type_mismatch; +mod attribute_check; mod await_in_sync; mod cast_type_mismatch; mod check_field; @@ -111,6 +112,7 @@ pub fn check_file(context: &mut DiagnosticContext, semantic_model: &SemanticMode run_check::(context, semantic_model); run_check::(context, semantic_model); run_check::(context, semantic_model); + run_check::(context, semantic_model); run_check::( context, diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/param_type_check.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/param_type_check.rs index da345a082..8a52b7457 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/param_type_check.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/param_type_check.rs @@ -105,9 +105,9 @@ fn check_call_expr( && let Some(add_diagnostic) = check_table_expr( context, semantic_model, + rowan::NodeOrToken::Node(arg_expr.syntax().clone()), arg_expr, Some(¶m_type), - Some(arg_type), ) && add_diagnostic { diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/return_type_mismatch.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/return_type_mismatch.rs index e69bf6aae..c8415cd13 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/return_type_mismatch.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/return_type_mismatch.rs @@ -94,9 +94,9 @@ fn check_return_stat( check_table_expr( context, semantic_model, + rowan::NodeOrToken::Node(return_expr.syntax().clone()), return_expr, Some(check_type), - Some(return_expr_type), ); } @@ -132,9 +132,9 @@ fn check_return_stat( if let Some(add_diagnostic) = check_table_expr( context, semantic_model, + rowan::NodeOrToken::Node(return_expr.syntax().clone()), return_expr, Some(return_type), - Some(return_expr_type), ) && add_diagnostic { return Some(()); diff --git a/crates/emmylua_code_analysis/src/diagnostic/lua_diagnostic_code.rs b/crates/emmylua_code_analysis/src/diagnostic/lua_diagnostic_code.rs index a8b697c63..8460a7eed 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/lua_diagnostic_code.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/lua_diagnostic_code.rs @@ -107,6 +107,12 @@ pub enum DiagnosticCode { ReadOnly, /// Global variable defined in non-module scope GlobalInNonModule, + /// attribute-param-type-mismatch + AttributeParamTypeMismatch, + /// attribute-missing-parameter + AttributeMissingParameter, + /// attribute-redundant-parameter + AttributeRedundantParameter, #[serde(other)] None, diff --git a/crates/emmylua_code_analysis/src/semantic/generic/mod.rs b/crates/emmylua_code_analysis/src/semantic/generic/mod.rs index a75f85d24..23f3a74a5 100644 --- a/crates/emmylua_code_analysis/src/semantic/generic/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/generic/mod.rs @@ -50,10 +50,10 @@ pub fn get_tpl_ref_extend_type( match decl.extra { LuaDeclExtra::Param { signature_id, .. } => { let signature = db.get_signature_index().get(&signature_id)?; - if let Some((_, param_type)) = + if let Some(generic_param) = signature.generic_params.get(tpl_id as usize) { - return param_type.clone(); + return generic_param.type_constraint.clone(); } } _ => return None, diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_call/mod.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_call/mod.rs index 515bbf797..573d6ed35 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_call/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_call/mod.rs @@ -247,8 +247,8 @@ fn infer_type_doc_function( let operator_index = db.get_operator_index(); let operator_ids = operator_index - .get_operators(&type_id.into(), LuaOperatorMetaMethod::Call) - .ok_or(InferFailReason::None)?; + .get_operators(&type_id.clone().into(), LuaOperatorMetaMethod::Call) + .ok_or(InferFailReason::UnResolveOperatorCall)?; let mut overloads = Vec::new(); for overload_id in operator_ids { let operator = operator_index diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_fail_reason.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_fail_reason.rs index 20c5f2193..5e1ee32db 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_fail_reason.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_fail_reason.rs @@ -11,6 +11,7 @@ pub enum InferFailReason { FieldNotFound, UnResolveDeclType(LuaDeclId), UnResolveMemberType(LuaMemberId), + UnResolveOperatorCall, } impl InferFailReason { @@ -22,6 +23,7 @@ impl InferFailReason { | InferFailReason::FieldNotFound | InferFailReason::UnResolveDeclType(_) | InferFailReason::UnResolveMemberType(_) + | InferFailReason::UnResolveOperatorCall ) } } diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_table.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_table.rs index 0b607aeae..3133f3b81 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_table.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_table.rs @@ -251,7 +251,7 @@ fn infer_table_type_by_calleee( call_arg_number += 1; } (true, false) => { - call_arg_number -= 1; + call_arg_number = call_arg_number.saturating_sub(1); } } let typ = param_types diff --git a/crates/emmylua_code_analysis/src/semantic/mod.rs b/crates/emmylua_code_analysis/src/semantic/mod.rs index 4865b8627..2c848a59c 100644 --- a/crates/emmylua_code_analysis/src/semantic/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/mod.rs @@ -40,7 +40,7 @@ use type_check::is_sub_type_of; pub use visibility::check_export_visibility; use visibility::check_visibility; -use crate::semantic::member::find_members_with_key; +pub use crate::semantic::member::find_members_with_key; use crate::semantic::type_check::check_type_compact_detail; use crate::{Emmyrc, LuaDocument, LuaSemanticDeclId, ModuleInfo, db_index::LuaTypeDeclId}; use crate::{ diff --git a/crates/emmylua_doc_cli/src/json_generator/export.rs b/crates/emmylua_doc_cli/src/json_generator/export.rs index dbdf6c45b..bc6e5f7aa 100644 --- a/crates/emmylua_doc_cli/src/json_generator/export.rs +++ b/crates/emmylua_doc_cli/src/json_generator/export.rs @@ -260,9 +260,10 @@ fn export_signature( generics: signature .generic_params .iter() - .map(|(name, typ)| TypeVar { - name: name.clone(), - base: typ + .map(|generic_param| TypeVar { + name: generic_param.name.to_string(), + base: generic_param + .type_constraint .as_ref() .map(|typ| render_typ(db, typ, RenderLevel::Simple)), }) diff --git a/crates/emmylua_ls/Cargo.toml b/crates/emmylua_ls/Cargo.toml index 921a7f5f4..6ee83ffbb 100644 --- a/crates/emmylua_ls/Cargo.toml +++ b/crates/emmylua_ls/Cargo.toml @@ -39,6 +39,7 @@ serde_yml.workspace = true itertools.workspace = true dirs.workspace = true wax.workspace = true +internment.workspace = true [dependencies.clap] workspace = true diff --git a/crates/emmylua_ls/locales/tags/en.yaml b/crates/emmylua_ls/locales/tags/en.yaml index 2ed9ac9b8..fa7aed3e9 100644 --- a/crates/emmylua_ls/locales/tags/en.yaml +++ b/crates/emmylua_ls/locales/tags/en.yaml @@ -245,3 +245,15 @@ tags.language: | INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com'); ]] ``` +tags.attribute: | + `attribute` tag defines an attribute. Attribute is used to attach extra information to a definition. + Example: + ```lua + ---@attribute deprecated(message: string?) + + ---@class A + ---@[deprecated("delete")] # `b` field is marked as deprecated + ---@field b string + ---@[deprecated] # If `attribute` allows no parameters, the parentheses can be omitted + ---@field c string + ``` diff --git a/crates/emmylua_ls/locales/tags/zh_CN.yaml b/crates/emmylua_ls/locales/tags/zh_CN.yaml index b6e0a04e0..df8075a92 100644 --- a/crates/emmylua_ls/locales/tags/zh_CN.yaml +++ b/crates/emmylua_ls/locales/tags/zh_CN.yaml @@ -245,3 +245,15 @@ tags.language: | INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com'); ]] ``` +tags.attribute: | + `attribute` 标签定义一个特性。特性用于附加额外信息到定义。 + 示例: + ```lua + ---@attribute deprecated(message: string?) + + ---@class A + ---@[deprecated("delete")] + ---@field b string # `b` 字段被标记为已弃用 + ---@[deprecated] # 如果`attribute`允许无参数,则可以省略括号 + ---@field c string + ``` diff --git a/crates/emmylua_ls/locales/tags/zh_HK.yaml b/crates/emmylua_ls/locales/tags/zh_HK.yaml index 4cdf3230b..4c4eb8ef8 100644 --- a/crates/emmylua_ls/locales/tags/zh_HK.yaml +++ b/crates/emmylua_ls/locales/tags/zh_HK.yaml @@ -243,3 +243,15 @@ tags.language: | INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com'); ]] ``` +tags.attribute: | + `attribute` 標籤定義一個特性。特性用於附加額外信息到定義。 + 示例: + ```lua + ---@attribute deprecated(message: string?) + + ---@class A + ---@[deprecated("delete")] # `b` 字段被標記為已棄用 + ---@field b string + ---@[deprecated] # 如果`attribute`允許無參數,則可以省略括號 + ---@field c string + ``` diff --git a/crates/emmylua_ls/src/handlers/completion/add_completions/add_member_completion.rs b/crates/emmylua_ls/src/handlers/completion/add_completions/add_member_completion.rs index aec80ea87..39f174341 100644 --- a/crates/emmylua_ls/src/handlers/completion/add_completions/add_member_completion.rs +++ b/crates/emmylua_ls/src/handlers/completion/add_completions/add_member_completion.rs @@ -1,5 +1,5 @@ use emmylua_code_analysis::{ - DbIndex, LuaMemberInfo, LuaMemberKey, LuaSemanticDeclId, LuaType, SemanticModel, + DbIndex, LuaMemberInfo, LuaMemberKey, LuaSemanticDeclId, LuaType, LuaTypeDeclId, SemanticModel, try_extract_signature_id_from_field, }; use emmylua_parser::{ @@ -129,8 +129,8 @@ pub fn add_member_completion( ); } - // 尝试添加别名补全项, 如果添加成功, 则不添加原本 `[index]` 补全项 - if !try_add_alias_completion_item(builder, &member_info, &completion_item, &label) + // 尝试添加别名补全项, 如果添加成功, 则不添加原来的 `[index]` 补全项 + if !try_add_alias_completion_item_new(builder, &member_info, &completion_item, &label) .unwrap_or(false) { builder.add_completion_item(completion_item)?; @@ -310,15 +310,13 @@ fn get_resolve_function_params_str(typ: &LuaType, display: CallDisplay) -> Optio } } -/// 添加索引成员的别名补全项 -fn try_add_alias_completion_item( +fn try_add_alias_completion_item_new( builder: &mut CompletionBuilder, member_info: &LuaMemberInfo, completion_item: &CompletionItem, label: &String, ) -> Option { - let alias_label = extract_index_member_alias(&builder.semantic_model, member_info)?; - + let alias_label = get_index_alias_name(&builder.semantic_model, member_info)?; let mut alias_completion_item = completion_item.clone(); alias_completion_item.label = alias_label; alias_completion_item.insert_text = Some(label.clone()); @@ -336,9 +334,7 @@ fn try_add_alias_completion_item( Some(true) } -/// 从注释中提取索引成员的别名, 只处理整数成员. -/// 格式为`-- [nameX]`. -pub fn extract_index_member_alias( +pub fn get_index_alias_name( semantic_model: &SemanticModel, member_info: &LuaMemberInfo, ) -> Option { @@ -351,7 +347,6 @@ pub fn extract_index_member_alias( let LuaSemanticDeclId::Member(member_id) = property_owner_id else { return None; }; - let common_property = match db.get_property_index().get_property(property_owner_id) { Some(common_property) => common_property, None => { @@ -364,34 +359,15 @@ pub fn extract_index_member_alias( } }; - let description = common_property.description()?; - - // 只去掉左侧空白字符,保留右侧内容以支持后续文本 - let left_trimmed = description.trim_start(); - if !left_trimmed.starts_with('[') { - return None; - } - - // 找到对应的右方括号 - let close_bracket_pos = left_trimmed.find(']')?; - - let content = left_trimmed[1..close_bracket_pos].trim(); - - if content.is_empty() { - return None; - } - - let first_char = content.chars().next()?; - if !first_char.is_alphabetic() && first_char != '_' { - return None; - } - - if !content.chars().all(|c| c.is_alphanumeric() || c == '_') { - return None; - } - if content.parse::().is_ok() || content.parse::().is_ok() { - return None; - } - - Some(content.to_string()) + let alias_label = common_property + .find_attribute_use(LuaTypeDeclId::new("index_alias"))? + .args + .first() + .and_then(|(_, typ)| typ.as_ref()) + .and_then(|param| match param { + LuaType::DocStringConst(s) => Some(s.as_ref()), + _ => None, + })? + .to_string(); + Some(alias_label) } diff --git a/crates/emmylua_ls/src/handlers/completion/add_completions/mod.rs b/crates/emmylua_ls/src/handlers/completion/add_completions/mod.rs index f9ad12346..53162ff91 100644 --- a/crates/emmylua_ls/src/handlers/completion/add_completions/mod.rs +++ b/crates/emmylua_ls/src/handlers/completion/add_completions/mod.rs @@ -3,7 +3,7 @@ mod add_member_completion; mod check_match_word; pub use add_decl_completion::add_decl_completion; -pub use add_member_completion::extract_index_member_alias; +pub use add_member_completion::get_index_alias_name; pub use add_member_completion::{CompletionTriggerStatus, add_member_completion}; pub use check_match_word::check_match_word; use emmylua_code_analysis::{LuaSemanticDeclId, LuaType, RenderLevel}; diff --git a/crates/emmylua_ls/src/handlers/completion/data/doc_tags.rs b/crates/emmylua_ls/src/handlers/completion/data/doc_tags.rs index 3760cc942..c0f764362 100644 --- a/crates/emmylua_ls/src/handlers/completion/data/doc_tags.rs +++ b/crates/emmylua_ls/src/handlers/completion/data/doc_tags.rs @@ -32,4 +32,5 @@ pub const DOC_TAGS: &[&str] = &[ "return_cast", "export", "language", + "attribute", ]; diff --git a/crates/emmylua_ls/src/handlers/completion/mod.rs b/crates/emmylua_ls/src/handlers/completion/mod.rs index 71aa20486..b93604f58 100644 --- a/crates/emmylua_ls/src/handlers/completion/mod.rs +++ b/crates/emmylua_ls/src/handlers/completion/mod.rs @@ -5,7 +5,7 @@ mod data; mod providers; mod resolve_completion; -pub use add_completions::extract_index_member_alias; +pub use add_completions::get_index_alias_name; use completion_builder::CompletionBuilder; use completion_data::CompletionData; use emmylua_code_analysis::{EmmyLuaAnalysis, FileId}; diff --git a/crates/emmylua_ls/src/handlers/completion/providers/auto_require_provider.rs b/crates/emmylua_ls/src/handlers/completion/providers/auto_require_provider.rs index 32bd29d7f..51e12aed3 100644 --- a/crates/emmylua_ls/src/handlers/completion/providers/auto_require_provider.rs +++ b/crates/emmylua_ls/src/handlers/completion/providers/auto_require_provider.rs @@ -12,7 +12,7 @@ use crate::{ completion_data::CompletionData, }, }, - util::{key_name_convert, module_name_convert}, + util::{file_name_convert, module_name_convert}, }; pub fn add_completion(builder: &mut CompletionBuilder) -> Option<()> { @@ -136,7 +136,7 @@ fn try_add_member_completion_items( LuaType::TableConst(_) | LuaType::Def(_) => { let member_infos = builder.semantic_model.get_member_infos(export_type)?; for member_info in member_infos { - let key_name = key_name_convert( + let key_name = file_name_convert( &member_info.key.to_path(), &member_info.typ, file_conversion, diff --git a/crates/emmylua_ls/src/handlers/completion/providers/desc_provider.rs b/crates/emmylua_ls/src/handlers/completion/providers/desc_provider.rs index f2c6ae390..4a3315aa6 100644 --- a/crates/emmylua_ls/src/handlers/completion/providers/desc_provider.rs +++ b/crates/emmylua_ls/src/handlers/completion/providers/desc_provider.rs @@ -87,7 +87,7 @@ fn add_global_completions(builder: &mut CompletionBuilder) -> Option<()> { } // Types in namespaces. - complete_types_by_prefix(builder, "", Some(&seen_types)); + complete_types_by_prefix(builder, "", Some(&seen_types), None); // Types in current module. if let Some(module) = builder.semantic_model.get_module() @@ -167,7 +167,7 @@ fn add_by_prefix( // Modules. add_modules(builder, &prefix, None); - complete_types_by_prefix(builder, &prefix, Some(&seen_types)); + complete_types_by_prefix(builder, &prefix, Some(&seen_types), None); } None diff --git a/crates/emmylua_ls/src/handlers/completion/providers/doc_name_token_provider.rs b/crates/emmylua_ls/src/handlers/completion/providers/doc_name_token_provider.rs index 023783342..982745c42 100644 --- a/crates/emmylua_ls/src/handlers/completion/providers/doc_name_token_provider.rs +++ b/crates/emmylua_ls/src/handlers/completion/providers/doc_name_token_provider.rs @@ -1,8 +1,8 @@ use std::collections::HashSet; -use emmylua_code_analysis::{DiagnosticCode, LuaTypeAttribute}; +use emmylua_code_analysis::{DiagnosticCode, LuaTypeFlag}; use emmylua_parser::{ - LuaAst, LuaAstNode, LuaClosureExpr, LuaComment, LuaDocAttribute, LuaDocTag, LuaSyntaxKind, + LuaAst, LuaAstNode, LuaClosureExpr, LuaComment, LuaDocTag, LuaDocTypeFlag, LuaSyntaxKind, LuaSyntaxToken, LuaTokenKind, }; use lsp_types::CompletionItem; @@ -29,8 +29,8 @@ pub fn add_completion(builder: &mut CompletionBuilder) -> Option<()> { DocCompletionExpected::DiagnosticCode => { add_tag_diagnostic_code_completion(builder); } - DocCompletionExpected::ClassAttr(node) => { - add_tag_class_attr_completion(builder, node); + DocCompletionExpected::TypeFlag(node) => { + add_tag_type_flag_completion(builder, node); } DocCompletionExpected::Namespace => { add_tag_namespace_completion(builder); @@ -84,10 +84,9 @@ fn get_doc_completion_expected(trigger_token: &LuaSyntaxToken) -> Option { Some(DocCompletionExpected::DiagnosticCode) } - LuaSyntaxKind::DocAttribute => { - let attr = LuaDocAttribute::cast(parent.clone())?; - Some(DocCompletionExpected::ClassAttr(attr)) - } + LuaSyntaxKind::DocTypeFlag => Some(DocCompletionExpected::TypeFlag( + LuaDocTypeFlag::cast(parent.clone())?, + )), _ => None, } } @@ -105,20 +104,18 @@ fn get_doc_completion_expected(trigger_token: &LuaSyntaxToken) -> Option Some(DocCompletionExpected::DiagnosticCode), - LuaSyntaxKind::DocAttribute => { - let attr = LuaDocAttribute::cast(parent.clone())?; - Some(DocCompletionExpected::ClassAttr(attr)) - } + LuaSyntaxKind::DocTypeFlag => Some(DocCompletionExpected::TypeFlag( + LuaDocTypeFlag::cast(parent.clone())?, + )), _ => None, } } LuaTokenKind::TkLeftParen => { let parent = trigger_token.parent()?; match parent.kind().into() { - LuaSyntaxKind::DocAttribute => { - let attr = LuaDocAttribute::cast(parent.clone())?; - Some(DocCompletionExpected::ClassAttr(attr)) - } + LuaSyntaxKind::DocTypeFlag => Some(DocCompletionExpected::TypeFlag( + LuaDocTypeFlag::cast(parent.clone())?, + )), _ => None, } } @@ -132,7 +129,7 @@ enum DocCompletionExpected { Cast, DiagnosticAction, DiagnosticCode, - ClassAttr(LuaDocAttribute), + TypeFlag(LuaDocTypeFlag), Namespace, Using, Export, @@ -228,33 +225,33 @@ fn add_tag_diagnostic_code_completion(builder: &mut CompletionBuilder) { } } -fn add_tag_class_attr_completion( +fn add_tag_type_flag_completion( builder: &mut CompletionBuilder, - node: LuaDocAttribute, + node: LuaDocTypeFlag, ) -> Option<()> { - let mut attributes = vec![(LuaTypeAttribute::Partial, "partial")]; + let mut flags = vec![(LuaTypeFlag::Partial, "partial")]; match LuaDocTag::cast(node.syntax().parent()?)? { LuaDocTag::Alias(_) => {} LuaDocTag::Class(_) => { - attributes.push((LuaTypeAttribute::Exact, "exact")); - attributes.push((LuaTypeAttribute::Constructor, "constructor")); + flags.push((LuaTypeFlag::Exact, "exact")); + flags.push((LuaTypeFlag::Constructor, "constructor")); } LuaDocTag::Enum(_) => { - attributes.insert(0, (LuaTypeAttribute::Key, "key")); - attributes.push((LuaTypeAttribute::Exact, "exact")); + flags.insert(0, (LuaTypeFlag::Key, "key")); + flags.push((LuaTypeFlag::Exact, "exact")); } _ => {} } // 已存在的属性 - let mut existing_attrs = HashSet::new(); + let mut existing_flags = HashSet::new(); for token in node.get_attrib_tokens() { let name_text = token.get_name_text().to_string(); - existing_attrs.insert(name_text); + existing_flags.insert(name_text); } - for (_, name) in attributes.iter() { - if existing_attrs.contains(*name) { + for (_, name) in flags.iter() { + if existing_flags.contains(*name) { continue; } let completion_item = CompletionItem { diff --git a/crates/emmylua_ls/src/handlers/completion/providers/doc_type_provider.rs b/crates/emmylua_ls/src/handlers/completion/providers/doc_type_provider.rs index 3b4d6c935..c8aad985f 100644 --- a/crates/emmylua_ls/src/handlers/completion/providers/doc_type_provider.rs +++ b/crates/emmylua_ls/src/handlers/completion/providers/doc_type_provider.rs @@ -1,5 +1,5 @@ use emmylua_code_analysis::LuaTypeDeclId; -use emmylua_parser::{LuaAstNode, LuaDocNameType, LuaSyntaxKind, LuaTokenKind}; +use emmylua_parser::{LuaAstNode, LuaDocAttributeUse, LuaDocNameType, LuaSyntaxKind, LuaTokenKind}; use lsp_types::CompletionItem; use std::collections::HashSet; @@ -12,7 +12,7 @@ pub fn add_completion(builder: &mut CompletionBuilder) -> Option<()> { return None; } - check_can_add_type_completion(builder)?; + let completion_type = check_can_add_type_completion(builder)?; let prefix_content = builder.trigger_token.text().to_string(); let prefix = if let Some(last_sep) = prefix_content.rfind('.') { @@ -21,7 +21,7 @@ pub fn add_completion(builder: &mut CompletionBuilder) -> Option<()> { } else { "" }; - complete_types_by_prefix(builder, prefix, None); + complete_types_by_prefix(builder, prefix, None, Some(completion_type)); builder.stop_here(); Some(()) } @@ -30,7 +30,9 @@ pub fn complete_types_by_prefix( builder: &mut CompletionBuilder, prefix: &str, filter: Option<&HashSet>, + completion_type: Option, ) -> Option<()> { + let completion_type = completion_type.or(Some(CompletionType::Type))?; let file_id = builder.semantic_model.get_file_id(); let type_index = builder.semantic_model.get_db().get_type_index(); let results = type_index.find_type_decls(file_id, prefix); @@ -43,18 +45,52 @@ pub fn complete_types_by_prefix( { continue; } - add_type_completion_item(builder, &name, type_decl); + match completion_type { + CompletionType::AttributeUse => { + if let Some(decl_id) = type_decl { + let type_decl = builder + .semantic_model + .get_db() + .get_type_index() + .get_type_decl(&decl_id)?; + if type_decl.is_attribute() { + add_type_completion_item(builder, &name, Some(decl_id)); + } + } + } + CompletionType::Type => { + if let Some(decl_id) = &type_decl { + let type_decl = builder + .semantic_model + .get_db() + .get_type_index() + .get_type_decl(decl_id)?; + if type_decl.is_attribute() { + continue; + } + } + add_type_completion_item(builder, &name, type_decl); + } + } } Some(()) } -fn check_can_add_type_completion(builder: &CompletionBuilder) -> Option<()> { +pub enum CompletionType { + Type, + AttributeUse, +} + +fn check_can_add_type_completion(builder: &CompletionBuilder) -> Option { match builder.trigger_token.kind().into() { LuaTokenKind::TkName => { let parent = builder.trigger_token.parent()?; - if LuaDocNameType::cast(parent).is_some() { - return Some(()); + if let Some(doc_name) = LuaDocNameType::cast(parent) { + if doc_name.get_parent::().is_some() { + return Some(CompletionType::AttributeUse); + } + return Some(CompletionType::Type); } None @@ -63,7 +99,7 @@ fn check_can_add_type_completion(builder: &CompletionBuilder) -> Option<()> { let left_token = builder.trigger_token.prev_token()?; match left_token.kind().into() { LuaTokenKind::TkTagReturn | LuaTokenKind::TkTagType => { - return Some(()); + return Some(CompletionType::Type); } LuaTokenKind::TkName => { let parent = left_token.parent()?; @@ -71,20 +107,20 @@ fn check_can_add_type_completion(builder: &CompletionBuilder) -> Option<()> { LuaSyntaxKind::DocTagParam | LuaSyntaxKind::DocTagField | LuaSyntaxKind::DocTagAlias - | LuaSyntaxKind::DocTagCast => return Some(()), + | LuaSyntaxKind::DocTagCast => return Some(CompletionType::Type), _ => {} } } LuaTokenKind::TkComma | LuaTokenKind::TkDocOr => { let parent = left_token.parent()?; if parent.kind() == LuaSyntaxKind::DocTypeList.into() { - return Some(()); + return Some(CompletionType::Type); } } LuaTokenKind::TkColon => { let parent = left_token.parent()?; if parent.kind() == LuaSyntaxKind::DocTagClass.into() { - return Some(()); + return Some(CompletionType::Type); } } _ => {} @@ -92,6 +128,7 @@ fn check_can_add_type_completion(builder: &CompletionBuilder) -> Option<()> { None } + LuaTokenKind::TkDocAttributeUse => Some(CompletionType::AttributeUse), _ => None, } } diff --git a/crates/emmylua_ls/src/handlers/completion/providers/function_provider.rs b/crates/emmylua_ls/src/handlers/completion/providers/function_provider.rs index dde93fe49..fe5795807 100644 --- a/crates/emmylua_ls/src/handlers/completion/providers/function_provider.rs +++ b/crates/emmylua_ls/src/handlers/completion/providers/function_provider.rs @@ -792,7 +792,12 @@ fn get_tpl_ref_extend_type(builder: &CompletionBuilder, tpl_id: &GenericTplId) - .get(&signature_id)?; let generic_param = signature.generic_params.get(tpl_id.get_idx()); if let Some(generic_param) = generic_param { - return Some(generic_param.1.clone().unwrap_or(LuaType::Any)); + return Some( + generic_param + .type_constraint + .clone() + .unwrap_or(LuaType::Any), + ); } } None diff --git a/crates/emmylua_ls/src/handlers/definition/goto_def_definition.rs b/crates/emmylua_ls/src/handlers/definition/goto_def_definition.rs index f75ba937f..2521ebe63 100644 --- a/crates/emmylua_ls/src/handlers/definition/goto_def_definition.rs +++ b/crates/emmylua_ls/src/handlers/definition/goto_def_definition.rs @@ -1,8 +1,8 @@ use std::str::FromStr; use emmylua_code_analysis::{ - LuaCompilation, LuaDeclId, LuaMemberId, LuaMemberInfo, LuaMemberKey, LuaSemanticDeclId, - LuaType, LuaTypeDeclId, SemanticDeclLevel, SemanticModel, + LuaCompilation, LuaDeclId, LuaMemberId, LuaMemberInfo, LuaMemberKey, LuaMemberOwner, + LuaSemanticDeclId, LuaType, LuaTypeDeclId, SemanticDeclLevel, SemanticModel, }; use emmylua_parser::{ LuaAstNode, LuaAstToken, LuaCallExpr, LuaExpr, LuaIndexExpr, LuaReturnStat, LuaStringToken, @@ -11,9 +11,14 @@ use emmylua_parser::{ use itertools::Itertools; use lsp_types::{GotoDefinitionResponse, Location, Position, Range, Uri}; -use crate::handlers::{ - definition::goto_function::{find_function_call_origin, find_matching_function_definitions}, - hover::{find_all_same_named_members, find_member_origin_owner}, +use crate::{ + handlers::{ + definition::goto_function::{ + find_function_call_origin, find_matching_function_definitions, + }, + hover::{find_all_same_named_members, find_member_origin_owner}, + }, + util::{to_camel_case, to_pascal_case, to_snake_case}, }; pub fn goto_def_definition( @@ -104,6 +109,8 @@ fn handle_member_definition( if let LuaSemanticDeclId::Member(member_id) = member && let Some(location) = get_member_location(semantic_model, &member_id) { + // 尝试添加访问器的位置 + try_set_accessor_locations(semantic_model, &member, &mut locations); locations.push(location); } } @@ -421,3 +428,139 @@ fn try_extract_str_tpl_ref_locations( } None } + +fn try_set_accessor_locations( + semantic_model: &SemanticModel, + semantic_decl_id: &LuaSemanticDeclId, + locations: &mut Vec, +) -> Option<()> { + #[derive(Clone, Copy)] + enum AccessorCaseConvention { + CamelCase, // camelCase + SnakeCase, // snake_case + PascalCase, // PascalCase + } + + impl AccessorCaseConvention { + fn build_name(self, prefix: &str, field_name: &str) -> Option { + if field_name.is_empty() { + return None; + } + let full_name = format!("{}_{}", prefix, field_name); + let name = match self { + AccessorCaseConvention::CamelCase => to_camel_case(&full_name), + AccessorCaseConvention::SnakeCase => to_snake_case(&full_name), + AccessorCaseConvention::PascalCase => to_pascal_case(&full_name), + }; + Some(name) + } + } + + let member_id = match semantic_decl_id { + LuaSemanticDeclId::Member(id) => id, + _ => return None, + }; + let current_owner = semantic_model + .get_db() + .get_member_index() + .get_current_owner(member_id)?; + let prefix_type = match current_owner { + LuaMemberOwner::Type(id) => LuaType::Ref(id.clone()), + _ => return None, + }; + let property = semantic_model + .get_db() + .get_property_index() + .get_property(&semantic_decl_id)?; + + let attribute_use = property.find_attribute_use(LuaTypeDeclId::new("field_accessor"))?; + let has_getter = + if let Some(LuaType::DocStringConst(getter)) = attribute_use.get_param_by_name("getter") { + try_add_accessor_location( + semantic_model, + &prefix_type, + getter.as_str().into(), + locations, + ) + } else { + false + }; + let has_setter = + if let Some(LuaType::DocStringConst(setter)) = attribute_use.get_param_by_name("setter") { + try_add_accessor_location( + semantic_model, + &prefix_type, + setter.as_str().into(), + locations, + ) + } else { + false + }; + + // 显式指定了获取器与设置器, 则不需要根据规则处理 + if has_getter && has_setter { + return Some(()); + } + // 根据规则处理 + // "camelCase"|"PascalCase"|"snake_case" + let convention = { + if let Some(LuaType::DocStringConst(convention)) = + attribute_use.get_param_by_name("convention") + { + match convention.as_str() { + "camelCase" => AccessorCaseConvention::CamelCase, + "snake_case" => AccessorCaseConvention::SnakeCase, + "PascalCase" => AccessorCaseConvention::PascalCase, + _ => AccessorCaseConvention::CamelCase, + } + } else { + AccessorCaseConvention::CamelCase + } + }; + + let Some(original_name) = semantic_model + .get_db() + .get_member_index() + .get_member(member_id)? + .get_key() + .get_name() + else { + return Some(()); + }; + + if !has_getter { + if let Some(getter_name) = convention.build_name("get", original_name) { + try_add_accessor_location(semantic_model, &prefix_type, getter_name, locations); + } + } + + if !has_setter { + if let Some(setter_name) = convention.build_name("set", original_name) { + try_add_accessor_location(semantic_model, &prefix_type, setter_name, locations); + } + } + + Some(()) +} + +/// 尝试添加访问器位置到位置列表中 +fn try_add_accessor_location( + semantic_model: &SemanticModel, + prefix_type: &LuaType, + accessor_name: String, + locations: &mut Vec, +) -> bool { + let accessor_key = LuaMemberKey::Name(accessor_name.as_str().into()); + if let Some(member_infos) = + semantic_model.get_member_info_with_key(prefix_type, accessor_key, false) + { + if let Some(member_info) = member_infos.first() + && let Some(LuaSemanticDeclId::Member(accessor_id)) = member_info.property_owner_id + && let Some(location) = get_member_location(semantic_model, &accessor_id) + { + locations.push(location); + return true; + } + } + false +} diff --git a/crates/emmylua_ls/src/handlers/hover/build_hover.rs b/crates/emmylua_ls/src/handlers/hover/build_hover.rs index f661c1ca8..2be213845 100644 --- a/crates/emmylua_ls/src/handlers/hover/build_hover.rs +++ b/crates/emmylua_ls/src/handlers/hover/build_hover.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use emmylua_code_analysis::humanize_type; use emmylua_code_analysis::{ DbIndex, LuaCompilation, LuaDeclId, LuaDocument, LuaMemberId, LuaMemberKey, LuaSemanticDeclId, - LuaSignatureId, LuaType, LuaTypeDeclId, RenderLevel, SemanticInfo, SemanticModel, + LuaSignatureId, LuaType, RenderLevel, SemanticInfo, SemanticModel, }; use emmylua_parser::{ LuaAssignStat, LuaAstNode, LuaCallArgList, LuaExpr, LuaSyntaxKind, LuaSyntaxToken, @@ -13,12 +13,13 @@ use lsp_types::{Hover, HoverContents, MarkedString, MarkupContent}; use rowan::TextRange; use crate::handlers::hover::function::{build_function_hover, is_function}; -use crate::handlers::hover::hover_humanize::hover_humanize_type; +use crate::handlers::hover::humanize_type_decl::build_type_decl_hover; +use crate::handlers::hover::humanize_types::hover_humanize_type; use super::{ find_origin::{find_decl_origin_owners, find_member_origin_owners}, hover_builder::HoverBuilder, - hover_humanize::hover_const_type, + humanize_types::hover_const_type, }; pub fn build_semantic_info_hover( @@ -270,35 +271,6 @@ fn build_member_hover( Some(()) } -fn build_type_decl_hover( - builder: &mut HoverBuilder, - db: &DbIndex, - type_decl_id: LuaTypeDeclId, -) -> Option<()> { - let type_decl = db.get_type_index().get_type_decl(&type_decl_id)?; - let type_description = if type_decl.is_alias() { - if let Some(origin) = type_decl.get_alias_origin(db, None) { - let origin_type = humanize_type(db, &origin, builder.detail_render_level); - format!("(alias) {} = {}", type_decl.get_name(), origin_type) - } else { - "".to_string() - } - } else if type_decl.is_enum() { - format!("(enum) {}", type_decl.get_name()) - } else { - let humanize_text = humanize_type( - db, - &LuaType::Def(type_decl_id.clone()), - builder.detail_render_level, - ); - format!("(class) {}", humanize_text) - }; - - builder.set_type_description(type_description); - builder.add_description(&LuaSemanticDeclId::TypeDecl(type_decl_id)); - Some(()) -} - pub fn add_signature_param_description( db: &DbIndex, marked_strings: &mut Vec, diff --git a/crates/emmylua_ls/src/handlers/hover/function/mod.rs b/crates/emmylua_ls/src/handlers/hover/function/mod.rs index 61095a6db..ee2aebe6e 100644 --- a/crates/emmylua_ls/src/handlers/hover/function/mod.rs +++ b/crates/emmylua_ls/src/handlers/hover/function/mod.rs @@ -9,7 +9,7 @@ use emmylua_code_analysis::{ use crate::handlers::hover::{ HoverBuilder, - hover_humanize::{ + humanize_types::{ DescriptionInfo, extract_description_from_property_owner, extract_owner_name_from_element, extract_parent_type_from_element, hover_humanize_type, }, @@ -385,6 +385,7 @@ fn convert_function_return_to_docs(func: &LuaFunctionType) -> Vec types .iter() @@ -392,6 +393,7 @@ fn convert_function_return_to_docs(func: &LuaFunctionType) -> Vec Vec Option<()> { + let type_decl = db.get_type_index().get_type_decl(&type_decl_id)?; + let type_description = if type_decl.is_alias() { + if let Some(origin) = type_decl.get_alias_origin(db, None) { + let origin_type = humanize_type(db, &origin, builder.detail_render_level); + format!("(alias) {} = {}", type_decl.get_name(), origin_type) + } else { + "".to_string() + } + } else if type_decl.is_enum() { + format!("(enum) {}", type_decl.get_name()) + } else if type_decl.is_attribute() { + build_attribute(db, type_decl.get_name(), type_decl.get_attribute_type()) + } else { + let humanize_text = humanize_type( + db, + &LuaType::Def(type_decl_id.clone()), + builder.detail_render_level, + ); + format!("(class) {}", humanize_text) + }; + + builder.set_type_description(type_description); + builder.add_description(&LuaSemanticDeclId::TypeDecl(type_decl_id)); + Some(()) +} + +fn build_attribute(db: &DbIndex, attribute_name: &str, attribute_type: Option<&LuaType>) -> String { + let Some(LuaType::DocAttribute(attribute)) = attribute_type else { + return format!("(attribute) {}", attribute_name); + }; + let params = attribute + .get_params() + .iter() + .map(|(name, typ)| match typ { + Some(typ) => { + let type_name = humanize_type(db, typ, RenderLevel::Normal); + format!("{}: {}", name, type_name) + } + None => name.to_string(), + }) + .collect::>(); + + if params.is_empty() { + format!("(attribute) {}", attribute_name) + } else { + format!("(attribute) {}({})", attribute_name, params.join(", ")) + } +} diff --git a/crates/emmylua_ls/src/handlers/hover/hover_humanize.rs b/crates/emmylua_ls/src/handlers/hover/humanize_types.rs similarity index 100% rename from crates/emmylua_ls/src/handlers/hover/hover_humanize.rs rename to crates/emmylua_ls/src/handlers/hover/humanize_types.rs diff --git a/crates/emmylua_ls/src/handlers/hover/mod.rs b/crates/emmylua_ls/src/handlers/hover/mod.rs index f0fb5883e..83f556173 100644 --- a/crates/emmylua_ls/src/handlers/hover/mod.rs +++ b/crates/emmylua_ls/src/handlers/hover/mod.rs @@ -2,7 +2,8 @@ mod build_hover; mod find_origin; mod function; mod hover_builder; -mod hover_humanize; +mod humanize_type_decl; +mod humanize_types; mod keyword_hover; mod std_hover; @@ -16,7 +17,7 @@ use emmylua_parser::{LuaAstNode, LuaDocDescription, LuaTokenKind}; use emmylua_parser_desc::parse_ref_target; pub use find_origin::{find_all_same_named_members, find_member_origin_owner}; pub use hover_builder::HoverBuilder; -pub use hover_humanize::infer_prefix_global_name; +pub use humanize_types::infer_prefix_global_name; use keyword_hover::{hover_keyword, is_keyword}; use lsp_types::{ ClientCapabilities, Hover, HoverContents, HoverParams, HoverProviderCapability, MarkupContent, diff --git a/crates/emmylua_ls/src/handlers/inlay_hint/build_inlay_hint.rs b/crates/emmylua_ls/src/handlers/inlay_hint/build_inlay_hint.rs index 17e55fec4..fcb8d6750 100644 --- a/crates/emmylua_ls/src/handlers/inlay_hint/build_inlay_hint.rs +++ b/crates/emmylua_ls/src/handlers/inlay_hint/build_inlay_hint.rs @@ -17,7 +17,7 @@ use rowan::NodeOrToken; use rowan::TokenAtOffset; -use crate::handlers::completion::extract_index_member_alias; +use crate::handlers::completion::get_index_alias_name; use crate::handlers::definition::compare_function_types; use crate::handlers::inlay_hint::build_function_hint::{build_closure_hint, build_label_parts}; @@ -641,7 +641,7 @@ fn build_index_expr_hint( let member_infos = semantic_model.get_member_info_with_key(&prefix_type, member_key, false)?; let member_info = member_infos.first()?; // 尝试提取别名 - let alias = extract_index_member_alias(semantic_model, member_info)?; + let alias = get_index_alias_name(semantic_model, member_info)?; // 创建 hint let document = semantic_model.get_document(); let position = { diff --git a/crates/emmylua_ls/src/handlers/semantic_token/build_semantic_tokens.rs b/crates/emmylua_ls/src/handlers/semantic_token/build_semantic_tokens.rs index 624ca97ba..2f20b10d3 100644 --- a/crates/emmylua_ls/src/handlers/semantic_token/build_semantic_tokens.rs +++ b/crates/emmylua_ls/src/handlers/semantic_token/build_semantic_tokens.rs @@ -12,8 +12,8 @@ use emmylua_code_analysis::{ }; use emmylua_parser::{ LuaAst, LuaAstNode, LuaAstToken, LuaCallArgList, LuaCallExpr, LuaComment, LuaDocFieldKey, - LuaDocObjectFieldKey, LuaExpr, LuaGeneralToken, LuaKind, LuaLiteralToken, LuaNameToken, - LuaSyntaxKind, LuaSyntaxNode, LuaSyntaxToken, LuaTokenKind, LuaVarExpr, + LuaDocObjectFieldKey, LuaDocType, LuaExpr, LuaGeneralToken, LuaKind, LuaLiteralToken, + LuaNameToken, LuaSyntaxKind, LuaSyntaxNode, LuaSyntaxToken, LuaTokenKind, LuaVarExpr, }; use emmylua_parser_desc::{CodeBlockHighlightKind, DescItem, DescItemKind}; use lsp_types::{SemanticToken, SemanticTokenModifier, SemanticTokenType}; @@ -202,7 +202,8 @@ fn build_tokens_semantic_token( | LuaTokenKind::TkTagSource | LuaTokenKind::TkTagReturnCast | LuaTokenKind::TkTagExport - | LuaTokenKind::TkLanguage => { + | LuaTokenKind::TkLanguage + | LuaTokenKind::TkTagAttribute => { builder.push_with_modifier( token, SemanticTokenType::KEYWORD, @@ -306,7 +307,7 @@ fn build_node_semantic_token( SemanticTokenModifier::DECLARATION, ); } - if let Some(attribs) = doc_class.get_attrib() { + if let Some(attribs) = doc_class.get_type_flag() { for token in attribs.tokens::() { builder.push(token.syntax(), SemanticTokenType::DECORATOR); } @@ -330,7 +331,7 @@ fn build_node_semantic_token( SemanticTokenType::ENUM, SemanticTokenModifier::DECLARATION, ); - if let Some(attribs) = doc_enum.get_attrib() { + if let Some(attribs) = doc_enum.get_type_flag() { for token in attribs.tokens::() { builder.push(token.syntax(), SemanticTokenType::DECORATOR); } @@ -371,7 +372,7 @@ fn build_node_semantic_token( ); } LuaAst::LuaDocTagReturn(doc_return) => { - let type_name_list = doc_return.get_type_and_name_list(); + let type_name_list = doc_return.get_info_list(); for (_, name) in type_name_list { if let Some(name) = name { builder.push(name.syntax(), SemanticTokenType::VARIABLE); @@ -783,6 +784,45 @@ fn build_node_semantic_token( fun_string_highlight(builder, semantic_model, call_expr, &string_token); } } + LuaAst::LuaDocTagAttributeUse(tag_use) => { + // 给 `@[` 染色 + if let Some(token) = tag_use.token_by_kind(LuaTokenKind::TkDocAttributeUse) { + builder.push(token.syntax(), SemanticTokenType::KEYWORD); + } + // `]`染色 + if let Some(token) = tag_use.syntax().last_token() { + builder.push(&token, SemanticTokenType::KEYWORD); + } + // 名称染色 + for attribute_use in tag_use.get_attribute_uses() { + if let Some(token) = attribute_use.get_type()?.get_name_token() { + builder.push_with_modifiers( + token.syntax(), + SemanticTokenType::DECORATOR, + &[ + SemanticTokenModifier::DECLARATION, + SemanticTokenModifier::DEFAULT_LIBRARY, + ], + ); + } + } + } + LuaAst::LuaDocTagAttribute(tag_attribute) => { + if let Some(name) = tag_attribute.get_name_token() { + builder.push_with_modifier( + name.syntax(), + SemanticTokenType::TYPE, + SemanticTokenModifier::DECLARATION, + ); + } + if let Some(LuaDocType::Attribute(attribute)) = tag_attribute.get_type() { + for param in attribute.get_params() { + if let Some(name) = param.get_name_token() { + builder.push(name.syntax(), SemanticTokenType::PARAMETER); + } + } + } + } _ => {} } diff --git a/crates/emmylua_ls/src/handlers/test/completion_test.rs b/crates/emmylua_ls/src/handlers/test/completion_test.rs index 5a9f85204..5fef9781b 100644 --- a/crates/emmylua_ls/src/handlers/test/completion_test.rs +++ b/crates/emmylua_ls/src/handlers/test/completion_test.rs @@ -1146,10 +1146,11 @@ mod tests { #[gtest] fn test_index_key_alias() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); + ws.def(" ---@attribute index_alias(name: string)"); check!(ws.check_completion( r#" local export = { - [1] = 1, -- [nameX] + [1] = 1, ---@[index_alias("nameX")] } export. @@ -1215,11 +1216,12 @@ mod tests { #[gtest] fn test_field_index_function() -> Result<()> { - let mut ws = ProviderVirtualWorkspace::new(); + let mut ws = ProviderVirtualWorkspace::new_with_init_std_lib(); ws.def( r#" ---@class A - ---@field [1] fun() # [next] + ---@[index_alias("next")] + ---@field [1] fun() A = {} "#, ); diff --git a/crates/emmylua_ls/src/handlers/test/definition_test.rs b/crates/emmylua_ls/src/handlers/test/definition_test.rs index e7a0e6a45..3a59361a2 100644 --- a/crates/emmylua_ls/src/handlers/test/definition_test.rs +++ b/crates/emmylua_ls/src/handlers/test/definition_test.rs @@ -508,4 +508,50 @@ mod tests { Ok(()) } + + #[gtest] + fn test_accessors() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new_with_init_std_lib(); + + ws.def( + r#" + ---@class A + ---@[field_accessor] + ---@field age number + local A + + ---@private + function A:getAge() + end + + ---@private + function A:setAge(value) + end + "#, + ); + + check!(ws.check_definition( + r#" + ---@type A + local obj + obj.age = 1 + "#, + vec![ + Expected { + file: "".to_string(), + line: 3, + }, + Expected { + file: "".to_string(), + line: 7, + }, + Expected { + file: "".to_string(), + line: 11, + } + ], + )); + + Ok(()) + } } diff --git a/crates/emmylua_ls/src/handlers/test/inlay_hint_test.rs b/crates/emmylua_ls/src/handlers/test/inlay_hint_test.rs index 58dd01dcc..4f62af542 100644 --- a/crates/emmylua_ls/src/handlers/test/inlay_hint_test.rs +++ b/crates/emmylua_ls/src/handlers/test/inlay_hint_test.rs @@ -1,7 +1,6 @@ #[cfg(test)] mod tests { use googletest::prelude::*; - use std::{ops::Deref, sync::Arc}; use crate::handlers::test_lib::{ProviderVirtualWorkspace, VirtualInlayHint, check}; @@ -137,29 +136,42 @@ mod tests { #[gtest] fn test_class_call_hint() -> Result<()> { - let mut ws = ProviderVirtualWorkspace::new(); - let mut emmyrc = ws.analysis.get_emmyrc().deref().clone(); - emmyrc.runtime.class_default_call.function_name = "__init".to_string(); - emmyrc.runtime.class_default_call.force_non_colon = true; - emmyrc.runtime.class_default_call.force_return_self = true; - ws.analysis.update_config(Arc::new(emmyrc)); + let mut ws = ProviderVirtualWorkspace::new_with_init_std_lib(); + ws.def( + r#" + ---@generic T + ---@[constructor("__init")] + ---@param name `T` + ---@return T + function meta(name) + end + "#, + ); check!(ws.check_inlay_hint( r#" ---@class MyClass - local A + local A = meta("MyClass") function A:__init(a) end A() "#, - vec![VirtualInlayHint { - label: "new".to_string(), - line: 7, - pos: 16, - ref_file: Some("".to_string()), - }] + vec![ + VirtualInlayHint { + label: "name:".to_string(), + line: 2, + pos: 31, + ref_file: Some("".to_string()), + }, + VirtualInlayHint { + label: "new".to_string(), + line: 7, + pos: 16, + ref_file: Some("".to_string()), + } + ] )); Ok(()) } @@ -167,16 +179,18 @@ mod tests { #[gtest] fn test_index_key_alias_hint() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); + ws.def(" ---@attribute index_alias(name: string)"); check!(ws.check_inlay_hint( r#" local export = { - [1] = 1, -- [nameX] + ---@[index_alias("nameX")] + [1] = 1, } print(export[1]) "#, vec![VirtualInlayHint { label: ": nameX".to_string(), - line: 4, + line: 5, pos: 30, ref_file: Some("".to_string()), }] diff --git a/crates/emmylua_ls/src/util/mod.rs b/crates/emmylua_ls/src/util/mod.rs index 3c1a29931..ddc8bfbfa 100644 --- a/crates/emmylua_ls/src/util/mod.rs +++ b/crates/emmylua_ls/src/util/mod.rs @@ -3,5 +3,7 @@ mod module_name_convert; mod time_cancel_token; pub use desc::*; -pub use module_name_convert::{key_name_convert, module_name_convert}; +pub use module_name_convert::{ + file_name_convert, module_name_convert, to_camel_case, to_pascal_case, to_snake_case, +}; pub use time_cancel_token::time_cancel_token; diff --git a/crates/emmylua_ls/src/util/module_name_convert.rs b/crates/emmylua_ls/src/util/module_name_convert.rs index ceb066713..0f61e283b 100644 --- a/crates/emmylua_ls/src/util/module_name_convert.rs +++ b/crates/emmylua_ls/src/util/module_name_convert.rs @@ -29,33 +29,33 @@ pub fn module_name_convert( module_name } -pub fn key_name_convert( +pub fn file_name_convert( key: &str, typ: &LuaType, file_conversion: EmmyrcFilenameConvention, ) -> String { - let mut key_name = key.to_string(); + let mut file_name = key.to_string(); match file_conversion { EmmyrcFilenameConvention::SnakeCase => { - key_name = to_snake_case(&key_name); + file_name = to_snake_case(&file_name); } EmmyrcFilenameConvention::CamelCase => { - key_name = to_camel_case(&key_name); + file_name = to_camel_case(&file_name); } EmmyrcFilenameConvention::PascalCase => { - key_name = to_pascal_case(&key_name); + file_name = to_pascal_case(&file_name); } EmmyrcFilenameConvention::Keep => {} EmmyrcFilenameConvention::KeepClass => { if let LuaType::Def(id) = typ { - key_name = id.get_simple_name().to_string(); + file_name = id.get_simple_name().to_string(); } } } - key_name + file_name } -fn to_snake_case(s: &str) -> String { +pub fn to_snake_case(s: &str) -> String { let mut result = String::new(); for (i, ch) in s.chars().enumerate() { if ch.is_uppercase() && i != 0 { @@ -68,7 +68,7 @@ fn to_snake_case(s: &str) -> String { result } -fn to_camel_case(s: &str) -> String { +pub fn to_camel_case(s: &str) -> String { let mut result = String::new(); let mut next_uppercase = false; for (i, ch) in s.chars().enumerate() { @@ -86,7 +86,7 @@ fn to_camel_case(s: &str) -> String { result } -fn to_pascal_case(s: &str) -> String { +pub fn to_pascal_case(s: &str) -> String { let mut result = String::new(); let mut next_uppercase = true; for ch in s.chars() { diff --git a/crates/emmylua_parser/src/grammar/doc/tag.rs b/crates/emmylua_parser/src/grammar/doc/tag.rs index 9e015cb89..c884a5941 100644 --- a/crates/emmylua_parser/src/grammar/doc/tag.rs +++ b/crates/emmylua_parser/src/grammar/doc/tag.rs @@ -8,7 +8,7 @@ use crate::{ use super::{ expect_token, if_token_bump, parse_description, - types::{parse_fun_type, parse_type, parse_type_list}, + types::{parse_fun_type, parse_type, parse_type_list, parse_typed_param}, }; pub fn parse_tag(p: &mut LuaDocParser) { @@ -57,6 +57,8 @@ fn parse_tag_detail(p: &mut LuaDocParser) -> DocParseResult { LuaTokenKind::TkTagMeta => parse_tag_meta(p), LuaTokenKind::TkTagExport => parse_tag_export(p), LuaTokenKind::TkLanguage => parse_tag_language(p), + LuaTokenKind::TkTagAttribute => parse_tag_attribute(p), + LuaTokenKind::TkDocAttributeUse => parse_tag_attribute_use(p, true), // simple tag LuaTokenKind::TkTagVisibility => parse_tag_simple(p, LuaSyntaxKind::DocTagVisibility), @@ -84,7 +86,7 @@ fn parse_tag_class(p: &mut LuaDocParser) -> DocParseResult { let m = p.mark(LuaSyntaxKind::DocTagClass); p.bump(); if p.current_token() == LuaTokenKind::TkLeftParen { - parse_tag_attribute(p)?; + parse_doc_type_flag(p)?; } expect_token(p, LuaTokenKind::TkName)?; @@ -104,8 +106,8 @@ fn parse_tag_class(p: &mut LuaDocParser) -> DocParseResult { } // (partial, global, local) -fn parse_tag_attribute(p: &mut LuaDocParser) -> DocParseResult { - let m = p.mark(LuaSyntaxKind::DocAttribute); +fn parse_doc_type_flag(p: &mut LuaDocParser) -> DocParseResult { + let m = p.mark(LuaSyntaxKind::DocTypeFlag); p.bump(); expect_token(p, LuaTokenKind::TkName)?; while p.current_token() == LuaTokenKind::TkComma { @@ -158,7 +160,7 @@ fn parse_tag_enum(p: &mut LuaDocParser) -> DocParseResult { let m = p.mark(LuaSyntaxKind::DocTagEnum); p.bump(); if p.current_token() == LuaTokenKind::TkLeftParen { - parse_tag_attribute(p)?; + parse_doc_type_flag(p)?; } expect_token(p, LuaTokenKind::TkName)?; @@ -245,7 +247,7 @@ fn parse_tag_field(p: &mut LuaDocParser) -> DocParseResult { let m = p.mark(LuaSyntaxKind::DocTagField); p.bump(); if p.current_token() == LuaTokenKind::TkLeftParen { - parse_tag_attribute(p)?; + parse_doc_type_flag(p)?; } p.set_state(LuaDocLexerState::Normal); @@ -653,3 +655,136 @@ fn parse_tag_language(p: &mut LuaDocParser) -> DocParseResult { parse_description(p); Ok(m.complete(p)) } + +// ---@attribute 名称(参数列表) +fn parse_tag_attribute(p: &mut LuaDocParser) -> DocParseResult { + p.set_state(LuaDocLexerState::Normal); + let m = p.mark(LuaSyntaxKind::DocTagAttribute); + p.bump(); + + // 解析属性名称 + expect_token(p, LuaTokenKind::TkName)?; + + // 解析参数列表 + parse_type_attribute(p)?; + + p.set_state(LuaDocLexerState::Description); + parse_description(p); + Ok(m.complete(p)) +} + +// (param1: type1, param2: type2, ...) +fn parse_type_attribute(p: &mut LuaDocParser) -> DocParseResult { + let m = p.mark(LuaSyntaxKind::TypeAttribute); + expect_token(p, LuaTokenKind::TkLeftParen)?; + + if p.current_token() != LuaTokenKind::TkRightParen { + parse_typed_param(p)?; + while p.current_token() == LuaTokenKind::TkComma { + p.bump(); + parse_typed_param(p)?; + } + } + + expect_token(p, LuaTokenKind::TkRightParen)?; + Ok(m.complete(p)) +} + +// ---@[attribute(arg1, arg2, ...)] +// ---@[attribute] +// ---@[attribute1, attribute2, ...] +// ---@generic [attribute] T +pub fn parse_tag_attribute_use(p: &mut LuaDocParser, allow_description: bool) -> DocParseResult { + let m = p.mark(LuaSyntaxKind::DocTagAttributeUse); + p.bump(); // consume '[' + + while p.current_token() == LuaTokenKind::TkName { + parse_doc_attribute_use(p)?; + if p.current_token() != LuaTokenKind::TkComma { + break; + } + p.bump(); // consume comma + } + + // 期望结束符号 ']' + expect_token(p, LuaTokenKind::TkRightBracket)?; + + // 属性使用解析完成后, 重置状态 + if allow_description { + p.set_state(LuaDocLexerState::Description); + parse_description(p); + } else { + p.set_state(LuaDocLexerState::Normal); + } + Ok(m.complete(p)) +} + +// attribute +// attribute(arg1, arg2, ...) +fn parse_doc_attribute_use(p: &mut LuaDocParser) -> DocParseResult { + let m = p.mark(LuaSyntaxKind::DocAttributeUse); + + // attribute 被视为类型 + parse_type(p)?; + + // 解析参数列表, 允许没有参数的特性在使用时省略括号 + if p.current_token() == LuaTokenKind::TkLeftParen { + parse_attribute_arg_list(p)?; + } + + Ok(m.complete(p)) +} + +// 解析属性参数列表 +fn parse_attribute_arg_list(p: &mut LuaDocParser) -> DocParseResult { + let m = p.mark(LuaSyntaxKind::DocAttributeCallArgList); + p.bump(); // consume '(' + + // 解析参数值列表 + if p.current_token() != LuaTokenKind::TkRightParen { + loop { + if p.current_token() == LuaTokenKind::TkEof { + break; + } + parse_attribute_arg(p)?; + if p.current_token() != LuaTokenKind::TkComma { + break; + } + p.bump(); // consume comma + if p.current_token() == LuaTokenKind::TkRightParen { + break; // trailing comma + } + } + } + + expect_token(p, LuaTokenKind::TkRightParen)?; + Ok(m.complete(p)) +} + +// 解析单个属性参数 +fn parse_attribute_arg(p: &mut LuaDocParser) -> DocParseResult { + let m = p.mark(LuaSyntaxKind::LiteralExpr); + + // TODO: 添加具名参数支持(name: value) + match p.current_token() { + LuaTokenKind::TkInt + | LuaTokenKind::TkFloat + | LuaTokenKind::TkComplex + | LuaTokenKind::TkNil + | LuaTokenKind::TkTrue + | LuaTokenKind::TkFalse + | LuaTokenKind::TkDots + | LuaTokenKind::TkString + | LuaTokenKind::TkLongString => { + p.bump(); + } + _ => { + return Err(LuaParseError::doc_error_from( + "Expected attribute argument value", + p.current_token_range(), + )); + } + }; + + Ok(m.complete(p)) +} diff --git a/crates/emmylua_parser/src/grammar/doc/test.rs b/crates/emmylua_parser/src/grammar/doc/test.rs index 91a0b09c5..b1add228a 100644 --- a/crates/emmylua_parser/src/grammar/doc/test.rs +++ b/crates/emmylua_parser/src/grammar/doc/test.rs @@ -2867,4 +2867,72 @@ Syntax(Chunk)@0..137 assert_ast_eq!(code, result); } + + #[test] + fn test_attribute_doc() { + let code = r#" + ---@attribute check_point(x: string, y: number) + ---@[Skip, check_point("a", 0)] + "#; + // print_ast(code); + // print_ast(r#" + // ---@alias a fun(x: string, y: number) + // check_point("a", 0) + // "#); + let result = r#" +Syntax(Chunk)@0..105 + Syntax(Block)@0..105 + Token(TkEndOfLine)@0..1 "\n" + Token(TkWhitespace)@1..9 " " + Syntax(Comment)@9..96 + Token(TkDocStart)@9..13 "---@" + Syntax(DocTagAttribute)@13..56 + Token(TkTagAttribute)@13..22 "attribute" + Token(TkWhitespace)@22..23 " " + Token(TkName)@23..34 "check_point" + Syntax(TypeAttribute)@34..56 + Token(TkLeftParen)@34..35 "(" + Syntax(DocTypedParameter)@35..44 + Token(TkName)@35..36 "x" + Token(TkColon)@36..37 ":" + Token(TkWhitespace)@37..38 " " + Syntax(TypeName)@38..44 + Token(TkName)@38..44 "string" + Token(TkComma)@44..45 "," + Token(TkWhitespace)@45..46 " " + Syntax(DocTypedParameter)@46..55 + Token(TkName)@46..47 "y" + Token(TkColon)@47..48 ":" + Token(TkWhitespace)@48..49 " " + Syntax(TypeName)@49..55 + Token(TkName)@49..55 "number" + Token(TkRightParen)@55..56 ")" + Token(TkEndOfLine)@56..57 "\n" + Token(TkWhitespace)@57..65 " " + Token(TkDocStart)@65..69 "---@" + Syntax(DocTagAttributeUse)@69..96 + Token(TkDocAttributeUse)@69..70 "[" + Syntax(DocAttributeUse)@70..74 + Syntax(TypeName)@70..74 + Token(TkName)@70..74 "Skip" + Token(TkComma)@74..75 "," + Token(TkWhitespace)@75..76 " " + Syntax(DocAttributeUse)@76..95 + Syntax(TypeName)@76..87 + Token(TkName)@76..87 "check_point" + Syntax(DocAttributeCallArgList)@87..95 + Token(TkLeftParen)@87..88 "(" + Syntax(LiteralExpr)@88..91 + Token(TkString)@88..91 "\"a\"" + Token(TkComma)@91..92 "," + Token(TkWhitespace)@92..93 " " + Syntax(LiteralExpr)@93..94 + Token(TkInt)@93..94 "0" + Token(TkRightParen)@94..95 ")" + Token(TkRightBracket)@95..96 "]" + Token(TkEndOfLine)@96..97 "\n" + Token(TkWhitespace)@97..105 " " + "#; + assert_ast_eq!(code, result); + } } diff --git a/crates/emmylua_parser/src/grammar/doc/types.rs b/crates/emmylua_parser/src/grammar/doc/types.rs index 2cbfcfe7f..45c5e0d8f 100644 --- a/crates/emmylua_parser/src/grammar/doc/types.rs +++ b/crates/emmylua_parser/src/grammar/doc/types.rs @@ -314,7 +314,7 @@ fn parse_fun_return_type(p: &mut LuaDocParser) -> DocParseResult { // ... : // // ... -fn parse_typed_param(p: &mut LuaDocParser) -> DocParseResult { +pub fn parse_typed_param(p: &mut LuaDocParser) -> DocParseResult { let m = p.mark(LuaSyntaxKind::DocTypedParameter); match p.current_token() { LuaTokenKind::TkName => { diff --git a/crates/emmylua_parser/src/kind/lua_syntax_kind.rs b/crates/emmylua_parser/src/kind/lua_syntax_kind.rs index 6f6c7d1d2..a6d646a5c 100644 --- a/crates/emmylua_parser/src/kind/lua_syntax_kind.rs +++ b/crates/emmylua_parser/src/kind/lua_syntax_kind.rs @@ -92,6 +92,8 @@ pub enum LuaSyntaxKind { DocTagReturnCast, DocTagExport, DocTagLanguage, + DocTagAttribute, + DocTagAttributeUse, // '@[' // doc Type TypeArray, // baseType [] @@ -108,6 +110,7 @@ pub enum LuaSyntaxKind { TypeNullable, // ? TypeStringTemplate, // prefixName.`T` TypeMultiLineUnion, // | simple type # description + TypeAttribute, // declare. attribute<(paramList)> // follow donot support now TypeMatch, @@ -124,10 +127,12 @@ pub enum LuaSyntaxKind { DocGenericDeclareList, DocDiagnosticNameList, DocTypeList, - DocAttribute, - DocOpType, // +, -, +? - DocMappedKeys, // [p in KeyType]? - DocEnumFieldList, // ---| + DocTypeFlag, // (partial, global, local, ...) + DocAttributeUse, // use. attribute in @[attribute1, attribute2, ...] + DocAttributeCallArgList, // use. argument list in @[attribute_name(arg1, arg2, ...)] + DocOpType, // +, -, +? + DocMappedKeys, // [p in KeyType]? + DocEnumFieldList, // ---| DocEnumField, // # description or # description or # description DocOneLineField, // # description DocDiagnosticCodeList, // unused-local, undefined-global ... diff --git a/crates/emmylua_parser/src/kind/lua_token_kind.rs b/crates/emmylua_parser/src/kind/lua_token_kind.rs index 0fe1ecae6..e32805d3a 100644 --- a/crates/emmylua_parser/src/kind/lua_token_kind.rs +++ b/crates/emmylua_parser/src/kind/lua_token_kind.rs @@ -135,6 +135,7 @@ pub enum LuaTokenKind { TkTagReturnCast, // return cast TkTagExport, // export TkLanguage, // language + TkTagAttribute, // attribute TkDocOr, // | TkDocAnd, // & @@ -158,6 +159,7 @@ pub enum LuaTokenKind { TkDocRegion, // region TkDocEndRegion, // endregion TkDocSeeContent, // see content + TkDocAttributeUse, // '@[', used for attribute usage } impl fmt::Display for LuaTokenKind { diff --git a/crates/emmylua_parser/src/lexer/lua_doc_lexer.rs b/crates/emmylua_parser/src/lexer/lua_doc_lexer.rs index aaac08bcf..a2dcc913d 100644 --- a/crates/emmylua_parser/src/lexer/lua_doc_lexer.rs +++ b/crates/emmylua_parser/src/lexer/lua_doc_lexer.rs @@ -27,6 +27,7 @@ pub enum LuaDocLexerState { Source, NormalDescription, CastExpr, + AttributeUse, } impl LuaDocLexer<'_> { @@ -73,6 +74,7 @@ impl LuaDocLexer<'_> { LuaDocLexerState::Source => self.lex_source(), LuaDocLexerState::NormalDescription => self.lex_normal_description(), LuaDocLexerState::CastExpr => self.lex_cast_expr(), + LuaDocLexerState::AttributeUse => self.lex_attribute_use(), } } @@ -148,6 +150,11 @@ impl LuaDocLexer<'_> { let text = reader.current_text(); to_tag(text) } + '[' => { + reader.bump(); + self.state = LuaDocLexerState::AttributeUse; + LuaTokenKind::TkDocAttributeUse + } _ => { reader.eat_while(|_| true); LuaTokenKind::TkDocTrivia @@ -272,10 +279,21 @@ impl LuaDocLexer<'_> { _ => LuaTokenKind::TkDocTrivia, } } - '#' | '@' => { + '#' => { reader.eat_while(|_| true); LuaTokenKind::TkDocDetail } + '@' => { + reader.bump(); + // 需要检查是否在使用 Attribute 语法 + if reader.current_char() == '[' { + reader.bump(); + LuaTokenKind::TkDocAttributeUse + } else { + reader.eat_while(|_| true); + LuaTokenKind::TkDocDetail + } + } ch if ch.is_ascii_digit() => { reader.eat_while(|ch| ch.is_ascii_digit()); LuaTokenKind::TkInt @@ -581,6 +599,58 @@ impl LuaDocLexer<'_> { _ => self.lex_normal(), } } + + fn lex_attribute_use(&mut self) -> LuaTokenKind { + let reader = self.reader.as_mut().unwrap(); + match reader.current_char() { + ch if is_doc_whitespace(ch) => { + reader.eat_while(is_doc_whitespace); + LuaTokenKind::TkWhitespace + } + '(' => { + reader.bump(); + LuaTokenKind::TkLeftParen + } + ')' => { + reader.bump(); + LuaTokenKind::TkRightParen + } + ',' => { + reader.bump(); + LuaTokenKind::TkComma + } + ']' => { + reader.bump(); + LuaTokenKind::TkRightBracket + } + ch if ch == '"' || ch == '\'' => { + reader.bump(); + reader.eat_while(|c| c != ch); + if reader.current_char() == ch { + reader.bump(); + } + LuaTokenKind::TkString + } + ch if ch.is_ascii_digit() => { + reader.eat_while(|ch| ch.is_ascii_digit()); + LuaTokenKind::TkInt + } + ch if is_name_start(ch) => { + reader.bump(); + reader.eat_while(is_name_continue); + let text = reader.current_text(); + if text == "nil" { + LuaTokenKind::TkNil + } else { + LuaTokenKind::TkName + } + } + _ => { + reader.bump(); + LuaTokenKind::TkDocTrivia + } + } + } } fn to_tag(text: &str) -> LuaTokenKind { @@ -617,6 +687,7 @@ fn to_tag(text: &str) -> LuaTokenKind { "source" => LuaTokenKind::TkTagSource, "export" => LuaTokenKind::TkTagExport, "language" => LuaTokenKind::TkLanguage, + "attribute" => LuaTokenKind::TkTagAttribute, _ => LuaTokenKind::TkTagOther, } } diff --git a/crates/emmylua_parser/src/parser/lua_doc_parser.rs b/crates/emmylua_parser/src/parser/lua_doc_parser.rs index 404833bd1..02457ec94 100644 --- a/crates/emmylua_parser/src/parser/lua_doc_parser.rs +++ b/crates/emmylua_parser/src/parser/lua_doc_parser.rs @@ -91,7 +91,10 @@ impl<'b> LuaDocParser<'_, 'b> { self.eat_current_and_lex_next(); } } - LuaDocLexerState::FieldStart | LuaDocLexerState::See | LuaDocLexerState::Source => { + LuaDocLexerState::FieldStart + | LuaDocLexerState::See + | LuaDocLexerState::Source + | LuaDocLexerState::AttributeUse => { while matches!(self.current_token, LuaTokenKind::TkWhitespace) { self.eat_current_and_lex_next(); } diff --git a/crates/emmylua_parser/src/syntax/node/doc/mod.rs b/crates/emmylua_parser/src/syntax/node/doc/mod.rs index 2a247e211..49082e5bc 100644 --- a/crates/emmylua_parser/src/syntax/node/doc/mod.rs +++ b/crates/emmylua_parser/src/syntax/node/doc/mod.rs @@ -11,7 +11,7 @@ use super::{ LuaAst, LuaBinaryOpToken, LuaLiteralToken, LuaNameToken, LuaNumberToken, LuaStringToken, }; use crate::{ - LuaAstChildren, LuaAstToken, LuaAstTokenChildren, LuaKind, LuaSyntaxNode, + LuaAstChildren, LuaAstToken, LuaAstTokenChildren, LuaKind, LuaLiteralExpr, LuaSyntaxNode, kind::{LuaSyntaxKind, LuaTokenKind}, syntax::traits::LuaAstNode, }; @@ -408,11 +408,11 @@ pub enum LuaDocObjectFieldKey { } #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct LuaDocAttribute { +pub struct LuaDocTypeFlag { syntax: LuaSyntaxNode, } -impl LuaAstNode for LuaDocAttribute { +impl LuaAstNode for LuaDocTypeFlag { fn syntax(&self) -> &LuaSyntaxNode { &self.syntax } @@ -421,7 +421,7 @@ impl LuaAstNode for LuaDocAttribute { where Self: Sized, { - kind == LuaSyntaxKind::DocAttribute + kind == LuaSyntaxKind::DocTypeFlag } fn cast(syntax: LuaSyntaxNode) -> Option @@ -436,7 +436,7 @@ impl LuaAstNode for LuaDocAttribute { } } -impl LuaDocAttribute { +impl LuaDocTypeFlag { pub fn get_attrib_tokens(&self) -> LuaAstTokenChildren { self.tokens() } @@ -487,3 +487,77 @@ impl LuaDocNamedReturnType { } } } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct LuaDocAttributeUse { + syntax: LuaSyntaxNode, +} + +impl LuaAstNode for LuaDocAttributeUse { + fn syntax(&self) -> &LuaSyntaxNode { + &self.syntax + } + + fn can_cast(kind: LuaSyntaxKind) -> bool + where + Self: Sized, + { + kind == LuaSyntaxKind::DocAttributeUse + } + + fn cast(syntax: LuaSyntaxNode) -> Option + where + Self: Sized, + { + if Self::can_cast(syntax.kind().into()) { + Some(Self { syntax }) + } else { + None + } + } +} + +impl LuaDocAttributeUse { + pub fn get_type(&self) -> Option { + self.child() + } + + pub fn get_arg_list(&self) -> Option { + self.child() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct LuaDocAttributeCallArgList { + syntax: LuaSyntaxNode, +} + +impl LuaAstNode for LuaDocAttributeCallArgList { + fn syntax(&self) -> &LuaSyntaxNode { + &self.syntax + } + + fn can_cast(kind: LuaSyntaxKind) -> bool + where + Self: Sized, + { + kind == LuaSyntaxKind::DocAttributeCallArgList + } + + fn cast(syntax: LuaSyntaxNode) -> Option + where + Self: Sized, + { + if Self::can_cast(syntax.kind().into()) { + Some(Self { syntax }) + } else { + None + } + } +} + +impl LuaDocAttributeCallArgList { + pub fn get_args(&self) -> LuaAstChildren { + self.children() + } +} diff --git a/crates/emmylua_parser/src/syntax/node/doc/tag.rs b/crates/emmylua_parser/src/syntax/node/doc/tag.rs index 525a185e8..8c6a9b23c 100644 --- a/crates/emmylua_parser/src/syntax/node/doc/tag.rs +++ b/crates/emmylua_parser/src/syntax/node/doc/tag.rs @@ -1,14 +1,14 @@ use crate::{ BinaryOperator, LuaAstChildren, LuaAstToken, LuaAstTokenChildren, LuaBinaryOpToken, - LuaDocVersionNumberToken, LuaDocVisibilityToken, LuaExpr, LuaGeneralToken, LuaKind, - LuaNameToken, LuaNumberToken, LuaPathToken, LuaStringToken, LuaSyntaxNode, LuaTokenKind, - LuaVersionCondition, + LuaDocAttributeUse, LuaDocVersionNumberToken, LuaDocVisibilityToken, LuaExpr, LuaGeneralToken, + LuaKind, LuaNameToken, LuaNumberToken, LuaPathToken, LuaStringToken, LuaSyntaxNode, + LuaTokenKind, LuaVersionCondition, kind::LuaSyntaxKind, syntax::{LuaDocDescriptionOwner, traits::LuaAstNode}, }; use super::{ - LuaDocAttribute, LuaDocGenericDeclList, LuaDocOpType, LuaDocType, LuaDocTypeList, + LuaDocGenericDeclList, LuaDocOpType, LuaDocType, LuaDocTypeFlag, LuaDocTypeList, description::LuaDocDetailOwner, }; @@ -17,6 +17,8 @@ pub enum LuaDocTag { Class(LuaDocTagClass), Enum(LuaDocTagEnum), Alias(LuaDocTagAlias), + Attribute(LuaDocTagAttribute), + AttributeUse(LuaDocTagAttributeUse), Type(LuaDocTagType), Param(LuaDocTagParam), Return(LuaDocTagReturn), @@ -51,6 +53,7 @@ impl LuaAstNode for LuaDocTag { LuaDocTag::Class(it) => it.syntax(), LuaDocTag::Enum(it) => it.syntax(), LuaDocTag::Alias(it) => it.syntax(), + LuaDocTag::Attribute(it) => it.syntax(), LuaDocTag::Type(it) => it.syntax(), LuaDocTag::Param(it) => it.syntax(), LuaDocTag::Return(it) => it.syntax(), @@ -77,6 +80,7 @@ impl LuaAstNode for LuaDocTag { LuaDocTag::ReturnCast(it) => it.syntax(), LuaDocTag::Export(it) => it.syntax(), LuaDocTag::Language(it) => it.syntax(), + LuaDocTag::AttributeUse(it) => it.syntax(), } } @@ -88,6 +92,7 @@ impl LuaAstNode for LuaDocTag { || kind == LuaSyntaxKind::DocTagEnum || kind == LuaSyntaxKind::DocTagAlias || kind == LuaSyntaxKind::DocTagType + || kind == LuaSyntaxKind::DocTagAttribute || kind == LuaSyntaxKind::DocTagParam || kind == LuaSyntaxKind::DocTagReturn || kind == LuaSyntaxKind::DocTagOverload @@ -113,6 +118,7 @@ impl LuaAstNode for LuaDocTag { || kind == LuaSyntaxKind::DocTagReturnCast || kind == LuaSyntaxKind::DocTagExport || kind == LuaSyntaxKind::DocTagLanguage + || kind == LuaSyntaxKind::DocTagAttributeUse } fn cast(syntax: LuaSyntaxNode) -> Option @@ -129,6 +135,12 @@ impl LuaAstNode for LuaDocTag { LuaSyntaxKind::DocTagAlias => { Some(LuaDocTag::Alias(LuaDocTagAlias::cast(syntax).unwrap())) } + LuaSyntaxKind::DocTagAttribute => Some(LuaDocTag::Attribute( + LuaDocTagAttribute::cast(syntax).unwrap(), + )), + LuaSyntaxKind::DocTagAttributeUse => Some(LuaDocTag::AttributeUse( + LuaDocTagAttributeUse::cast(syntax).unwrap(), + )), LuaSyntaxKind::DocTagType => { Some(LuaDocTag::Type(LuaDocTagType::cast(syntax).unwrap())) } @@ -252,7 +264,7 @@ impl LuaDocTagClass { self.child() } - pub fn get_attrib(&self) -> Option { + pub fn get_type_flag(&self) -> Option { self.child() } } @@ -301,7 +313,7 @@ impl LuaDocTagEnum { self.child() } - pub fn get_attrib(&self) -> Option { + pub fn get_type_flag(&self) -> Option { self.child() } } @@ -518,7 +530,7 @@ impl LuaDocTagReturn { self.children() } - pub fn get_type_and_name_list(&self) -> Vec<(LuaDocType, Option)> { + pub fn get_info_list(&self) -> Vec<(LuaDocType, Option)> { let mut result = Vec::new(); let mut current_type = None; let mut current_name = None; @@ -537,7 +549,6 @@ impl LuaDocTagReturn { k if LuaDocType::can_cast(k.into()) => { current_type = Some(LuaDocType::cast(child.into_node().unwrap()).unwrap()); } - _ => {} } } @@ -679,7 +690,7 @@ impl LuaDocTagField { self.token() } - pub fn get_attrib(&self) -> Option { + pub fn get_type_flag(&self) -> Option { self.child() } } @@ -1545,3 +1556,72 @@ impl LuaDocTagLanguage { self.token() } } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct LuaDocTagAttribute { + syntax: LuaSyntaxNode, +} + +impl LuaAstNode for LuaDocTagAttribute { + fn syntax(&self) -> &LuaSyntaxNode { + &self.syntax + } + + fn can_cast(kind: LuaSyntaxKind) -> bool { + kind == LuaSyntaxKind::DocTagAttribute + } + + fn cast(syntax: LuaSyntaxNode) -> Option { + if Self::can_cast(syntax.kind().into()) { + Some(Self { syntax }) + } else { + None + } + } +} + +impl LuaDocDescriptionOwner for LuaDocTagAttribute {} + +impl LuaDocTagAttribute { + pub fn get_name_token(&self) -> Option { + self.token() + } + + pub fn get_type(&self) -> Option { + self.child() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct LuaDocTagAttributeUse { + syntax: LuaSyntaxNode, +} + +impl LuaAstNode for LuaDocTagAttributeUse { + fn syntax(&self) -> &LuaSyntaxNode { + &self.syntax + } + + fn can_cast(kind: LuaSyntaxKind) -> bool { + kind == LuaSyntaxKind::DocTagAttributeUse + } + + fn cast(syntax: LuaSyntaxNode) -> Option { + if Self::can_cast(syntax.kind().into()) { + Some(Self { syntax }) + } else { + None + } + } +} + +impl LuaDocTagAttributeUse { + pub fn get_name_token(&self) -> Option { + self.token() + } + + /// 获取所有使用的属性 + pub fn get_attribute_uses(&self) -> LuaAstChildren { + self.children() + } +} diff --git a/crates/emmylua_parser/src/syntax/node/doc/types.rs b/crates/emmylua_parser/src/syntax/node/doc/types.rs index 2860baea2..85479c92d 100644 --- a/crates/emmylua_parser/src/syntax/node/doc/types.rs +++ b/crates/emmylua_parser/src/syntax/node/doc/types.rs @@ -22,6 +22,7 @@ pub enum LuaDocType { Generic(LuaDocGenericType), StrTpl(LuaDocStrTplType), MultiLineUnion(LuaDocMultiLineUnionType), + Attribute(LuaDocAttributeType), } impl LuaAstNode for LuaDocType { @@ -41,6 +42,7 @@ impl LuaAstNode for LuaDocType { LuaDocType::Generic(it) => it.syntax(), LuaDocType::StrTpl(it) => it.syntax(), LuaDocType::MultiLineUnion(it) => it.syntax(), + LuaDocType::Attribute(it) => it.syntax(), } } @@ -64,6 +66,7 @@ impl LuaAstNode for LuaDocType { | LuaSyntaxKind::TypeGeneric | LuaSyntaxKind::TypeStringTemplate | LuaSyntaxKind::TypeMultiLineUnion + | LuaSyntaxKind::TypeAttribute ) } @@ -100,6 +103,9 @@ impl LuaAstNode for LuaDocType { LuaSyntaxKind::TypeMultiLineUnion => Some(LuaDocType::MultiLineUnion( LuaDocMultiLineUnionType::cast(syntax)?, )), + LuaSyntaxKind::TypeAttribute => { + Some(LuaDocType::Attribute(LuaDocAttributeType::cast(syntax)?)) + } _ => None, } } @@ -732,3 +738,38 @@ impl LuaDocOneLineField { self.child() } } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct LuaDocAttributeType { + syntax: LuaSyntaxNode, +} + +impl LuaAstNode for LuaDocAttributeType { + fn syntax(&self) -> &LuaSyntaxNode { + &self.syntax + } + + fn can_cast(kind: LuaSyntaxKind) -> bool + where + Self: Sized, + { + kind == LuaSyntaxKind::TypeAttribute + } + + fn cast(syntax: LuaSyntaxNode) -> Option + where + Self: Sized, + { + if Self::can_cast(syntax.kind().into()) { + Some(Self { syntax }) + } else { + None + } + } +} + +impl LuaDocAttributeType { + pub fn get_params(&self) -> LuaAstChildren { + self.children() + } +} diff --git a/crates/emmylua_parser/src/syntax/node/lua/stat.rs b/crates/emmylua_parser/src/syntax/node/lua/stat.rs index 37ddb2df4..1685dbc1d 100644 --- a/crates/emmylua_parser/src/syntax/node/lua/stat.rs +++ b/crates/emmylua_parser/src/syntax/node/lua/stat.rs @@ -219,6 +219,7 @@ impl LuaLocalStat { self.children() } + /// 仅从`AST`分析, 并不是绝对准确的, 因为允许最后一个表达式返回多个值 pub fn get_local_name_by_value(&self, value: LuaExpr) -> Option { let local_names = self.get_local_name_list(); let value_exprs = self.get_value_exprs().collect::>(); diff --git a/crates/emmylua_parser/src/syntax/node/mod.rs b/crates/emmylua_parser/src/syntax/node/mod.rs index 33084c029..f72bc2653 100644 --- a/crates/emmylua_parser/src/syntax/node/mod.rs +++ b/crates/emmylua_parser/src/syntax/node/mod.rs @@ -89,6 +89,8 @@ pub enum LuaAst { LuaDocTagReturnCast(LuaDocTagReturnCast), LuaDocTagExport(LuaDocTagExport), LuaDocTagLanguage(LuaDocTagLanguage), + LuaDocTagAttribute(LuaDocTagAttribute), + LuaDocTagAttributeUse(LuaDocTagAttributeUse), // doc description LuaDocDescription(LuaDocDescription), @@ -176,6 +178,8 @@ impl LuaAstNode for LuaAst { LuaAst::LuaDocTagAs(node) => node.syntax(), LuaAst::LuaDocTagReturnCast(node) => node.syntax(), LuaAst::LuaDocTagExport(node) => node.syntax(), + LuaAst::LuaDocTagAttribute(node) => node.syntax(), + LuaAst::LuaDocTagAttributeUse(node) => node.syntax(), LuaAst::LuaDocTagLanguage(node) => node.syntax(), LuaAst::LuaDocDescription(node) => node.syntax(), LuaAst::LuaDocNameType(node) => node.syntax(), @@ -287,6 +291,7 @@ impl LuaAstNode for LuaAst { | LuaSyntaxKind::TypeGeneric | LuaSyntaxKind::TypeStringTemplate | LuaSyntaxKind::TypeMultiLineUnion + | LuaSyntaxKind::DocAttributeUse ) } @@ -359,6 +364,9 @@ impl LuaAstNode for LuaAst { LuaSyntaxKind::DocTagClass => LuaDocTagClass::cast(syntax).map(LuaAst::LuaDocTagClass), LuaSyntaxKind::DocTagEnum => LuaDocTagEnum::cast(syntax).map(LuaAst::LuaDocTagEnum), LuaSyntaxKind::DocTagAlias => LuaDocTagAlias::cast(syntax).map(LuaAst::LuaDocTagAlias), + LuaSyntaxKind::DocTagAttribute => { + LuaDocTagAttribute::cast(syntax).map(LuaAst::LuaDocTagAttribute) + } LuaSyntaxKind::DocTagType => LuaDocTagType::cast(syntax).map(LuaAst::LuaDocTagType), LuaSyntaxKind::DocTagParam => LuaDocTagParam::cast(syntax).map(LuaAst::LuaDocTagParam), LuaSyntaxKind::DocTagReturn => { @@ -449,6 +457,9 @@ impl LuaAstNode for LuaAst { LuaSyntaxKind::TypeMultiLineUnion => { LuaDocMultiLineUnionType::cast(syntax).map(LuaAst::LuaDocMultiLineUnionType) } + LuaSyntaxKind::DocTagAttributeUse => { + LuaDocTagAttributeUse::cast(syntax).map(LuaAst::LuaDocTagAttributeUse) + } _ => None, } }