diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 7d8b865..a0d8e85 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2005,4 +2005,13 @@ add_executable(step333_test tests/step333_test.cpp) target_include_directories(step333_test PRIVATE src) target_link_libraries(step333_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step334_test tests/step334_test.cpp) +target_include_directories(step334_test PRIVATE src) +target_link_libraries(step334_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/ast/CppParser.h b/editor/src/ast/CppParser.h index e2f45c9..e4074d8 100644 --- a/editor/src/ast/CppParser.h +++ b/editor/src/ast/CppParser.h @@ -89,16 +89,31 @@ private: } } if (!ts_node_is_null(baseClause)) { - uint32_t bc = ts_node_named_child_count(baseClause); - for (uint32_t i = 0; i < bc; ++i) { - TSNode base = ts_node_named_child(baseClause, i); - std::string baseText = nodeText(base, source); - // Strip access specifier prefix - if (baseText.find("public ") == 0) baseText = baseText.substr(7); - else if (baseText.find("private ") == 0) baseText = baseText.substr(8); - else if (baseText.find("protected ") == 0) baseText = baseText.substr(10); - if (!baseText.empty() && cls->superClass.empty()) { - cls->superClass = baseText; + // tree-sitter-cpp base_class_clause children (ALL, not just named): + // ":" [access_specifier] ("virtual")? [type_identifier|template_type] "," + // ... repeating for each base + // "virtual" is an unnamed node, so we must iterate ALL children. + uint32_t totalChildren = ts_node_child_count(baseClause); + std::string pendingAccess = isStruct ? "public" : "private"; + bool pendingVirtual = false; + for (uint32_t i = 0; i < totalChildren; ++i) { + TSNode child = ts_node_child(baseClause, i); + std::string ctype = ts_node_type(child); + if (ctype == "access_specifier") { + std::string accessText = nodeText(child, source); + if (accessText.find("public") != std::string::npos) pendingAccess = "public"; + else if (accessText.find("protected") != std::string::npos) pendingAccess = "protected"; + else if (accessText.find("private") != std::string::npos) pendingAccess = "private"; + } else if (ctype == "virtual") { + pendingVirtual = true; + } else if (ctype == "type_identifier" || ctype == "template_type" || + ctype == "qualified_identifier" || ctype == "dependent_type") { + std::string baseName = nodeText(child, source); + if (!baseName.empty()) { + cls->addBase(baseName, pendingAccess, pendingVirtual); + } + pendingAccess = isStruct ? "public" : "private"; + pendingVirtual = false; } } } diff --git a/editor/tests/step334_test.cpp b/editor/tests/step334_test.cpp new file mode 100644 index 0000000..01a4e33 --- /dev/null +++ b/editor/tests/step334_test.cpp @@ -0,0 +1,277 @@ +// Step 334: C++ Parser — Inheritance + Templates (12 tests) +// Tests multiple inheritance, virtual, access specifiers, template classes, CRTP + +#include +#include +#include +#include +#include "ast/Parser.h" +#include "ast/ClassDeclaration.h" +#include "ast/GenericType.h" +#include "ast/Serialization.h" + +int main() { + int passed = 0; + + // Test 1: Single inheritance backward compat + { + std::string src = R"( +class Foo : public Bar { +public: + void doStuff() {} +}; +)"; + auto mod = TreeSitterParser::parseCpp(src); + auto classes = mod->getChildren("classes"); + assert(classes.size() >= 1); + auto* cls = dynamic_cast(classes[0]); + assert(cls != nullptr); + assert(cls->name == "Foo"); + // Should have at least superClass set + assert(!cls->superClass.empty() || !cls->baseClasses.empty()); + std::cout << "Test 1 PASSED: Single inheritance backward compat\n"; + passed++; + } + + // Test 2: Multiple inheritance parsed + { + std::string src = R"( +class Widget : public Base, public Mixin { + void draw() {} +}; +)"; + auto mod = TreeSitterParser::parseCpp(src); + auto classes = mod->getChildren("classes"); + assert(!classes.empty()); + auto* cls = dynamic_cast(classes[0]); + assert(cls->name == "Widget"); + auto bases = cls->getBases(); + assert(bases.size() >= 2); + std::cout << "Test 2 PASSED: Multiple inheritance parsed (" << bases.size() << " bases)\n"; + passed++; + } + + // Test 3: Access specifiers detected + { + std::string src = R"( +class D : public A, protected B, private C { + void f() {} +}; +)"; + auto mod = TreeSitterParser::parseCpp(src); + auto classes = mod->getChildren("classes"); + assert(!classes.empty()); + auto* cls = dynamic_cast(classes[0]); + auto bases = cls->getBases(); + assert(bases.size() >= 3); + // Check that at least different specifiers exist + bool hasPublic = false, hasProtected = false, hasPrivate = false; + for (const auto& b : bases) { + if (b.accessSpecifier == "public") hasPublic = true; + if (b.accessSpecifier == "protected") hasProtected = true; + if (b.accessSpecifier == "private") hasPrivate = true; + } + assert(hasPublic); + assert(hasProtected); + assert(hasPrivate); + std::cout << "Test 3 PASSED: Access specifiers detected\n"; + passed++; + } + + // Test 4: Virtual inheritance flag + { + std::string src = R"( +class D : public virtual Base { + void f() {} +}; +)"; + auto mod = TreeSitterParser::parseCpp(src); + auto classes = mod->getChildren("classes"); + assert(!classes.empty()); + auto* cls = dynamic_cast(classes[0]); + auto bases = cls->getBases(); + assert(!bases.empty()); + assert(bases[0].isVirtual); + std::cout << "Test 4 PASSED: Virtual inheritance flag detected\n"; + passed++; + } + + // Test 5: Template class parsed + { + std::string src = R"( +template +class Container { +public: + void add(T item) {} +}; +)"; + auto mod = TreeSitterParser::parseCpp(src); + auto classes = mod->getChildren("classes"); + assert(!classes.empty()); + auto* cls = dynamic_cast(classes[0]); + assert(cls->name == "Container"); + auto tps = cls->getChildren("typeParameters"); + assert(tps.size() >= 1); + auto* tp = dynamic_cast(tps[0]); + assert(tp != nullptr); + assert(tp->name == "T"); + std::cout << "Test 5 PASSED: Template class parsed with TypeParameter\n"; + passed++; + } + + // Test 6: CRTP pattern detected via base class name + { + std::string src = R"( +template +class Base { +}; + +class Derived : public Base { + void f() {} +}; +)"; + auto mod = TreeSitterParser::parseCpp(src); + auto classes = mod->getChildren("classes"); + // Find Derived class + ClassDeclaration* derived = nullptr; + for (auto* c : classes) { + auto* cls = dynamic_cast(c); + if (cls && cls->name == "Derived") { + derived = cls; + break; + } + } + assert(derived != nullptr); + auto bases = derived->getBases(); + assert(!bases.empty()); + // Check CRTP: Base contains "Derived" as template arg + std::vector baseNames; + for (const auto& b : bases) baseNames.push_back(b.name); + assert(isCRTPClass("Derived", baseNames)); + std::cout << "Test 6 PASSED: CRTP pattern detected\n"; + passed++; + } + + // Test 7: struct vs class default access + { + std::string src = R"( +struct Point { + int x; + int y; +}; +)"; + auto mod = TreeSitterParser::parseCpp(src); + auto classes = mod->getChildren("classes"); + assert(!classes.empty()); + auto* cls = dynamic_cast(classes[0]); + assert(cls->name == "Point"); + std::cout << "Test 7 PASSED: struct parsed as ClassDeclaration\n"; + passed++; + } + + // Test 8: Template with variadic (typename... Args) + { + std::string src = R"( +template +class Pair { + void f() {} +}; +)"; + auto mod = TreeSitterParser::parseCpp(src); + auto classes = mod->getChildren("classes"); + assert(!classes.empty()); + auto* cls = dynamic_cast(classes[0]); + assert(cls->name == "Pair"); + auto tps = cls->getChildren("typeParameters"); + assert(tps.size() == 2); + std::cout << "Test 8 PASSED: Template with 2 type params\n"; + passed++; + } + + // Test 9: Combined template + multiple inheritance + { + std::string src = R"( +template +class MyClass : public Base, public Interface { + void f() {} +}; +)"; + auto mod = TreeSitterParser::parseCpp(src); + auto classes = mod->getChildren("classes"); + assert(!classes.empty()); + auto* cls = dynamic_cast(classes[0]); + assert(cls->name == "MyClass"); + auto bases = cls->getBases(); + assert(bases.size() >= 2); + auto tps = cls->getChildren("typeParameters"); + assert(tps.size() >= 1); + std::cout << "Test 9 PASSED: Template + multiple inheritance combined\n"; + passed++; + } + + // Test 10: Methods inside class body still parsed + { + std::string src = R"( +class Foo : public Bar { +public: + virtual void draw() {} + static void create() {} +private: + void helper() {} +}; +)"; + auto mod = TreeSitterParser::parseCpp(src); + auto classes = mod->getChildren("classes"); + assert(!classes.empty()); + auto* cls = dynamic_cast(classes[0]); + auto methods = cls->getChildren("methods"); + assert(methods.size() >= 2); // at least some methods parsed + std::cout << "Test 10 PASSED: Methods inside class body parsed (" << methods.size() << " methods)\n"; + passed++; + } + + // Test 11: Whetstone-style class signature + { + std::string src = R"( +class KotlinGenerator : public ProjectionGenerator, public AnnotationVisitorExtended { +public: + void generate() {} +}; +)"; + auto mod = TreeSitterParser::parseCpp(src); + auto classes = mod->getChildren("classes"); + assert(!classes.empty()); + auto* cls = dynamic_cast(classes[0]); + assert(cls->name == "KotlinGenerator"); + auto bases = cls->getBases(); + assert(bases.size() >= 2); + std::cout << "Test 11 PASSED: Whetstone-style class signature\n"; + passed++; + } + + // Test 12: JSON roundtrip preserves parsed class with bases + { + std::string src = R"( +class D : public A, protected B { + void f() {} +}; +)"; + auto mod = TreeSitterParser::parseCpp(src); + json j = toJson(mod.get()); + ASTNode* restored = fromJson(j); + auto* rmod = dynamic_cast(restored); + assert(rmod != nullptr); + auto classes = rmod->getChildren("classes"); + assert(!classes.empty()); + auto* cls = dynamic_cast(classes[0]); + assert(cls->name == "D"); + auto bases = cls->getBases(); + assert(bases.size() >= 2); + deleteTree(restored); + std::cout << "Test 12 PASSED: JSON roundtrip preserves parsed class\n"; + passed++; + } + + std::cout << "\nResults: " << passed << "/12 tests passed\n"; + return (passed == 12) ? 0 : 1; +}