diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 69f31d6..311fdfd 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2542,4 +2542,13 @@ target_link_libraries(step406_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step407_test tests/step407_test.cpp) +target_include_directories(step407_test PRIVATE src) +target_link_libraries(step407_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/Pipeline.h b/editor/src/Pipeline.h index 13a8138..fe0e4fc 100644 --- a/editor/src/Pipeline.h +++ b/editor/src/Pipeline.h @@ -122,6 +122,10 @@ public: auto pr = FSharpParser::parseFSharpWithDiagnostics(source); diags = std::move(pr.diagnostics); return std::move(pr.module); + } else if (language == "vbnet" || language == "vb" || language == "vb.net") { + auto pr = VBNetParser::parseVBNetWithDiagnostics(source); + diags = std::move(pr.diagnostics); + return std::move(pr.module); } else if (language == "c") { auto pr = CParser::parseCWithDiagnostics(source); diags = std::move(pr.diagnostics); diff --git a/editor/src/ast/Parser.h b/editor/src/ast/Parser.h index ce87ff8..b0ffcee 100644 --- a/editor/src/ast/Parser.h +++ b/editor/src/ast/Parser.h @@ -165,6 +165,7 @@ private: #include "ast/KotlinParser.h" #include "ast/CSharpParser.h" #include "ast/FSharpParser.h" +#include "ast/VBNetParser.h" #include "ast/CParser.h" #include "ast/WatParser.h" #include "ast/CommonLispParser.h" diff --git a/editor/src/ast/VBNetParser.h b/editor/src/ast/VBNetParser.h new file mode 100644 index 0000000..437ed6e --- /dev/null +++ b/editor/src/ast/VBNetParser.h @@ -0,0 +1,184 @@ +#pragma once + +#include "ASTNode.h" +#include "Module.h" +#include "Function.h" +#include "Variable.h" +#include "Statement.h" +#include "ClassDeclaration.h" +#include "EnumNamespaceNodes.h" +#include "../ast/Parser.h" + +#include +#include +#include + +class VBNetParser { +public: + static std::unique_ptr parseVBNet(const std::string& source) { + auto module = std::make_unique(); + module->id = IdGenerator::next("mod"); + module->name = "parsed_vbnet_module"; + module->targetLanguage = "vbnet"; + parseTopLevel(source, module.get()); + return module; + } + + static ParseResult parseVBNetWithDiagnostics(const std::string& source) { + ParseResult result; + result.module = parseVBNet(source); + return result; + } + +private: + static void parseTopLevel(const std::string& source, Module* module) { + std::istringstream stream(source); + std::string line; + + while (std::getline(stream, line)) { + std::string trimmed = trim(line); + if (trimmed.empty()) continue; + if (startsWithCaseInsensitive(trimmed, "'")) continue; + + if (startsWithCaseInsensitive(trimmed, "Class ")) { + auto* cls = new ClassDeclaration(); + cls->id = IdGenerator::next("cls"); + cls->name = extractNameAfterKeyword(trimmed, "Class "); + module->addChild("classes", cls); + continue; + } + + if (startsWithCaseInsensitive(trimmed, "Interface ")) { + auto* iface = new InterfaceDeclaration(); + iface->id = IdGenerator::next("iface"); + iface->name = extractNameAfterKeyword(trimmed, "Interface "); + module->addChild("interfaces", iface); + continue; + } + + if (startsWithCaseInsensitive(trimmed, "Module ")) { + auto* ns = new NamespaceDeclaration(); + ns->id = IdGenerator::next("ns"); + ns->name = extractNameAfterKeyword(trimmed, "Module "); + module->addChild("statements", ns); + continue; + } + + if (containsCaseInsensitive(trimmed, "Sub ") && + !startsWithCaseInsensitive(trimmed, "End Sub")) { + auto* fn = new Function(); + fn->id = IdGenerator::next("fn"); + fn->name = extractMethodName(trimmed, "Sub "); + module->addChild("functions", fn); + continue; + } + + if (containsCaseInsensitive(trimmed, "Function ") && + !startsWithCaseInsensitive(trimmed, "End Function")) { + auto* fn = new Function(); + fn->id = IdGenerator::next("fn"); + fn->name = extractMethodName(trimmed, "Function "); + module->addChild("functions", fn); + continue; + } + + if (startsWithCaseInsensitive(trimmed, "Dim ")) { + auto* var = new Variable(); + var->id = IdGenerator::next("var"); + var->name = extractDimName(trimmed); + module->addChild("variables", var); + continue; + } + + if (startsWithCaseInsensitive(trimmed, "If ") && + containsCaseInsensitive(trimmed, " Then")) { + auto* ifs = new IfStatement(); + ifs->id = IdGenerator::next("if"); + module->addChild("statements", ifs); + continue; + } + + if (startsWithCaseInsensitive(trimmed, "For Each ") && + containsCaseInsensitive(trimmed, " In ")) { + auto* loop = new ForLoop(); + loop->id = IdGenerator::next("for"); + loop->iteratorName = extractForEachIterator(trimmed); + module->addChild("statements", loop); + continue; + } + } + } + + static std::string extractForEachIterator(const std::string& line) { + std::string lower = toLower(line); + size_t start = lower.find("for each "); + if (start == std::string::npos) return "item"; + start += 9; + size_t inPos = lower.find(" in ", start); + if (inPos == std::string::npos) return "item"; + std::string token = trim(line.substr(start, inPos - start)); + // "x As Type" => keep x only + auto asPos = toLower(token).find(" as "); + if (asPos != std::string::npos) token = trim(token.substr(0, asPos)); + return token.empty() ? "item" : token; + } + + static std::string extractMethodName(const std::string& line, const std::string& keyword) { + std::string lower = toLower(line); + std::string lowerKeyword = toLower(keyword); + auto pos = lower.find(lowerKeyword); + if (pos == std::string::npos) return "unknown"; + pos += keyword.size(); + auto end = line.find('(', pos); + if (end == std::string::npos) end = line.find(' ', pos); + if (end == std::string::npos) end = line.size(); + return trim(line.substr(pos, end - pos)); + } + + static std::string extractDimName(const std::string& line) { + auto after = trim(line.substr(4)); // after "Dim " + auto asPos = toLower(after).find(" as "); + auto eqPos = after.find('='); + size_t end = std::string::npos; + if (asPos != std::string::npos && eqPos != std::string::npos) end = std::min(asPos, eqPos); + else if (asPos != std::string::npos) end = asPos; + else if (eqPos != std::string::npos) end = eqPos; + if (end == std::string::npos) end = after.size(); + return trim(after.substr(0, end)); + } + + static std::string extractNameAfterKeyword(const std::string& line, const std::string& keyword) { + auto pos = line.find(keyword); + if (pos == std::string::npos) return "unknown"; + pos += keyword.size(); + auto end = line.find_first_of("(: ", pos); + if (end == std::string::npos) end = line.size(); + return trim(line.substr(pos, end - pos)); + } + + static bool startsWithCaseInsensitive(const std::string& s, const std::string& prefix) { + if (prefix.size() > s.size()) return false; + for (size_t i = 0; i < prefix.size(); ++i) { + if (std::tolower(static_cast(s[i])) != + std::tolower(static_cast(prefix[i]))) return false; + } + return true; + } + + static bool containsCaseInsensitive(const std::string& s, const std::string& needle) { + return toLower(s).find(toLower(needle)) != std::string::npos; + } + + static std::string toLower(const std::string& s) { + std::string out = s; + for (char& c : out) c = static_cast(std::tolower(static_cast(c))); + return out; + } + + static std::string trim(const std::string& s) { + auto start = s.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) return ""; + auto end = s.find_last_not_of(" \t\r\n"); + return s.substr(start, end - start + 1); + } +}; diff --git a/editor/tests/step407_test.cpp b/editor/tests/step407_test.cpp new file mode 100644 index 0000000..a5178c0 --- /dev/null +++ b/editor/tests/step407_test.cpp @@ -0,0 +1,205 @@ +// Step 407: VB.NET Parser Tests (12 tests) + +#include "ast/VBNetParser.h" +#include "Pipeline.h" +#include "ast/Function.h" +#include "ast/Variable.h" +#include "ast/ClassDeclaration.h" +#include "ast/EnumNamespaceNodes.h" +#include "ast/Statement.h" + +#include +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +void test_parse_sub_method() { + TEST(parse_sub_method); + auto mod = VBNetParser::parseVBNet( + "Public Sub DoWork(x As Integer)\n" + "End Sub\n"); + CHECK(mod != nullptr, "module is null"); + auto fns = mod->getChildren("functions"); + CHECK(fns.size() == 1, "expected 1 function"); + auto* fn = static_cast(fns[0]); + CHECK(fn->name == "DoWork", "expected method DoWork"); + PASS(); +} + +void test_parse_function_method() { + TEST(parse_function_method); + auto mod = VBNetParser::parseVBNet( + "Public Function Sum(a As Integer, b As Integer) As Integer\n" + "End Function\n"); + CHECK(mod != nullptr, "module is null"); + auto fns = mod->getChildren("functions"); + CHECK(fns.size() == 1, "expected 1 function"); + auto* fn = static_cast(fns[0]); + CHECK(fn->name == "Sum", "expected method Sum"); + PASS(); +} + +void test_parse_class() { + TEST(parse_class); + auto mod = VBNetParser::parseVBNet( + "Class Person\n" + "End Class\n"); + CHECK(mod != nullptr, "module is null"); + auto classes = mod->getChildren("classes"); + CHECK(classes.size() == 1, "expected 1 class"); + auto* cls = static_cast(classes[0]); + CHECK(cls->name == "Person", "expected class Person"); + PASS(); +} + +void test_parse_interface() { + TEST(parse_interface); + auto mod = VBNetParser::parseVBNet( + "Interface IWorker\n" + "End Interface\n"); + CHECK(mod != nullptr, "module is null"); + auto ifaces = mod->getChildren("interfaces"); + CHECK(ifaces.size() == 1, "expected 1 interface"); + auto* iface = static_cast(ifaces[0]); + CHECK(iface->name == "IWorker", "expected interface IWorker"); + PASS(); +} + +void test_parse_module_namespace() { + TEST(parse_module_namespace); + auto mod = VBNetParser::parseVBNet( + "Module Utils\n" + "End Module\n"); + CHECK(mod != nullptr, "module is null"); + auto stmts = mod->getChildren("statements"); + NamespaceDeclaration* ns = nullptr; + for (auto* s : stmts) { + if (s->conceptType == "NamespaceDeclaration") ns = static_cast(s); + } + CHECK(ns != nullptr, "expected namespace declaration"); + CHECK(ns->name == "Utils", "expected module name Utils"); + PASS(); +} + +void test_parse_dim_variable() { + TEST(parse_dim_variable); + auto mod = VBNetParser::parseVBNet("Dim x As Integer = 5\n"); + CHECK(mod != nullptr, "module is null"); + auto vars = mod->getChildren("variables"); + CHECK(vars.size() == 1, "expected 1 variable"); + auto* var = static_cast(vars[0]); + CHECK(var->name == "x", "expected variable x"); + PASS(); +} + +void test_parse_if_then_block() { + TEST(parse_if_then_block); + auto mod = VBNetParser::parseVBNet( + "If x > 0 Then\n" + "ElseIf x < 0 Then\n" + "Else\n" + "End If\n"); + CHECK(mod != nullptr, "module is null"); + bool foundIf = false; + for (auto* s : mod->getChildren("statements")) { + if (s->conceptType == "IfStatement") foundIf = true; + } + CHECK(foundIf, "expected IfStatement marker"); + PASS(); +} + +void test_parse_for_each_block() { + TEST(parse_for_each_block); + auto mod = VBNetParser::parseVBNet( + "For Each item In collection\n" + "Next\n"); + CHECK(mod != nullptr, "module is null"); + bool foundFor = false; + for (auto* s : mod->getChildren("statements")) { + if (s->conceptType == "ForLoop") { + auto* loop = static_cast(s); + CHECK(loop->iteratorName == "item", "expected iterator item"); + foundFor = true; + } + } + CHECK(foundFor, "expected ForLoop marker"); + PASS(); +} + +void test_case_insensitive_keywords() { + TEST(case_insensitive_keywords); + auto mod = VBNetParser::parseVBNet( + "public sub Run()\n" + "end sub\n"); + CHECK(mod != nullptr, "module is null"); + CHECK(mod->getChildren("functions").size() == 1, "expected function with lowercase keyword"); + PASS(); +} + +void test_parse_with_diagnostics() { + TEST(parse_with_diagnostics); + auto result = VBNetParser::parseVBNetWithDiagnostics( + "Function Echo(value As String) As String\n" + "End Function\n"); + CHECK(result.module != nullptr, "module is null"); + CHECK(result.module->getChildren("functions").size() == 1, "expected 1 function"); + PASS(); +} + +void test_parse_mixed_file() { + TEST(parse_mixed_file); + auto mod = VBNetParser::parseVBNet( + "' comment\n" + "Module App\n" + "End Module\n" + "Class Person\n" + "End Class\n" + "Public Sub Main()\n" + "End Sub\n" + "Dim count As Integer = 1\n"); + CHECK(mod != nullptr, "module is null"); + CHECK(mod->getChildren("statements").size() >= 1, "expected module statement"); + CHECK(mod->getChildren("classes").size() == 1, "expected class"); + CHECK(mod->getChildren("functions").size() == 1, "expected function"); + CHECK(mod->getChildren("variables").size() == 1, "expected variable"); + PASS(); +} + +void test_pipeline_parse_routing() { + TEST(pipeline_parse_routing); + Pipeline p; + std::vector d1, d2, d3; + auto m1 = p.parse("Sub Main()\nEnd Sub\n", "vbnet", d1); + auto m2 = p.parse("Sub Main()\nEnd Sub\n", "vb", d2); + auto m3 = p.parse("Sub Main()\nEnd Sub\n", "vb.net", d3); + CHECK(m1 != nullptr && m2 != nullptr && m3 != nullptr, "expected parse success for aliases"); + CHECK(m1->targetLanguage == "vbnet", "expected vbnet target language"); + CHECK(m2->targetLanguage == "vbnet", "expected vb alias target language"); + CHECK(m3->targetLanguage == "vbnet", "expected vb.net alias target language"); + PASS(); +} + +int main() { + std::cout << "Step 407: VB.NET Parser Tests\n"; + + test_parse_sub_method(); // 1 + test_parse_function_method(); // 2 + test_parse_class(); // 3 + test_parse_interface(); // 4 + test_parse_module_namespace(); // 5 + test_parse_dim_variable(); // 6 + test_parse_if_then_block(); // 7 + test_parse_for_each_block(); // 8 + test_case_insensitive_keywords(); // 9 + test_parse_with_diagnostics(); // 10 + test_parse_mixed_file(); // 11 + test_pipeline_parse_routing(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index c379e23..2645e05 100644 --- a/progress.md +++ b/progress.md @@ -3725,6 +3725,54 @@ output, record-style class output, and F#-style type mappings. - `editor/src/MCPServer.h` (`1679` > `600`) - `editor/src/HeadlessAgentRPCHandler.h` (`2623` > `600`) +### Step 407: VB.NET Parser +**Status:** PASS (12/12 tests) + +Added a standalone VB.NET parser with case-insensitive keyword handling for +core block forms and declarations. Covers `Sub`/`Function`, `Class`, +`Interface`, `Module`, `Dim`, `If...Then`, and `For Each...In` recognition. + +**Files created:** +- `editor/src/ast/VBNetParser.h` — VB.NET parse support: + - `parseVBNet(...)` and `parseVBNetWithDiagnostics(...)` + - `Sub` and `Function` signature extraction + - `Class` and `Interface` declaration extraction + - `Module` mapping to `NamespaceDeclaration` + - `Dim ... As ...` variable extraction + - `If ... Then` marker to `IfStatement` + - `For Each ... In ...` marker to `ForLoop` with iterator extraction + - case-insensitive token matching helpers +- `editor/tests/step407_test.cpp` — 12 tests covering: + 1. `Sub` parsing + 2. `Function` parsing + 3. class parsing + 4. interface parsing + 5. module/namespace parsing + 6. `Dim` variable parsing + 7. `If...Then` mapping + 8. `For Each...In` mapping + 9. case-insensitive keyword handling + 10. diagnostics entry point + 11. mixed-file parsing + 12. pipeline parse routing for `vbnet` / `vb` / `vb.net` + +**Files modified:** +- `editor/src/ast/Parser.h` — include `ast/VBNetParser.h` +- `editor/src/Pipeline.h` — parse routing for `vbnet`, `vb`, `vb.net` +- `editor/CMakeLists.txt` — `step407_test` target + +**Verification run:** +- `step407_test` — PASS (12/12) new step coverage +- `step406_test` — PASS (12/12) regression coverage +- `step405_test` — PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ast/VBNetParser.h` within header-size limit (`184` <= `600`) +- `editor/tests/step407_test.cpp` within test-file size guidance (`205` lines) +- Legacy oversized headers persist: + - `editor/src/MCPServer.h` (`1679` > `600`) + - `editor/src/HeadlessAgentRPCHandler.h` (`2623` > `600`) + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)