Step 536: add argument shape validator for constrained ops
This commit is contained in:
@@ -3703,4 +3703,13 @@ target_link_libraries(step535_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step536_test tests/step536_test.cpp)
|
||||
target_include_directories(step536_test PRIVATE src)
|
||||
target_link_libraries(step536_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)
|
||||
|
||||
113
editor/src/ArgumentShapeValidator.h
Normal file
113
editor/src/ArgumentShapeValidator.h
Normal file
@@ -0,0 +1,113 @@
|
||||
#pragma once
|
||||
// Step 536: Argument Shape Validator
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "TypedTaskitemContractSchema.h"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
struct ArgumentShapeResult {
|
||||
bool valid = false;
|
||||
std::vector<std::string> errors;
|
||||
};
|
||||
|
||||
class ArgumentShapeValidator {
|
||||
public:
|
||||
static ArgumentShapeResult validate(const TaskitemContract& contract,
|
||||
const std::string& operation,
|
||||
const std::string& symbol,
|
||||
const json& args) {
|
||||
ArgumentShapeResult result;
|
||||
|
||||
if (!TypedTaskitemContractSchema::opAllowed(contract, operation)) {
|
||||
result.errors.push_back("op_not_allowed:" + operation);
|
||||
}
|
||||
if (!TypedTaskitemContractSchema::symbolAllowed(contract, symbol)) {
|
||||
result.errors.push_back("symbol_not_allowed:" + symbol);
|
||||
}
|
||||
if (!args.is_object()) {
|
||||
result.errors.push_back("args_not_object");
|
||||
result.valid = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (operation == "rename") {
|
||||
requireString(args, "newName", result.errors);
|
||||
rejectSameValue(args, "newName", symbol, "rename_noop", result.errors);
|
||||
forbidKeys(args, {"replacement", "insertText", "position", "mode", "target"}, result.errors);
|
||||
} else if (operation == "update") {
|
||||
requireString(args, "replacement", result.errors);
|
||||
forbidKeys(args, {"newName", "insertText", "position", "target"}, result.errors);
|
||||
} else if (operation == "insert") {
|
||||
requireString(args, "insertText", result.errors);
|
||||
requireString(args, "position", result.errors);
|
||||
validateEnum(args, "position", {"before", "after", "inside"}, result.errors);
|
||||
forbidKeys(args, {"newName", "replacement"}, result.errors);
|
||||
} else if (operation == "extract") {
|
||||
requireString(args, "target", result.errors);
|
||||
requireString(args, "mode", result.errors);
|
||||
validateEnum(args, "mode", {"function", "constant"}, result.errors);
|
||||
} else if (operation == "delete") {
|
||||
requireBoolean(args, "confirm", result.errors);
|
||||
forbidKeys(args, {"newName", "replacement", "insertText"}, result.errors);
|
||||
} else {
|
||||
result.errors.push_back("unsupported_operation_shape:" + operation);
|
||||
}
|
||||
|
||||
result.valid = result.errors.empty();
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
static void requireString(const json& args,
|
||||
const std::string& key,
|
||||
std::vector<std::string>& errors) {
|
||||
if (!args.contains(key) || !args[key].is_string() || args[key].get<std::string>().empty()) {
|
||||
errors.push_back("missing_or_invalid_string:" + key);
|
||||
}
|
||||
}
|
||||
|
||||
static void requireBoolean(const json& args,
|
||||
const std::string& key,
|
||||
std::vector<std::string>& errors) {
|
||||
if (!args.contains(key) || !args[key].is_boolean()) {
|
||||
errors.push_back("missing_or_invalid_boolean:" + key);
|
||||
}
|
||||
}
|
||||
|
||||
static void rejectSameValue(const json& args,
|
||||
const std::string& key,
|
||||
const std::string& value,
|
||||
const std::string& code,
|
||||
std::vector<std::string>& errors) {
|
||||
if (!args.contains(key) || !args[key].is_string()) return;
|
||||
if (args[key].get<std::string>() == value) {
|
||||
errors.push_back(code);
|
||||
}
|
||||
}
|
||||
|
||||
static void validateEnum(const json& args,
|
||||
const std::string& key,
|
||||
const std::set<std::string>& allowed,
|
||||
std::vector<std::string>& errors) {
|
||||
if (!args.contains(key) || !args[key].is_string()) return;
|
||||
if (allowed.count(args[key].get<std::string>()) == 0) {
|
||||
errors.push_back("invalid_enum:" + key);
|
||||
}
|
||||
}
|
||||
|
||||
static void forbidKeys(const json& args,
|
||||
const std::set<std::string>& forbidden,
|
||||
std::vector<std::string>& errors) {
|
||||
for (const auto& key : forbidden) {
|
||||
if (args.contains(key)) {
|
||||
errors.push_back("forbidden_key:" + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
144
editor/tests/step536_test.cpp
Normal file
144
editor/tests/step536_test.cpp
Normal file
@@ -0,0 +1,144 @@
|
||||
// Step 536: Argument Shape Validator (12 tests)
|
||||
|
||||
#include "ArgumentShapeValidator.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
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 TaskitemContract contract() {
|
||||
TaskitemContract c;
|
||||
c.id = "ti-536";
|
||||
c.nodeId = "n-1";
|
||||
c.language = "cpp";
|
||||
c.allowedTargets = {"Function"};
|
||||
c.allowedOps = {"rename", "update", "insert", "extract", "delete"};
|
||||
c.allowedSymbols = {"cursor", "count", "render"};
|
||||
c.forbiddenSymbols = {"unsafeGlobal"};
|
||||
c.expectedDiagnosticsAdd = {"none"};
|
||||
return c;
|
||||
}
|
||||
|
||||
static bool hasError(const ArgumentShapeResult& r, const std::string& codePrefix) {
|
||||
for (const auto& e : r.errors) {
|
||||
if (e.find(codePrefix) == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void test_valid_rename_args_pass() {
|
||||
TEST(valid_rename_args_pass);
|
||||
auto r = ArgumentShapeValidator::validate(contract(), "rename", "cursor",
|
||||
json{{"newName", "cursor2"}});
|
||||
CHECK(r.valid, "valid rename should pass");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_rename_missing_new_name_fails() {
|
||||
TEST(rename_missing_new_name_fails);
|
||||
auto r = ArgumentShapeValidator::validate(contract(), "rename", "cursor", json::object());
|
||||
CHECK(hasError(r, "missing_or_invalid_string:newName"), "newName error expected");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_rename_noop_fails() {
|
||||
TEST(rename_noop_fails);
|
||||
auto r = ArgumentShapeValidator::validate(contract(), "rename", "cursor",
|
||||
json{{"newName", "cursor"}});
|
||||
CHECK(hasError(r, "rename_noop"), "rename noop should fail");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_update_requires_replacement() {
|
||||
TEST(update_requires_replacement);
|
||||
auto r = ArgumentShapeValidator::validate(contract(), "update", "cursor", json::object());
|
||||
CHECK(hasError(r, "missing_or_invalid_string:replacement"), "replacement error expected");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_insert_requires_position_and_insert_text() {
|
||||
TEST(insert_requires_position_and_insert_text);
|
||||
auto r = ArgumentShapeValidator::validate(contract(), "insert", "cursor",
|
||||
json{{"insertText", "x"}});
|
||||
CHECK(hasError(r, "missing_or_invalid_string:position"), "position required");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_insert_rejects_invalid_position_enum() {
|
||||
TEST(insert_rejects_invalid_position_enum);
|
||||
auto r = ArgumentShapeValidator::validate(contract(), "insert", "cursor",
|
||||
json{{"insertText", "x"}, {"position", "middle"}});
|
||||
CHECK(hasError(r, "invalid_enum:position"), "invalid position enum expected");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_extract_requires_target_and_mode() {
|
||||
TEST(extract_requires_target_and_mode);
|
||||
auto r = ArgumentShapeValidator::validate(contract(), "extract", "cursor", json::object());
|
||||
CHECK(hasError(r, "missing_or_invalid_string:target"), "target required");
|
||||
CHECK(hasError(r, "missing_or_invalid_string:mode"), "mode required");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_extract_rejects_invalid_mode() {
|
||||
TEST(extract_rejects_invalid_mode);
|
||||
auto r = ArgumentShapeValidator::validate(contract(), "extract", "cursor",
|
||||
json{{"target", "tmp"}, {"mode", "lambda"}});
|
||||
CHECK(hasError(r, "invalid_enum:mode"), "invalid mode expected");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_delete_requires_confirm_bool() {
|
||||
TEST(delete_requires_confirm_bool);
|
||||
auto r = ArgumentShapeValidator::validate(contract(), "delete", "cursor", json::object());
|
||||
CHECK(hasError(r, "missing_or_invalid_boolean:confirm"), "confirm required");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_disallowed_operation_fails() {
|
||||
TEST(disallowed_operation_fails);
|
||||
auto c = contract();
|
||||
c.allowedOps = {"rename"};
|
||||
auto r = ArgumentShapeValidator::validate(c, "delete", "cursor", json{{"confirm", true}});
|
||||
CHECK(hasError(r, "op_not_allowed:delete"), "disallowed op should fail");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_disallowed_symbol_fails() {
|
||||
TEST(disallowed_symbol_fails);
|
||||
auto r = ArgumentShapeValidator::validate(contract(), "rename", "ghost",
|
||||
json{{"newName", "ghost2"}});
|
||||
CHECK(hasError(r, "symbol_not_allowed:ghost"), "disallowed symbol should fail");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_args_must_be_object() {
|
||||
TEST(args_must_be_object);
|
||||
auto r = ArgumentShapeValidator::validate(contract(), "rename", "cursor", json::array());
|
||||
CHECK(hasError(r, "args_not_object"), "args object requirement expected");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 536: Argument Shape Validator\n";
|
||||
|
||||
test_valid_rename_args_pass(); // 1
|
||||
test_rename_missing_new_name_fails(); // 2
|
||||
test_rename_noop_fails(); // 3
|
||||
test_update_requires_replacement(); // 4
|
||||
test_insert_requires_position_and_insert_text(); // 5
|
||||
test_insert_rejects_invalid_position_enum(); // 6
|
||||
test_extract_requires_target_and_mode(); // 7
|
||||
test_extract_rejects_invalid_mode(); // 8
|
||||
test_delete_requires_confirm_bool(); // 9
|
||||
test_disallowed_operation_fails(); // 10
|
||||
test_disallowed_symbol_fails(); // 11
|
||||
test_args_must_be_object(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
36
progress.md
36
progress.md
@@ -9037,3 +9037,39 @@ category-based filtering.
|
||||
- `editor/src/SymbolSelectorAPI.h` within header-size limit (`81` <= `600`)
|
||||
- `editor/tests/step535_test.cpp` within test-file size guidance (`163` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
### Step 536: Argument Shape Validator
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Implements argument-shape validation for constrained operations, enforcing
|
||||
operation-specific required fields, enum constraints, forbidden keys, and
|
||||
contract legality for operation/symbol pairs.
|
||||
|
||||
**Files added:**
|
||||
- `editor/src/ArgumentShapeValidator.h` - argument shape validation module:
|
||||
- operation/symbol contract allow checks
|
||||
- per-operation required argument schemas (`rename/update/insert/extract/delete`)
|
||||
- enum validation for constrained argument fields
|
||||
- semantic no-op guard for rename operations
|
||||
- malformed-argument object checks and forbidden-key enforcement
|
||||
- `editor/tests/step536_test.cpp` - 12 tests covering:
|
||||
- valid rename path
|
||||
- required field failures per operation
|
||||
- enum validation failures
|
||||
- delete confirm-type requirement
|
||||
- disallowed op/symbol failure paths
|
||||
- malformed args (non-object) edge case
|
||||
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` - `step536_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `cmake -S editor -B editor/build-native` - PASS
|
||||
- `cmake --build editor/build-native --target step536_test step535_test` - PASS
|
||||
- `./editor/build-native/step536_test` - PASS (12/12)
|
||||
- `./editor/build-native/step535_test` - PASS (12/12) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/ArgumentShapeValidator.h` within header-size limit (`113` <= `600`)
|
||||
- `editor/tests/step536_test.cpp` within test-file size guidance (`144` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
Reference in New Issue
Block a user