From 2f1b602f85155398715a4789a0439e2416f0073f Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sun, 18 Jan 2026 16:07:58 -0600 Subject: [PATCH 1/5] feat: Implement Lua Support --- Cargo.lock | 11 + Cargo.toml | 1 + README.md | 2 +- contributing/development/language-support.md | 2 + contributing/parsers/lua/AUDIT_REPORT.md | 45 + contributing/parsers/lua/GRAMMAR_ANALYSIS.md | 95 ++ contributing/parsers/lua/node_discovery.txt | 52 + contributing/tree-sitter/README.md | 2 +- .../scripts/check-grammar-updates.sh | 2 +- .../tree-sitter/scripts/compare-nodes.sh | 5 +- contributing/tree-sitter/scripts/setup.sh | 3 +- .../scripts/update-grammar-lock.sh | 3 +- examples/lua/comprehensive.lua | 383 ++++++ examples/lua/main.lua | 87 ++ examples/lua/utils/helper.lua | 72 ++ src/cli/commands/benchmark.rs | 104 +- src/io/parse.rs | 1 + src/parsing/factory.rs | 16 +- src/parsing/language.rs | 7 + src/parsing/lua/audit.rs | 241 ++++ src/parsing/lua/behavior.rs | 345 ++++++ src/parsing/lua/definition.rs | 165 +++ src/parsing/lua/mod.rs | 60 + src/parsing/lua/parser.rs | 1076 +++++++++++++++++ src/parsing/lua/resolution.rs | 318 +++++ src/parsing/mod.rs | 2 + src/parsing/registry.rs | 1 + tests/exploration/abi15_grammar_audit.rs | 224 +++- tests/fixtures/lua/basic.lua | 47 + tests/fixtures/lua/comments.lua | 78 ++ tests/fixtures/lua/methods.lua | 108 ++ tests/fixtures/lua/modules.lua | 58 + tests/fixtures/lua/oop.lua | 82 ++ 33 files changed, 3681 insertions(+), 17 deletions(-) create mode 100644 contributing/parsers/lua/AUDIT_REPORT.md create mode 100644 contributing/parsers/lua/GRAMMAR_ANALYSIS.md create mode 100644 contributing/parsers/lua/node_discovery.txt create mode 100644 examples/lua/comprehensive.lua create mode 100644 examples/lua/main.lua create mode 100644 examples/lua/utils/helper.lua create mode 100644 src/parsing/lua/audit.rs create mode 100644 src/parsing/lua/behavior.rs create mode 100644 src/parsing/lua/definition.rs create mode 100644 src/parsing/lua/mod.rs create mode 100644 src/parsing/lua/parser.rs create mode 100644 src/parsing/lua/resolution.rs create mode 100644 tests/fixtures/lua/basic.lua create mode 100644 tests/fixtures/lua/comments.lua create mode 100644 tests/fixtures/lua/methods.lua create mode 100644 tests/fixtures/lua/modules.lua create mode 100644 tests/fixtures/lua/oop.lua diff --git a/Cargo.lock b/Cargo.lock index 97087330..7aec2989 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -809,6 +809,7 @@ dependencies = [ "tree-sitter-java", "tree-sitter-javascript", "tree-sitter-kotlin-codanna", + "tree-sitter-lua", "tree-sitter-php", "tree-sitter-python", "tree-sitter-rust", @@ -5227,6 +5228,16 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae62f7eae5eb549c71b76658648b72cc6111f2d87d24a1e31fa907f4943e3ce" +[[package]] +name = "tree-sitter-lua" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb9adf0965fec58e7660cbb3a059dbb12ebeec9459e6dcbae3db004739641e" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-php" version = "0.24.2" diff --git a/Cargo.toml b/Cargo.toml index 5a34a90e..8844f0ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,7 @@ git2 = { version = "0.20.3", features = ["vendored-openssl"] } tempfile = "3.24.0" serde_json5 = "0.2.1" tree-sitter-swift = "0.7.1" +tree-sitter-lua = "0.2.0" glob = "0.3.3" async-trait = "0.1.89" sysinfo = "0.37.2" diff --git a/README.md b/README.md index 10ac4f6e..dca6838d 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ The difference: Codanna understands code structure. It knows `parseConfig` is a **Performance:** Sub-10ms lookups, 75,000+ symbols/second parsing. -**Languages:** Rust, Python, JavaScript, TypeScript, Java, Kotlin, Go, PHP, C, C++, C#, Swift, GDScript. +**Languages:** Rust, Python, JavaScript, TypeScript, Java, Kotlin, Go, PHP, C, C++, C#, Lua, Swift, GDScript. ## Integration diff --git a/contributing/development/language-support.md b/contributing/development/language-support.md index e1470d8c..3f069de4 100644 --- a/contributing/development/language-support.md +++ b/contributing/development/language-support.md @@ -14,6 +14,7 @@ Languages self-register via the modular registry system. Each language lives in - **TypeScript** - Interfaces, type aliases, generics, inheritance tracking, TSX/JSX support - **Java** - Classes, interfaces, enums, methods, fields, package-based modules with Maven integration - **Kotlin** - Classes, objects, interfaces, data classes, companion objects, nested scopes +- **Lua** - Tables, metatables, functions, modules, colon-syntax methods - **Python** - Classes, functions, type hints, inheritance - **PHP** - Classes, traits, interfaces, namespaces - **Go** - Structs, interfaces, methods, generics (1.18+), package visibility @@ -34,6 +35,7 @@ All languages have custom resolution contexts with language-specific scoping: | **Rust** | RustResolutionContext | Crate hierarchy | ✅ Traits | ✅ use statements | | **Java** | JavaResolutionContext | Package hierarchy | ✅ Interfaces + Abstract | ✅ import + Maven pom.xml | | **Kotlin** | KotlinResolutionContext | Package-based | ✅ Interfaces | ✅ import statements | +| **Lua** | LuaResolutionContext | Function/block scope | ✅ Metatables | ✅ require() | | **Python** | PythonResolutionContext | LEGB scoping | ✅ Classes | ✅ import/from | | **Go** | GoResolutionContext | Package-level | ✅ Interfaces (implicit) | ✅ go.mod imports | | **PHP** | PhpResolutionContext | Namespace-based | ✅ Traits + Interfaces | ✅ use/namespace | diff --git a/contributing/parsers/lua/AUDIT_REPORT.md b/contributing/parsers/lua/AUDIT_REPORT.md new file mode 100644 index 00000000..b5f332a2 --- /dev/null +++ b/contributing/parsers/lua/AUDIT_REPORT.md @@ -0,0 +1,45 @@ +# Lua Parser Symbol Extraction Coverage Report + +*Generated: 2026-01-14 03:17:09 UTC* + +## Summary +- Key nodes: 21/21 (100%) +- Symbol kinds extracted: 6 + +> **Note:** Key nodes are symbol-producing constructs (functions, tables, imports). + +## Coverage Table + +| Node Type | ID | Status | +|-----------|-----|--------| +| chunk | 72 | ✅ implemented | +| function_declaration | 92 | ✅ implemented | +| function_definition | 110 | ✅ implemented | +| variable_declaration | 98 | ✅ implemented | +| assignment_statement | 77 | ✅ implemented | +| table_constructor | 122 | ✅ implemented | +| field | 125 | ✅ implemented | +| function_call | 118 | ✅ implemented | +| method_index_expression | 119 | ✅ implemented | +| dot_index_expression | 117 | ✅ implemented | +| bracket_index_expression | 116 | ✅ implemented | +| for_statement | 88 | ✅ implemented | +| for_generic_clause | 89 | ✅ implemented | +| for_numeric_clause | 90 | ✅ implemented | +| while_statement | 83 | ✅ implemented | +| repeat_statement | 84 | ✅ implemented | +| if_statement | 85 | ✅ implemented | +| do_statement | 82 | ✅ implemented | +| block | 73 | ✅ implemented | +| return_statement | 75 | ✅ implemented | +| comment | 128 | ✅ implemented | + +## Legend + +- ✅ **implemented**: Node type is recognized and handled by the parser +- ⚠️ **gap**: Node type exists in the grammar but not handled by parser (needs implementation) +- ❌ **not found**: Node type not present in the example file (may need better examples) + +## Recommended Actions + +✨ **Excellent coverage!** All key nodes are implemented. diff --git a/contributing/parsers/lua/GRAMMAR_ANALYSIS.md b/contributing/parsers/lua/GRAMMAR_ANALYSIS.md new file mode 100644 index 00000000..651763dd --- /dev/null +++ b/contributing/parsers/lua/GRAMMAR_ANALYSIS.md @@ -0,0 +1,95 @@ +# Lua Grammar Analysis + +*Generated: 2026-01-14 03:17:09 UTC* + +## Statistics +- Nodes found in comprehensive.lua: 75 +- Nodes handled by parser: 75 +- Symbol kinds extracted: 6 + +## ✅ Successfully Handled Nodes +These nodes are in examples and handled by parser: +- " +- # +- ( +- ) +- * +- + +- , +- - +- -- +- . +- .. +- / +- : +- <= +- = +- == +- > +- [ +- ] +- and +- arguments +- assignment_statement +- binary_expression +- block +- bracket_index_expression +- chunk +- comment +- comment_content +- do +- do_statement +- dot_index_expression +- else +- else_statement +- elseif +- elseif_statement +- end +- expression_list +- false +- field +- for +- for_generic_clause +- for_numeric_clause +- for_statement +- function +- function_call +- function_declaration +- function_definition +- identifier +- if +- if_statement +- in +- local +- method_index_expression +- nil +- number +- or +- parameters +- repeat +- repeat_statement +- return +- return_statement +- string +- string_content +- table_constructor +- then +- true +- unary_expression +- until +- vararg_expression +- variable_declaration +- variable_list +- while +- while_statement +- { +- } + +## 🎯 Symbol Kinds Extracted +- Constant +- Field +- Function +- Method +- Parameter +- Variable + diff --git a/contributing/parsers/lua/node_discovery.txt b/contributing/parsers/lua/node_discovery.txt new file mode 100644 index 00000000..cc36d796 --- /dev/null +++ b/contributing/parsers/lua/node_discovery.txt @@ -0,0 +1,52 @@ +=== Lua Language COMPREHENSIVE NODE MAPPING === + Generated: 2026-01-14 03:17:09 UTC + ABI Version: 14 + Node kind count: 75 + +== FUNCTION NODES == +✓ function_declaration (ID: 92) +✓ function_definition (ID: 110) +✓ function_call (ID: 118) +✓ parameters (ID: 112) +✓ return_statement (ID: 75) + +== VARIABLE NODES == +✓ variable_declaration (ID: 98) +✓ assignment_statement (ID: 77) +✓ variable_list (ID: 78) +✓ expression_list (ID: 79) +✓ identifier (ID: 1) + +== TABLE NODES == +✓ table_constructor (ID: 122) +✓ field (ID: 125) +✓ dot_index_expression (ID: 117) +✓ bracket_index_expression (ID: 116) +✓ method_index_expression (ID: 119) + +== CONTROL FLOW NODES == +✓ if_statement (ID: 85) +✓ elseif_statement (ID: 86) +✓ else_statement (ID: 87) +✓ for_statement (ID: 88) +✗ for_in_statement (not found) +✓ while_statement (ID: 83) +✓ repeat_statement (ID: 84) +✓ do_statement (ID: 82) +✓ block (ID: 73) + +== EXPRESSION NODES == +✓ binary_expression (ID: 126) +✓ unary_expression (ID: 127) +✗ parenthesized_expression (not found) +✓ string (ID: 105) +✓ number (ID: 30) +✓ true (ID: 29) +✓ false (ID: 28) +✓ nil (ID: 27) + +== COMMENT NODES == +✓ comment (ID: 128) + + +Legend: ✓ = found in file, ○ = in grammar but not in file, ✗ = not in grammar diff --git a/contributing/tree-sitter/README.md b/contributing/tree-sitter/README.md index 0234d1d9..3205524f 100644 --- a/contributing/tree-sitter/README.md +++ b/contributing/tree-sitter/README.md @@ -38,7 +38,7 @@ The setup script configures tree-sitter and installs grammars on-demand: ./contributing/tree-sitter/scripts/setup.sh go ``` -Supported languages: typescript, javascript, python, rust, go, php, c, cpp, csharp, java, kotlin, swift, gdscript +Supported languages: typescript, javascript, python, rust, go, php, c, cpp, csharp, java, kotlin, lua, swift, gdscript ## Available Scripts diff --git a/contributing/tree-sitter/scripts/check-grammar-updates.sh b/contributing/tree-sitter/scripts/check-grammar-updates.sh index 56cf97e0..141b60bf 100755 --- a/contributing/tree-sitter/scripts/check-grammar-updates.sh +++ b/contributing/tree-sitter/scripts/check-grammar-updates.sh @@ -8,7 +8,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" LOCKFILE="$PROJECT_ROOT/contributing/parsers/grammar-versions.lock" # Supported languages -LANGUAGES="c cpp csharp gdscript go java javascript kotlin php python rust swift typescript" +LANGUAGES="c cpp csharp gdscript go java javascript kotlin lua php python rust swift typescript" echo "🔍 Checking for grammar updates from remote..." echo "" diff --git a/contributing/tree-sitter/scripts/compare-nodes.sh b/contributing/tree-sitter/scripts/compare-nodes.sh index 46160b49..2332ec49 100755 --- a/contributing/tree-sitter/scripts/compare-nodes.sh +++ b/contributing/tree-sitter/scripts/compare-nodes.sh @@ -18,7 +18,7 @@ if [ -z "$INPUT" ]; then echo " $0 # Compare using comprehensive.* (with audit)" echo " $0 # Compare specific file (output to .log)" echo "" - echo "Languages: typescript, javascript, python, rust, go, php, c, cpp, csharp, gdscript, java, kotlin" + echo "Languages: typescript, javascript, python, rust, go, php, c, cpp, csharp, gdscript, java, kotlin, lua" exit 1 fi @@ -163,9 +163,10 @@ else gdscript) EXT="gd" ;; java) EXT="java" ;; kotlin) EXT="kt" ;; + lua) EXT="lua" ;; *) echo "❌ Unsupported language: $LANG" - echo "Supported: typescript, javascript, python, rust, go, php, c, cpp, csharp, gdscript, java, kotlin" + echo "Supported: typescript, javascript, python, rust, go, php, c, cpp, csharp, gdscript, java, kotlin, lua" exit 1 ;; esac diff --git a/contributing/tree-sitter/scripts/setup.sh b/contributing/tree-sitter/scripts/setup.sh index 1321f48c..9a56e93d 100755 --- a/contributing/tree-sitter/scripts/setup.sh +++ b/contributing/tree-sitter/scripts/setup.sh @@ -36,10 +36,11 @@ if [ -n "$LANG" ]; then gdscript) REPO="https://github.com/PrestonKnopp/tree-sitter-gdscript" ;; kotlin) REPO="https://github.com/bartolli/tree-sitter-kotlin" ;; java) REPO="https://github.com/tree-sitter/tree-sitter-java" ;; + lua) REPO="https://github.com/tree-sitter-grammars/tree-sitter-lua" ;; swift) REPO="https://github.com/alex-pinkus/tree-sitter-swift" ;; *) echo "❌ Unknown language: $LANG" - echo "Supported: typescript, javascript, python, rust, go, php, c, cpp, csharp, gdscript, kotlin, java, swift" + echo "Supported: typescript, javascript, python, rust, go, php, c, cpp, csharp, gdscript, kotlin, java, lua, swift" exit 1 ;; esac diff --git a/contributing/tree-sitter/scripts/update-grammar-lock.sh b/contributing/tree-sitter/scripts/update-grammar-lock.sh index 552701d3..99ebf391 100755 --- a/contributing/tree-sitter/scripts/update-grammar-lock.sh +++ b/contributing/tree-sitter/scripts/update-grammar-lock.sh @@ -18,6 +18,7 @@ get_repo_url() { java) echo "https://github.com/tree-sitter/tree-sitter-java" ;; javascript) echo "https://github.com/tree-sitter/tree-sitter-javascript" ;; kotlin) echo "https://github.com/bartolli/tree-sitter-kotlin" ;; + lua) echo "https://github.com/tree-sitter-grammars/tree-sitter-lua" ;; php) echo "https://github.com/tree-sitter/tree-sitter-php" ;; python) echo "https://github.com/tree-sitter/tree-sitter-python" ;; rust) echo "https://github.com/tree-sitter/tree-sitter-rust" ;; @@ -27,7 +28,7 @@ get_repo_url() { } # Supported languages -LANGUAGES="c cpp csharp gdscript go java javascript kotlin php python rust swift typescript" +LANGUAGES="c cpp csharp gdscript go java javascript kotlin lua php python rust swift typescript" echo "🔍 Checking grammar versions..." echo "" diff --git a/examples/lua/comprehensive.lua b/examples/lua/comprehensive.lua new file mode 100644 index 00000000..d21da683 --- /dev/null +++ b/examples/lua/comprehensive.lua @@ -0,0 +1,383 @@ +--- Comprehensive Lua test file for parser maturity assessment +--- Tests all major Lua language features and constructs +--- +--- @module comprehensive +--- @author Codanna + +local M = {} + +-- Module-level constants (convention: SCREAMING_CASE) +local MAX_SIZE = 1024 +local DEFAULT_NAME = "default" +M.PUBLIC_CONSTANT = 42 + +-- Module-level variables +local counter = 0 +local instance = nil + +--- +--- Configuration class using metatables +--- @class Config +--- @field name string The config name +--- @field port number The port number +--- @field enabled boolean Whether config is enabled +--- +local Config = {} +Config.__index = Config + +--- Create a new Config instance +--- @param name string The configuration name +--- @param port number? Optional port (defaults to 8080) +--- @return Config +function Config:new(name, port) + local self = setmetatable({}, Config) + self.name = name or DEFAULT_NAME + self.port = port or 8080 + self.enabled = true + self._private_field = "internal" + return self +end + +--- Get the port number +--- @return number +function Config:getPort() + return self.port +end + +--- Set the port number +--- @param port number The new port +function Config:setPort(port) + self.port = port +end + +--- Convert config to string representation +--- @return string +function Config:toString() + return string.format("Config{name=%s, port=%d}", self.name, self.port) +end + +--- Static method to create default config +--- @return Config +function Config.createDefault() + return Config:new(DEFAULT_NAME, 8080) +end + +M.Config = Config + +--- +--- Generic container class +--- @class Container +--- @field items table Array of items +--- +local Container = {} +Container.__index = Container + +function Container:new() + local self = setmetatable({}, Container) + self.items = {} + self._count = 0 + return self +end + +function Container:add(item) + table.insert(self.items, item) + self._count = self._count + 1 +end + +function Container:get(index) + return self.items[index] +end + +function Container:size() + return self._count +end + +--- Iterator for container items +--- @return function +function Container:iter() + local i = 0 + return function() + i = i + 1 + return self.items[i] + end +end + +M.Container = Container + +--- +--- Inheritance example: ExtendedConfig inherits from Config +--- @class ExtendedConfig : Config +--- @field extra string Extra configuration data +--- +local ExtendedConfig = setmetatable({}, { __index = Config }) +ExtendedConfig.__index = ExtendedConfig + +function ExtendedConfig:new(name, port, extra) + local self = setmetatable(Config:new(name, port), ExtendedConfig) + self.extra = extra or "" + return self +end + +function ExtendedConfig:getExtra() + return self.extra +end + +--- Override parent method +function ExtendedConfig:toString() + return string.format("ExtendedConfig{name=%s, port=%d, extra=%s}", + self.name, self.port, self.extra) +end + +M.ExtendedConfig = ExtendedConfig + +-- Enum-like pattern using tables +M.Status = { + ACTIVE = "active", + INACTIVE = "inactive", + PENDING = "pending", +} + +-- Result type pattern +local function Ok(value) + return { ok = true, value = value } +end + +local function Err(message) + return { ok = false, error = message } +end + +M.Ok = Ok +M.Err = Err + +--- +--- Complex function with multiple parameters +--- @param reference string A reference string +--- @param items table A table of items +--- @param callback function A callback function +--- @return string, table +--- +function M.complexFunction(reference, items, callback) + local results = {} + for i, item in ipairs(items) do + results[i] = callback(item) + end + return reference, results +end + +--- Async-like pattern using coroutines +--- @param url string The URL to fetch +--- @return thread +function M.asyncOperation(url) + return coroutine.create(function() + -- Simulate async work + coroutine.yield("connecting") + coroutine.yield("fetching") + return Ok(url) + end) +end + +--- Higher-order function +--- @param f function The function to wrap +--- @return function +function M.withLogging(f) + return function(...) + print("Calling function with args:", ...) + local result = f(...) + print("Function returned:", result) + return result + end +end + +--- Closure example +--- @param initial number Initial counter value +--- @return function, function +function M.createCounter(initial) + local count = initial or 0 + + local function increment() + count = count + 1 + return count + end + + local function decrement() + count = count - 1 + return count + end + + return increment, decrement +end + +--- Variadic function +--- @vararg any +--- @return number +function M.sum(...) + local total = 0 + for _, v in ipairs({...}) do + total = total + v + end + return total +end + +--- Multiple return values +--- @param x number +--- @param y number +--- @return number, number, number +function M.minMaxSum(x, y) + local min = math.min(x, y) + local max = math.max(x, y) + local sum = x + y + return min, max, sum +end + +--- Pattern matching equivalent using table lookup +local handlers = { + add = function(a, b) return a + b end, + sub = function(a, b) return a - b end, + mul = function(a, b) return a * b end, + div = function(a, b) return a / b end, +} + +function M.calculate(op, a, b) + local handler = handlers[op] + if handler then + return Ok(handler(a, b)) + else + return Err("Unknown operation: " .. op) + end +end + +--- Metatable-based operator overloading +local Vector = {} +Vector.__index = Vector + +function Vector:new(x, y) + return setmetatable({ x = x or 0, y = y or 0 }, Vector) +end + +function Vector.__add(a, b) + return Vector:new(a.x + b.x, a.y + b.y) +end + +function Vector.__sub(a, b) + return Vector:new(a.x - b.x, a.y - b.y) +end + +function Vector.__mul(a, scalar) + return Vector:new(a.x * scalar, a.y * scalar) +end + +function Vector.__tostring(v) + return string.format("Vector(%d, %d)", v.x, v.y) +end + +function Vector:magnitude() + return math.sqrt(self.x * self.x + self.y * self.y) +end + +M.Vector = Vector + +--- Mixin pattern +local Loggable = {} + +function Loggable:log(message) + print(string.format("[%s] %s", self.name or "unknown", message)) +end + +function Loggable:debug(message) + print(string.format("[DEBUG][%s] %s", self.name or "unknown", message)) +end + +--- Apply mixin to a class +--- @param class table The class to extend +function M.makeLoggable(class) + for k, v in pairs(Loggable) do + if class[k] == nil then + class[k] = v + end + end +end + +-- Apply mixin to Config +M.makeLoggable(Config) + +--- Factory function pattern +--- @param type string The type of object to create +--- @return table|nil +function M.createObject(type) + if type == "config" then + return Config:new("factory-created") + elseif type == "container" then + return Container:new() + elseif type == "vector" then + return Vector:new(0, 0) + else + return nil + end +end + +--- Lazy initialization pattern +local _lazyValue = nil + +function M.getLazyValue() + if _lazyValue == nil then + _lazyValue = { + initialized = true, + timestamp = os.time(), + } + end + return _lazyValue +end + +--- Error handling pattern +--- @param fn function The function to call safely +--- @return boolean, any +function M.pcallWrapper(fn, ...) + local ok, result = pcall(fn, ...) + if ok then + return Ok(result) + else + return Err(result) + end +end + +--- Control flow examples for parser coverage +--- @param items table Items to process +--- @param limit number Maximum iterations +--- @return table Processed results +function M.controlFlowExamples(items, limit) + local results = {} + local i = 1 + + while i <= limit and i <= #items do + table.insert(results, items[i]) + i = i + 1 + end + + local j = 1 + repeat + if results[j] then + results[j] = results[j] * 2 + end + j = j + 1 + until j > #results + + do + local temp = {} + for idx = #results, 1, -1 do + table.insert(temp, results[idx]) + end + results = temp + end + + return results +end + +--- Module initialization +local function _init() + counter = 0 + instance = nil +end + +_init() + +return M diff --git a/examples/lua/main.lua b/examples/lua/main.lua new file mode 100644 index 00000000..2bea61af --- /dev/null +++ b/examples/lua/main.lua @@ -0,0 +1,87 @@ +--- Main entry point demonstrating module usage +--- @module main + +local comprehensive = require("comprehensive") +local helper = require("utils.helper") + +-- Use Config class +local config = comprehensive.Config:new("main-app", 3000) +print(config:toString()) + +-- Use inheritance +local extConfig = comprehensive.ExtendedConfig:new("extended", 4000, "extra-data") +print(extConfig:toString()) +print("Extra:", extConfig:getExtra()) + +-- Use Container +local container = comprehensive.Container:new() +container:add("first") +container:add("second") +container:add("third") + +print("Container size:", container:size()) +for item in container:iter() do + print(" Item:", item) +end + +-- Use Vector with operator overloading +local v1 = comprehensive.Vector:new(3, 4) +local v2 = comprehensive.Vector:new(1, 2) +local v3 = v1 + v2 +print("Vector sum:", tostring(v3)) +print("Magnitude:", v1:magnitude()) + +-- Use helper utilities +local data = { name = "test", nested = { value = 1 } } +local copied = helper.deepCopy(data) +copied.nested.value = 2 +print("Original nested value:", data.nested.value) +print("Copied nested value:", copied.nested.value) + +-- Use Result pattern +local result = comprehensive.calculate("add", 10, 5) +if result.ok then + print("Calculation result:", result.value) +else + print("Error:", result.error) +end + +-- Use counter closure +local inc, dec = comprehensive.createCounter(10) +print("Increment:", inc()) +print("Increment:", inc()) +print("Decrement:", dec()) + +-- Use variadic function +print("Sum:", comprehensive.sum(1, 2, 3, 4, 5)) + +-- Use multiple returns +local min, max, sum = comprehensive.minMaxSum(3, 7) +print(string.format("Min: %d, Max: %d, Sum: %d", min, max, sum)) + +-- Use higher-order function +local loggedSum = comprehensive.withLogging(function(a, b) + return a + b +end) +loggedSum(2, 3) + +-- Use factory +local obj = comprehensive.createObject("vector") +print("Factory created:", tostring(obj)) + +-- Use string utilities +local trimmed = helper.trim(" hello world ") +print("Trimmed:", "'" .. trimmed .. "'") + +local parts = helper.split("a,b,c", ",") +print("Split parts:", table.concat(parts, " | ")) + +-- Use merge +local base = { a = 1, b = { x = 10 } } +local override = { b = { y = 20 }, c = 3 } +local merged = helper.merge(base, override) +print("Merged b.x:", merged.b.x) +print("Merged b.y:", merged.b.y) +print("Merged c:", merged.c) + +print("\nAll examples completed successfully!") diff --git a/examples/lua/utils/helper.lua b/examples/lua/utils/helper.lua new file mode 100644 index 00000000..18c81112 --- /dev/null +++ b/examples/lua/utils/helper.lua @@ -0,0 +1,72 @@ +--- Utility helper module +--- @module utils.helper + +local M = {} + +--- Check if a value is nil or empty +--- @param value any The value to check +--- @return boolean +function M.isEmpty(value) + if value == nil then + return true + end + if type(value) == "string" and value == "" then + return true + end + if type(value) == "table" and next(value) == nil then + return true + end + return false +end + +--- Deep copy a table +--- @param original table The table to copy +--- @return table +function M.deepCopy(original) + if type(original) ~= "table" then + return original + end + + local copy = {} + for key, value in pairs(original) do + copy[M.deepCopy(key)] = M.deepCopy(value) + end + return setmetatable(copy, getmetatable(original)) +end + +--- Merge two tables +--- @param base table The base table +--- @param override table The override table +--- @return table +function M.merge(base, override) + local result = M.deepCopy(base) + for key, value in pairs(override) do + if type(value) == "table" and type(result[key]) == "table" then + result[key] = M.merge(result[key], value) + else + result[key] = value + end + end + return result +end + +--- String trim +--- @param s string The string to trim +--- @return string +function M.trim(s) + return s:match("^%s*(.-)%s*$") +end + +--- Split string by delimiter +--- @param s string The string to split +--- @param delimiter string The delimiter +--- @return table +function M.split(s, delimiter) + local result = {} + for match in (s .. delimiter):gmatch("(.-)" .. delimiter) do + table.insert(result, match) + end + return result +end + +return M diff --git a/src/cli/commands/benchmark.rs b/src/cli/commands/benchmark.rs index b2b5dd0f..a32c9f15 100644 --- a/src/cli/commands/benchmark.rs +++ b/src/cli/commands/benchmark.rs @@ -6,7 +6,8 @@ use std::time::Instant; use crate::display::tables::create_benchmark_table; use crate::display::theme::Theme; use crate::parsing::{ - CSharpParser, GoParser, LanguageParser, PhpParser, PythonParser, RustParser, TypeScriptParser, + CSharpParser, GoParser, LanguageParser, LuaParser, PhpParser, PythonParser, RustParser, + TypeScriptParser, }; use crate::types::{FileId, SymbolCounter}; use console::style; @@ -29,23 +30,26 @@ pub fn run(language: &str, custom_file: Option) { "php" => benchmark_php_parser(custom_file), "typescript" | "ts" => benchmark_typescript_parser(custom_file), "go" => benchmark_go_parser(custom_file), + "lua" => benchmark_lua_parser(custom_file), "csharp" | "c#" | "cs" => benchmark_csharp_parser(custom_file), "all" => { - benchmark_rust_parser(None); + benchmark_csharp_parser(None); println!(); - benchmark_python_parser(None); + benchmark_go_parser(None); + println!(); + benchmark_lua_parser(None); println!(); benchmark_php_parser(None); println!(); - benchmark_typescript_parser(None); + benchmark_python_parser(None); println!(); - benchmark_go_parser(None); + benchmark_rust_parser(None); println!(); - benchmark_csharp_parser(None); + benchmark_typescript_parser(None); } _ => { eprintln!("Unknown language: {language}"); - eprintln!("Available languages: rust, python, php, typescript, go, csharp, all"); + eprintln!("Available languages: csharp, go, lua, php, python, rust, typescript, all"); std::process::exit(1); } } @@ -146,6 +150,21 @@ fn benchmark_go_parser(custom_file: Option) { benchmark_parser("Go", &mut parser, &code, file_path); } +fn benchmark_lua_parser(custom_file: Option) { + let (code, file_path) = if let Some(path) = custom_file { + let content = std::fs::read_to_string(&path).unwrap_or_else(|e| { + eprintln!("Failed to read {}: {e}", path.display()); + std::process::exit(1); + }); + (content, Some(path)) + } else { + (generate_lua_benchmark_code(), None) + }; + + let mut parser = LuaParser::new().expect("Failed to create Lua parser"); + benchmark_parser("Lua", &mut parser, &code, file_path); +} + fn benchmark_csharp_parser(custom_file: Option) { let (code, file_path) = if let Some(path) = custom_file { let content = std::fs::read_to_string(&path).unwrap_or_else(|e| { @@ -567,6 +586,77 @@ func main() { code } +fn generate_lua_benchmark_code() -> String { + let mut code = String::from("-- Lua benchmark file\n\nlocal M = {}\n\n"); + + // Generate 500 functions + for i in 0..500 { + code.push_str(&format!( + r#"--- Function {i} documentation +--- @param param1 number The first parameter +--- @param param2 string The second parameter +--- @return boolean +function M.function_{i}(param1, param2) + local result = param1 * 2 + return result > 0 and #param2 > 0 +end + +"# + )); + } + + // Generate 50 "classes" (table-based OOP) + for i in 0..50 { + code.push_str(&format!( + r#"--- Class {i} documentation +local Class{i} = {{}} +Class{i}.__index = Class{i} + +function Class{i}:new(value) + local instance = setmetatable({{}}, self) + instance.value = value + return instance +end + +function Class{i}:methodA() + return self.value * 2 +end + +function Class{i}:methodB(param) + return string.upper(param) +end + +M.Class{i} = Class{i} + +"# + )); + } + + // Generate 25 local helper functions + for i in 0..25 { + code.push_str(&format!( + r#"local function _helper_{i}(data) + return data * {i} +end + +"# + )); + } + + // Generate some module-level variables and constants + for i in 0..25 { + code.push_str(&format!( + r#"local CONSTANT_{i} = {i} +local variable_{i} = "value_{i}" + +"# + )); + } + + code.push_str("return M\n"); + code +} + fn generate_csharp_benchmark_code() -> String { let mut code = String::from( "// C# benchmark file\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace BenchmarkNamespace\n{\n", diff --git a/src/io/parse.rs b/src/io/parse.rs index c1728208..6d7978fa 100644 --- a/src/io/parse.rs +++ b/src/io/parse.rs @@ -262,6 +262,7 @@ pub fn execute_parse( Language::Gdscript => tree_sitter_gdscript::LANGUAGE.into(), Language::Java => tree_sitter_java::LANGUAGE.into(), Language::Kotlin => tree_sitter_kotlin::language(), + Language::Lua => tree_sitter_lua::LANGUAGE.into(), Language::Swift => tree_sitter_swift::LANGUAGE.into(), }; diff --git a/src/parsing/factory.rs b/src/parsing/factory.rs index c9b092cd..0a1be88f 100644 --- a/src/parsing/factory.rs +++ b/src/parsing/factory.rs @@ -7,8 +7,9 @@ use super::{ CBehavior, CParser, CSharpBehavior, CSharpParser, CppBehavior, CppParser, GdscriptBehavior, GdscriptParser, GoBehavior, GoParser, JavaBehavior, JavaParser, JavaScriptBehavior, JavaScriptParser, KotlinBehavior, KotlinParser, Language, LanguageBehavior, LanguageId, - LanguageParser, PhpBehavior, PhpParser, PythonBehavior, PythonParser, RustBehavior, RustParser, - SwiftBehavior, SwiftParser, TypeScriptBehavior, TypeScriptParser, get_registry, + LanguageParser, LuaBehavior, LuaParser, PhpBehavior, PhpParser, PythonBehavior, PythonParser, + RustBehavior, RustParser, SwiftBehavior, SwiftParser, TypeScriptBehavior, TypeScriptParser, + get_registry, }; use crate::{IndexError, IndexResult, Settings}; use std::sync::Arc; @@ -176,6 +177,10 @@ impl ParserFactory { let parser = KotlinParser::new().map_err(|e| IndexError::General(e.to_string()))?; Ok(Box::new(parser)) } + Language::Lua => { + let parser = LuaParser::new().map_err(|e| IndexError::General(e.to_string()))?; + Ok(Box::new(parser)) + } Language::Swift => { let parser = SwiftParser::new().map_err(|e| IndexError::General(e.to_string()))?; Ok(Box::new(parser)) @@ -304,6 +309,13 @@ impl ParserFactory { behavior: Box::new(KotlinBehavior::new()), } } + Language::Lua => { + let parser = LuaParser::new().map_err(|e| IndexError::General(e.to_string()))?; + ParserWithBehavior { + parser: Box::new(parser), + behavior: Box::new(LuaBehavior::new()), + } + } Language::Swift => { let parser = SwiftParser::new().map_err(|e| IndexError::General(e.to_string()))?; ParserWithBehavior { diff --git a/src/parsing/language.rs b/src/parsing/language.rs index 5fd48ff2..e407aac5 100644 --- a/src/parsing/language.rs +++ b/src/parsing/language.rs @@ -20,6 +20,7 @@ pub enum Language { Gdscript, Java, Kotlin, + Lua, Swift, } @@ -43,6 +44,7 @@ impl Language { Language::Gdscript => super::LanguageId::new("gdscript"), Language::Java => super::LanguageId::new("java"), Language::Kotlin => super::LanguageId::new("kotlin"), + Language::Lua => super::LanguageId::new("lua"), Language::Swift => super::LanguageId::new("swift"), } } @@ -65,6 +67,7 @@ impl Language { "gdscript" => Some(Language::Gdscript), "java" => Some(Language::Java), "kotlin" => Some(Language::Kotlin), + "lua" => Some(Language::Lua), "swift" => Some(Language::Swift), _ => None, } @@ -102,6 +105,7 @@ impl Language { "gd" => Some(Language::Gdscript), "java" => Some(Language::Java), "kt" | "kts" => Some(Language::Kotlin), + "lua" => Some(Language::Lua), "swift" => Some(Language::Swift), _ => None, } @@ -131,6 +135,7 @@ impl Language { Language::Gdscript => &["gd"], Language::Java => &["java"], Language::Kotlin => &["kt", "kts"], + Language::Lua => &["lua"], Language::Swift => &["swift"], } } @@ -150,6 +155,7 @@ impl Language { Language::Gdscript => "gdscript", Language::Java => "java", Language::Kotlin => "kotlin", + Language::Lua => "lua", Language::Swift => "swift", } } @@ -169,6 +175,7 @@ impl Language { Language::Gdscript => "GDScript", Language::Java => "Java", Language::Kotlin => "Kotlin", + Language::Lua => "Lua", Language::Swift => "Swift", } } diff --git a/src/parsing/lua/audit.rs b/src/parsing/lua/audit.rs new file mode 100644 index 00000000..23f1da7c --- /dev/null +++ b/src/parsing/lua/audit.rs @@ -0,0 +1,241 @@ +//! Lua parser audit module +//! +//! Tracks which AST nodes the parser handles vs what's available in the grammar. + +use super::LuaParser; +use crate::io::format::format_utc_timestamp; +use crate::parsing::NodeTracker; +use crate::types::FileId; +use std::collections::{HashMap, HashSet}; +use thiserror::Error; +use tree_sitter::{Node, Parser}; + +#[derive(Error, Debug)] +pub enum AuditError { + #[error("Failed to read file: {0}")] + FileRead(#[from] std::io::Error), + + #[error("Failed to set language: {0}")] + LanguageSetup(String), + + #[error("Failed to parse code")] + ParseFailure, + + #[error("Failed to create parser: {0}")] + ParserCreation(String), +} + +pub struct LuaParserAudit { + pub grammar_nodes: HashMap, + pub implemented_nodes: HashSet, + pub extracted_symbol_kinds: HashSet, +} + +impl LuaParserAudit { + pub fn audit_file(file_path: &str) -> Result { + let code = std::fs::read_to_string(file_path)?; + Self::audit_code(&code) + } + + pub fn audit_code(code: &str) -> Result { + let mut parser = Parser::new(); + let language = tree_sitter_lua::LANGUAGE.into(); + parser + .set_language(&language) + .map_err(|e| AuditError::LanguageSetup(e.to_string()))?; + + let tree = parser.parse(code, None).ok_or(AuditError::ParseFailure)?; + + let mut grammar_nodes = HashMap::new(); + discover_nodes(tree.root_node(), &mut grammar_nodes); + + let mut lua_parser = + LuaParser::new().map_err(|e| AuditError::ParserCreation(e.to_string()))?; + let file_id = FileId(1); + let mut symbol_counter = crate::types::SymbolCounter::new(); + let symbols = lua_parser.parse(code, file_id, &mut symbol_counter); + + let mut extracted_symbol_kinds = HashSet::new(); + for symbol in &symbols { + extracted_symbol_kinds.insert(format!("{:?}", symbol.kind)); + } + + let implemented_nodes: HashSet = lua_parser + .get_handled_nodes() + .iter() + .map(|handled_node| handled_node.name.clone()) + .collect(); + + Ok(Self { + grammar_nodes, + implemented_nodes, + extracted_symbol_kinds, + }) + } + + pub fn generate_report(&self) -> String { + let mut report = String::new(); + + report.push_str("# Lua Parser Symbol Extraction Coverage Report\n\n"); + report.push_str(&format!("*Generated: {}*\n\n", format_utc_timestamp())); + + let key_nodes = vec![ + "chunk", + "function_declaration", + "function_definition", + "variable_declaration", + "assignment_statement", + "table_constructor", + "field", + "function_call", + "method_index_expression", + "dot_index_expression", + "bracket_index_expression", + "for_statement", + "for_generic_clause", + "for_numeric_clause", + "while_statement", + "repeat_statement", + "if_statement", + "do_statement", + "block", + "return_statement", + "comment", + ]; + + let key_implemented = key_nodes + .iter() + .filter(|n| self.implemented_nodes.contains(**n)) + .count(); + + report.push_str("## Summary\n"); + report.push_str(&format!( + "- Key nodes: {}/{} ({}%)\n", + key_implemented, + key_nodes.len(), + (key_implemented * 100) / key_nodes.len() + )); + report.push_str(&format!( + "- Symbol kinds extracted: {}\n", + self.extracted_symbol_kinds.len() + )); + report.push_str( + "\n> **Note:** Key nodes are symbol-producing constructs (functions, tables, imports).\n\n", + ); + + report.push_str("## Coverage Table\n\n"); + report.push_str("| Node Type | ID | Status |\n"); + report.push_str("|-----------|-----|--------|\n"); + + let mut gaps = Vec::new(); + let mut missing = Vec::new(); + + for node_name in &key_nodes { + let status = if let Some(id) = self.grammar_nodes.get(*node_name) { + if self.implemented_nodes.contains(*node_name) { + format!("{id} | ✅ implemented") + } else { + gaps.push(node_name); + format!("{id} | ⚠️ gap") + } + } else { + missing.push(node_name); + "- | ❌ not found".to_string() + }; + report.push_str(&format!("| {node_name} | {status} |\n")); + } + + report.push_str("\n## Legend\n\n"); + report + .push_str("- ✅ **implemented**: Node type is recognized and handled by the parser\n"); + report.push_str("- ⚠️ **gap**: Node type exists in the grammar but not handled by parser (needs implementation)\n"); + report.push_str("- ❌ **not found**: Node type not present in the example file (may need better examples)\n"); + + report.push_str("\n## Recommended Actions\n\n"); + + if !gaps.is_empty() { + report.push_str("### Priority 1: Implementation Gaps\n"); + report.push_str("These nodes exist in your code but aren't being captured:\n\n"); + for gap in &gaps { + report.push_str(&format!("- `{gap}`: Add parsing logic in parser.rs\n")); + } + report.push('\n'); + } + + if !missing.is_empty() { + report.push_str("### Priority 2: Missing Examples\n"); + report.push_str("These nodes aren't in the comprehensive example. Consider:\n\n"); + for node in &missing { + report.push_str(&format!( + "- `{node}`: Add example to comprehensive.lua or verify node name\n" + )); + } + report.push('\n'); + } + + if gaps.is_empty() && missing.is_empty() { + report.push_str("✨ **Excellent coverage!** All key nodes are implemented.\n"); + } + + report + } +} + +fn discover_nodes(node: Node, registry: &mut HashMap) { + registry.insert(node.kind().to_string(), node.kind_id()); + + let mut cursor = node.walk(); + for child in node.children(&mut cursor) { + discover_nodes(child, registry); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_audit_simple_lua() { + let code = r#" +-- A simple Lua module +local M = {} + +function M.hello(name) + print("Hello, " .. name) +end + +local function helper() + return 42 +end + +return M +"#; + + let audit = LuaParserAudit::audit_code(code).unwrap(); + + assert!(audit.grammar_nodes.contains_key("function_declaration")); + assert!(audit.grammar_nodes.contains_key("variable_declaration")); + + assert!(audit.extracted_symbol_kinds.contains("Function")); + } + + #[test] + fn test_control_flow_node_names() { + let code = r#" +for i, v in ipairs(t) do print(v) end +for i = 1, 10 do print(i) end +while x > 0 do x = x - 1 end +repeat x = x + 1 until x > 10 +do local scoped = true end +"#; + + let audit = LuaParserAudit::audit_code(code).unwrap(); + + assert!(audit.grammar_nodes.contains_key("for_statement")); + assert!(audit.grammar_nodes.contains_key("for_generic_clause")); + assert!(audit.grammar_nodes.contains_key("for_numeric_clause")); + assert!(audit.grammar_nodes.contains_key("while_statement")); + assert!(audit.grammar_nodes.contains_key("repeat_statement")); + assert!(audit.grammar_nodes.contains_key("do_statement")); + } +} diff --git a/src/parsing/lua/behavior.rs b/src/parsing/lua/behavior.rs new file mode 100644 index 00000000..56a02b54 --- /dev/null +++ b/src/parsing/lua/behavior.rs @@ -0,0 +1,345 @@ +//! Lua-specific language behavior implementation + +use crate::Visibility; +use crate::parsing::LanguageBehavior; +use crate::parsing::behavior_state::{BehaviorState, StatefulBehavior}; +use crate::parsing::resolution::{InheritanceResolver, ResolutionScope}; +use crate::types::FileId; +use std::path::{Path, PathBuf}; +use tree_sitter::Language; + +use super::resolution::{LuaInheritanceResolver, LuaResolutionContext}; + +/// Lua language behavior implementation +#[derive(Clone)] +pub struct LuaBehavior { + state: BehaviorState, +} + +impl LuaBehavior { + pub fn new() -> Self { + Self { + state: BehaviorState::new(), + } + } +} + +impl Default for LuaBehavior { + fn default() -> Self { + Self::new() + } +} + +impl StatefulBehavior for LuaBehavior { + fn state(&self) -> &BehaviorState { + &self.state + } +} + +impl LanguageBehavior for LuaBehavior { + fn language_id(&self) -> crate::parsing::registry::LanguageId { + crate::parsing::registry::LanguageId::new("lua") + } + + fn format_module_path(&self, base_path: &str, _symbol_name: &str) -> String { + base_path.to_string() + } + + fn get_language(&self) -> Language { + tree_sitter_lua::LANGUAGE.into() + } + + fn module_separator(&self) -> &'static str { + "." + } + + fn format_path_as_module(&self, components: &[&str]) -> Option { + if components.is_empty() { + Some(".".to_string()) + } else { + Some(components.join(".")) + } + } + + fn module_path_from_file( + &self, + file_path: &Path, + project_root: &Path, + extensions: &[&str], + ) -> Option { + use crate::parsing::paths::strip_extension; + + let relative_path = file_path + .strip_prefix(project_root) + .ok() + .or_else(|| file_path.strip_prefix("./").ok()) + .unwrap_or(file_path); + + let path = relative_path.to_str()?; + let path_clean = path.trim_start_matches("./"); + let module_path = strip_extension(path_clean, extensions); + + // Convert path separators to dots (Lua module convention) + let module_path = module_path.replace(['/', '\\'], "."); + + if module_path.is_empty() { + Some(".".to_string()) + } else { + Some(module_path) + } + } + + /// Parse visibility from Lua symbol + /// + /// Lua visibility is determined by: + /// - `local` keyword -> Private + /// - Underscore prefix convention -> Private + /// - Otherwise -> Public + fn parse_visibility(&self, signature: &str) -> Visibility { + // Check for local keyword + if signature.starts_with("local ") { + return Visibility::Private; + } + + // Extract the actual symbol name and check underscore prefix + let name = if signature.starts_with("function ") { + // For "function M.process()" or "function Foo:bar()", extract the last identifier + let after_function = signature.trim_start_matches("function "); + // First, get everything before the parameters + let before_params = after_function.split('(').next().unwrap_or(""); + // Then get the last part after . or : + before_params + .split(['.', ':']) + .next_back() + .unwrap_or("") + .trim() + } else { + // For assignments like "M.field = value", get the last identifier + let before_equals = signature.split('=').next().unwrap_or(""); + before_equals + .split(['.', ' ']) + .filter(|s| !s.is_empty()) + .next_back() + .unwrap_or("") + .trim() + }; + + if name.starts_with('_') { + Visibility::Private + } else { + Visibility::Public + } + } + + fn supports_traits(&self) -> bool { + false + } + + fn supports_inherent_methods(&self) -> bool { + true + } + + fn create_resolution_context(&self, file_id: FileId) -> Box { + Box::new(LuaResolutionContext::new(file_id)) + } + + fn create_inheritance_resolver(&self) -> Box { + Box::new(LuaInheritanceResolver::new()) + } + + fn inheritance_relation_name(&self) -> &'static str { + "extends" + } + + fn map_relationship(&self, language_specific: &str) -> crate::relationship::RelationKind { + use crate::relationship::RelationKind; + + match language_specific { + "extends" => RelationKind::Extends, + "uses" => RelationKind::Uses, + "calls" => RelationKind::Calls, + "defines" => RelationKind::Defines, + _ => RelationKind::References, + } + } + + fn register_file(&self, path: PathBuf, file_id: FileId, module_path: String) { + self.register_file_with_state(path, file_id, module_path); + } + + fn add_import(&self, import: crate::parsing::Import) { + self.add_import_with_state(import); + } + + fn get_imports_for_file(&self, file_id: FileId) -> Vec { + self.get_imports_from_state(file_id) + } + + fn is_resolvable_symbol(&self, symbol: &crate::Symbol) -> bool { + use crate::SymbolKind; + use crate::symbol::ScopeContext; + + let module_level_symbol = matches!( + symbol.kind, + SymbolKind::Function | SymbolKind::Class | SymbolKind::Constant | SymbolKind::Variable + ); + + if module_level_symbol { + return true; + } + + if matches!(symbol.kind, SymbolKind::Method) { + return true; + } + + if let Some(ref scope_context) = symbol.scope_context { + match scope_context { + ScopeContext::Module | ScopeContext::Global | ScopeContext::Package => true, + ScopeContext::Local { .. } | ScopeContext::Parameter => false, + ScopeContext::ClassMember { .. } => { + matches!(symbol.visibility, Visibility::Public) + } + } + } else { + matches!(symbol.kind, SymbolKind::Variable) + } + } + + fn get_module_path_for_file(&self, file_id: FileId) -> Option { + self.state.get_module_path(file_id) + } + + fn configure_symbol(&self, symbol: &mut crate::Symbol, module_path: Option<&str>) { + if let Some(path) = module_path { + symbol.module_path = Some(path.to_string().into()); + } + + if let Some(ref sig) = symbol.signature { + symbol.visibility = self.parse_visibility(sig); + } + + if symbol.module_path.is_none() { + symbol.module_path = Some(".".to_string().into()); + } + } + + fn import_matches_symbol( + &self, + import_path: &str, + symbol_module_path: &str, + _importing_module: Option<&str>, + ) -> bool { + // Direct match + if import_path == symbol_module_path { + return true; + } + + // Convert require path to module path format + // require("foo.bar") should match module path "foo.bar" + let normalized_import = import_path.replace(['/', '\\'], "."); + if normalized_import == symbol_module_path { + return true; + } + + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Visibility; + use std::path::Path; + + #[test] + fn test_module_separator() { + let behavior = LuaBehavior::new(); + assert_eq!(behavior.module_separator(), "."); + } + + #[test] + fn test_module_path_from_file() { + let behavior = LuaBehavior::new(); + let project_root = Path::new("/home/user/project"); + let extensions = &["lua"]; + + let file_path = Path::new("/home/user/project/lib/utils.lua"); + assert_eq!( + behavior.module_path_from_file(file_path, project_root, extensions), + Some("lib.utils".to_string()) + ); + + let file_path = Path::new("/home/user/project/main.lua"); + assert_eq!( + behavior.module_path_from_file(file_path, project_root, extensions), + Some("main".to_string()) + ); + } + + #[test] + fn test_parse_visibility() { + let behavior = LuaBehavior::new(); + + assert_eq!( + behavior.parse_visibility("function publicFunc()"), + Visibility::Public + ); + assert_eq!( + behavior.parse_visibility("local function privateFunc()"), + Visibility::Private + ); + assert_eq!( + behavior.parse_visibility("function _internal()"), + Visibility::Private + ); + assert_eq!( + behavior.parse_visibility("local counter = 0"), + Visibility::Private + ); + assert_eq!( + behavior.parse_visibility("GLOBAL_CONST = 100"), + Visibility::Public + ); + // Module function patterns + assert_eq!( + behavior.parse_visibility("function M.process()"), + Visibility::Public + ); + assert_eq!( + behavior.parse_visibility("function M._internal()"), + Visibility::Private + ); + assert_eq!( + behavior.parse_visibility("function MyClass:method()"), + Visibility::Public + ); + assert_eq!( + behavior.parse_visibility("function MyClass:_privateMethod()"), + Visibility::Private + ); + // Field assignments + assert_eq!(behavior.parse_visibility("M.VERSION"), Visibility::Public); + assert_eq!(behavior.parse_visibility("M._config"), Visibility::Private); + } + + #[test] + fn test_supports_traits() { + let behavior = LuaBehavior::new(); + assert!(!behavior.supports_traits()); + } + + #[test] + fn test_supports_inherent_methods() { + let behavior = LuaBehavior::new(); + assert!(behavior.supports_inherent_methods()); + } + + #[test] + fn test_import_matches_symbol() { + let behavior = LuaBehavior::new(); + + assert!(behavior.import_matches_symbol("mymodule.utils", "mymodule.utils", None)); + assert!(behavior.import_matches_symbol("mymodule/utils", "mymodule.utils", None)); + assert!(!behavior.import_matches_symbol("mymodule.utils", "other.module", None)); + } +} diff --git a/src/parsing/lua/definition.rs b/src/parsing/lua/definition.rs new file mode 100644 index 00000000..0ae7f906 --- /dev/null +++ b/src/parsing/lua/definition.rs @@ -0,0 +1,165 @@ +//! Lua language definition and registration +//! +//! This module defines the Lua language support for Codanna, providing +//! tree-sitter-based parsing and symbol extraction for Lua codebases. +//! +//! ## AST Node Types and Symbol Mappings +//! +//! The Lua parser uses tree-sitter-lua and handles the following +//! primary node types and their corresponding symbol classifications: +//! +//! ### Function Declarations +//! - **Global functions** (`function_declaration`) -> `SymbolKind::Function` +//! - **Local functions** (`local_function_declaration`) -> `SymbolKind::Function` (Private) +//! - **Methods** (colon syntax) -> `SymbolKind::Method` +//! +//! ### Variable Declarations +//! - **Local variables** (`variable_declaration`) -> `SymbolKind::Variable` (Private) +//! - **Global assignments** (`assignment_statement`) -> `SymbolKind::Variable` +//! +//! ### Table Constructs +//! - **Table constructors** (`table_constructor`) -> `SymbolKind::Class` (when pattern detected) +//! - **Table fields** (`field`) -> `SymbolKind::Field` +//! +//! ## Lua-Specific Language Features +//! +//! The Lua parser handles unique Lua constructs including: +//! - Module patterns (returning tables from files) +//! - Metatable-based OOP patterns +//! - Visibility via `local` keyword and underscore convention +//! - require() for module imports + +use crate::parsing::{ + LanguageBehavior, LanguageDefinition, LanguageId, LanguageParser, LanguageRegistry, +}; +use crate::{IndexError, IndexResult, Settings}; +use std::sync::Arc; + +use super::{LuaBehavior, LuaParser}; + +/// Lua language definition +/// +/// Provides factory methods for creating Lua parsers and behaviors, +/// and defines language metadata like file extensions and identification. +pub struct LuaLanguage; + +impl LanguageDefinition for LuaLanguage { + fn id(&self) -> LanguageId { + LanguageId::new("lua") + } + + fn name(&self) -> &'static str { + "Lua" + } + + fn extensions(&self) -> &'static [&'static str] { + &["lua"] + } + + fn create_parser(&self, _settings: &Settings) -> IndexResult> { + let parser = LuaParser::new().map_err(|e| IndexError::General(e.to_string()))?; + Ok(Box::new(parser)) + } + + fn create_behavior(&self) -> Box { + Box::new(LuaBehavior::new()) + } + + fn default_enabled(&self) -> bool { + true + } + + fn is_enabled(&self, settings: &Settings) -> bool { + settings + .languages + .get("Lua") + .map(|config| config.enabled) + .unwrap_or(self.default_enabled()) + } +} + +/// Register Lua language with the registry +pub(crate) fn register(registry: &mut LanguageRegistry) { + registry.register(Arc::new(LuaLanguage)); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_lua_language_id() { + let lua_lang = LuaLanguage; + assert_eq!(lua_lang.id(), LanguageId::new("lua")); + } + + #[test] + fn test_lua_language_name() { + let lua_lang = LuaLanguage; + assert_eq!(lua_lang.name(), "Lua"); + } + + #[test] + fn test_lua_file_extensions() { + let lua_lang = LuaLanguage; + assert_eq!(lua_lang.extensions(), &["lua"]); + } + + #[test] + fn test_lua_enabled_by_default() { + let lua_lang = LuaLanguage; + assert!(lua_lang.default_enabled()); + } + + #[test] + fn test_lua_enabled_with_default_settings() { + let lua_lang = LuaLanguage; + let settings = Settings::default(); + assert!(lua_lang.is_enabled(&settings)); + } + + #[test] + fn test_lua_parser_creation() { + let lua_lang = LuaLanguage; + let settings = Settings::default(); + + let parser_result = lua_lang.create_parser(&settings); + assert!(parser_result.is_ok(), "Lua parser creation should succeed"); + + let parser = parser_result.unwrap(); + assert_eq!(parser.language(), crate::parsing::Language::Lua); + } + + #[test] + fn test_lua_behavior_creation() { + let lua_lang = LuaLanguage; + let behavior = lua_lang.create_behavior(); + + assert_eq!(behavior.module_separator(), "."); + assert!(behavior.supports_inherent_methods()); + assert!(!behavior.supports_traits()); + } + + #[test] + fn test_lua_language_registry_registration() { + use crate::parsing::LanguageRegistry; + + let mut registry = LanguageRegistry::new(); + register(&mut registry); + + let lua_id = LanguageId::new("lua"); + assert!(registry.get(lua_id).is_some()); + } + + #[test] + fn test_lua_file_extension_recognition() { + use crate::parsing::LanguageRegistry; + + let mut registry = LanguageRegistry::new(); + register(&mut registry); + + let detected = registry.get_by_extension("lua"); + assert!(detected.is_some()); + assert_eq!(detected.unwrap().id(), LanguageId::new("lua")); + } +} diff --git a/src/parsing/lua/mod.rs b/src/parsing/lua/mod.rs new file mode 100644 index 00000000..fe3776e1 --- /dev/null +++ b/src/parsing/lua/mod.rs @@ -0,0 +1,60 @@ +//! Lua language parser implementation +//! +//! This module provides Lua language support for Codanna's code intelligence system, +//! enabling symbol extraction, relationship tracking, and semantic analysis of Lua codebases. +//! +//! ## Overview +//! +//! The Lua parser uses tree-sitter-lua to provide support for Lua language features +//! including functions, tables, metatables, and module patterns. +//! +//! ## Key Features +//! +//! ### Symbol Extraction +//! - **Functions**: Global and local function declarations +//! - **Methods**: Colon-syntax methods with implicit self +//! - **Tables**: Table constructors and class-like patterns +//! - **Variables**: Local and global variable declarations +//! - **Fields**: Table field definitions +//! +//! ### Lua-Specific Language Features +//! - **Module System**: require() pattern for imports +//! - **Visibility**: local keyword and underscore prefix conventions +//! - **Tables as Classes**: Metatable-based OOP patterns +//! - **Colon Syntax**: Method calls with implicit self parameter +//! +//! ## Module Components +//! +//! - [`parser`]: Core tree-sitter integration and symbol extraction +//! - [`behavior`]: Lua-specific language behaviors and formatting rules +//! - [`definition`]: Language registration and tree-sitter node mappings +//! - [`resolution`]: Symbol resolution and scope management +//! +//! ## Example Usage +//! +//! ```rust,no_run +//! use codanna::parsing::lua::{LuaParser, LuaBehavior}; +//! use codanna::parsing::{LanguageParser, LanguageBehavior}; +//! +//! let parser = LuaParser::new(); +//! let behavior = LuaBehavior::new(); +//! ``` +//! +//! ## Documentation References +//! +//! - [`definition`] module for complete AST node mappings +//! - `contributing/parsers/lua/NODE_MAPPING.md` for tree-sitter node types +//! - `tests/fixtures/lua/` for comprehensive code examples + +pub mod audit; +pub mod behavior; +pub mod definition; +pub mod parser; +pub mod resolution; + +pub use behavior::LuaBehavior; +pub use definition::LuaLanguage; +pub use parser::LuaParser; +pub use resolution::{LuaInheritanceResolver, LuaResolutionContext}; + +pub(crate) use definition::register; diff --git a/src/parsing/lua/parser.rs b/src/parsing/lua/parser.rs new file mode 100644 index 00000000..abfa42de --- /dev/null +++ b/src/parsing/lua/parser.rs @@ -0,0 +1,1076 @@ +//! Lua parser implementation +//! +//! Uses tree-sitter-lua crate's LANGUAGE constant for parsing Lua source code. + +use crate::parsing::parser::check_recursion_depth; +use crate::parsing::{ + HandledNode, Import, LanguageParser, MethodCall, NodeTracker, NodeTrackingState, ParserContext, + ScopeType, +}; +use crate::types::SymbolCounter; +use crate::{FileId, Range, Symbol, SymbolKind, Visibility}; +use std::any::Any; +use tree_sitter::{Node, Parser, Tree}; + +/// Lua language parser +pub struct LuaParser { + parser: Parser, + context: ParserContext, + node_tracker: NodeTrackingState, +} + +fn range_from_node(node: &Node) -> Range { + let start = node.start_position(); + let end = node.end_position(); + Range::new( + start.row as u32 + 1, + start.column as u16, + end.row as u32 + 1, + end.column as u16, + ) +} + +impl LuaParser { + /// Parse Lua source code and extract all symbols + /// + /// Handles function declarations (global and local), method definitions (colon syntax), + /// variable declarations, table constructors, and field assignments. + pub fn parse( + &mut self, + code: &str, + file_id: FileId, + symbol_counter: &mut SymbolCounter, + ) -> Vec { + self.context = ParserContext::new(); + let mut symbols = Vec::new(); + + if let Some(tree) = self.parser.parse(code, None) { + let root_node = tree.root_node(); + self.extract_symbols_from_node( + root_node, + code, + file_id, + symbol_counter, + &mut symbols, + "", + 0, + ); + } + + symbols + } + + fn create_symbol( + &self, + id: crate::types::SymbolId, + name: String, + kind: SymbolKind, + file_id: FileId, + range: Range, + signature: Option, + doc_comment: Option, + module_path: &str, + visibility: Visibility, + ) -> Symbol { + let mut symbol = Symbol::new(id, name, kind, file_id, range); + + if let Some(sig) = signature { + symbol = symbol.with_signature(sig); + } + if let Some(doc) = doc_comment { + symbol = symbol.with_doc(doc); + } + if !module_path.is_empty() { + symbol = symbol.with_module_path(module_path); + } + symbol = symbol.with_visibility(visibility); + symbol.scope_context = Some(self.context.current_scope_context()); + + symbol + } + + /// Create a new Lua parser + pub fn new() -> Result { + let mut parser = Parser::new(); + let lang = tree_sitter_lua::LANGUAGE; + parser + .set_language(&lang.into()) + .map_err(|e| format!("Failed to set Lua language: {e}"))?; + + Ok(Self { + parser, + context: ParserContext::new(), + node_tracker: NodeTrackingState::new(), + }) + } + + /// Extract symbols from a Lua AST node recursively + /// + /// Handles Lua-specific constructs: + /// - Function declarations (global, local, and method syntax) + /// - Variable declarations and assignments + /// - Table constructors and field definitions + /// - Control flow blocks (for, while, if, do) + fn extract_symbols_from_node( + &mut self, + node: Node, + code: &str, + file_id: FileId, + counter: &mut SymbolCounter, + symbols: &mut Vec, + module_path: &str, + depth: usize, + ) { + if !check_recursion_depth(depth, node) { + return; + } + + match node.kind() { + "function_declaration" => { + self.register_node_recursively(node); + if let Some(symbol) = + self.process_function_declaration(node, code, file_id, counter, module_path) + { + let func_name = symbol.name.to_string(); + symbols.push(symbol); + + self.context.enter_scope(ScopeType::hoisting_function()); + let saved_function = self.context.current_function().map(|s| s.to_string()); + self.context.set_current_function(Some(func_name)); + + if let Some(params) = node.child_by_field_name("parameters") { + self.process_parameters( + params, + code, + file_id, + counter, + symbols, + module_path, + ); + } + + if let Some(body) = node.child_by_field_name("body") { + self.extract_symbols_from_node( + body, + code, + file_id, + counter, + symbols, + module_path, + depth + 1, + ); + } + + self.context.exit_scope(); + self.context.set_current_function(saved_function); + } + } + "function_definition" => { + self.register_handled_node("function_definition", node.kind_id()); + } + "variable_declaration" => { + self.register_node_recursively(node); + self.process_variable_declaration( + node, + code, + file_id, + counter, + symbols, + module_path, + ); + } + "assignment_statement" => { + self.register_node_recursively(node); + self.process_assignment(node, code, file_id, counter, symbols, module_path, depth); + } + "table_constructor" => { + self.register_handled_node("table_constructor", node.kind_id()); + for child in node.children(&mut node.walk()) { + if child.kind() == "field" { + self.extract_symbols_from_node( + child, + code, + file_id, + counter, + symbols, + module_path, + depth + 1, + ); + } + } + } + "field" => { + self.register_handled_node("field", node.kind_id()); + if let Some(symbol) = + self.process_table_field(node, code, file_id, counter, module_path) + { + symbols.push(symbol); + } + } + "for_statement" | "while_statement" | "repeat_statement" => { + self.register_handled_node(node.kind(), node.kind_id()); + self.context.enter_scope(ScopeType::Block); + for child in node.children(&mut node.walk()) { + self.extract_symbols_from_node( + child, + code, + file_id, + counter, + symbols, + module_path, + depth + 1, + ); + } + self.context.exit_scope(); + } + "if_statement" => { + self.register_handled_node("if_statement", node.kind_id()); + self.context.enter_scope(ScopeType::Block); + for child in node.children(&mut node.walk()) { + self.extract_symbols_from_node( + child, + code, + file_id, + counter, + symbols, + module_path, + depth + 1, + ); + } + self.context.exit_scope(); + } + "do_statement" => { + self.register_handled_node("do_statement", node.kind_id()); + self.context.enter_scope(ScopeType::Block); + for child in node.children(&mut node.walk()) { + self.extract_symbols_from_node( + child, + code, + file_id, + counter, + symbols, + module_path, + depth + 1, + ); + } + self.context.exit_scope(); + } + "block" => { + self.register_handled_node("block", node.kind_id()); + for child in node.children(&mut node.walk()) { + self.extract_symbols_from_node( + child, + code, + file_id, + counter, + symbols, + module_path, + depth + 1, + ); + } + } + "chunk" | "program" => { + self.register_handled_node(node.kind(), node.kind_id()); + for child in node.children(&mut node.walk()) { + self.extract_symbols_from_node( + child, + code, + file_id, + counter, + symbols, + module_path, + depth + 1, + ); + } + } + "return_statement" | "break_statement" | "goto_statement" | "label_statement" => { + self.register_handled_node(node.kind(), node.kind_id()); + } + "function_call" + | "method_index_expression" + | "dot_index_expression" + | "bracket_index_expression" => { + self.register_handled_node(node.kind(), node.kind_id()); + } + "comment" => { + self.register_handled_node("comment", node.kind_id()); + } + _ => { + for child in node.children(&mut node.walk()) { + self.extract_symbols_from_node( + child, + code, + file_id, + counter, + symbols, + module_path, + depth + 1, + ); + } + } + } + } + + fn process_function_declaration( + &mut self, + node: Node, + code: &str, + file_id: FileId, + counter: &mut SymbolCounter, + module_path: &str, + ) -> Option { + let name_node = node.child_by_field_name("name")?; + let name_text = &code[name_node.byte_range()]; + + let is_local = node + .children(&mut node.walk()) + .any(|child| child.kind() == "local"); + + let (name, kind, visibility) = if name_text.contains(':') { + let parts: Vec<&str> = name_text.split(':').collect(); + let method_name = parts.last().unwrap_or(&name_text).to_string(); + let vis = if is_local { + Visibility::Private + } else { + Visibility::Public + }; + (method_name, SymbolKind::Method, vis) + } else if name_text.contains('.') { + let parts: Vec<&str> = name_text.split('.').collect(); + let func_name = parts.last().unwrap_or(&name_text).to_string(); + let vis = if is_local || func_name.starts_with('_') { + Visibility::Private + } else { + Visibility::Public + }; + (func_name, SymbolKind::Function, vis) + } else { + let vis = if is_local || name_text.starts_with('_') { + Visibility::Private + } else { + Visibility::Public + }; + (name_text.to_string(), SymbolKind::Function, vis) + }; + + let range = range_from_node(&node); + let signature = if is_local { + format!("local {}", self.extract_function_signature(node, code)) + } else { + self.extract_function_signature(node, code) + }; + let doc_comment = self.extract_lua_doc_comment(&node, code); + + Some(self.create_symbol( + counter.next_id(), + name, + kind, + file_id, + range, + Some(signature), + doc_comment, + module_path, + visibility, + )) + } + + fn process_variable_declaration( + &mut self, + node: Node, + code: &str, + file_id: FileId, + counter: &mut SymbolCounter, + symbols: &mut Vec, + module_path: &str, + ) { + for child in node.children(&mut node.walk()) { + if child.kind() == "assignment_statement" { + for assign_child in child.children(&mut child.walk()) { + if assign_child.kind() == "variable_list" { + for var_child in assign_child.children(&mut assign_child.walk()) { + if var_child.kind() == "identifier" { + let name = code[var_child.byte_range()].to_string(); + let range = range_from_node(&var_child); + + let kind = if name.chars().all(|c| c.is_uppercase() || c == '_') + && name.contains('_') + { + SymbolKind::Constant + } else { + SymbolKind::Variable + }; + + let signature = format!("local {name}"); + let doc_comment = self.extract_lua_doc_comment(&node, code); + + let symbol = self.create_symbol( + counter.next_id(), + name, + kind, + file_id, + range, + Some(signature), + doc_comment, + module_path, + Visibility::Private, + ); + symbols.push(symbol); + } + } + } + } + } + } + } + + fn process_assignment( + &mut self, + node: Node, + code: &str, + file_id: FileId, + counter: &mut SymbolCounter, + symbols: &mut Vec, + module_path: &str, + depth: usize, + ) { + let mut has_function_value = false; + for child in node.children(&mut node.walk()) { + if child.kind() == "expression_list" { + for expr_child in child.children(&mut child.walk()) { + if expr_child.kind() == "function_definition" { + has_function_value = true; + break; + } + } + } + } + + for child in node.children(&mut node.walk()) { + if child.kind() == "variable_list" { + for var_child in child.children(&mut child.walk()) { + match var_child.kind() { + "identifier" => { + let name = code[var_child.byte_range()].to_string(); + + if self.context.current_function().is_some() { + continue; + } + + let range = range_from_node(&var_child); + let kind = if has_function_value { + SymbolKind::Function + } else if name.chars().all(|c| c.is_uppercase() || c == '_') + && name.contains('_') + { + SymbolKind::Constant + } else { + SymbolKind::Variable + }; + + let visibility = if name.starts_with('_') { + Visibility::Private + } else { + Visibility::Public + }; + + let doc_comment = self.extract_lua_doc_comment(&node, code); + + let symbol = self.create_symbol( + counter.next_id(), + name.clone(), + kind, + file_id, + range, + Some(name), + doc_comment, + module_path, + visibility, + ); + symbols.push(symbol); + } + "dot_index_expression" => { + self.process_dot_index_assignment( + var_child, + node, + code, + file_id, + counter, + symbols, + module_path, + has_function_value, + ); + } + _ => {} + } + } + } + } + + for child in node.children(&mut node.walk()) { + if child.kind() == "expression_list" { + for expr_child in child.children(&mut child.walk()) { + if expr_child.kind() == "table_constructor" { + self.extract_symbols_from_node( + expr_child, + code, + file_id, + counter, + symbols, + module_path, + depth + 1, + ); + } + } + } + } + } + + fn process_dot_index_assignment( + &mut self, + node: Node, + parent_node: Node, + code: &str, + file_id: FileId, + counter: &mut SymbolCounter, + symbols: &mut Vec, + module_path: &str, + is_function: bool, + ) { + if let Some(field_node) = node.child_by_field_name("field") { + let field_name = code[field_node.byte_range()].to_string(); + let range = range_from_node(&node); + + let kind = if is_function { + SymbolKind::Function + } else if field_name.chars().all(|c| c.is_uppercase() || c == '_') + && field_name.contains('_') + { + SymbolKind::Constant + } else { + SymbolKind::Field + }; + + let visibility = if field_name.starts_with('_') { + Visibility::Private + } else { + Visibility::Public + }; + + let signature = code[node.byte_range()].to_string(); + let doc_comment = self.extract_lua_doc_comment(&parent_node, code); + + let symbol = self.create_symbol( + counter.next_id(), + field_name, + kind, + file_id, + range, + Some(signature), + doc_comment, + module_path, + visibility, + ); + symbols.push(symbol); + } + } + + fn process_table_field( + &mut self, + node: Node, + code: &str, + file_id: FileId, + counter: &mut SymbolCounter, + module_path: &str, + ) -> Option { + if let Some(name_node) = node.child_by_field_name("name") { + let name = code[name_node.byte_range()].to_string(); + let range = range_from_node(&node); + + let mut is_function = false; + if let Some(value_node) = node.child_by_field_name("value") { + is_function = value_node.kind() == "function_definition"; + } + + let kind = if is_function { + SymbolKind::Method + } else { + SymbolKind::Field + }; + + let visibility = if name.starts_with('_') { + Visibility::Private + } else { + Visibility::Public + }; + + let signature = code[node.byte_range()].to_string(); + + return Some(self.create_symbol( + counter.next_id(), + name, + kind, + file_id, + range, + Some(signature), + None, + module_path, + visibility, + )); + } + + None + } + + fn process_parameters( + &mut self, + node: Node, + code: &str, + file_id: FileId, + counter: &mut SymbolCounter, + symbols: &mut Vec, + module_path: &str, + ) { + for child in node.children(&mut node.walk()) { + if child.kind() == "identifier" { + let name = code[child.byte_range()].to_string(); + let range = range_from_node(&child); + + let symbol = self.create_symbol( + counter.next_id(), + name.clone(), + SymbolKind::Parameter, + file_id, + range, + Some(name), + None, + module_path, + Visibility::Private, + ); + symbols.push(symbol); + } + } + } + + fn extract_function_signature(&self, node: Node, code: &str) -> String { + let mut sig = String::from("function"); + + if let Some(name_node) = node.child_by_field_name("name") { + sig.push(' '); + sig.push_str(&code[name_node.byte_range()]); + } + + if let Some(params_node) = node.child_by_field_name("parameters") { + sig.push_str(&code[params_node.byte_range()]); + } + + sig + } + + fn extract_lua_doc_comment(&self, node: &Node, code: &str) -> Option { + let mut doc_lines = Vec::new(); + let mut current = node.prev_sibling(); + + while let Some(sibling) = current { + if sibling.kind() == "comment" { + let comment_text = &code[sibling.byte_range()]; + + if comment_text.starts_with("---") { + let content = comment_text.trim_start_matches("---").trim(); + doc_lines.insert(0, content.to_string()); + current = sibling.prev_sibling(); + } else if comment_text.starts_with("--") && !comment_text.starts_with("--[[") { + let content = comment_text.trim_start_matches("--").trim(); + doc_lines.insert(0, content.to_string()); + current = sibling.prev_sibling(); + } else if comment_text.starts_with("--[[") { + let content = comment_text + .trim_start_matches("--[[") + .trim_end_matches("]]") + .trim(); + doc_lines.insert(0, content.to_string()); + break; + } else { + break; + } + } else { + break; + } + } + + if !doc_lines.is_empty() { + let filtered: Vec = doc_lines.into_iter().filter(|l| !l.is_empty()).collect(); + if !filtered.is_empty() { + return Some(filtered.join("\n")); + } + } + + None + } + + fn extract_method_calls_from_tree(&self, tree: &Tree, code: &str) -> Vec { + let mut calls = Vec::new(); + extract_method_calls_recursive(&tree.root_node(), code, &mut calls); + calls + } +} + +fn extract_method_calls_recursive(node: &Node, code: &str, calls: &mut Vec) { + if node.kind() == "function_call" { + if let Some(name_node) = node.child_by_field_name("name") { + if name_node.kind() == "method_index_expression" { + if let Some(method_node) = name_node.child_by_field_name("method") { + let method_name = code[method_node.byte_range()].to_string(); + let range = range_from_node(node); + + let receiver = name_node + .child_by_field_name("table") + .map(|n| code[n.byte_range()].to_string()); + + calls.push(MethodCall { + caller: String::new(), + method_name, + receiver, + is_static: false, + range, + caller_range: Some(range), + }); + } + } + } + } + + for child in node.children(&mut node.walk()) { + extract_method_calls_recursive(&child, code, calls); + } +} + +fn extract_imports_recursive(node: &Node, code: &str, file_id: FileId, imports: &mut Vec) { + // Look for variable_declaration containing require() calls + // Pattern: local foo = require("module") + if node.kind() == "variable_declaration" { + let mut alias: Option = None; + let mut require_call: Option = None; + + for child in node.children(&mut node.walk()) { + if child.kind() == "assignment_statement" { + for assign_child in child.children(&mut child.walk()) { + if assign_child.kind() == "variable_list" { + // Get the variable name (alias) + for var_child in assign_child.children(&mut assign_child.walk()) { + if var_child.kind() == "identifier" { + alias = Some(code[var_child.byte_range()].to_string()); + break; + } + } + } else if assign_child.kind() == "expression_list" { + // Check if value is a require() call + for expr_child in assign_child.children(&mut assign_child.walk()) { + if expr_child.kind() == "function_call" { + require_call = Some(expr_child); + break; + } + } + } + } + } + } + + if let Some(call_node) = require_call { + if let Some(import) = try_extract_require_call(&call_node, code, file_id, alias) { + imports.push(import); + return; // Don't recurse into this node again + } + } + } + + // Also check for standalone require() calls (without assignment) + if node.kind() == "function_call" { + if let Some(import) = try_extract_require_call(node, code, file_id, None) { + imports.push(import); + return; // Found a require call, don't recurse + } + } + + for child in node.children(&mut node.walk()) { + extract_imports_recursive(&child, code, file_id, imports); + } +} + +fn try_extract_require_call( + node: &Node, + code: &str, + file_id: FileId, + alias: Option, +) -> Option { + if node.kind() != "function_call" { + return None; + } + + let name_node = node.child_by_field_name("name")?; + let func_name = &code[name_node.byte_range()]; + + if func_name != "require" { + return None; + } + + let args_node = node.child_by_field_name("arguments")?; + + // Find the string argument inside the arguments + for arg_child in args_node.children(&mut args_node.walk()) { + if arg_child.kind() == "string" { + // Extract string content (remove quotes) + let full_string = &code[arg_child.byte_range()]; + let module_path = full_string + .trim_start_matches('"') + .trim_start_matches('\'') + .trim_end_matches('"') + .trim_end_matches('\'') + .to_string(); + + if !module_path.is_empty() { + return Some(Import { + path: module_path, + alias, + file_id, + is_glob: false, + is_type_only: false, + }); + } + } + } + + None +} + +impl NodeTracker for LuaParser { + fn get_handled_nodes(&self) -> &std::collections::HashSet { + self.node_tracker.get_handled_nodes() + } + + fn register_handled_node(&mut self, node_kind: &str, node_id: u16) { + self.node_tracker.register_handled_node(node_kind, node_id); + } +} + +impl LuaParser { + fn register_node_recursively(&mut self, node: Node) { + self.node_tracker + .register_handled_node(node.kind(), node.kind_id()); + for child in node.children(&mut node.walk()) { + self.register_node_recursively(child); + } + } +} + +impl LanguageParser for LuaParser { + fn parse( + &mut self, + code: &str, + file_id: FileId, + symbol_counter: &mut SymbolCounter, + ) -> Vec { + self.parse(code, file_id, symbol_counter) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn extract_doc_comment(&self, node: &Node, code: &str) -> Option { + self.extract_lua_doc_comment(node, code) + } + + fn find_calls<'a>(&mut self, _code: &'a str) -> Vec<(&'a str, &'a str, Range)> { + Vec::new() + } + + /// Extract method calls from Lua source code + /// + /// Returns MethodCall structs for colon-syntax method invocations (obj:method()). + fn find_method_calls(&mut self, code: &str) -> Vec { + let tree = match self.parser.parse(code, None) { + Some(tree) => tree, + None => return Vec::new(), + }; + + self.extract_method_calls_from_tree(&tree, code) + } + + /// Lua uses duck typing - no explicit interface implementations + fn find_implementations<'a>(&mut self, _code: &'a str) -> Vec<(&'a str, &'a str, Range)> { + Vec::new() + } + + /// Lua uses metatables for inheritance - no explicit extends declarations + fn find_extends<'a>(&mut self, _code: &'a str) -> Vec<(&'a str, &'a str, Range)> { + Vec::new() + } + + fn find_uses<'a>(&mut self, _code: &'a str) -> Vec<(&'a str, &'a str, Range)> { + Vec::new() + } + + fn find_defines<'a>(&mut self, _code: &'a str) -> Vec<(&'a str, &'a str, Range)> { + Vec::new() + } + + /// Extract require() imports from Lua source code + /// + /// Parses patterns like: + /// - `local foo = require("path.to.module")` + /// - `local bar = require('module')` + /// - `require("module")` (without assignment) + fn find_imports(&mut self, code: &str, file_id: FileId) -> Vec { + let tree = match self.parser.parse(code, None) { + Some(tree) => tree, + None => return Vec::new(), + }; + + let mut imports = Vec::new(); + extract_imports_recursive(&tree.root_node(), code, file_id, &mut imports); + imports + } + + fn language(&self) -> crate::parsing::Language { + crate::parsing::Language::Lua + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_global_function() { + let mut parser = LuaParser::new().unwrap(); + let code = r#" +function hello(name) + print("Hello, " .. name) +end +"#; + + let file_id = FileId::new(1).unwrap(); + let mut counter = SymbolCounter::new(); + let symbols = parser.parse(code, file_id, &mut counter); + + assert!(!symbols.is_empty()); + let func = symbols.iter().find(|s| s.name.as_ref() == "hello"); + assert!(func.is_some()); + assert_eq!(func.unwrap().kind, SymbolKind::Function); + } + + #[test] + fn test_parse_local_function() { + let mut parser = LuaParser::new().unwrap(); + let code = r#" +local function helper() + return 42 +end +"#; + + let file_id = FileId::new(1).unwrap(); + let mut counter = SymbolCounter::new(); + let symbols = parser.parse(code, file_id, &mut counter); + + let func = symbols.iter().find(|s| s.name.as_ref() == "helper"); + assert!(func.is_some()); + assert_eq!(func.unwrap().visibility, Visibility::Private); + } + + #[test] + fn test_parse_local_variable() { + let mut parser = LuaParser::new().unwrap(); + let code = r#" +local counter = 0 +local MAX_VALUE = 100 +"#; + + let file_id = FileId::new(1).unwrap(); + let mut counter_sym = SymbolCounter::new(); + let symbols = parser.parse(code, file_id, &mut counter_sym); + + let var = symbols.iter().find(|s| s.name.as_ref() == "counter"); + assert!(var.is_some()); + assert_eq!(var.unwrap().kind, SymbolKind::Variable); + + let const_sym = symbols.iter().find(|s| s.name.as_ref() == "MAX_VALUE"); + assert!(const_sym.is_some()); + assert_eq!(const_sym.unwrap().kind, SymbolKind::Constant); + } + + #[test] + fn test_parse_method() { + let mut parser = LuaParser::new().unwrap(); + let code = r#" +function MyClass:greet() + return "Hello" +end +"#; + + let file_id = FileId::new(1).unwrap(); + let mut counter = SymbolCounter::new(); + let symbols = parser.parse(code, file_id, &mut counter); + + let method = symbols.iter().find(|s| s.name.as_ref() == "greet"); + assert!(method.is_some()); + assert_eq!(method.unwrap().kind, SymbolKind::Method); + } + + #[test] + fn test_find_imports_with_alias() { + use crate::parsing::LanguageParser; + + let mut parser = LuaParser::new().unwrap(); + let code = r#" +local json = require("cjson") +local utils = require("myapp.utils") +"#; + + let file_id = FileId::new(1).unwrap(); + let imports = parser.find_imports(code, file_id); + + assert_eq!(imports.len(), 2); + + let json_import = imports.iter().find(|i| i.path == "cjson"); + assert!(json_import.is_some()); + assert_eq!(json_import.unwrap().alias, Some("json".to_string())); + + let utils_import = imports.iter().find(|i| i.path == "myapp.utils"); + assert!(utils_import.is_some()); + assert_eq!(utils_import.unwrap().alias, Some("utils".to_string())); + } + + #[test] + fn test_find_imports_single_quotes() { + use crate::parsing::LanguageParser; + + let mut parser = LuaParser::new().unwrap(); + let code = r#" +local foo = require('single.quoted') +"#; + + let file_id = FileId::new(1).unwrap(); + let imports = parser.find_imports(code, file_id); + + assert_eq!(imports.len(), 1); + assert_eq!(imports[0].path, "single.quoted"); + assert_eq!(imports[0].alias, Some("foo".to_string())); + } + + #[test] + fn test_find_imports_standalone() { + use crate::parsing::LanguageParser; + + let mut parser = LuaParser::new().unwrap(); + let code = r#" +require("some.module") +"#; + + let file_id = FileId::new(1).unwrap(); + let imports = parser.find_imports(code, file_id); + + assert_eq!(imports.len(), 1); + assert_eq!(imports[0].path, "some.module"); + assert_eq!(imports[0].alias, None); + } +} diff --git a/src/parsing/lua/resolution.rs b/src/parsing/lua/resolution.rs new file mode 100644 index 00000000..2bdd2c98 --- /dev/null +++ b/src/parsing/lua/resolution.rs @@ -0,0 +1,318 @@ +//! Lua-specific resolution and inheritance implementation + +use crate::parsing::{InheritanceResolver, ResolutionScope, ScopeLevel, ScopeType}; +use crate::symbol::ScopeContext; +use crate::{FileId, SymbolId}; +use std::any::Any; +use std::collections::HashMap; + +/// Lua-specific resolution context +#[derive(Debug)] +pub struct LuaResolutionContext { + scope_stack: Vec, + imports: HashMap, + global_symbols: HashMap, + module_symbols: HashMap, + local_symbols: HashMap, +} + +#[derive(Debug)] +struct LuaScope { + symbols: HashMap, + #[allow(dead_code)] + scope_type: ScopeType, +} + +impl Default for LuaResolutionContext { + fn default() -> Self { + Self { + scope_stack: vec![LuaScope { + symbols: HashMap::new(), + scope_type: ScopeType::Module, + }], + imports: HashMap::new(), + global_symbols: HashMap::new(), + module_symbols: HashMap::new(), + local_symbols: HashMap::new(), + } + } +} + +impl LuaResolutionContext { + pub fn new(_file_id: FileId) -> Self { + Self { + scope_stack: vec![LuaScope { + symbols: HashMap::new(), + scope_type: ScopeType::Module, + }], + imports: HashMap::new(), + global_symbols: HashMap::new(), + module_symbols: HashMap::new(), + local_symbols: HashMap::new(), + } + } + + pub fn add_import_symbol(&mut self, name: String, symbol_id: SymbolId, _is_type_only: bool) { + self.imports.insert(name, symbol_id); + } + + pub fn add_symbol_with_context( + &mut self, + name: String, + symbol_id: SymbolId, + scope_context: Option<&ScopeContext>, + ) { + let scope_level = match scope_context { + Some(ScopeContext::Global) => ScopeLevel::Global, + Some(ScopeContext::Module) | Some(ScopeContext::Package) => ScopeLevel::Module, + Some(ScopeContext::Local { hoisted: true, .. }) => ScopeLevel::Module, + Some(ScopeContext::Local { hoisted: false, .. }) => ScopeLevel::Local, + Some(ScopeContext::Parameter) => ScopeLevel::Local, + Some(ScopeContext::ClassMember { .. }) => ScopeLevel::Module, + None => ScopeLevel::Module, + }; + + self.add_symbol(name, symbol_id, scope_level); + } +} + +impl ResolutionScope for LuaResolutionContext { + fn add_symbol(&mut self, name: String, symbol_id: SymbolId, scope_level: ScopeLevel) { + match scope_level { + ScopeLevel::Global => { + self.global_symbols.insert(name, symbol_id); + } + ScopeLevel::Module | ScopeLevel::Package => { + self.module_symbols.insert(name, symbol_id); + } + ScopeLevel::Local => { + self.local_symbols.insert(name.clone(), symbol_id); + if let Some(current_scope) = self.scope_stack.last_mut() { + current_scope.symbols.insert(name, symbol_id); + } + } + } + } + + fn resolve(&self, name: &str) -> Option { + for scope in self.scope_stack.iter().rev() { + if let Some(id) = scope.symbols.get(name) { + return Some(*id); + } + } + + if let Some(id) = self.imports.get(name) { + return Some(*id); + } + + if let Some(id) = self.module_symbols.get(name) { + return Some(*id); + } + + if let Some(id) = self.global_symbols.get(name) { + return Some(*id); + } + + None + } + + fn clear_local_scope(&mut self) { + self.local_symbols.clear(); + if let Some(scope) = self.scope_stack.last_mut() { + scope.symbols.clear(); + } + } + + fn enter_scope(&mut self, scope_type: ScopeType) { + self.scope_stack.push(LuaScope { + symbols: HashMap::new(), + scope_type, + }); + } + + fn exit_scope(&mut self) { + if self.scope_stack.len() > 1 { + self.scope_stack.pop(); + } + } + + fn symbols_in_scope(&self) -> Vec<(String, SymbolId, ScopeLevel)> { + let mut result = Vec::new(); + + for (name, id) in &self.local_symbols { + result.push((name.clone(), *id, ScopeLevel::Local)); + } + + for (name, id) in &self.module_symbols { + result.push((name.clone(), *id, ScopeLevel::Module)); + } + + for (name, id) in &self.global_symbols { + result.push((name.clone(), *id, ScopeLevel::Global)); + } + + result + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +/// Lua inheritance resolver +#[derive(Debug, Default)] +pub struct LuaInheritanceResolver { + inheritance: HashMap>, + type_methods: HashMap>, +} + +impl LuaInheritanceResolver { + pub fn new() -> Self { + Self { + inheritance: HashMap::new(), + type_methods: HashMap::new(), + } + } +} + +impl InheritanceResolver for LuaInheritanceResolver { + fn add_inheritance(&mut self, child: String, parent: String, kind: &str) { + self.inheritance + .entry(child) + .or_default() + .push((parent, kind.to_string())); + } + + fn resolve_method(&self, type_name: &str, method: &str) -> Option { + if let Some(methods) = self.type_methods.get(type_name) { + if methods.contains(&method.to_string()) { + return Some(type_name.to_string()); + } + } + + if let Some(parents) = self.inheritance.get(type_name) { + for (parent, _kind) in parents { + if let Some(result) = self.resolve_method(parent, method) { + return Some(result); + } + } + } + + None + } + + fn get_inheritance_chain(&self, type_name: &str) -> Vec { + let mut chain = vec![type_name.to_string()]; + let mut visited = std::collections::HashSet::new(); + visited.insert(type_name.to_string()); + + let mut to_visit = vec![type_name.to_string()]; + + while let Some(current) = to_visit.pop() { + if let Some(parents) = self.inheritance.get(¤t) { + for (parent, _kind) in parents { + if visited.insert(parent.clone()) { + chain.push(parent.clone()); + to_visit.push(parent.clone()); + } + } + } + } + + chain + } + + fn is_subtype(&self, child: &str, parent: &str) -> bool { + if child == parent { + return true; + } + + let chain = self.get_inheritance_chain(child); + chain.contains(&parent.to_string()) + } + + fn add_type_methods(&mut self, type_name: String, methods: Vec) { + self.type_methods.insert(type_name, methods); + } + + fn get_all_methods(&self, type_name: &str) -> Vec { + let mut methods = Vec::new(); + let chain = self.get_inheritance_chain(type_name); + + for ancestor in chain { + if let Some(type_methods) = self.type_methods.get(&ancestor) { + for method in type_methods { + if !methods.contains(method) { + methods.push(method.clone()); + } + } + } + } + + methods + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_lua_resolution_context() { + let file_id = FileId::new(1).unwrap(); + let mut context = LuaResolutionContext::new(file_id); + + let sym1 = SymbolId::new(1).unwrap(); + let sym2 = SymbolId::new(2).unwrap(); + + context.add_symbol("globalFunc".to_string(), sym1, ScopeLevel::Global); + context.add_symbol("moduleVar".to_string(), sym2, ScopeLevel::Module); + + assert_eq!(context.resolve("globalFunc"), Some(sym1)); + assert_eq!(context.resolve("moduleVar"), Some(sym2)); + assert_eq!(context.resolve("unknown"), None); + } + + #[test] + fn test_lua_scope_stack() { + let file_id = FileId::new(1).unwrap(); + let mut context = LuaResolutionContext::new(file_id); + + let outer = SymbolId::new(1).unwrap(); + let inner = SymbolId::new(2).unwrap(); + + context.add_symbol("x".to_string(), outer, ScopeLevel::Local); + + context.enter_scope(ScopeType::Block); + context.add_symbol("x".to_string(), inner, ScopeLevel::Local); + + assert_eq!(context.resolve("x"), Some(inner)); + + context.exit_scope(); + + assert_eq!(context.resolve("x"), Some(outer)); + } + + #[test] + fn test_lua_inheritance_resolver() { + let mut resolver = LuaInheritanceResolver::new(); + + resolver.add_inheritance("Dog".to_string(), "Animal".to_string(), "metatable"); + resolver.add_inheritance("Cat".to_string(), "Animal".to_string(), "metatable"); + + assert!(resolver.is_subtype("Dog", "Animal")); + assert!(resolver.is_subtype("Cat", "Animal")); + assert!(!resolver.is_subtype("Animal", "Dog")); + + let chain = resolver.get_inheritance_chain("Dog"); + assert!(chain.contains(&"Dog".to_string())); + assert!(chain.contains(&"Animal".to_string())); + + resolver.add_type_methods("Animal".to_string(), vec!["speak".to_string()]); + resolver.add_type_methods("Dog".to_string(), vec!["bark".to_string()]); + + let methods = resolver.get_all_methods("Dog"); + assert!(methods.contains(&"bark".to_string())); + assert!(methods.contains(&"speak".to_string())); + } +} diff --git a/src/parsing/mod.rs b/src/parsing/mod.rs index 1b955336..61484cb8 100644 --- a/src/parsing/mod.rs +++ b/src/parsing/mod.rs @@ -12,6 +12,7 @@ pub mod javascript; pub mod kotlin; pub mod language; pub mod language_behavior; +pub mod lua; pub mod method_call; pub mod parser; pub mod paths; @@ -38,6 +39,7 @@ pub use language::Language; pub use language_behavior::{ LanguageBehavior, LanguageMetadata, RelationRole, default_relationship_compatibility, }; +pub use lua::{LuaBehavior, LuaParser}; pub use method_call::{MethodCall, MethodCallResolver}; pub use parser::{ HandledNode, LanguageParser, NodeTracker, NodeTrackingState, safe_substring_window, diff --git a/src/parsing/registry.rs b/src/parsing/registry.rs index d181cad2..cceae1a7 100644 --- a/src/parsing/registry.rs +++ b/src/parsing/registry.rs @@ -382,6 +382,7 @@ fn initialize_registry(registry: &mut LanguageRegistry) { super::gdscript::register(registry); super::java::register(registry); super::kotlin::register(registry); + super::lua::register(registry); super::swift::register(registry); } diff --git a/tests/exploration/abi15_grammar_audit.rs b/tests/exploration/abi15_grammar_audit.rs index e62907b9..8e23928c 100644 --- a/tests/exploration/abi15_grammar_audit.rs +++ b/tests/exploration/abi15_grammar_audit.rs @@ -29,7 +29,7 @@ mod tests { c::audit::CParserAudit, cpp::audit::CppParserAudit, csharp::audit::CSharpParserAudit, gdscript::audit::GdscriptParserAudit, go::audit::GoParserAudit, java::audit::JavaParserAudit, javascript::audit::JavaScriptParserAudit, - kotlin::audit::KotlinParserAudit, php::audit::PhpParserAudit, + kotlin::audit::KotlinParserAudit, lua::audit::LuaParserAudit, php::audit::PhpParserAudit, python::audit::PythonParserAudit, rust::audit::RustParserAudit, swift::audit::SwiftParserAudit, typescript::audit::TypeScriptParserAudit, }; @@ -321,6 +321,113 @@ mod tests { println!("✅ Go node_discovery.txt saved"); } + #[test] + fn comprehensive_lua_analysis() { + println!("=== Lua Comprehensive Grammar Analysis ===\n"); + + // Run the parser audit to get everything at once + let audit = match LuaParserAudit::audit_file("examples/lua/comprehensive.lua") { + Ok(audit) => audit, + Err(e) => { + println!("Warning: Failed to audit Lua file: {e}"); + LuaParserAudit { + grammar_nodes: HashMap::new(), + implemented_nodes: HashSet::new(), + extracted_symbol_kinds: HashSet::new(), + } + } + }; + + let example_nodes: HashSet = audit.grammar_nodes.keys().cloned().collect(); + + // Save the audit report + let report = audit.generate_report(); + fs::write("contributing/parsers/lua/AUDIT_REPORT.md", &report) + .expect("Failed to write Lua audit report"); + + // Generate comprehensive analysis + let mut analysis = String::new(); + analysis.push_str("# Lua Grammar Analysis\n\n"); + analysis.push_str(&format!("*Generated: {}*\n\n", get_formatted_timestamp())); + analysis.push_str("## Statistics\n"); + analysis.push_str(&format!( + "- Nodes found in comprehensive.lua: {}\n", + example_nodes.len() + )); + analysis.push_str(&format!( + "- Nodes handled by parser: {}\n", + audit.implemented_nodes.len() + )); + analysis.push_str(&format!( + "- Symbol kinds extracted: {}\n", + audit.extracted_symbol_kinds.len() + )); + analysis.push('\n'); + + // Categorize nodes + let mut in_example_not_handled: Vec<_> = example_nodes + .iter() + .filter(|n| !audit.implemented_nodes.contains(n.as_str())) + .collect(); + let mut handled_well: Vec<_> = audit + .implemented_nodes + .iter() + .filter(|n| example_nodes.contains(n.as_str())) + .collect(); + + in_example_not_handled.sort(); + handled_well.sort(); + + if !handled_well.is_empty() { + analysis.push_str("## ✅ Successfully Handled Nodes\n"); + analysis.push_str("These nodes are in examples and handled by parser:\n"); + for node in &handled_well { + analysis.push_str(&format!("- {node}\n")); + } + analysis.push('\n'); + } + + if !in_example_not_handled.is_empty() { + analysis.push_str("## ⚠️ Implementation Gaps\n"); + analysis.push_str("These nodes appear in comprehensive.lua but aren't handled:\n"); + for node in &in_example_not_handled { + analysis.push_str(&format!("- {node}\n")); + } + analysis.push('\n'); + } + + if !audit.extracted_symbol_kinds.is_empty() { + analysis.push_str("## 🎯 Symbol Kinds Extracted\n"); + let mut kinds: Vec<_> = audit.extracted_symbol_kinds.iter().collect(); + kinds.sort(); + for kind in kinds { + analysis.push_str(&format!("- {kind}\n")); + } + analysis.push('\n'); + } + + fs::write("contributing/parsers/lua/GRAMMAR_ANALYSIS.md", &analysis) + .expect("Failed to write Lua grammar analysis"); + + // Also generate node_discovery.txt + let node_discovery = generate_lua_node_discovery(); + fs::write( + "contributing/parsers/lua/node_discovery.txt", + node_discovery, + ) + .expect("Failed to write Lua node discovery"); + + println!("📄 Lua Analysis:"); + println!(" - Example nodes: {}", example_nodes.len()); + println!(" - Handled nodes: {}", audit.implemented_nodes.len()); + println!(" - Symbol kinds: {:?}", audit.extracted_symbol_kinds); + println!( + " - Coverage: {:.1}%", + audit.implemented_nodes.len() as f32 / example_nodes.len() as f32 * 100.0 + ); + println!("✅ Lua node_discovery.txt saved"); + } + #[test] fn comprehensive_python_analysis() { println!("=== Python Comprehensive Grammar Analysis ===\n"); @@ -1768,6 +1875,121 @@ tree-sitter-gdscript/src/node-types.json to {grammar_path}." output } + fn generate_lua_node_discovery() -> String { + use super::abi15_exploration_common::print_node_tree; + use tree_sitter::{Language, Parser}; + + let mut output = String::new(); + output.push_str("=== Lua Language COMPREHENSIVE NODE MAPPING ===\n"); + output.push_str(&format!(" Generated: {}\n", get_formatted_timestamp())); + + let language: Language = tree_sitter_lua::LANGUAGE.into(); + output.push_str(&format!(" ABI Version: {}\n", language.abi_version())); + + let mut parser = Parser::new(); + parser.set_language(&language).unwrap(); + + let code = fs::read_to_string("examples/lua/comprehensive.lua") + .unwrap_or_else(|_| "-- Lua module\nlocal M = {}\nreturn M\n".to_string()); + + let tree = parser.parse(&code, None).unwrap(); + let root = tree.root_node(); + + if std::env::var("DEBUG_TREE").is_ok() { + println!("\n=== Lua Tree Structure ==="); + print_node_tree(root, &code, 0); + } + + let mut node_registry: HashMap = HashMap::new(); + let mut found_in_file = HashSet::new(); + discover_nodes_with_ids(root, &mut node_registry, &mut found_in_file); + + output.push_str(&format!(" Node kind count: {}\n\n", node_registry.len())); + + let node_categories = vec![ + ( + "FUNCTION NODES", + vec![ + "function_declaration", + "function_definition", + "function_call", + "parameters", + "return_statement", + ], + ), + ( + "VARIABLE NODES", + vec![ + "variable_declaration", + "assignment_statement", + "variable_list", + "expression_list", + "identifier", + ], + ), + ( + "TABLE NODES", + vec![ + "table_constructor", + "field", + "dot_index_expression", + "bracket_index_expression", + "method_index_expression", + ], + ), + ( + "CONTROL FLOW NODES", + vec![ + "if_statement", + "elseif_statement", + "else_statement", + "for_statement", + "for_in_statement", + "while_statement", + "repeat_statement", + "do_statement", + "block", + ], + ), + ( + "EXPRESSION NODES", + vec![ + "binary_expression", + "unary_expression", + "parenthesized_expression", + "string", + "number", + "true", + "false", + "nil", + ], + ), + ("COMMENT NODES", vec!["comment"]), + ]; + + for (category_name, expected_nodes) in &node_categories { + output.push_str(&format!("== {category_name} ==\n")); + for node_name in expected_nodes { + if let Some(&id) = node_registry.get(*node_name) { + let in_file = if found_in_file.contains(*node_name) { + "✓" + } else { + "○" + }; + output.push_str(&format!("{in_file} {node_name} (ID: {id})\n")); + } else { + output.push_str(&format!("✗ {node_name} (not found)\n")); + } + } + output.push('\n'); + } + + output.push_str( + "\nLegend: ✓ = found in file, ○ = in grammar but not in file, ✗ = not in grammar\n", + ); + output + } + fn generate_gdscript_node_discovery() -> String { use super::abi15_exploration_common::print_node_tree; use tree_sitter::{Language, Parser}; diff --git a/tests/fixtures/lua/basic.lua b/tests/fixtures/lua/basic.lua new file mode 100644 index 00000000..c4edf88f --- /dev/null +++ b/tests/fixtures/lua/basic.lua @@ -0,0 +1,47 @@ +-- Basic Lua constructs + +-- Global function +function greet(name) + return "Hello, " .. name +end + +-- Local function +local function helper(x) + return x * 2 +end + +-- Local variables +local counter = 0 +local name = "test" + +-- Global variable (module-level constant convention) +VERSION = "1.0.0" + +-- Screaming case constant +local MAX_RETRIES = 5 + +-- Table as data structure +local config = { + host = "localhost", + port = 8080, + debug = true +} + +-- Function with multiple parameters +function calculate(a, b, operation) + if operation == "add" then + return a + b + elseif operation == "sub" then + return a - b + else + return 0 + end +end + +-- Nested function +function outer() + local function inner() + return "inner" + end + return inner() +end diff --git a/tests/fixtures/lua/comments.lua b/tests/fixtures/lua/comments.lua new file mode 100644 index 00000000..ff71ef53 --- /dev/null +++ b/tests/fixtures/lua/comments.lua @@ -0,0 +1,78 @@ +-- Documentation comments in Lua + +--- Calculate the factorial of a number +--- @param n number The input number +--- @return number The factorial result +function factorial(n) + if n <= 1 then + return 1 + end + return n * factorial(n - 1) +end + +--- Check if a number is prime +--- @param n number The number to check +--- @return boolean True if prime, false otherwise +local function isPrime(n) + if n < 2 then + return false + end + for i = 2, math.sqrt(n) do + if n % i == 0 then + return false + end + end + return true +end + +--[[ + Multi-line block comment + This describes a complex data structure +]] +local ComplexData = { + values = {}, + metadata = {} +} + +--- Add a value to the data structure +--- @param value any The value to add +--- @param meta table Optional metadata +function ComplexData:add(value, meta) + table.insert(self.values, value) + if meta then + self.metadata[#self.values] = meta + end +end + +--- Get all values +--- @return table Array of values +function ComplexData:getValues() + return self.values +end + +-- Regular single-line comment (not doc comment) +local helper = function() end + +--- Module for string utilities +local StringUtils = {} + +--- Capitalize the first letter of a string +--- @param s string Input string +--- @return string Capitalized string +function StringUtils.capitalize(s) + return s:sub(1, 1):upper() .. s:sub(2) +end + +--- Reverse a string +--- @param s string Input string +--- @return string Reversed string +function StringUtils.reverse(s) + return s:reverse() +end + +return { + factorial = factorial, + isPrime = isPrime, + ComplexData = ComplexData, + StringUtils = StringUtils +} diff --git a/tests/fixtures/lua/methods.lua b/tests/fixtures/lua/methods.lua new file mode 100644 index 00000000..6610991d --- /dev/null +++ b/tests/fixtures/lua/methods.lua @@ -0,0 +1,108 @@ +-- Method definitions in Lua + +-- Object with colon-style methods +local Counter = {} +Counter.__index = Counter + +function Counter.new(initial) + local self = setmetatable({}, Counter) + self.value = initial or 0 + return self +end + +-- Colon syntax method (implicit self) +function Counter:increment() + self.value = self.value + 1 +end + +function Counter:decrement() + self.value = self.value - 1 +end + +function Counter:getValue() + return self.value +end + +function Counter:reset() + self.value = 0 +end + +-- Dot syntax method (explicit self parameter) +function Counter.add(self, amount) + self.value = self.value + amount +end + +-- Static method (no self) +function Counter.create() + return Counter.new(0) +end + +-- String buffer with method chaining +local StringBuilder = {} +StringBuilder.__index = StringBuilder + +function StringBuilder.new() + local self = setmetatable({}, StringBuilder) + self.parts = {} + return self +end + +function StringBuilder:append(str) + table.insert(self.parts, str) + return self -- Enable chaining +end + +function StringBuilder:appendLine(str) + table.insert(self.parts, str) + table.insert(self.parts, "\n") + return self +end + +function StringBuilder:toString() + return table.concat(self.parts) +end + +function StringBuilder:clear() + self.parts = {} + return self +end + +-- Table with metamethods +local Vector = {} +Vector.__index = Vector + +function Vector.new(x, y) + local self = setmetatable({}, Vector) + self.x = x or 0 + self.y = y or 0 + return self +end + +function Vector:length() + return math.sqrt(self.x * self.x + self.y * self.y) +end + +function Vector:normalize() + local len = self:length() + if len > 0 then + self.x = self.x / len + self.y = self.y / len + end + return self +end + +-- Metamethod for addition +function Vector.__add(a, b) + return Vector.new(a.x + b.x, a.y + b.y) +end + +-- Metamethod for string representation +function Vector.__tostring(v) + return string.format("Vector(%g, %g)", v.x, v.y) +end + +return { + Counter = Counter, + StringBuilder = StringBuilder, + Vector = Vector +} diff --git a/tests/fixtures/lua/modules.lua b/tests/fixtures/lua/modules.lua new file mode 100644 index 00000000..4d033fd4 --- /dev/null +++ b/tests/fixtures/lua/modules.lua @@ -0,0 +1,58 @@ +-- Lua module patterns + +local M = {} + +-- Module-level constant +M.VERSION = "2.0.0" +M.AUTHOR = "Test Author" + +-- Public function +function M.process(data) + return _privateHelper(data) +end + +-- Another public function +function M.validate(input) + if type(input) ~= "string" then + return false, "Expected string" + end + return true, nil +end + +-- Public utility +function M.formatOutput(result) + return string.format("Result: %s", tostring(result)) +end + +-- Private helper (underscore prefix convention) +local function _privateHelper(data) + return data +end + +-- Private validator +local function _validateInput(input) + return input ~= nil +end + +-- Nested module structure +M.utils = {} + +function M.utils.trim(s) + return s:match("^%s*(.-)%s*$") +end + +function M.utils.split(s, sep) + local result = {} + for match in (s .. sep):gmatch("(.-)" .. sep) do + table.insert(result, match) + end + return result +end + +-- Initialization function +function M.init(config) + M._config = config or {} + return M +end + +return M diff --git a/tests/fixtures/lua/oop.lua b/tests/fixtures/lua/oop.lua new file mode 100644 index 00000000..fdd0c052 --- /dev/null +++ b/tests/fixtures/lua/oop.lua @@ -0,0 +1,82 @@ +-- Object-Oriented Programming patterns in Lua + +-- Class definition using metatables +local Animal = {} +Animal.__index = Animal + +--- Create a new Animal instance +function Animal.new(name) + local self = setmetatable({}, Animal) + self.name = name + return self +end + +--- Get the animal's name +function Animal:getName() + return self.name +end + +--- Make the animal speak +function Animal:speak() + return "..." +end + +-- Inheritance: Dog extends Animal +local Dog = setmetatable({}, { __index = Animal }) +Dog.__index = Dog + +function Dog.new(name, breed) + local self = setmetatable(Animal.new(name), Dog) + self.breed = breed + return self +end + +function Dog:speak() + return "Woof!" +end + +function Dog:getBreed() + return self.breed +end + +-- Another subclass: Cat +local Cat = setmetatable({}, { __index = Animal }) +Cat.__index = Cat + +function Cat.new(name, indoor) + local self = setmetatable(Animal.new(name), Cat) + self.indoor = indoor + return self +end + +function Cat:speak() + return "Meow!" +end + +function Cat:isIndoor() + return self.indoor +end + +-- Singleton pattern +local Logger = { + _instance = nil +} + +function Logger:getInstance() + if not self._instance then + self._instance = { + level = "INFO", + log = function(self, msg) + print("[" .. self.level .. "] " .. msg) + end + } + end + return self._instance +end + +return { + Animal = Animal, + Dog = Dog, + Cat = Cat, + Logger = Logger +} From 1701d1c4d9c03206debacadba506bee7755f21da Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sun, 25 Jan 2026 09:45:11 -0600 Subject: [PATCH 2/5] fix: compact findings from copilot and local testing --- Cargo.lock | 4 +- Cargo.toml | 2 +- examples/lua/comprehensive.lua | 9 +-- examples/lua/utils/helper.lua | 27 +++++++- src/io/parse.rs | 2 +- src/parsing/factory.rs | 17 +++-- src/parsing/language.rs | 9 ++- src/parsing/lua/audit.rs | 16 +++-- src/parsing/lua/behavior.rs | 5 +- src/parsing/lua/definition.rs | 2 +- src/parsing/lua/parser.rs | 81 ++++++++++++++---------- src/parsing/lua/resolution.rs | 34 +++++----- src/parsing/registry.rs | 18 ++++-- tests/exploration/abi15_grammar_audit.rs | 77 ++++++++++++++++++---- tests/fixtures/lua/modules.lua | 29 +++++---- 15 files changed, 229 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7aec2989..1497d2bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5230,9 +5230,9 @@ checksum = "4ae62f7eae5eb549c71b76658648b72cc6111f2d87d24a1e31fa907f4943e3ce" [[package]] name = "tree-sitter-lua" -version = "0.2.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb9adf0965fec58e7660cbb3a059dbb12ebeec9459e6dcbae3db004739641e" +checksum = "ea992f4164d83f371ef1239ae178c4d4596c296c09055e9a48bb02a2760403af" dependencies = [ "cc", "tree-sitter-language", diff --git a/Cargo.toml b/Cargo.toml index 8844f0ad..02a5b5ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,7 +94,7 @@ git2 = { version = "0.20.3", features = ["vendored-openssl"] } tempfile = "3.24.0" serde_json5 = "0.2.1" tree-sitter-swift = "0.7.1" -tree-sitter-lua = "0.2.0" +tree-sitter-lua = "0.4.1" glob = "0.3.3" async-trait = "0.1.89" sysinfo = "0.37.2" diff --git a/examples/lua/comprehensive.lua b/examples/lua/comprehensive.lua index d21da683..5c09762b 100644 --- a/examples/lua/comprehensive.lua +++ b/examples/lua/comprehensive.lua @@ -182,9 +182,9 @@ end function M.withLogging(f) return function(...) print("Calling function with args:", ...) - local result = f(...) - print("Function returned:", result) - return result + local results = table.pack(f(...)) + print("Function returned:", table.unpack(results, 1, results.n)) + return table.unpack(results, 1, results.n) end end @@ -329,8 +329,9 @@ function M.getLazyValue() end --- Error handling pattern +--- Wraps a function call in pcall and returns an Ok/Err table --- @param fn function The function to call safely ---- @return boolean, any +--- @return table # Ok(result) table on success, or Err(error) table on failure function M.pcallWrapper(fn, ...) local ok, result = pcall(fn, ...) if ok then diff --git a/examples/lua/utils/helper.lua b/examples/lua/utils/helper.lua index 18c81112..edf36626 100644 --- a/examples/lua/utils/helper.lua +++ b/examples/lua/utils/helper.lua @@ -21,15 +21,27 @@ end --- Deep copy a table --- @param original table The table to copy +--- @param seen table|nil Optional memoization table to handle cycles --- @return table -function M.deepCopy(original) +function M.deepCopy(original, seen) if type(original) ~= "table" then return original end + -- Initialize seen table for cycle detection + seen = seen or {} + + -- Return memoized copy if we've already seen this table + if seen[original] then + return seen[original] + end + local copy = {} + -- Memoize before recursing to handle self-references + seen[original] = copy + for key, value in pairs(original) do - copy[M.deepCopy(key)] = M.deepCopy(value) + copy[M.deepCopy(key, seen)] = M.deepCopy(value, seen) end return setmetatable(copy, getmetatable(original)) end @@ -62,8 +74,17 @@ end --- @param delimiter string The delimiter --- @return table function M.split(s, delimiter) + -- Guard against nil or empty delimiter + if not delimiter or delimiter == "" then + return {s} + end + + -- Escape pattern-magic characters in delimiter + -- Escapes: . * + ? ^ $ ( ) [ ] % - + local escaped_delimiter = delimiter:gsub("([%.%*%+%?%^%$%(%)%[%]%%%-])", "%%%1") + local result = {} - for match in (s .. delimiter):gmatch("(.-)" .. delimiter) do + for match in (s .. delimiter):gmatch("(.-)" .. escaped_delimiter) do table.insert(result, match) end return result diff --git a/src/io/parse.rs b/src/io/parse.rs index 6d7978fa..534b9bce 100644 --- a/src/io/parse.rs +++ b/src/io/parse.rs @@ -15,7 +15,7 @@ pub enum ParseError { FileNotFound { path: String }, #[error( - "Unable to detect language from file extension: {extension}\nSuggestion: Use a supported file extension (rs, py, ts, tsx, js, jsx, php, go, c, cpp)" + "Unable to detect language from file extension: {extension}\nSuggestion: Use a supported file extension (rs, py, ts, tsx, js, jsx, php, go, c, cpp, cs, gd, java, kt, lua, swift)" )] UnsupportedLanguage { extension: String }, diff --git a/src/parsing/factory.rs b/src/parsing/factory.rs index 0a1be88f..67914015 100644 --- a/src/parsing/factory.rs +++ b/src/parsing/factory.rs @@ -350,15 +350,20 @@ impl ParserFactory { /// Filters all supported languages against settings.languages map. pub fn enabled_languages(&self) -> Vec { vec![ - Language::Rust, - Language::Python, - Language::JavaScript, - Language::TypeScript, - Language::Php, - Language::Go, Language::C, Language::Cpp, + Language::CSharp, Language::Gdscript, + Language::Go, + Language::Java, + Language::JavaScript, + Language::Kotlin, + Language::Lua, + Language::Php, + Language::Python, + Language::Rust, + Language::Swift, + Language::TypeScript, ] .into_iter() .filter(|&lang| self.is_language_enabled(lang)) diff --git a/src/parsing/language.rs b/src/parsing/language.rs index e407aac5..dee45563 100644 --- a/src/parsing/language.rs +++ b/src/parsing/language.rs @@ -209,8 +209,10 @@ mod tests { assert_eq!(Language::from_extension("go"), Some(Language::Go)); assert_eq!(Language::from_extension("go.mod"), Some(Language::Go)); assert_eq!(Language::from_extension("go.sum"), Some(Language::Go)); - assert_eq!(Language::from_extension("txt"), None); assert_eq!(Language::from_extension("gd"), Some(Language::Gdscript)); + assert_eq!(Language::from_extension("lua"), Some(Language::Lua)); + assert_eq!(Language::from_extension("LUA"), Some(Language::Lua)); + assert_eq!(Language::from_extension("txt"), None); } #[test] @@ -264,6 +266,10 @@ mod tests { Language::from_path(Path::new("player.gd")), Some(Language::Gdscript) ); + assert_eq!( + Language::from_path(Path::new("script.lua")), + Some(Language::Lua) + ); assert_eq!(Language::from_path(Path::new("README.md")), None); } @@ -280,5 +286,6 @@ mod tests { assert!(Language::Go.extensions().contains(&"go.mod")); assert!(Language::Go.extensions().contains(&"go.sum")); assert!(Language::Gdscript.extensions().contains(&"gd")); + assert!(Language::Lua.extensions().contains(&"lua")); } } diff --git a/src/parsing/lua/audit.rs b/src/parsing/lua/audit.rs index 23f1da7c..3939bdf7 100644 --- a/src/parsing/lua/audit.rs +++ b/src/parsing/lua/audit.rs @@ -182,11 +182,17 @@ impl LuaParserAudit { } fn discover_nodes(node: Node, registry: &mut HashMap) { - registry.insert(node.kind().to_string(), node.kind_id()); - - let mut cursor = node.walk(); - for child in node.children(&mut cursor) { - discover_nodes(child, registry); + // Use iterative traversal with an explicit stack to avoid stack overflow on large ASTs + let mut stack = vec![node]; + + while let Some(current_node) = stack.pop() { + registry.insert(current_node.kind().to_string(), current_node.kind_id()); + + let mut cursor = current_node.walk(); + // Push children onto the stack for processing + for child in current_node.children(&mut cursor) { + stack.push(child); + } } } diff --git a/src/parsing/lua/behavior.rs b/src/parsing/lua/behavior.rs index 56a02b54..3490af21 100644 --- a/src/parsing/lua/behavior.rs +++ b/src/parsing/lua/behavior.rs @@ -72,8 +72,7 @@ impl LanguageBehavior for LuaBehavior { let relative_path = file_path .strip_prefix(project_root) .ok() - .or_else(|| file_path.strip_prefix("./").ok()) - .unwrap_or(file_path); + .or_else(|| file_path.strip_prefix("./").ok())?; let path = relative_path.to_str()?; let path_clean = path.trim_start_matches("./"); @@ -201,7 +200,7 @@ impl LanguageBehavior for LuaBehavior { } } } else { - matches!(symbol.kind, SymbolKind::Variable) + false } } diff --git a/src/parsing/lua/definition.rs b/src/parsing/lua/definition.rs index 0ae7f906..ee3446c4 100644 --- a/src/parsing/lua/definition.rs +++ b/src/parsing/lua/definition.rs @@ -72,7 +72,7 @@ impl LanguageDefinition for LuaLanguage { fn is_enabled(&self, settings: &Settings) -> bool { settings .languages - .get("Lua") + .get(self.id().as_str()) .map(|config| config.enabled) .unwrap_or(self.default_enabled()) } diff --git a/src/parsing/lua/parser.rs b/src/parsing/lua/parser.rs index abfa42de..9e086927 100644 --- a/src/parsing/lua/parser.rs +++ b/src/parsing/lua/parser.rs @@ -23,9 +23,9 @@ fn range_from_node(node: &Node) -> Range { let start = node.start_position(); let end = node.end_position(); Range::new( - start.row as u32 + 1, + start.row as u32, start.column as u16, - end.row as u32 + 1, + end.row as u32, end.column as u16, ) } @@ -329,7 +329,7 @@ impl LuaParser { let (name, kind, visibility) = if name_text.contains(':') { let parts: Vec<&str> = name_text.split(':').collect(); let method_name = parts.last().unwrap_or(&name_text).to_string(); - let vis = if is_local { + let vis = if is_local || method_name.starts_with('_') { Visibility::Private } else { Visibility::Public @@ -385,40 +385,58 @@ impl LuaParser { ) { for child in node.children(&mut node.walk()) { if child.kind() == "assignment_statement" { + let mut var_names = Vec::new(); + let mut expr_kinds = Vec::new(); + for assign_child in child.children(&mut child.walk()) { if assign_child.kind() == "variable_list" { for var_child in assign_child.children(&mut assign_child.walk()) { if var_child.kind() == "identifier" { - let name = code[var_child.byte_range()].to_string(); - let range = range_from_node(&var_child); - - let kind = if name.chars().all(|c| c.is_uppercase() || c == '_') - && name.contains('_') - { - SymbolKind::Constant - } else { - SymbolKind::Variable - }; - - let signature = format!("local {name}"); - let doc_comment = self.extract_lua_doc_comment(&node, code); - - let symbol = self.create_symbol( - counter.next_id(), - name, - kind, - file_id, - range, - Some(signature), - doc_comment, - module_path, - Visibility::Private, - ); - symbols.push(symbol); + var_names.push(var_child); } } + } else if assign_child.kind() == "expression_list" { + for expr_child in assign_child.children(&mut assign_child.walk()) { + expr_kinds.push(expr_child.kind() == "function_definition"); + } } } + + for (i, var_node) in var_names.iter().enumerate() { + let name = code[var_node.byte_range()].to_string(); + let range = range_from_node(var_node); + let is_function = expr_kinds.get(i).copied().unwrap_or(false); + + let kind = if is_function { + SymbolKind::Function + } else if name.chars().all(|c| c.is_uppercase() || c == '_') + && name.contains('_') + { + SymbolKind::Constant + } else { + SymbolKind::Variable + }; + + let signature = if is_function { + format!("local function {name}") + } else { + format!("local {name}") + }; + let doc_comment = self.extract_lua_doc_comment(&node, code); + + let symbol = self.create_symbol( + counter.next_id(), + name, + kind, + file_id, + range, + Some(signature), + doc_comment, + module_path, + Visibility::Private, + ); + symbols.push(symbol); + } } } } @@ -678,10 +696,6 @@ impl LuaParser { let content = comment_text.trim_start_matches("---").trim(); doc_lines.insert(0, content.to_string()); current = sibling.prev_sibling(); - } else if comment_text.starts_with("--") && !comment_text.starts_with("--[[") { - let content = comment_text.trim_start_matches("--").trim(); - doc_lines.insert(0, content.to_string()); - current = sibling.prev_sibling(); } else if comment_text.starts_with("--[[") { let content = comment_text .trim_start_matches("--[[") @@ -690,6 +704,7 @@ impl LuaParser { doc_lines.insert(0, content.to_string()); break; } else { + // Stop on first non-doc comment (regular -- comments) break; } } else { diff --git a/src/parsing/lua/resolution.rs b/src/parsing/lua/resolution.rs index 2bdd2c98..0f05c86c 100644 --- a/src/parsing/lua/resolution.rs +++ b/src/parsing/lua/resolution.rs @@ -13,7 +13,6 @@ pub struct LuaResolutionContext { imports: HashMap, global_symbols: HashMap, module_symbols: HashMap, - local_symbols: HashMap, } #[derive(Debug)] @@ -33,7 +32,6 @@ impl Default for LuaResolutionContext { imports: HashMap::new(), global_symbols: HashMap::new(), module_symbols: HashMap::new(), - local_symbols: HashMap::new(), } } } @@ -48,7 +46,6 @@ impl LuaResolutionContext { imports: HashMap::new(), global_symbols: HashMap::new(), module_symbols: HashMap::new(), - local_symbols: HashMap::new(), } } @@ -86,7 +83,6 @@ impl ResolutionScope for LuaResolutionContext { self.module_symbols.insert(name, symbol_id); } ScopeLevel::Local => { - self.local_symbols.insert(name.clone(), symbol_id); if let Some(current_scope) = self.scope_stack.last_mut() { current_scope.symbols.insert(name, symbol_id); } @@ -117,7 +113,6 @@ impl ResolutionScope for LuaResolutionContext { } fn clear_local_scope(&mut self) { - self.local_symbols.clear(); if let Some(scope) = self.scope_stack.last_mut() { scope.symbols.clear(); } @@ -139,8 +134,10 @@ impl ResolutionScope for LuaResolutionContext { fn symbols_in_scope(&self) -> Vec<(String, SymbolId, ScopeLevel)> { let mut result = Vec::new(); - for (name, id) in &self.local_symbols { - result.push((name.clone(), *id, ScopeLevel::Local)); + for scope in &self.scope_stack { + for (name, id) in &scope.symbols { + result.push((name.clone(), *id, ScopeLevel::Local)); + } } for (name, id) in &self.module_symbols { @@ -184,16 +181,23 @@ impl InheritanceResolver for LuaInheritanceResolver { } fn resolve_method(&self, type_name: &str, method: &str) -> Option { - if let Some(methods) = self.type_methods.get(type_name) { - if methods.contains(&method.to_string()) { - return Some(type_name.to_string()); + let mut to_visit = vec![type_name.to_string()]; + let mut visited = std::collections::HashSet::new(); + + while let Some(current) = to_visit.pop() { + if !visited.insert(current.clone()) { + continue; } - } - if let Some(parents) = self.inheritance.get(type_name) { - for (parent, _kind) in parents { - if let Some(result) = self.resolve_method(parent, method) { - return Some(result); + if let Some(methods) = self.type_methods.get(¤t) { + if methods.iter().any(|m| m == method) { + return Some(current); + } + } + + if let Some(parents) = self.inheritance.get(¤t) { + for (parent, _kind) in parents { + to_visit.push(parent.clone()); } } } diff --git a/src/parsing/registry.rs b/src/parsing/registry.rs index cceae1a7..81093e60 100644 --- a/src/parsing/registry.rs +++ b/src/parsing/registry.rs @@ -75,14 +75,20 @@ impl<'de> Deserialize<'de> for LanguageId { // Convert to a static string by matching known languages // This is necessary because LanguageId requires &'static str let static_str = match s.as_str() { - "rust" => "rust", - "python" => "python", - "javascript" => "javascript", - "typescript" => "typescript", - "php" => "php", - "go" => "go", + "c" => "c", + "cpp" => "cpp", "csharp" => "csharp", + "gdscript" => "gdscript", + "go" => "go", + "java" => "java", + "javascript" => "javascript", "kotlin" => "kotlin", + "lua" => "lua", + "php" => "php", + "python" => "python", + "rust" => "rust", + "swift" => "swift", + "typescript" => "typescript", // For unknown languages, we leak the string to get 'static lifetime // This is safe because language identifiers are typically created once // at startup and live for the entire program diff --git a/tests/exploration/abi15_grammar_audit.rs b/tests/exploration/abi15_grammar_audit.rs index 8e23928c..703c2e71 100644 --- a/tests/exploration/abi15_grammar_audit.rs +++ b/tests/exploration/abi15_grammar_audit.rs @@ -325,7 +325,44 @@ mod tests { fn comprehensive_lua_analysis() { println!("=== Lua Comprehensive Grammar Analysis ===\n"); - // Run the parser audit to get everything at once + fs::create_dir_all("contributing/parsers/lua") + .expect("Failed to create Lua parser output directory"); + + let grammar_path = "contributing/parsers/lua/node-types.json"; + let mut all_grammar_nodes = HashSet::new(); + let mut grammar_warning = None; + + match fs::read_to_string(grammar_path) { + Ok(json) => match serde_json::from_str::(&json) { + Ok(Value::Array(nodes)) => { + for node in nodes { + if let (Some(Value::Bool(true)), Some(Value::String(node_type))) = + (node.get("named"), node.get("type")) + { + all_grammar_nodes.insert(node_type.clone()); + } + } + } + Ok(_) => { + grammar_warning = + Some("Unexpected grammar JSON structure for Lua.".to_string()); + } + Err(err) => { + grammar_warning = Some(format!( + "Failed to parse Lua grammar JSON: {err}. \ +Run `tree-sitter generate` and copy node-types.json to {grammar_path}." + )); + } + }, + Err(err) => { + grammar_warning = Some(format!( + "Missing node-types.json for Lua ({err}). \ +Run `./contributing/tree-sitter/scripts/setup.sh lua` and copy \ +tree-sitter-lua/src/node-types.json to {grammar_path}." + )); + } + } + let audit = match LuaParserAudit::audit_file("examples/lua/comprehensive.lua") { Ok(audit) => audit, Err(e) => { @@ -340,16 +377,18 @@ mod tests { let example_nodes: HashSet = audit.grammar_nodes.keys().cloned().collect(); - // Save the audit report let report = audit.generate_report(); fs::write("contributing/parsers/lua/AUDIT_REPORT.md", &report) .expect("Failed to write Lua audit report"); - // Generate comprehensive analysis let mut analysis = String::new(); analysis.push_str("# Lua Grammar Analysis\n\n"); analysis.push_str(&format!("*Generated: {}*\n\n", get_formatted_timestamp())); analysis.push_str("## Statistics\n"); + analysis.push_str(&format!( + "- Total nodes in grammar JSON: {}\n", + all_grammar_nodes.len() + )); analysis.push_str(&format!( "- Nodes found in comprehensive.lua: {}\n", example_nodes.len() @@ -364,7 +403,13 @@ mod tests { )); analysis.push('\n'); - // Categorize nodes + if let Some(warning) = &grammar_warning { + analysis.push_str("## Warning\n"); + analysis.push_str(warning); + analysis.push_str("\n\n"); + } + + let mut in_grammar_only: Vec<_> = all_grammar_nodes.difference(&example_nodes).collect(); let mut in_example_not_handled: Vec<_> = example_nodes .iter() .filter(|n| !audit.implemented_nodes.contains(n.as_str())) @@ -375,12 +420,12 @@ mod tests { .filter(|n| example_nodes.contains(n.as_str())) .collect(); + in_grammar_only.sort(); in_example_not_handled.sort(); handled_well.sort(); if !handled_well.is_empty() { analysis.push_str("## ✅ Successfully Handled Nodes\n"); - analysis.push_str("These nodes are in examples and handled by parser:\n"); for node in &handled_well { analysis.push_str(&format!("- {node}\n")); } @@ -389,15 +434,22 @@ mod tests { if !in_example_not_handled.is_empty() { analysis.push_str("## ⚠️ Implementation Gaps\n"); - analysis.push_str("These nodes appear in comprehensive.lua but aren't handled:\n"); for node in &in_example_not_handled { analysis.push_str(&format!("- {node}\n")); } analysis.push('\n'); } + if !in_grammar_only.is_empty() { + analysis.push_str("## ⭕ Missing from Examples\n"); + for node in &in_grammar_only { + analysis.push_str(&format!("- {node}\n")); + } + analysis.push('\n'); + } + if !audit.extracted_symbol_kinds.is_empty() { - analysis.push_str("## 🎯 Symbol Kinds Extracted\n"); + analysis.push_str("## 🔍 Symbol Kinds Extracted\n"); let mut kinds: Vec<_> = audit.extracted_symbol_kinds.iter().collect(); kinds.sort(); for kind in kinds { @@ -406,10 +458,12 @@ mod tests { analysis.push('\n'); } - fs::write("contributing/parsers/lua/GRAMMAR_ANALYSIS.md", &analysis) - .expect("Failed to write Lua grammar analysis"); + fs::write( + "contributing/parsers/lua/GRAMMAR_ANALYSIS.md", + &analysis, + ) + .expect("Failed to write Lua grammar analysis"); - // Also generate node_discovery.txt let node_discovery = generate_lua_node_discovery(); fs::write( "contributing/parsers/lua/node_discovery.txt", @@ -417,7 +471,8 @@ mod tests { ) .expect("Failed to write Lua node discovery"); - println!("📄 Lua Analysis:"); + println!("✅ Lua Analysis:"); + println!(" - Grammar nodes: {}", all_grammar_nodes.len()); println!(" - Example nodes: {}", example_nodes.len()); println!(" - Handled nodes: {}", audit.implemented_nodes.len()); println!(" - Symbol kinds: {:?}", audit.extracted_symbol_kinds); diff --git a/tests/fixtures/lua/modules.lua b/tests/fixtures/lua/modules.lua index 4d033fd4..2a2cc91e 100644 --- a/tests/fixtures/lua/modules.lua +++ b/tests/fixtures/lua/modules.lua @@ -6,6 +6,11 @@ local M = {} M.VERSION = "2.0.0" M.AUTHOR = "Test Author" +-- Private helper (underscore prefix convention) +local function _privateHelper(data) + return data +end + -- Public function function M.process(data) return _privateHelper(data) @@ -13,6 +18,9 @@ end -- Another public function function M.validate(input) + if input == nil then + return false, "Input cannot be nil" + end if type(input) ~= "string" then return false, "Expected string" end @@ -24,16 +32,6 @@ function M.formatOutput(result) return string.format("Result: %s", tostring(result)) end --- Private helper (underscore prefix convention) -local function _privateHelper(data) - return data -end - --- Private validator -local function _validateInput(input) - return input ~= nil -end - -- Nested module structure M.utils = {} @@ -42,8 +40,17 @@ function M.utils.trim(s) end function M.utils.split(s, sep) + if sep == "" then + local result = {} + for i = 1, #s do + result[i] = s:sub(i, i) + end + return result + end + + local escaped_sep = sep:gsub("([%.%+%-%*%?%[%]%^%$%(%)%%])", "%%%1") local result = {} - for match in (s .. sep):gmatch("(.-)" .. sep) do + for match in (s .. sep):gmatch("(.-)" .. escaped_sep) do table.insert(result, match) end return result From 3198de1a83c5991a306e59a199a37a825243c236 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sun, 25 Jan 2026 13:57:04 -0600 Subject: [PATCH 3/5] feat add missing find_calls logic --- src/parsing/lua/parser.rs | 229 ++++++++++++- tests/parsers/lua/mod.rs | 2 + tests/parsers/lua/test_call_tracking.rs | 413 ++++++++++++++++++++++++ tests/parsers/lua/test_relationships.rs | 259 +++++++++++++++ tests/parsers_tests.rs | 6 + 5 files changed, 907 insertions(+), 2 deletions(-) create mode 100644 tests/parsers/lua/mod.rs create mode 100644 tests/parsers/lua/test_call_tracking.rs create mode 100644 tests/parsers/lua/test_relationships.rs diff --git a/src/parsing/lua/parser.rs b/src/parsing/lua/parser.rs index 9e086927..cdefde37 100644 --- a/src/parsing/lua/parser.rs +++ b/src/parsing/lua/parser.rs @@ -875,6 +875,118 @@ impl LuaParser { self.register_node_recursively(child); } } + + fn find_calls_in_node<'a>( + &mut self, + node: Node, + code: &'a str, + calls: &mut Vec<(&'a str, &'a str, Range)>, + current_function: &mut Option<&'a str>, + ) { + match node.kind() { + "function_declaration" => { + self.register_handled_node(node.kind(), node.kind_id()); + self.process_function_for_calls(node, code, calls, current_function); + } + "function_call" => { + self.register_handled_node(node.kind(), node.kind_id()); + self.process_call(node, code, calls, current_function); + } + "function_definition" => { + // Anonymous function - process body with current context + self.register_handled_node(node.kind(), node.kind_id()); + self.process_children_for_calls(node, code, calls, current_function); + } + _ => { + self.process_children_for_calls(node, code, calls, current_function); + } + } + } + + fn process_function_for_calls<'a>( + &mut self, + node: Node, + code: &'a str, + calls: &mut Vec<(&'a str, &'a str, Range)>, + current_function: &mut Option<&'a str>, + ) { + if let Some(name_node) = node.child_by_field_name("name") { + let name_text = &code[name_node.byte_range()]; + + // Extract simple function name (strip Table: or Table. prefix) + let simple_name = if let Some(colon_pos) = name_text.rfind(':') { + &name_text[colon_pos + 1..] + } else if let Some(dot_pos) = name_text.rfind('.') { + &name_text[dot_pos + 1..] + } else { + name_text + }; + + let old_function = *current_function; + *current_function = Some(simple_name); + + self.process_children_for_calls(node, code, calls, current_function); + + *current_function = old_function; + } else { + // No name (shouldn't happen), process children without context change + self.process_children_for_calls(node, code, calls, current_function); + } + } + + fn process_call<'a>( + &mut self, + node: Node, + code: &'a str, + calls: &mut Vec<(&'a str, &'a str, Range)>, + current_function: &mut Option<&'a str>, + ) { + if let Some(name_node) = node.child_by_field_name("name") { + let callee = match name_node.kind() { + "identifier" => { + // Direct call: func() + &code[name_node.byte_range()] + } + "method_index_expression" => { + // Method call: obj:method() + if let Some(method_node) = name_node.child_by_field_name("method") { + &code[method_node.byte_range()] + } else { + return; // Can't extract method name + } + } + "dot_index_expression" => { + // Dot call: table.insert() or math.sqrt() + if let Some(field_node) = name_node.child_by_field_name("field") { + &code[field_node.byte_range()] + } else { + // Fallback to full expression + &code[name_node.byte_range()] + } + } + _ => return, // Unknown call pattern + }; + + let range = range_from_node(&node); + let caller = (*current_function).unwrap_or(""); + calls.push((caller, callee, range)); + } + + // Process children to catch nested calls in arguments + self.process_children_for_calls(node, code, calls, current_function); + } + + fn process_children_for_calls<'a>( + &mut self, + node: Node, + code: &'a str, + calls: &mut Vec<(&'a str, &'a str, Range)>, + current_function: &mut Option<&'a str>, + ) { + for child in node.children(&mut node.walk()) { + self.find_calls_in_node(child, code, calls, current_function); + } + } } impl LanguageParser for LuaParser { @@ -895,8 +1007,18 @@ impl LanguageParser for LuaParser { self.extract_lua_doc_comment(node, code) } - fn find_calls<'a>(&mut self, _code: &'a str) -> Vec<(&'a str, &'a str, Range)> { - Vec::new() + fn find_calls<'a>(&mut self, code: &'a str) -> Vec<(&'a str, &'a str, Range)> { + let tree = match self.parser.parse(code, None) { + Some(tree) => tree, + None => return Vec::new(), + }; + + let mut calls = Vec::new(); + let root_node = tree.root_node(); + let mut current_function: Option<&'a str> = None; + + self.find_calls_in_node(root_node, code, &mut calls, &mut current_function); + calls } /// Extract method calls from Lua source code @@ -1088,4 +1210,107 @@ require("some.module") assert_eq!(imports[0].path, "some.module"); assert_eq!(imports[0].alias, None); } + + #[test] + fn test_find_calls_basic() { + use crate::parsing::LanguageParser; + + let mut parser = LuaParser::new().unwrap(); + let code = r#" +function foo() + bar() + baz() +end + +function test() + foo() +end +"#; + + let calls = parser.find_calls(code); + + assert_eq!(calls.len(), 3); + + // Check that foo calls bar and baz + let foo_calls: Vec<_> = calls.iter().filter(|(c, _, _)| *c == "foo").collect(); + assert_eq!(foo_calls.len(), 2); + assert!(foo_calls.iter().any(|(_, callee, _)| *callee == "bar")); + assert!(foo_calls.iter().any(|(_, callee, _)| *callee == "baz")); + + // Check that test calls foo + let test_calls: Vec<_> = calls.iter().filter(|(c, _, _)| *c == "test").collect(); + assert_eq!(test_calls.len(), 1); + assert_eq!(test_calls[0].1, "foo"); + } + + #[test] + fn test_find_calls_method_syntax() { + use crate::parsing::LanguageParser; + + let mut parser = LuaParser::new().unwrap(); + let code = r#" +function MyClass:new() + self:init() + return self +end + +function MyClass:init() + self.value = 0 +end +"#; + + let calls = parser.find_calls(code); + + // new should call init + let new_calls: Vec<_> = calls.iter().filter(|(c, _, _)| *c == "new").collect(); + assert_eq!(new_calls.len(), 1); + assert_eq!(new_calls[0].1, "init"); + } + + #[test] + fn test_find_calls_module_level() { + use crate::parsing::LanguageParser; + + let mut parser = LuaParser::new().unwrap(); + let code = r#" +local config = require("config") +print("Starting...") + +function main() + print("In main") +end +"#; + + let calls = parser.find_calls(code); + + // Module-level calls should use "" as caller + let module_calls: Vec<_> = calls.iter().filter(|(c, _, _)| *c == "").collect(); + assert_eq!(module_calls.len(), 2); + assert!(module_calls.iter().any(|(_, callee, _)| *callee == "require")); + assert!(module_calls.iter().any(|(_, callee, _)| *callee == "print")); + + // main should call print + let main_calls: Vec<_> = calls.iter().filter(|(c, _, _)| *c == "main").collect(); + assert_eq!(main_calls.len(), 1); + assert_eq!(main_calls[0].1, "print"); + } + + #[test] + fn test_find_calls_dot_notation() { + use crate::parsing::LanguageParser; + + let mut parser = LuaParser::new().unwrap(); + let code = r#" +function process() + table.insert(items, 1) + math.sqrt(25) +end +"#; + + let calls = parser.find_calls(code); + + assert_eq!(calls.len(), 2); + assert!(calls.iter().any(|(c, callee, _)| *c == "process" && *callee == "insert")); + assert!(calls.iter().any(|(c, callee, _)| *c == "process" && *callee == "sqrt")); + } } diff --git a/tests/parsers/lua/mod.rs b/tests/parsers/lua/mod.rs new file mode 100644 index 00000000..73956e56 --- /dev/null +++ b/tests/parsers/lua/mod.rs @@ -0,0 +1,2 @@ +mod test_call_tracking; +mod test_relationships; diff --git a/tests/parsers/lua/test_call_tracking.rs b/tests/parsers/lua/test_call_tracking.rs new file mode 100644 index 00000000..77f5135f --- /dev/null +++ b/tests/parsers/lua/test_call_tracking.rs @@ -0,0 +1,413 @@ +use codanna::parsing::LanguageParser; +use codanna::parsing::lua::LuaParser; + +#[test] +fn test_lua_basic_function_calls() { + let code = r#" +function foo() + bar() + baz() +end + +function main() + foo() + print("hello") +end +"#; + + let mut parser = LuaParser::new().expect("Failed to create parser"); + let calls = parser.find_calls(code); + + println!("Found {} calls:", calls.len()); + for (caller, callee, range) in &calls { + println!(" {} -> {} at line {}", caller, callee, range.start_line); + } + + // Verify specific calls + let call_pairs: Vec<(String, String)> = calls + .iter() + .map(|(caller, callee, _)| (caller.to_string(), callee.to_string())) + .collect(); + + assert!( + call_pairs.contains(&("foo".to_string(), "bar".to_string())), + "foo should call bar" + ); + assert!( + call_pairs.contains(&("foo".to_string(), "baz".to_string())), + "foo should call baz" + ); + assert!( + call_pairs.contains(&("main".to_string(), "foo".to_string())), + "main should call foo" + ); + assert!( + call_pairs.contains(&("main".to_string(), "print".to_string())), + "main should call print" + ); +} + +#[test] +fn test_lua_method_calls_colon_syntax() { + let code = r#" +function MyClass:new() + self:init() + self:setup() + return self +end + +function MyClass:init() + self.value = 0 +end + +function MyClass:setup() + self:reset() +end + +function MyClass:reset() + self.value = 0 +end +"#; + + let mut parser = LuaParser::new().expect("Failed to create parser"); + let calls = parser.find_calls(code); + + let call_pairs: Vec<(String, String)> = calls + .iter() + .map(|(caller, callee, _)| (caller.to_string(), callee.to_string())) + .collect(); + + assert!( + call_pairs.contains(&("new".to_string(), "init".to_string())), + "new should call init" + ); + assert!( + call_pairs.contains(&("new".to_string(), "setup".to_string())), + "new should call setup" + ); + assert!( + call_pairs.contains(&("setup".to_string(), "reset".to_string())), + "setup should call reset" + ); +} + +#[test] +fn test_lua_module_level_calls() { + let code = r#" +-- Module-level calls +local json = require("json") +local config = loadConfig() +print("Module loaded") + +function initialize() + print("Initializing...") + setupDatabase() +end +"#; + + let mut parser = LuaParser::new().expect("Failed to create parser"); + let calls = parser.find_calls(code); + + println!("Module-level calls:"); + for (caller, callee, _) in &calls { + if caller == &"" { + println!(" -> {}", callee); + } + } + + let call_pairs: Vec<(String, String)> = calls + .iter() + .map(|(caller, callee, _)| (caller.to_string(), callee.to_string())) + .collect(); + + // Module-level calls + assert!( + call_pairs.contains(&("".to_string(), "require".to_string())), + "Module should call require" + ); + assert!( + call_pairs.contains(&("".to_string(), "loadConfig".to_string())), + "Module should call loadConfig" + ); + assert!( + call_pairs.contains(&("".to_string(), "print".to_string())), + "Module should call print at module level" + ); + + // Function-level calls + assert!( + call_pairs.contains(&("initialize".to_string(), "print".to_string())), + "initialize should call print" + ); + assert!( + call_pairs.contains(&("initialize".to_string(), "setupDatabase".to_string())), + "initialize should call setupDatabase" + ); +} + +#[test] +fn test_lua_dot_notation_calls() { + let code = r#" +function process() + table.insert(items, 1) + table.remove(items, 1) + math.sqrt(25) + math.abs(-10) + string.format("Hello %s", name) +end +"#; + + let mut parser = LuaParser::new().expect("Failed to create parser"); + let calls = parser.find_calls(code); + + let call_pairs: Vec<(String, String)> = calls + .iter() + .map(|(caller, callee, _)| (caller.to_string(), callee.to_string())) + .collect(); + + // Verify we extract just the function name, not the table prefix + assert!( + call_pairs.contains(&("process".to_string(), "insert".to_string())), + "Should extract 'insert' from table.insert" + ); + assert!( + call_pairs.contains(&("process".to_string(), "remove".to_string())), + "Should extract 'remove' from table.remove" + ); + assert!( + call_pairs.contains(&("process".to_string(), "sqrt".to_string())), + "Should extract 'sqrt' from math.sqrt" + ); + assert!( + call_pairs.contains(&("process".to_string(), "abs".to_string())), + "Should extract 'abs' from math.abs" + ); + assert!( + call_pairs.contains(&("process".to_string(), "format".to_string())), + "Should extract 'format' from string.format" + ); +} + +#[test] +fn test_lua_nested_calls_in_arguments() { + let code = r#" +function outer() + print(string.upper(getName())) + result = calculate(getValue(), getMax()) +end +"#; + + let mut parser = LuaParser::new().expect("Failed to create parser"); + let calls = parser.find_calls(code); + + let call_pairs: Vec<(String, String)> = calls + .iter() + .map(|(caller, callee, _)| (caller.to_string(), callee.to_string())) + .collect(); + + // All calls should be detected + assert!( + call_pairs.contains(&("outer".to_string(), "print".to_string())), + "Should detect print call" + ); + assert!( + call_pairs.contains(&("outer".to_string(), "upper".to_string())), + "Should detect string.upper call" + ); + assert!( + call_pairs.contains(&("outer".to_string(), "getName".to_string())), + "Should detect getName call nested in arguments" + ); + assert!( + call_pairs.contains(&("outer".to_string(), "calculate".to_string())), + "Should detect calculate call" + ); + assert!( + call_pairs.contains(&("outer".to_string(), "getValue".to_string())), + "Should detect getValue call in arguments" + ); + assert!( + call_pairs.contains(&("outer".to_string(), "getMax".to_string())), + "Should detect getMax call in arguments" + ); +} + +#[test] +fn test_lua_anonymous_function_calls() { + let code = r#" +function createHandler() + return function(data) + process(data) + validate(data) + end +end + +local callback = function() + notify() + cleanup() +end +"#; + + let mut parser = LuaParser::new().expect("Failed to create parser"); + let calls = parser.find_calls(code); + + let call_pairs: Vec<(String, String)> = calls + .iter() + .map(|(caller, callee, _)| (caller.to_string(), callee.to_string())) + .collect(); + + // Calls within anonymous function in createHandler should use createHandler as caller + assert!( + call_pairs.contains(&("createHandler".to_string(), "process".to_string())), + "Anonymous function in createHandler should call process" + ); + assert!( + call_pairs.contains(&("createHandler".to_string(), "validate".to_string())), + "Anonymous function in createHandler should call validate" + ); + + // Module-level anonymous function calls should use as caller + assert!( + call_pairs.contains(&("".to_string(), "notify".to_string())), + "Module-level anonymous function should call notify" + ); + assert!( + call_pairs.contains(&("".to_string(), "cleanup".to_string())), + "Module-level anonymous function should call cleanup" + ); +} + +#[test] +fn test_lua_constructor_patterns() { + let code = r#" +function Animal.new(name) + local self = setmetatable({}, Animal) + self.name = name + return self +end + +function Dog.new(name, breed) + local self = setmetatable(Animal.new(name), Dog) + self.breed = breed + return self +end +"#; + + let mut parser = LuaParser::new().expect("Failed to create parser"); + let calls = parser.find_calls(code); + + let call_pairs: Vec<(String, String)> = calls + .iter() + .map(|(caller, callee, _)| (caller.to_string(), callee.to_string())) + .collect(); + + // Animal.new should be simplified to just "new" + assert!( + call_pairs.contains(&("new".to_string(), "setmetatable".to_string())), + "Animal.new should call setmetatable" + ); + + // Dog.new should also be simplified to "new" and call both setmetatable and Animal.new + let dog_new_calls: Vec<_> = calls.iter().filter(|(c, _, _)| *c == "new").collect(); + assert!( + dog_new_calls.len() >= 2, + "Dog.new should make at least 2 calls (setmetatable + nested)" + ); +} + +#[test] +fn test_lua_method_chaining_detection() { + let code = r#" +function StringBuilder:append(str) + table.insert(self.parts, str) + return self +end + +function StringBuilder:build() + result = self:append("a"):append("b"):toString() + return result +end +"#; + + let mut parser = LuaParser::new().expect("Failed to create parser"); + let calls = parser.find_calls(code); + + let call_pairs: Vec<(String, String)> = calls + .iter() + .map(|(caller, callee, _)| (caller.to_string(), callee.to_string())) + .collect(); + + assert!( + call_pairs.contains(&("append".to_string(), "insert".to_string())), + "append should call table.insert" + ); + + // Method chaining: each call in the chain is a separate function_call node + assert!( + call_pairs.contains(&("build".to_string(), "append".to_string())), + "build should call append (at least once in chain)" + ); + assert!( + call_pairs.contains(&("build".to_string(), "toString".to_string())), + "build should call toString at end of chain" + ); +} + +#[test] +fn test_lua_calls_in_conditionals_and_loops() { + let code = r#" +function validate(data) + if isValid(data) then + process(data) + else + handleError(data) + end + + for i = 1, getCount() do + doWork(i) + end + + while hasMore() do + fetchNext() + end +end +"#; + + let mut parser = LuaParser::new().expect("Failed to create parser"); + let calls = parser.find_calls(code); + + let call_pairs: Vec<(String, String)> = calls + .iter() + .map(|(caller, callee, _)| (caller.to_string(), callee.to_string())) + .collect(); + + // All calls should be detected regardless of control flow structure + assert!( + call_pairs.contains(&("validate".to_string(), "isValid".to_string())), + "Should detect call in if condition" + ); + assert!( + call_pairs.contains(&("validate".to_string(), "process".to_string())), + "Should detect call in if block" + ); + assert!( + call_pairs.contains(&("validate".to_string(), "handleError".to_string())), + "Should detect call in else block" + ); + assert!( + call_pairs.contains(&("validate".to_string(), "getCount".to_string())), + "Should detect call in for loop condition" + ); + assert!( + call_pairs.contains(&("validate".to_string(), "doWork".to_string())), + "Should detect call in for loop body" + ); + assert!( + call_pairs.contains(&("validate".to_string(), "hasMore".to_string())), + "Should detect call in while condition" + ); + assert!( + call_pairs.contains(&("validate".to_string(), "fetchNext".to_string())), + "Should detect call in while body" + ); +} diff --git a/tests/parsers/lua/test_relationships.rs b/tests/parsers/lua/test_relationships.rs new file mode 100644 index 00000000..9617c80a --- /dev/null +++ b/tests/parsers/lua/test_relationships.rs @@ -0,0 +1,259 @@ +use codanna::parsing::LanguageParser; +use codanna::parsing::lua::LuaParser; + +fn load_oop_fixture() -> &'static str { + include_str!("../../fixtures/lua/oop.lua") +} + +fn load_methods_fixture() -> &'static str { + include_str!("../../fixtures/lua/methods.lua") +} + +fn load_modules_fixture() -> &'static str { + include_str!("../../fixtures/lua/modules.lua") +} + +#[test] +fn test_lua_oop_fixture_inheritance_calls() { + let code = load_oop_fixture(); + let mut parser = LuaParser::new().expect("Failed to create Lua parser"); + + let calls = parser.find_calls(code); + + println!("OOP fixture calls:"); + for (caller, callee, range) in &calls { + println!(" {} -> {} at line {}", caller, callee, range.start_line); + } + + // Animal.new calls setmetatable + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "new" && *callee == "setmetatable"), + "Animal.new should call setmetatable, got {calls:?}" + ); + + // Dog.new calls Animal.new (inheritance pattern) + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "new" && *callee == "new"), + "Dog.new should call Animal.new for inheritance, got {calls:?}" + ); + + // Logger:getInstance calls print + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "getInstance" && *callee == "print"), + "Logger:getInstance should call print, got {calls:?}" + ); + + // Module-level setmetatable calls for inheritance setup + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "" && *callee == "setmetatable"), + "Module level should have setmetatable calls for inheritance, got {calls:?}" + ); +} + +#[test] +fn test_lua_methods_fixture_table_operations() { + let code = load_methods_fixture(); + let mut parser = LuaParser::new().expect("Failed to create Lua parser"); + + let calls = parser.find_calls(code); + + println!("Methods fixture calls:"); + for (caller, callee, range) in &calls { + println!(" {} -> {} at line {}", caller, callee, range.start_line); + } + + // Counter.new calls setmetatable + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "new" && *callee == "setmetatable"), + "Counter.new should call setmetatable, got {calls:?}" + ); + + // Counter.create calls Counter.new + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "create" && *callee == "new"), + "Counter.create should call Counter.new, got {calls:?}" + ); + + // StringBuilder:append calls table.insert + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "append" && *callee == "insert"), + "StringBuilder:append should call table.insert, got {calls:?}" + ); + + // StringBuilder:appendLine calls table.insert (twice) + let append_line_inserts: Vec<_> = calls + .iter() + .filter(|(caller, callee, _)| *caller == "appendLine" && *callee == "insert") + .collect(); + assert!( + append_line_inserts.len() >= 2, + "StringBuilder:appendLine should call table.insert at least twice, got {calls:?}" + ); + + // StringBuilder:toString calls table.concat + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "toString" && *callee == "concat"), + "StringBuilder:toString should call table.concat, got {calls:?}" + ); + + // Vector:length calls math.sqrt + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "length" && *callee == "sqrt"), + "Vector:length should call math.sqrt, got {calls:?}" + ); + + // Vector:normalize calls self:length (method call) + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "normalize" && *callee == "length"), + "Vector:normalize should call self:length, got {calls:?}" + ); + + // Vector.__add calls Vector.new + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "__add" && *callee == "new"), + "Vector.__add should call Vector.new, got {calls:?}" + ); + + // Vector.__tostring calls string.format + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "__tostring" && *callee == "format"), + "Vector.__tostring should call string.format, got {calls:?}" + ); +} + +#[test] +fn test_lua_modules_fixture_function_calls() { + let code = load_modules_fixture(); + let mut parser = LuaParser::new().expect("Failed to create Lua parser"); + + let calls = parser.find_calls(code); + + println!("Modules fixture calls:"); + for (caller, callee, range) in &calls { + println!(" {} -> {} at line {}", caller, callee, range.start_line); + } + + // M.process calls _privateHelper + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "process" && *callee == "_privateHelper"), + "M.process should call _privateHelper, got {calls:?}" + ); + + // M.formatOutput calls string.format and tostring + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "formatOutput" && *callee == "format"), + "M.formatOutput should call string.format, got {calls:?}" + ); + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "formatOutput" && *callee == "tostring"), + "M.formatOutput should call tostring, got {calls:?}" + ); + + // M.utils.split calls table.insert + assert!( + calls + .iter() + .any(|(caller, callee, _)| *caller == "split" && *callee == "insert"), + "M.utils.split should call table.insert, got {calls:?}" + ); +} + +#[test] +fn test_lua_call_count_in_fixtures() { + let mut parser = LuaParser::new().expect("Failed to create Lua parser"); + + // Test that we extract a reasonable number of calls from each fixture + let oop_calls = parser.find_calls(load_oop_fixture()); + assert!( + oop_calls.len() >= 5, + "OOP fixture should have at least 5 calls, found {}", + oop_calls.len() + ); + + let methods_calls = parser.find_calls(load_methods_fixture()); + assert!( + methods_calls.len() >= 10, + "Methods fixture should have at least 10 calls, found {}", + methods_calls.len() + ); + + let modules_calls = parser.find_calls(load_modules_fixture()); + assert!( + modules_calls.len() >= 3, + "Modules fixture should have at least 3 calls, found {}", + modules_calls.len() + ); +} + +#[test] +fn test_lua_no_duplicate_calls() { + let code = r#" +function test() + print("hello") +end +"#; + + let mut parser = LuaParser::new().expect("Failed to create Lua parser"); + let calls = parser.find_calls(code); + + // Should only have one call: test -> print + assert_eq!(calls.len(), 1, "Should have exactly one call"); + assert_eq!(calls[0].0, "test"); + assert_eq!(calls[0].1, "print"); +} + +#[test] +fn test_lua_calls_preserve_line_numbers() { + let code = r#" +function foo() + bar() +end + +function baz() + qux() +end +"#; + + let mut parser = LuaParser::new().expect("Failed to create Lua parser"); + let calls = parser.find_calls(code); + + // bar() is on line 2 (0-indexed: line 2) + let bar_call = calls.iter().find(|(_, callee, _)| *callee == "bar"); + assert!(bar_call.is_some()); + assert_eq!(bar_call.unwrap().2.start_line, 2); + + // qux() is on line 6 (0-indexed: line 6) + let qux_call = calls.iter().find(|(_, callee, _)| *callee == "qux"); + assert!(qux_call.is_some()); + assert_eq!(qux_call.unwrap().2.start_line, 6); +} diff --git a/tests/parsers_tests.rs b/tests/parsers_tests.rs index c1759a28..ec79b460 100644 --- a/tests/parsers_tests.rs +++ b/tests/parsers_tests.rs @@ -106,5 +106,11 @@ mod test_php_readonly_class; #[path = "parsers/kotlin/test_context_receiver.rs"] mod test_kotlin_context_receiver; +#[path = "parsers/lua/test_call_tracking.rs"] +mod test_lua_call_tracking; + +#[path = "parsers/lua/test_relationships.rs"] +mod test_lua_relationships; + #[path = "parsers/swift/test_nested_types.rs"] mod test_swift_nested_types; From 6178f883ed519aef6d3cc8d3d91db76a77094ec9 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sun, 25 Jan 2026 20:59:10 -0600 Subject: [PATCH 4/5] refactor: minor remaining cleanup --- contributing/parsers/lua/AUDIT_REPORT.md | 2 +- contributing/parsers/lua/GRAMMAR_ANALYSIS.md | 7 +- contributing/parsers/lua/node_discovery.txt | 4 +- src/parsing/lua/behavior.rs | 11 +- src/parsing/lua/parser.rs | 167 +++++++++++-------- src/parsing/lua/resolution.rs | 30 ++-- tests/exploration/abi15_grammar_audit.rs | 2 +- tests/fixtures/lua/modules.lua | 2 +- 8 files changed, 131 insertions(+), 94 deletions(-) diff --git a/contributing/parsers/lua/AUDIT_REPORT.md b/contributing/parsers/lua/AUDIT_REPORT.md index b5f332a2..7f4bb710 100644 --- a/contributing/parsers/lua/AUDIT_REPORT.md +++ b/contributing/parsers/lua/AUDIT_REPORT.md @@ -1,6 +1,6 @@ # Lua Parser Symbol Extraction Coverage Report -*Generated: 2026-01-14 03:17:09 UTC* +*Generated: 2026-01-25 22:44:47 UTC* ## Summary - Key nodes: 21/21 (100%) diff --git a/contributing/parsers/lua/GRAMMAR_ANALYSIS.md b/contributing/parsers/lua/GRAMMAR_ANALYSIS.md index 651763dd..088e1919 100644 --- a/contributing/parsers/lua/GRAMMAR_ANALYSIS.md +++ b/contributing/parsers/lua/GRAMMAR_ANALYSIS.md @@ -1,14 +1,17 @@ # Lua Grammar Analysis -*Generated: 2026-01-14 03:17:09 UTC* +*Generated: 2026-01-25 22:44:47 UTC* ## Statistics +- Total nodes in grammar JSON: 0 - Nodes found in comprehensive.lua: 75 - Nodes handled by parser: 75 - Symbol kinds extracted: 6 +## Warning +Missing node-types.json for Lua (No such file or directory (os error 2)). Run `./contributing/tree-sitter/scripts/setup.sh lua` and copy tree-sitter-lua/src/node-types.json to contributing/parsers/lua/node-types.json. + ## ✅ Successfully Handled Nodes -These nodes are in examples and handled by parser: - " - # - ( diff --git a/contributing/parsers/lua/node_discovery.txt b/contributing/parsers/lua/node_discovery.txt index cc36d796..ad47882a 100644 --- a/contributing/parsers/lua/node_discovery.txt +++ b/contributing/parsers/lua/node_discovery.txt @@ -1,6 +1,6 @@ === Lua Language COMPREHENSIVE NODE MAPPING === - Generated: 2026-01-14 03:17:09 UTC - ABI Version: 14 + Generated: 2026-01-25 22:44:47 UTC + ABI Version: 15 Node kind count: 75 == FUNCTION NODES == diff --git a/src/parsing/lua/behavior.rs b/src/parsing/lua/behavior.rs index 3490af21..c954443d 100644 --- a/src/parsing/lua/behavior.rs +++ b/src/parsing/lua/behavior.rs @@ -69,10 +69,13 @@ impl LanguageBehavior for LuaBehavior { ) -> Option { use crate::parsing::paths::strip_extension; - let relative_path = file_path - .strip_prefix(project_root) - .ok() - .or_else(|| file_path.strip_prefix("./").ok())?; + let relative_path = if file_path.is_absolute() { + // For absolute paths, must be within project_root + file_path.strip_prefix(project_root).ok()? + } else { + // For relative paths, use as-is + file_path + }; let path = relative_path.to_str()?; let path_clean = path.trim_start_matches("./"); diff --git a/src/parsing/lua/parser.rs b/src/parsing/lua/parser.rs index cdefde37..daf7a0da 100644 --- a/src/parsing/lua/parser.rs +++ b/src/parsing/lua/parser.rs @@ -451,21 +451,19 @@ impl LuaParser { module_path: &str, depth: usize, ) { - let mut has_function_value = false; + // Build position-aligned Vec for each expression value + let mut function_value_flags = Vec::new(); for child in node.children(&mut node.walk()) { if child.kind() == "expression_list" { for expr_child in child.children(&mut child.walk()) { - if expr_child.kind() == "function_definition" { - has_function_value = true; - break; - } + function_value_flags.push(expr_child.kind() == "function_definition"); } } } for child in node.children(&mut node.walk()) { if child.kind() == "variable_list" { - for var_child in child.children(&mut child.walk()) { + for (index, var_child) in child.children(&mut child.walk()).enumerate() { match var_child.kind() { "identifier" => { let name = code[var_child.byte_range()].to_string(); @@ -475,7 +473,11 @@ impl LuaParser { } let range = range_from_node(&var_child); - let kind = if has_function_value { + let is_function = function_value_flags + .get(index) + .copied() + .unwrap_or(false); + let kind = if is_function { SymbolKind::Function } else if name.chars().all(|c| c.is_uppercase() || c == '_') && name.contains('_') @@ -507,6 +509,10 @@ impl LuaParser { symbols.push(symbol); } "dot_index_expression" => { + let is_function = function_value_flags + .get(index) + .copied() + .unwrap_or(false); self.process_dot_index_assignment( var_child, node, @@ -515,7 +521,7 @@ impl LuaParser { counter, symbols, module_path, - has_function_value, + is_function, ); } _ => {} @@ -730,85 +736,98 @@ impl LuaParser { } fn extract_method_calls_recursive(node: &Node, code: &str, calls: &mut Vec) { - if node.kind() == "function_call" { - if let Some(name_node) = node.child_by_field_name("name") { - if name_node.kind() == "method_index_expression" { - if let Some(method_node) = name_node.child_by_field_name("method") { - let method_name = code[method_node.byte_range()].to_string(); - let range = range_from_node(node); - - let receiver = name_node - .child_by_field_name("table") - .map(|n| code[n.byte_range()].to_string()); - - calls.push(MethodCall { - caller: String::new(), - method_name, - receiver, - is_static: false, - range, - caller_range: Some(range), - }); + let mut stack = vec![*node]; + + while let Some(current_node) = stack.pop() { + if current_node.kind() == "function_call" { + if let Some(name_node) = current_node.child_by_field_name("name") { + if name_node.kind() == "method_index_expression" { + if let Some(method_node) = name_node.child_by_field_name("method") { + let method_name = code[method_node.byte_range()].to_string(); + let range = range_from_node(¤t_node); + + let receiver = name_node + .child_by_field_name("table") + .map(|n| code[n.byte_range()].to_string()); + + calls.push(MethodCall { + caller: String::new(), + method_name, + receiver, + is_static: false, + range, + caller_range: Some(range), + }); + } } } } - } - for child in node.children(&mut node.walk()) { - extract_method_calls_recursive(&child, code, calls); + for child in current_node.children(&mut current_node.walk()) { + stack.push(child); + } } } fn extract_imports_recursive(node: &Node, code: &str, file_id: FileId, imports: &mut Vec) { - // Look for variable_declaration containing require() calls - // Pattern: local foo = require("module") - if node.kind() == "variable_declaration" { - let mut alias: Option = None; - let mut require_call: Option = None; - - for child in node.children(&mut node.walk()) { - if child.kind() == "assignment_statement" { - for assign_child in child.children(&mut child.walk()) { - if assign_child.kind() == "variable_list" { - // Get the variable name (alias) - for var_child in assign_child.children(&mut assign_child.walk()) { - if var_child.kind() == "identifier" { - alias = Some(code[var_child.byte_range()].to_string()); - break; + let mut stack = vec![*node]; + + while let Some(current_node) = stack.pop() { + let mut found_import = false; + + // Look for variable_declaration containing require() calls + // Pattern: local foo = require("module") + if current_node.kind() == "variable_declaration" { + let mut alias: Option = None; + let mut require_call: Option = None; + + for child in current_node.children(&mut current_node.walk()) { + if child.kind() == "assignment_statement" { + for assign_child in child.children(&mut child.walk()) { + if assign_child.kind() == "variable_list" { + // Get the variable name (alias) + for var_child in assign_child.children(&mut assign_child.walk()) { + if var_child.kind() == "identifier" { + alias = Some(code[var_child.byte_range()].to_string()); + break; + } } - } - } else if assign_child.kind() == "expression_list" { - // Check if value is a require() call - for expr_child in assign_child.children(&mut assign_child.walk()) { - if expr_child.kind() == "function_call" { - require_call = Some(expr_child); - break; + } else if assign_child.kind() == "expression_list" { + // Check if value is a require() call + for expr_child in assign_child.children(&mut assign_child.walk()) { + if expr_child.kind() == "function_call" { + require_call = Some(expr_child); + break; + } } } } } } + + if let Some(call_node) = require_call { + if let Some(import) = try_extract_require_call(&call_node, code, file_id, alias) { + imports.push(import); + found_import = true; + } + } } - if let Some(call_node) = require_call { - if let Some(import) = try_extract_require_call(&call_node, code, file_id, alias) { + // Also check for standalone require() calls (without assignment) + if !found_import && current_node.kind() == "function_call" { + if let Some(import) = try_extract_require_call(¤t_node, code, file_id, None) { imports.push(import); - return; // Don't recurse into this node again + found_import = true; } } - } - // Also check for standalone require() calls (without assignment) - if node.kind() == "function_call" { - if let Some(import) = try_extract_require_call(node, code, file_id, None) { - imports.push(import); - return; // Found a require call, don't recurse + // Only push children if we didn't find an import (preserve skip behavior) + if !found_import { + for child in current_node.children(&mut current_node.walk()) { + stack.push(child); + } } } - - for child in node.children(&mut node.walk()) { - extract_imports_recursive(&child, code, file_id, imports); - } } fn try_extract_require_call( @@ -869,10 +888,20 @@ impl NodeTracker for LuaParser { impl LuaParser { fn register_node_recursively(&mut self, node: Node) { - self.node_tracker - .register_handled_node(node.kind(), node.kind_id()); - for child in node.children(&mut node.walk()) { - self.register_node_recursively(child); + let mut stack = vec![(node, 0)]; // (node, depth) + const MAX_DEPTH: usize = 1000; + + while let Some((current_node, depth)) = stack.pop() { + if depth > MAX_DEPTH { + continue; // Skip nodes that are too deep + } + + self.node_tracker + .register_handled_node(current_node.kind(), current_node.kind_id()); + + for child in current_node.children(&mut current_node.walk()) { + stack.push((child, depth + 1)); + } } } diff --git a/src/parsing/lua/resolution.rs b/src/parsing/lua/resolution.rs index 0f05c86c..0c148fb8 100644 --- a/src/parsing/lua/resolution.rs +++ b/src/parsing/lua/resolution.rs @@ -38,15 +38,7 @@ impl Default for LuaResolutionContext { impl LuaResolutionContext { pub fn new(_file_id: FileId) -> Self { - Self { - scope_stack: vec![LuaScope { - symbols: HashMap::new(), - scope_type: ScopeType::Module, - }], - imports: HashMap::new(), - global_symbols: HashMap::new(), - module_symbols: HashMap::new(), - } + Self::default() } pub fn add_import_symbol(&mut self, name: String, symbol_id: SymbolId, _is_type_only: bool) { @@ -133,19 +125,28 @@ impl ResolutionScope for LuaResolutionContext { fn symbols_in_scope(&self) -> Vec<(String, SymbolId, ScopeLevel)> { let mut result = Vec::new(); + let mut seen = std::collections::HashSet::new(); - for scope in &self.scope_stack { + // Iterate scopes in reverse (innermost first) to handle shadowing + for scope in self.scope_stack.iter().rev() { for (name, id) in &scope.symbols { - result.push((name.clone(), *id, ScopeLevel::Local)); + if seen.insert(name.clone()) { + result.push((name.clone(), *id, ScopeLevel::Local)); + } } } + // Module and global symbols are only included if not shadowed by locals for (name, id) in &self.module_symbols { - result.push((name.clone(), *id, ScopeLevel::Module)); + if seen.insert(name.clone()) { + result.push((name.clone(), *id, ScopeLevel::Module)); + } } for (name, id) in &self.global_symbols { - result.push((name.clone(), *id, ScopeLevel::Global)); + if seen.insert(name.clone()) { + result.push((name.clone(), *id, ScopeLevel::Global)); + } } result @@ -241,12 +242,13 @@ impl InheritanceResolver for LuaInheritanceResolver { fn get_all_methods(&self, type_name: &str) -> Vec { let mut methods = Vec::new(); + let mut seen = std::collections::HashSet::new(); let chain = self.get_inheritance_chain(type_name); for ancestor in chain { if let Some(type_methods) = self.type_methods.get(&ancestor) { for method in type_methods { - if !methods.contains(method) { + if seen.insert(method.clone()) { methods.push(method.clone()); } } diff --git a/tests/exploration/abi15_grammar_audit.rs b/tests/exploration/abi15_grammar_audit.rs index 703c2e71..00ca3419 100644 --- a/tests/exploration/abi15_grammar_audit.rs +++ b/tests/exploration/abi15_grammar_audit.rs @@ -1474,7 +1474,7 @@ tree-sitter-gdscript/src/node-types.json to {grammar_path}." } if !audit.extracted_symbol_kinds.is_empty() { - analysis.push_str("## 🔍 Symbol Kinds Extracted\n"); + analysis.push_str("## 🎯 Symbol Kinds Extracted\n"); let mut kinds: Vec<_> = audit.extracted_symbol_kinds.iter().collect(); kinds.sort(); for kind in kinds { diff --git a/tests/fixtures/lua/modules.lua b/tests/fixtures/lua/modules.lua index 2a2cc91e..a3902c6a 100644 --- a/tests/fixtures/lua/modules.lua +++ b/tests/fixtures/lua/modules.lua @@ -40,7 +40,7 @@ function M.utils.trim(s) end function M.utils.split(s, sep) - if sep == "" then + if not sep or sep == "" then local result = {} for i = 1, #s do result[i] = s:sub(i, i) From b5edb6dc04d8f68e0dc58cf34415952464e351f6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 03:03:52 +0000 Subject: [PATCH 5/5] fix: auto-apply rustfmt, clippy, and cargo fix --- src/parsing/lua/audit.rs | 4 ++-- src/parsing/lua/behavior.rs | 4 +--- src/parsing/lua/parser.rs | 30 +++++++++++++++--------- tests/exploration/abi15_grammar_audit.rs | 7 ++---- tests/parsers/lua/test_call_tracking.rs | 2 +- 5 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/parsing/lua/audit.rs b/src/parsing/lua/audit.rs index 3939bdf7..76c01f80 100644 --- a/src/parsing/lua/audit.rs +++ b/src/parsing/lua/audit.rs @@ -184,10 +184,10 @@ impl LuaParserAudit { fn discover_nodes(node: Node, registry: &mut HashMap) { // Use iterative traversal with an explicit stack to avoid stack overflow on large ASTs let mut stack = vec![node]; - + while let Some(current_node) = stack.pop() { registry.insert(current_node.kind().to_string(), current_node.kind_id()); - + let mut cursor = current_node.walk(); // Push children onto the stack for processing for child in current_node.children(&mut cursor) { diff --git a/src/parsing/lua/behavior.rs b/src/parsing/lua/behavior.rs index c954443d..23b24fb0 100644 --- a/src/parsing/lua/behavior.rs +++ b/src/parsing/lua/behavior.rs @@ -119,9 +119,7 @@ impl LanguageBehavior for LuaBehavior { // For assignments like "M.field = value", get the last identifier let before_equals = signature.split('=').next().unwrap_or(""); before_equals - .split(['.', ' ']) - .filter(|s| !s.is_empty()) - .next_back() + .split(['.', ' ']).rfind(|s| !s.is_empty()) .unwrap_or("") .trim() }; diff --git a/src/parsing/lua/parser.rs b/src/parsing/lua/parser.rs index daf7a0da..2eda2c8f 100644 --- a/src/parsing/lua/parser.rs +++ b/src/parsing/lua/parser.rs @@ -473,10 +473,8 @@ impl LuaParser { } let range = range_from_node(&var_child); - let is_function = function_value_flags - .get(index) - .copied() - .unwrap_or(false); + let is_function = + function_value_flags.get(index).copied().unwrap_or(false); let kind = if is_function { SymbolKind::Function } else if name.chars().all(|c| c.is_uppercase() || c == '_') @@ -509,10 +507,8 @@ impl LuaParser { symbols.push(symbol); } "dot_index_expression" => { - let is_function = function_value_flags - .get(index) - .copied() - .unwrap_or(false); + let is_function = + function_value_flags.get(index).copied().unwrap_or(false); self.process_dot_index_assignment( var_child, node, @@ -1315,7 +1311,11 @@ end // Module-level calls should use "" as caller let module_calls: Vec<_> = calls.iter().filter(|(c, _, _)| *c == "").collect(); assert_eq!(module_calls.len(), 2); - assert!(module_calls.iter().any(|(_, callee, _)| *callee == "require")); + assert!( + module_calls + .iter() + .any(|(_, callee, _)| *callee == "require") + ); assert!(module_calls.iter().any(|(_, callee, _)| *callee == "print")); // main should call print @@ -1339,7 +1339,15 @@ end let calls = parser.find_calls(code); assert_eq!(calls.len(), 2); - assert!(calls.iter().any(|(c, callee, _)| *c == "process" && *callee == "insert")); - assert!(calls.iter().any(|(c, callee, _)| *c == "process" && *callee == "sqrt")); + assert!( + calls + .iter() + .any(|(c, callee, _)| *c == "process" && *callee == "insert") + ); + assert!( + calls + .iter() + .any(|(c, callee, _)| *c == "process" && *callee == "sqrt") + ); } } diff --git a/tests/exploration/abi15_grammar_audit.rs b/tests/exploration/abi15_grammar_audit.rs index 00ca3419..11f83929 100644 --- a/tests/exploration/abi15_grammar_audit.rs +++ b/tests/exploration/abi15_grammar_audit.rs @@ -458,11 +458,8 @@ tree-sitter-lua/src/node-types.json to {grammar_path}." analysis.push('\n'); } - fs::write( - "contributing/parsers/lua/GRAMMAR_ANALYSIS.md", - &analysis, - ) - .expect("Failed to write Lua grammar analysis"); + fs::write("contributing/parsers/lua/GRAMMAR_ANALYSIS.md", &analysis) + .expect("Failed to write Lua grammar analysis"); let node_discovery = generate_lua_node_discovery(); fs::write( diff --git a/tests/parsers/lua/test_call_tracking.rs b/tests/parsers/lua/test_call_tracking.rs index 77f5135f..c6e98679 100644 --- a/tests/parsers/lua/test_call_tracking.rs +++ b/tests/parsers/lua/test_call_tracking.rs @@ -111,7 +111,7 @@ end println!("Module-level calls:"); for (caller, callee, _) in &calls { if caller == &"" { - println!(" -> {}", callee); + println!(" -> {callee}"); } }