From 3ee198c9e698f8c92be0fb241f7e04eef6efee58 Mon Sep 17 00:00:00 2001 From: "efve.zff" Date: Mon, 20 Apr 2026 16:20:37 +0800 Subject: [PATCH 1/2] test: add test cases for @cast bracket index type narrowing - test_cast_bracket_index_narrows_type: verify @cast addresses[1] -nil narrows string|nil to string, and without @cast it reports mismatch - test_array_index_with_cast: verify [1] on string[] with/without @cast does not report undefined-field --- .../diagnostic/test/param_type_check_test.rs | 32 +++++++++++++++++++ .../diagnostic/test/undefined_field_test.rs | 26 +++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/param_type_check_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/param_type_check_test.rs index c721e741e..514e35efb 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/param_type_check_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/param_type_check_test.rs @@ -1555,4 +1555,36 @@ mod test { "#, )); } + + #[test] + fn test_cast_bracket_index_narrows_type() { + let mut ws = VirtualWorkspace::new(); + + // @cast addresses[1] -nil should narrow addresses[1] from string|nil to string + assert!(ws.check_code_for( + DiagnosticCode::ParamTypeMismatch, + r#" + ---@param addr string + local function connect(addr) end + + ---@type string[] + local addresses = { "127.0.0.1" } + ---@cast addresses[1] -nil + connect(addresses[1]) + "#, + )); + + // Without @cast, addresses[1] is string|nil, should report param-type-mismatch + assert!(!ws.check_code_for( + DiagnosticCode::ParamTypeMismatch, + r#" + ---@param addr string + local function connect(addr) end + + ---@type string[] + local addresses = { "127.0.0.1" } + connect(addresses[1]) + "#, + )); + } } diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/undefined_field_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/undefined_field_test.rs index a66815238..6b8540274 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/undefined_field_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/undefined_field_test.rs @@ -815,4 +815,30 @@ mod test { "# )); } + + #[test] + fn test_array_index_with_cast() { + let mut ws = VirtualWorkspace::new(); + + // Accessing [1] on a string[] should not report undefined-field + assert!(ws.check_code_for( + DiagnosticCode::UndefinedField, + r#" + ---@type string[] + local addresses + local a = addresses[1] + "# + )); + + // Accessing [1] on a string[] with @cast should not report undefined-field + assert!(ws.check_code_for( + DiagnosticCode::UndefinedField, + r#" + ---@type string[] + local addresses + ---@cast addresses[1] -nil + local a = addresses[1] + "# + )); + } } From da8e1eb0a9ec78c3e2e52f6a77303e5944fae197 Mon Sep 17 00:00:00 2001 From: "efve.zff" Date: Mon, 20 Apr 2026 16:24:54 +0800 Subject: [PATCH 2/2] fix: support bracket index in @cast expressions Add support for bracket index access in @cast doc expressions (e.g. `---@cast addresses[1] -nil`), enabling type narrowing for array element access. Lexer (lex_cast_expr): recognize '[', ']', and integer literals. Use explicit char range '0'..='9' instead of is_ascii_digit() to avoid matching non-digit suffixes like '0z'. Parser (parse_cast_expr): handle bracket indexing alongside dot access. Wrap TkInt/TkString in LiteralExpr and TkName in NameExpr so that LuaIndexExpr::get_index_key() finds a proper child Node. --- crates/emmylua_parser/src/grammar/doc/tag.rs | 43 +++++++-- crates/emmylua_parser/src/grammar/doc/test.rs | 91 +++++++++++++++++++ .../emmylua_parser/src/lexer/lua_doc_lexer.rs | 13 +++ 3 files changed, 138 insertions(+), 9 deletions(-) diff --git a/crates/emmylua_parser/src/grammar/doc/tag.rs b/crates/emmylua_parser/src/grammar/doc/tag.rs index 9c77d608e..87353eefe 100644 --- a/crates/emmylua_parser/src/grammar/doc/tag.rs +++ b/crates/emmylua_parser/src/grammar/doc/tag.rs @@ -498,16 +498,41 @@ fn parse_cast_expr(p: &mut LuaDocParser) -> DocParseResult { let m = p.mark(LuaSyntaxKind::NameExpr); p.bump(); let mut cm = m.complete(p); - // 处理多级字段访问 - while p.current_token() == LuaTokenKind::TkDot { - let index_m = cm.precede(p, LuaSyntaxKind::IndexExpr); - p.bump(); - if p.current_token() == LuaTokenKind::TkName { - p.bump(); - } else { - // 找不到也不报错 + // 处理多级字段访问(支持 `.` 和 `[]` 索引) + loop { + match p.current_token() { + LuaTokenKind::TkDot => { + let index_m = cm.precede(p, LuaSyntaxKind::IndexExpr); + p.bump(); + if p.current_token() == LuaTokenKind::TkName { + p.bump(); + } + cm = index_m.complete(p); + } + LuaTokenKind::TkLeftBracket => { + let index_m = cm.precede(p, LuaSyntaxKind::IndexExpr); + p.bump(); + // Wrap the index value in a LiteralExpr/NameExpr node so that + // LuaIndexExpr::get_index_key() can find it (it expects + // a child Node, not a bare token). + if p.current_token() == LuaTokenKind::TkInt + || p.current_token() == LuaTokenKind::TkString + { + let literal_m = p.mark(LuaSyntaxKind::LiteralExpr); + p.bump(); + literal_m.complete(p); + } else if p.current_token() == LuaTokenKind::TkName { + let name_m = p.mark(LuaSyntaxKind::NameExpr); + p.bump(); + name_m.complete(p); + } + if p.current_token() == LuaTokenKind::TkRightBracket { + p.bump(); + } + cm = index_m.complete(p); + } + _ => break, } - cm = index_m.complete(p); } Ok(cm) diff --git a/crates/emmylua_parser/src/grammar/doc/test.rs b/crates/emmylua_parser/src/grammar/doc/test.rs index fc6d2eeed..25fade9f3 100644 --- a/crates/emmylua_parser/src/grammar/doc/test.rs +++ b/crates/emmylua_parser/src/grammar/doc/test.rs @@ -1898,6 +1898,97 @@ Syntax(Chunk)@0..88 println!("Actual AST structure:\n{}", result); } + #[test] + fn test_cast_bracket_index() { + let code = r#" +---@cast a[1] -nil +---@cast a["key"] string +---@cast a.b[1].c number +---@cast a[index] -nil + "#; + let result = r#" +Syntax(Chunk)@0..101 + Syntax(Block)@0..101 + Token(TkEndOfLine)@0..1 "\n" + Syntax(Comment)@1..92 + Token(TkDocStart)@1..5 "---@" + Syntax(DocTagCast)@5..19 + Token(TkTagCast)@5..9 "cast" + Token(TkWhitespace)@9..10 " " + Syntax(IndexExpr)@10..14 + Syntax(NameExpr)@10..11 + Token(TkName)@10..11 "a" + Token(TkLeftBracket)@11..12 "[" + Syntax(LiteralExpr)@12..13 + Token(TkInt)@12..13 "1" + Token(TkRightBracket)@13..14 "]" + Token(TkWhitespace)@14..15 " " + Syntax(DocOpType)@15..19 + Token(TkMinus)@15..16 "-" + Syntax(TypeName)@16..19 + Token(TkName)@16..19 "nil" + Token(TkEndOfLine)@19..20 "\n" + Token(TkDocStart)@20..24 "---@" + Syntax(DocTagCast)@24..44 + Token(TkTagCast)@24..28 "cast" + Token(TkWhitespace)@28..29 " " + Syntax(IndexExpr)@29..37 + Syntax(NameExpr)@29..30 + Token(TkName)@29..30 "a" + Token(TkLeftBracket)@30..31 "[" + Syntax(LiteralExpr)@31..36 + Token(TkString)@31..36 "\"key\"" + Token(TkRightBracket)@36..37 "]" + Token(TkWhitespace)@37..38 " " + Syntax(DocOpType)@38..44 + Syntax(TypeName)@38..44 + Token(TkName)@38..44 "string" + Token(TkEndOfLine)@44..45 "\n" + Token(TkDocStart)@45..49 "---@" + Syntax(DocTagCast)@49..69 + Token(TkTagCast)@49..53 "cast" + Token(TkWhitespace)@53..54 " " + Syntax(IndexExpr)@54..62 + Syntax(IndexExpr)@54..60 + Syntax(IndexExpr)@54..57 + Syntax(NameExpr)@54..55 + Token(TkName)@54..55 "a" + Token(TkDot)@55..56 "." + Token(TkName)@56..57 "b" + Token(TkLeftBracket)@57..58 "[" + Syntax(LiteralExpr)@58..59 + Token(TkInt)@58..59 "1" + Token(TkRightBracket)@59..60 "]" + Token(TkDot)@60..61 "." + Token(TkName)@61..62 "c" + Token(TkWhitespace)@62..63 " " + Syntax(DocOpType)@63..69 + Syntax(TypeName)@63..69 + Token(TkName)@63..69 "number" + Token(TkEndOfLine)@69..70 "\n" + Token(TkDocStart)@70..74 "---@" + Syntax(DocTagCast)@74..92 + Token(TkTagCast)@74..78 "cast" + Token(TkWhitespace)@78..79 " " + Syntax(IndexExpr)@79..87 + Syntax(NameExpr)@79..80 + Token(TkName)@79..80 "a" + Token(TkLeftBracket)@80..81 "[" + Syntax(NameExpr)@81..86 + Token(TkName)@81..86 "index" + Token(TkRightBracket)@86..87 "]" + Token(TkWhitespace)@87..88 " " + Syntax(DocOpType)@88..92 + Token(TkMinus)@88..89 "-" + Syntax(TypeName)@89..92 + Token(TkName)@89..92 "nil" + Token(TkEndOfLine)@92..93 "\n" + Token(TkWhitespace)@93..101 " " + "#; + + assert_ast_eq!(code, result); + } + #[test] fn test_compact_luals_param() { let code = r#" diff --git a/crates/emmylua_parser/src/lexer/lua_doc_lexer.rs b/crates/emmylua_parser/src/lexer/lua_doc_lexer.rs index 7799757a2..5fe169ae6 100644 --- a/crates/emmylua_parser/src/lexer/lua_doc_lexer.rs +++ b/crates/emmylua_parser/src/lexer/lua_doc_lexer.rs @@ -599,11 +599,24 @@ impl LuaDocLexer<'_> { reader.bump(); LuaTokenKind::TkDot } + '[' => { + reader.bump(); + LuaTokenKind::TkLeftBracket + } + ']' => { + reader.bump(); + LuaTokenKind::TkRightBracket + } ch if is_name_start(ch) => { reader.bump(); reader.eat_while(is_name_continue); LuaTokenKind::TkName } + '0'..='9' => { + reader.bump(); + reader.eat_while(|c| c.is_ascii_digit()); + LuaTokenKind::TkInt + } _ => self.lex_normal(), } }