diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 5924395..c1b9f7e 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3613,4 +3613,13 @@ target_link_libraries(step525_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step526_test tests/step526_test.cpp) +target_include_directories(step526_test PRIVATE src) +target_link_libraries(step526_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/SymbolScopeExtractor.h b/editor/src/SymbolScopeExtractor.h new file mode 100644 index 0000000..3d64d06 --- /dev/null +++ b/editor/src/SymbolScopeExtractor.h @@ -0,0 +1,102 @@ +#pragma once +// Step 526: Symbol Scope Extractor + +#include +#include +#include +#include +#include + +struct ScopedSymbol { + std::string name; + std::string kind; + int scopeDepth = 0; +}; + +struct ScopeSnapshot { + bool supported = false; + std::vector candidates; +}; + +class SymbolScopeExtractor { +public: + SymbolScopeExtractor() { initDefaults(); } + + void registerRule(const std::string& language, + const std::string& nodeKind, + const std::vector& allowedKinds) { + rules_[key(language, nodeKind)] = + std::set(allowedKinds.begin(), allowedKinds.end()); + } + + ScopeSnapshot buildSnapshot(const std::string& language, + const std::string& nodeKind, + const std::vector& visible, + const std::set& forbidden = {}) const { + auto it = rules_.find(key(language, nodeKind)); + if (it == rules_.end()) return {}; + + std::map selectedByName; + for (const auto& symbol : visible) { + if (it->second.count(symbol.kind) == 0) continue; + if (forbidden.count(symbol.name) != 0) continue; + + auto existing = selectedByName.find(symbol.name); + if (existing == selectedByName.end() || + symbol.scopeDepth > existing->second.scopeDepth) { + selectedByName[symbol.name] = symbol; + } + } + + ScopeSnapshot snapshot; + snapshot.supported = true; + for (const auto& entry : selectedByName) { + snapshot.candidates.push_back(entry.second); + } + return snapshot; + } + + std::vector unresolvedSymbols( + const ScopeSnapshot& snapshot, + const std::vector& requestedSymbols) const { + std::set candidateNames; + for (const auto& symbol : snapshot.candidates) { + candidateNames.insert(symbol.name); + } + + std::set unresolved; + for (const auto& requested : requestedSymbols) { + if (candidateNames.count(requested) == 0) { + unresolved.insert(requested); + } + } + return std::vector(unresolved.begin(), unresolved.end()); + } + + bool canApply(const ScopeSnapshot& snapshot, + const std::vector& requestedSymbols) const { + if (!snapshot.supported) return false; + return unresolvedSymbols(snapshot, requestedSymbols).empty(); + } + +private: + std::map> rules_; + + static std::string key(const std::string& language, + const std::string& nodeKind) { + return language + "::" + nodeKind; + } + + void initDefaults() { + registerRule("cpp", "Function", + {"Function", "Variable", "Parameter", "Type", "Namespace"}); + registerRule("cpp", "Class", {"Method", "Field", "Type", "Namespace"}); + registerRule("python", "Function", {"Function", "Variable", "Parameter", "Class"}); + registerRule("python", "Class", {"Function", "Variable", "Class"}); + registerRule("typescript", "Function", + {"Function", "Variable", "Parameter", "Type", "Import"}); + registerRule("typescript", "Class", {"Function", "Variable", "Type", "Import"}); + registerRule("rust", "Function", {"Function", "Variable", "Parameter", "Type", "Trait"}); + registerRule("go", "Function", {"Function", "Variable", "Parameter", "Type", "Import"}); + } +}; diff --git a/editor/tests/step526_test.cpp b/editor/tests/step526_test.cpp new file mode 100644 index 0000000..11ea1ae --- /dev/null +++ b/editor/tests/step526_test.cpp @@ -0,0 +1,158 @@ +// Step 526: Symbol Scope Extractor (12 tests) + +#include "SymbolScopeExtractor.h" + +#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 {} + +static std::vector sampleCppFunctionSymbols() { + return { + {"render", "Function", 1}, + {"buffer", "Variable", 2}, + {"cursor", "Variable", 3}, + {"cursor", "Variable", 1}, + {"count", "Parameter", 3}, + {"EditorState", "Type", 0}, + {"std", "Namespace", 0}, + {"#include", "Import", 0} + }; +} + +void test_cpp_function_snapshot_filters_by_allowed_kinds() { + TEST(cpp_function_snapshot_filters_by_allowed_kinds); + SymbolScopeExtractor e; + auto snapshot = e.buildSnapshot("cpp", "Function", sampleCppFunctionSymbols()); + CHECK(snapshot.supported, "cpp function should be supported"); + CHECK(snapshot.candidates.size() == 6, "only allowed symbol kinds should be included"); + PASS(); +} + +void test_snapshot_prefers_nearest_scope_for_same_symbol_name() { + TEST(snapshot_prefers_nearest_scope_for_same_symbol_name); + SymbolScopeExtractor e; + auto snapshot = e.buildSnapshot("cpp", "Function", sampleCppFunctionSymbols()); + int depth = -1; + for (const auto& c : snapshot.candidates) { + if (c.name == "cursor") depth = c.scopeDepth; + } + CHECK(depth == 3, "nearest symbol depth should be retained"); + PASS(); +} + +void test_unknown_language_is_unsupported() { + TEST(unknown_language_is_unsupported); + SymbolScopeExtractor e; + auto snapshot = e.buildSnapshot("haskell", "Function", sampleCppFunctionSymbols()); + CHECK(!snapshot.supported, "unknown language should be unsupported"); + CHECK(snapshot.candidates.empty(), "unsupported snapshot should have no candidates"); + PASS(); +} + +void test_unknown_node_kind_is_unsupported() { + TEST(unknown_node_kind_is_unsupported); + SymbolScopeExtractor e; + auto snapshot = e.buildSnapshot("cpp", "LambdaMagic", sampleCppFunctionSymbols()); + CHECK(!snapshot.supported, "unknown node kind should be unsupported"); + PASS(); +} + +void test_unresolved_symbols_empty_for_in_scope_requests() { + TEST(unresolved_symbols_empty_for_in_scope_requests); + SymbolScopeExtractor e; + auto snapshot = e.buildSnapshot("cpp", "Function", sampleCppFunctionSymbols()); + auto unresolved = e.unresolvedSymbols(snapshot, {"cursor", "count", "EditorState"}); + CHECK(unresolved.empty(), "all requested symbols are in scope"); + PASS(); +} + +void test_unresolved_symbols_reports_out_of_scope_requests() { + TEST(unresolved_symbols_reports_out_of_scope_requests); + SymbolScopeExtractor e; + auto snapshot = e.buildSnapshot("cpp", "Function", sampleCppFunctionSymbols()); + auto unresolved = e.unresolvedSymbols(snapshot, {"cursor", "ghost", "unknownType"}); + CHECK(unresolved.size() == 2, "two symbols should be unresolved"); + CHECK(unresolved[0] == "ghost", "results should be sorted"); + CHECK(unresolved[1] == "unknownType", "results should include both unresolved names"); + PASS(); +} + +void test_can_apply_blocks_unresolved_symbol_requests() { + TEST(can_apply_blocks_unresolved_symbol_requests); + SymbolScopeExtractor e; + auto snapshot = e.buildSnapshot("cpp", "Function", sampleCppFunctionSymbols()); + CHECK(!e.canApply(snapshot, {"cursor", "ghost"}), "unresolved symbol should block apply"); + PASS(); +} + +void test_can_apply_false_when_snapshot_is_unsupported() { + TEST(can_apply_false_when_snapshot_is_unsupported); + SymbolScopeExtractor e; + auto snapshot = e.buildSnapshot("zig", "Function", sampleCppFunctionSymbols()); + CHECK(!e.canApply(snapshot, {"cursor"}), "unsupported snapshot should block apply"); + PASS(); +} + +void test_forbidden_symbols_are_excluded_from_snapshot() { + TEST(forbidden_symbols_are_excluded_from_snapshot); + SymbolScopeExtractor e; + auto snapshot = e.buildSnapshot("cpp", "Function", sampleCppFunctionSymbols(), {"cursor"}); + auto unresolved = e.unresolvedSymbols(snapshot, {"cursor"}); + CHECK(unresolved.size() == 1, "forbidden symbol should be excluded"); + CHECK(unresolved[0] == "cursor", "forbidden symbol should be unresolved"); + PASS(); +} + +void test_register_rule_adds_new_language_mapping() { + TEST(register_rule_adds_new_language_mapping); + SymbolScopeExtractor e; + e.registerRule("java", "Method", {"Variable", "Parameter"}); + auto snapshot = e.buildSnapshot("java", "Method", {{"value", "Variable", 1}}); + CHECK(snapshot.supported, "new rule should become supported"); + CHECK(snapshot.candidates.size() == 1, "registered rule should include matching kind"); + PASS(); +} + +void test_register_rule_overrides_existing_mapping() { + TEST(register_rule_overrides_existing_mapping); + SymbolScopeExtractor e; + e.registerRule("cpp", "Function", {"Function"}); + auto snapshot = e.buildSnapshot("cpp", "Function", sampleCppFunctionSymbols()); + CHECK(snapshot.supported, "overridden rule should remain supported"); + CHECK(snapshot.candidates.size() == 1, "override should restrict allowed kinds"); + CHECK(snapshot.candidates[0].name == "render", "function should remain"); + PASS(); +} + +void test_empty_visible_symbol_set_yields_empty_candidates() { + TEST(empty_visible_symbol_set_yields_empty_candidates); + SymbolScopeExtractor e; + auto snapshot = e.buildSnapshot("cpp", "Function", {}); + CHECK(snapshot.supported, "supported rule should remain supported"); + CHECK(snapshot.candidates.empty(), "empty input should produce empty candidates"); + PASS(); +} + +int main() { + std::cout << "Step 526: Symbol Scope Extractor\n"; + + test_cpp_function_snapshot_filters_by_allowed_kinds(); // 1 + test_snapshot_prefers_nearest_scope_for_same_symbol_name(); // 2 + test_unknown_language_is_unsupported(); // 3 + test_unknown_node_kind_is_unsupported(); // 4 + test_unresolved_symbols_empty_for_in_scope_requests(); // 5 + test_unresolved_symbols_reports_out_of_scope_requests(); // 6 + test_can_apply_blocks_unresolved_symbol_requests(); // 7 + test_can_apply_false_when_snapshot_is_unsupported(); // 8 + test_forbidden_symbols_are_excluded_from_snapshot(); // 9 + test_register_rule_adds_new_language_mapping(); // 10 + test_register_rule_overrides_existing_mapping(); // 11 + test_empty_visible_symbol_set_yields_empty_candidates(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 4f99cfe..f39175f 100644 --- a/progress.md +++ b/progress.md @@ -8624,3 +8624,38 @@ before execution. - `editor/src/LegalOperationGraph.h` within header-size limit (`79` <= `600`) - `editor/tests/step525_test.cpp` within test-file size guidance (`131` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 526: Symbol Scope Extractor +**Status:** PASS (12/12 tests) + +Implements symbol-scope extraction for constrained taskitems by building a +language/node-aware in-scope candidate snapshot and explicitly rejecting +unresolved symbol requests before apply. + +**Files added:** +- `editor/src/SymbolScopeExtractor.h` - scope extraction module: + - default per-language/per-node symbol-kind rules + - nearest-scope candidate selection for duplicate names + - forbidden-symbol filtering + - unresolved symbol detection and apply-eligibility check +- `editor/tests/step526_test.cpp` - 12 tests covering: + - default filtering and nearest-scope selection behavior + - unsupported language/node handling + - unresolved symbol reporting and apply blocking + - forbidden symbol exclusion behavior + - dynamic rule registration/override behavior + - empty symbol-table edge case + +**Files modified:** +- `editor/CMakeLists.txt` - `step526_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step526_test step525_test` - PASS +- `./editor/build-native/step526_test` - PASS (12/12) +- `./editor/build-native/step525_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/SymbolScopeExtractor.h` within header-size limit (`102` <= `600`) +- `editor/tests/step526_test.cpp` within test-file size guidance (`158` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`