diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 73eed46..a7185af 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2830,4 +2830,94 @@ target_link_libraries(step438_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step439_test tests/step439_test.cpp) +target_include_directories(step439_test PRIVATE src) +target_link_libraries(step439_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) + +add_executable(step440_test tests/step440_test.cpp) +target_include_directories(step440_test PRIVATE src) +target_link_libraries(step440_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) + +add_executable(step441_test tests/step441_test.cpp) +target_include_directories(step441_test PRIVATE src) +target_link_libraries(step441_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) + +add_executable(step442_test tests/step442_test.cpp) +target_include_directories(step442_test PRIVATE src) +target_link_libraries(step442_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) + +add_executable(step443_test tests/step443_test.cpp) +target_include_directories(step443_test PRIVATE src) +target_link_libraries(step443_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) + +add_executable(step444_test tests/step444_test.cpp) +target_include_directories(step444_test PRIVATE src) +target_link_libraries(step444_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) + +add_executable(step445_test tests/step445_test.cpp) +target_include_directories(step445_test PRIVATE src) +target_link_libraries(step445_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) + +add_executable(step446_test tests/step446_test.cpp) +target_include_directories(step446_test PRIVATE src) +target_link_libraries(step446_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) + +add_executable(step447_test tests/step447_test.cpp) +target_include_directories(step447_test PRIVATE src) +target_link_libraries(step447_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) + +add_executable(step448_test tests/step448_test.cpp) +target_include_directories(step448_test PRIVATE src) +target_link_libraries(step448_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/APIBoundaryPreserver.h b/editor/src/APIBoundaryPreserver.h new file mode 100644 index 0000000..1ad710f --- /dev/null +++ b/editor/src/APIBoundaryPreserver.h @@ -0,0 +1,253 @@ +#pragma once + +// Step 445: API Boundary Preservation — ensures migrated modules maintain +// their public API surface with FFI annotations and @Contract verification. + +#include "MigrationPlanGenerator.h" + +#include +#include + +struct FunctionSignature { + std::string name; + std::string returnType; + std::vector paramTypes; + std::vector paramNames; + bool isPublic = true; +}; + +struct TypeMapping { + std::string sourceName; + std::string sourceLanguage; + std::string targetName; + std::string targetLanguage; + std::string note; // compatibility note +}; + +struct ContractAnnotation { + std::string functionName; + std::string precondition; + std::string postcondition; +}; + +struct FFIBoundary { + std::string functionName; + std::string sourceLanguage; + std::string targetLanguage; + std::string annotation; // e.g. @FFI(extern "C"), #[no_mangle], etc. +}; + +struct APIBoundaryReport { + std::string moduleName; + std::string sourceLanguage; + std::string targetLanguage; + std::vector publicAPI; + std::vector typeMappings; + std::vector contracts; + std::vector ffiBoundaries; + bool apiPreserved = true; + + bool hasFunction(const std::string& name) const { + for (const auto& f : publicAPI) + if (f.name == name) return true; + return false; + } + + bool hasTypeMapping(const std::string& sourceName) const { + for (const auto& t : typeMappings) + if (t.sourceName == sourceName) return true; + return false; + } + + bool hasContract(const std::string& funcName) const { + for (const auto& c : contracts) + if (c.functionName == funcName) return true; + return false; + } + + bool hasFFI(const std::string& funcName) const { + for (const auto& f : ffiBoundaries) + if (f.functionName == funcName) return true; + return false; + } +}; + +class APIBoundaryPreserver { +public: + static APIBoundaryReport analyze(const std::string& source, + const MigrationUnit& unit) { + APIBoundaryReport report; + report.moduleName = unit.moduleName; + report.sourceLanguage = unit.sourceLanguage; + report.targetLanguage = unit.targetLanguage; + + // Extract public API from source + report.publicAPI = extractPublicFunctions(source, unit.exports); + + // Generate type mappings + report.typeMappings = generateTypeMappings(source, unit); + + // Generate contracts for public functions + report.contracts = generateContracts(report.publicAPI, source); + + // Generate FFI boundaries for mixed-language operation + report.ffiBoundaries = generateFFIBoundaries(report.publicAPI, unit); + + report.apiPreserved = true; + return report; + } + + static bool verifyPreservation(const APIBoundaryReport& original, + const APIBoundaryReport& migrated) { + // Every original public function must exist in migrated + for (const auto& f : original.publicAPI) { + if (!migrated.hasFunction(f.name)) return false; + } + return true; + } + +private: + static std::vector extractPublicFunctions( + const std::string& source, const std::vector& exports) { + std::vector sigs; + for (const auto& name : exports) { + FunctionSignature sig; + sig.name = name; + sig.isPublic = true; + // Extract return type and params from source (heuristic) + extractSignatureDetails(source, name, sig); + sigs.push_back(std::move(sig)); + } + return sigs; + } + + static void extractSignatureDetails(const std::string& source, + const std::string& name, + FunctionSignature& sig) { + // Find "type name(" pattern + size_t namePos = source.find(name + "("); + if (namePos == std::string::npos) { + sig.returnType = "int"; // default for C + return; + } + // Extract return type (word before function name) + size_t start = namePos; + while (start > 0 && source[start - 1] == ' ') --start; + size_t typeEnd = start; + while (start > 0 && (std::isalnum((unsigned char)source[start - 1]) || + source[start - 1] == '_' || source[start - 1] == '*')) --start; + sig.returnType = source.substr(start, typeEnd - start); + if (sig.returnType.empty()) sig.returnType = "int"; + + // Extract parameter list + size_t parenOpen = source.find('(', namePos); + size_t parenClose = source.find(')', parenOpen); + if (parenOpen != std::string::npos && parenClose != std::string::npos) { + std::string params = source.substr(parenOpen + 1, parenClose - parenOpen - 1); + parseParams(params, sig); + } + } + + static void parseParams(const std::string& params, FunctionSignature& sig) { + if (params.empty() || params == "void") return; + size_t pos = 0; + while (pos < params.size()) { + size_t comma = params.find(',', pos); + if (comma == std::string::npos) comma = params.size(); + std::string param = trim(params.substr(pos, comma - pos)); + if (!param.empty()) { + // Split "type name" — last word is name, rest is type + size_t lastSpace = param.rfind(' '); + if (lastSpace != std::string::npos) { + sig.paramTypes.push_back(trim(param.substr(0, lastSpace))); + sig.paramNames.push_back(trim(param.substr(lastSpace + 1))); + } else { + sig.paramTypes.push_back(param); + sig.paramNames.push_back("arg" + std::to_string(sig.paramNames.size())); + } + } + pos = comma + 1; + } + } + + static std::vector generateTypeMappings(const std::string& source, + const MigrationUnit& unit) { + std::vector mappings; + std::string tgt = unit.targetLanguage; + + // Common C → target type mappings + auto addIf = [&](const std::string& pattern, const std::string& src, + const std::string& dst, const std::string& note) { + if (source.find(pattern) != std::string::npos) + mappings.push_back({src, unit.sourceLanguage, dst, tgt, note}); + }; + + if (tgt == "rust") { + addIf("int ", "int", "i32", "C int maps to Rust i32"); + addIf("char*", "char*", "&str / String", "C string to Rust string type"); + addIf("void*", "void*", "*mut c_void", "Opaque pointer in FFI"); + addIf("struct ", "struct", "struct", "C struct maps to Rust struct"); + } else if (tgt == "java") { + addIf("int ", "int", "int", "Direct mapping"); + addIf("char*", "char*", "String", "C string to Java String"); + addIf("struct ", "struct", "class", "C struct maps to Java class"); + } else if (tgt == "python") { + addIf("int ", "int", "int", "Direct mapping"); + addIf("char*", "char*", "str", "C string to Python str"); + } + return mappings; + } + + static std::vector generateContracts( + const std::vector& api, const std::string& source) { + std::vector contracts; + for (const auto& f : api) { + ContractAnnotation c; + c.functionName = f.name; + // Generate pre/post conditions from signature + if (!f.paramTypes.empty()) { + // Pointer params get non-null precondition + for (size_t i = 0; i < f.paramTypes.size(); ++i) { + if (f.paramTypes[i].find('*') != std::string::npos) { + c.precondition += f.paramNames[i] + " != NULL; "; + } + } + } + if (f.returnType.find('*') != std::string::npos) { + c.postcondition = "result may be NULL (caller must check)"; + } else if (f.returnType == "int") { + c.postcondition = "returns int result"; + } + if (!c.precondition.empty() || !c.postcondition.empty()) + contracts.push_back(std::move(c)); + } + return contracts; + } + + static std::vector generateFFIBoundaries( + const std::vector& api, const MigrationUnit& unit) { + std::vector boundaries; + for (const auto& f : api) { + FFIBoundary ffi; + ffi.functionName = f.name; + ffi.sourceLanguage = unit.sourceLanguage; + ffi.targetLanguage = unit.targetLanguage; + if (unit.targetLanguage == "rust") { + ffi.annotation = "@FFI(#[no_mangle] pub extern \"C\")"; + } else if (unit.targetLanguage == "java") { + ffi.annotation = "@FFI(JNI native)"; + } else { + ffi.annotation = "@FFI(extern)"; + } + boundaries.push_back(std::move(ffi)); + } + return boundaries; + } + + static std::string trim(const std::string& s) { + size_t start = 0, end = s.size(); + while (start < end && std::isspace((unsigned char)s[start])) ++start; + while (end > start && std::isspace((unsigned char)s[end - 1])) --end; + return s.substr(start, end - start); + } +}; diff --git a/editor/src/MigrationExecutor.h b/editor/src/MigrationExecutor.h new file mode 100644 index 0000000..0e8b18f --- /dev/null +++ b/editor/src/MigrationExecutor.h @@ -0,0 +1,199 @@ +#pragma once + +// Step 447: Migration Execution Integration — hooks migration plans into the +// orchestration engine with cross-unit dependencies, rollback annotations, +// and progressive (partial) migration support. + +#include "MigrationPlanGenerator.h" +#include "APIBoundaryPreserver.h" +#include "MigrationTestGenerator.h" + +#include +#include + +enum class MigrationStatus { Pending, InProgress, Completed, Failed, Skipped }; + +struct MigrationWorkItem { + std::string id; + std::string unitId; + std::string filePath; + std::string title; + MigrationRouting routing = MigrationRouting::LLM; + MigrationStatus status = MigrationStatus::Pending; + std::vector dependsOn; + bool requiresHumanReview = false; + std::string rollbackAnnotation; +}; + +struct PartialMigrationState { + std::string unitId; + std::string filePath; + bool migrated = false; + bool hasFfiBoundary = false; + std::string ffiAnnotation; +}; + +struct MigrationExecution { + std::string projectName; + std::string sourceLanguage; + std::string targetLanguage; + std::vector workItems; + std::vector partialState; + int currentPhase = 0; + + int countByStatus(MigrationStatus s) const { + int n = 0; + for (const auto& w : workItems) + if (w.status == s) ++n; + return n; + } + + bool isUnitMigrated(const std::string& unitId) const { + for (const auto& p : partialState) + if (p.unitId == unitId) return p.migrated; + return false; + } + + std::vector readyItems() const { + std::vector out; + for (const auto& w : workItems) { + if (w.status != MigrationStatus::Pending) continue; + bool blocked = false; + for (const auto& dep : w.dependsOn) { + for (const auto& other : workItems) { + if (other.id == dep && other.status != MigrationStatus::Completed) + blocked = true; + } + } + if (!blocked) out.push_back(w); + } + return out; + } + + int countFfiBoundaries() const { + int n = 0; + for (const auto& p : partialState) + if (p.hasFfiBoundary) ++n; + return n; + } +}; + +class MigrationExecutor { +public: + static MigrationExecution createExecution(const MigrationPlan& plan, + const std::vector& files) { + MigrationExecution exec; + exec.projectName = plan.projectName; + exec.sourceLanguage = plan.sourceLanguage; + exec.targetLanguage = plan.targetLanguage; + exec.currentPhase = 0; + + int idx = 0; + auto ordered = plan.orderedUnits(); + for (const auto& unit : ordered) { + MigrationWorkItem item; + item.id = "mig-" + std::to_string(idx++); + item.unitId = unit.id; + item.filePath = unit.filePath; + item.title = "Migrate " + unit.moduleName + " (" + + unit.sourceLanguage + " -> " + unit.targetLanguage + ")"; + item.routing = unit.routing; + item.requiresHumanReview = (unit.risk >= 3 || unit.routing == MigrationRouting::Human); + item.rollbackAnnotation = "@Rollback(revert_to=\"" + unit.filePath + "\")"; + + // Map unit dependencies to work item dependencies + for (const auto& depId : unit.dependsOn) { + for (const auto& wi : exec.workItems) { + if (wi.unitId == depId) + item.dependsOn.push_back(wi.id); + } + } + + exec.workItems.push_back(std::move(item)); + } + + // Initialize partial migration state + for (const auto& unit : plan.units) { + PartialMigrationState ps; + ps.unitId = unit.id; + ps.filePath = unit.filePath; + ps.migrated = false; + ps.hasFfiBoundary = false; + exec.partialState.push_back(std::move(ps)); + } + + return exec; + } + + // Mark a unit as completed and update FFI boundaries + static void completeUnit(MigrationExecution& exec, const std::string& workItemId) { + for (auto& w : exec.workItems) { + if (w.id == workItemId) { + w.status = MigrationStatus::Completed; + // Update partial state + for (auto& ps : exec.partialState) { + if (ps.unitId == w.unitId) { + ps.migrated = true; + } + } + } + } + // Update FFI boundaries for mixed-language state + updateFfiBoundaries(exec); + } + + // Mark a unit as failed (requires human review) + static void failUnit(MigrationExecution& exec, const std::string& workItemId) { + for (auto& w : exec.workItems) { + if (w.id == workItemId) { + w.status = MigrationStatus::Failed; + w.requiresHumanReview = true; + } + } + } + + // Check if execution is in a partial (mixed-language) state + static bool isPartialMigration(const MigrationExecution& exec) { + bool hasMigrated = false, hasUnmigrated = false; + for (const auto& ps : exec.partialState) { + if (ps.migrated) hasMigrated = true; + else hasUnmigrated = true; + } + return hasMigrated && hasUnmigrated; + } + +private: + static void updateFfiBoundaries(MigrationExecution& exec) { + for (auto& ps : exec.partialState) { + if (!ps.migrated) { + // Check if any migrated unit depends on this unmigrated one + // or this unmigrated one depends on a migrated one + bool needsFfi = false; + for (const auto& other : exec.partialState) { + if (other.unitId == ps.unitId) continue; + if (other.migrated != ps.migrated) { + // Check if they have a dependency relationship + for (const auto& w : exec.workItems) { + if (w.unitId == ps.unitId || w.unitId == other.unitId) { + for (const auto& dep : w.dependsOn) { + for (const auto& w2 : exec.workItems) { + if (w2.id == dep && + (w2.unitId == ps.unitId || w2.unitId == other.unitId)) + needsFfi = true; + } + } + } + } + } + } + ps.hasFfiBoundary = needsFfi; + if (needsFfi) + ps.ffiAnnotation = "@FFI(boundary=" + exec.sourceLanguage + + "/" + exec.targetLanguage + ")"; + } else { + ps.hasFfiBoundary = false; + ps.ffiAnnotation = ""; + } + } + } +}; diff --git a/editor/src/MigrationPlanGenerator.h b/editor/src/MigrationPlanGenerator.h new file mode 100644 index 0000000..24b4a6e --- /dev/null +++ b/editor/src/MigrationPlanGenerator.h @@ -0,0 +1,205 @@ +#pragma once + +// Step 444: Migration Plan Generator — analyzes a multi-file project, +// identifies migration units, determines order, and annotates with effort/risk/routing. + +#include +#include +#include + +enum class MigrationRouting { Deterministic, LLM, Human }; + +struct MigrationUnit { + std::string id; + std::string filePath; + std::string moduleName; + std::string sourceLanguage; + std::string targetLanguage; + int effort = 1; // 1=low, 2=medium, 3=high + int risk = 1; // 1=low, 2=medium, 3=high + MigrationRouting routing = MigrationRouting::LLM; + std::vector dependsOn; // unit IDs this depends on + std::vector exports; // public API symbols + std::vector imports; // symbols imported from other units + int phase = 0; // migration phase (0 = first) +}; + +struct MigrationPlan { + std::string projectName; + std::string sourceLanguage; + std::string targetLanguage; + std::vector units; + int phaseCount = 0; + + bool hasUnit(const std::string& filePath) const { + for (const auto& u : units) + if (u.filePath == filePath) return true; + return false; + } + + MigrationUnit unitByPath(const std::string& filePath) const { + for (const auto& u : units) + if (u.filePath == filePath) return u; + return {}; + } + + std::vector unitsInPhase(int phase) const { + std::vector out; + for (const auto& u : units) + if (u.phase == phase) out.push_back(u); + return out; + } + + std::vector orderedUnits() const { + auto sorted = units; + std::sort(sorted.begin(), sorted.end(), + [](const MigrationUnit& a, const MigrationUnit& b) { + if (a.phase != b.phase) return a.phase < b.phase; + return a.risk < b.risk; // lower risk first within phase + }); + return sorted; + } + + int countByRouting(MigrationRouting r) const { + int n = 0; + for (const auto& u : units) + if (u.routing == r) ++n; + return n; + } +}; + +struct FileInfo { + std::string filePath; + std::string source; + std::vector exports; // public function/type names + std::vector imports; // referenced external symbols +}; + +class MigrationPlanGenerator { +public: + static MigrationPlan generate(const std::string& projectName, + const std::vector& files, + const std::string& sourceLanguage, + const std::string& targetLanguage) { + MigrationPlan plan; + plan.projectName = projectName; + plan.sourceLanguage = sourceLanguage; + plan.targetLanguage = targetLanguage; + + // Create migration units from files + for (size_t i = 0; i < files.size(); ++i) { + MigrationUnit unit; + unit.id = "unit-" + std::to_string(i); + unit.filePath = files[i].filePath; + unit.moduleName = extractModuleName(files[i].filePath); + unit.sourceLanguage = sourceLanguage; + unit.targetLanguage = targetLanguage; + unit.exports = files[i].exports; + unit.imports = files[i].imports; + plan.units.push_back(std::move(unit)); + } + + // Resolve dependencies between units + resolveDependencies(plan); + + // Assign phases (topological order — leaves first) + assignPhases(plan); + + // Classify effort/risk/routing + for (auto& unit : plan.units) + classifyUnit(unit, files); + + return plan; + } + +private: + static std::string extractModuleName(const std::string& path) { + size_t slash = path.rfind('/'); + size_t dot = path.rfind('.'); + size_t start = (slash == std::string::npos) ? 0 : slash + 1; + size_t end = (dot == std::string::npos) ? path.size() : dot; + return path.substr(start, end - start); + } + + static void resolveDependencies(MigrationPlan& plan) { + // For each unit, find which other units provide its imports + for (auto& unit : plan.units) { + for (const auto& imp : unit.imports) { + for (const auto& other : plan.units) { + if (other.id == unit.id) continue; + for (const auto& exp : other.exports) { + if (exp == imp) { + unit.dependsOn.push_back(other.id); + } + } + } + } + // Deduplicate + std::sort(unit.dependsOn.begin(), unit.dependsOn.end()); + unit.dependsOn.erase( + std::unique(unit.dependsOn.begin(), unit.dependsOn.end()), + unit.dependsOn.end()); + } + } + + static void assignPhases(MigrationPlan& plan) { + // Leaf modules (no dependencies) go in phase 0 + // Units depending on phase-N units go in phase N+1 + int maxPhase = 0; + bool changed = true; + // Initialize all to phase 0 + for (auto& u : plan.units) u.phase = 0; + + while (changed) { + changed = false; + for (auto& unit : plan.units) { + for (const auto& depId : unit.dependsOn) { + for (const auto& dep : plan.units) { + if (dep.id == depId && dep.phase >= unit.phase) { + unit.phase = dep.phase + 1; + maxPhase = std::max(maxPhase, unit.phase); + changed = true; + } + } + } + } + } + plan.phaseCount = maxPhase + 1; + } + + static void classifyUnit(MigrationUnit& unit, + const std::vector& files) { + // Find corresponding file + const FileInfo* fi = nullptr; + for (const auto& f : files) + if (f.filePath == unit.filePath) { fi = &f; break; } + if (!fi) return; + + int lineCount = countLines(fi->source); + int exportCount = (int)fi->exports.size(); + + // Effort based on size + if (lineCount > 200) unit.effort = 3; + else if (lineCount > 50) unit.effort = 2; + else unit.effort = 1; + + // Risk based on API surface + dependencies + if (exportCount > 5 || !unit.dependsOn.empty()) unit.risk = 3; + else if (exportCount > 2) unit.risk = 2; + else unit.risk = 1; + + // Routing + if (unit.effort == 1 && unit.risk == 1) + unit.routing = MigrationRouting::Deterministic; + else if (unit.effort == 3 || unit.risk == 3) + unit.routing = MigrationRouting::Human; + else + unit.routing = MigrationRouting::LLM; + } + + static int countLines(const std::string& src) { + int n = 1; + for (char c : src) if (c == '\n') ++n; + return n; + } +}; diff --git a/editor/src/MigrationTestGenerator.h b/editor/src/MigrationTestGenerator.h new file mode 100644 index 0000000..dfa5bcc --- /dev/null +++ b/editor/src/MigrationTestGenerator.h @@ -0,0 +1,213 @@ +#pragma once + +// Step 446: Test Generation for Migration Validation — auto-generates +// equivalence, edge case, and performance regression tests for migrated modules. + +#include "APIBoundaryPreserver.h" + +#include +#include + +enum class MigrationTestKind { Equivalence, EdgeCase, Performance }; +enum class TestRouting { SLM, LLM }; + +struct MigrationTest { + std::string id; + std::string functionName; + std::string title; + std::string testBody; // skeleton test code + std::string language; // target language + MigrationTestKind kind = MigrationTestKind::Equivalence; + TestRouting routing = TestRouting::SLM; +}; + +struct MigrationTestSuite { + std::string moduleName; + std::string targetLanguage; + std::vector tests; + + int countByKind(MigrationTestKind k) const { + int n = 0; + for (const auto& t : tests) + if (t.kind == k) ++n; + return n; + } + + bool hasTestFor(const std::string& funcName) const { + for (const auto& t : tests) + if (t.functionName == funcName) return true; + return false; + } + + std::vector testsForFunction(const std::string& funcName) const { + std::vector out; + for (const auto& t : tests) + if (t.functionName == funcName) out.push_back(t); + return out; + } +}; + +class MigrationTestGenerator { +public: + static MigrationTestSuite generate(const APIBoundaryReport& api) { + MigrationTestSuite suite; + suite.moduleName = api.moduleName; + suite.targetLanguage = api.targetLanguage; + + int idx = 0; + for (const auto& func : api.publicAPI) { + // Equivalence test for each public function + generateEquivalenceTest(func, api, suite, idx); + ++idx; + + // Edge case tests from contracts + generateEdgeCaseTests(func, api, suite, idx); + + // Performance regression test + generatePerformanceTest(func, api, suite, idx); + ++idx; + } + + return suite; + } + +private: + static void generateEquivalenceTest(const FunctionSignature& func, + const APIBoundaryReport& api, + MigrationTestSuite& suite, int& idx) { + MigrationTest test; + test.id = "test-eq-" + std::to_string(idx); + test.functionName = func.name; + test.title = "equivalence: " + func.name + " produces same output"; + test.language = api.targetLanguage; + test.kind = MigrationTestKind::Equivalence; + test.routing = TestRouting::SLM; + test.testBody = generateEquivalenceBody(func, api.targetLanguage); + suite.tests.push_back(std::move(test)); + } + + static void generateEdgeCaseTests(const FunctionSignature& func, + const APIBoundaryReport& api, + MigrationTestSuite& suite, int& idx) { + // Generate edge case tests from contract annotations + for (const auto& contract : api.contracts) { + if (contract.functionName != func.name) continue; + + // Null pointer edge case + if (contract.precondition.find("NULL") != std::string::npos) { + MigrationTest test; + test.id = "test-edge-" + std::to_string(idx++); + test.functionName = func.name; + test.title = "edge case: " + func.name + " null input handling"; + test.language = api.targetLanguage; + test.kind = MigrationTestKind::EdgeCase; + test.routing = TestRouting::LLM; + test.testBody = generateNullEdgeCaseBody(func, api.targetLanguage); + suite.tests.push_back(std::move(test)); + } + + // Return value edge case + if (!contract.postcondition.empty()) { + MigrationTest test; + test.id = "test-edge-" + std::to_string(idx++); + test.functionName = func.name; + test.title = "edge case: " + func.name + " return value contract"; + test.language = api.targetLanguage; + test.kind = MigrationTestKind::EdgeCase; + test.routing = TestRouting::LLM; + test.testBody = generateReturnEdgeCaseBody(func, contract, api.targetLanguage); + suite.tests.push_back(std::move(test)); + } + } + } + + static void generatePerformanceTest(const FunctionSignature& func, + const APIBoundaryReport& api, + MigrationTestSuite& suite, int& idx) { + MigrationTest test; + test.id = "test-perf-" + std::to_string(idx); + test.functionName = func.name; + test.title = "performance: " + func.name + " not slower than original"; + test.language = api.targetLanguage; + test.kind = MigrationTestKind::Performance; + test.routing = TestRouting::LLM; + test.testBody = generatePerformanceBody(func, api.targetLanguage); + suite.tests.push_back(std::move(test)); + } + + // --- Test body generators by target language --- + + static std::string generateEquivalenceBody(const FunctionSignature& func, + const std::string& lang) { + if (lang == "rust") { + return "#[test]\nfn test_" + func.name + "_equivalence() {\n" + " // TODO: call " + func.name + " with known inputs\n" + " // assert_eq!(result, expected_from_c_implementation);\n" + "}\n"; + } + if (lang == "java") { + return "@Test\npublic void test" + capitalize(func.name) + "Equivalence() {\n" + " // TODO: call " + func.name + " with known inputs\n" + " // assertEquals(expected, result);\n" + "}\n"; + } + if (lang == "python") { + return "def test_" + func.name + "_equivalence():\n" + " # TODO: call " + func.name + " with known inputs\n" + " # assert result == expected_from_c\n"; + } + return "// TODO: " + func.name + " equivalence test\n"; + } + + static std::string generateNullEdgeCaseBody(const FunctionSignature& func, + const std::string& lang) { + if (lang == "rust") { + return "#[test]\nfn test_" + func.name + "_null_input() {\n" + " // TODO: pass None/null equivalent, verify behavior\n" + "}\n"; + } + if (lang == "java") { + return "@Test(expected = NullPointerException.class)\n" + "public void test" + capitalize(func.name) + "NullInput() {\n" + " // TODO: pass null, verify exception\n" + "}\n"; + } + return "// TODO: " + func.name + " null edge case\n"; + } + + static std::string generateReturnEdgeCaseBody(const FunctionSignature& func, + const ContractAnnotation& contract, + const std::string& lang) { + if (lang == "rust") { + return "#[test]\nfn test_" + func.name + "_return_contract() {\n" + " // Contract: " + contract.postcondition + "\n" + " // TODO: verify return value matches contract\n" + "}\n"; + } + return "// Contract: " + contract.postcondition + "\n" + "// TODO: verify " + func.name + " return contract\n"; + } + + static std::string generatePerformanceBody(const FunctionSignature& func, + const std::string& lang) { + if (lang == "rust") { + return "#[test]\nfn test_" + func.name + "_performance() {\n" + " // TODO: benchmark " + func.name + " and compare with C baseline\n" + " // assert!(elapsed < c_baseline * 1.1); // allow 10% regression\n" + "}\n"; + } + if (lang == "java") { + return "@Test\npublic void test" + capitalize(func.name) + "Performance() {\n" + " // TODO: benchmark and compare with C baseline\n" + "}\n"; + } + return "// TODO: " + func.name + " performance regression test\n"; + } + + static std::string capitalize(const std::string& s) { + if (s.empty()) return s; + std::string result = s; + result[0] = (char)std::toupper((unsigned char)result[0]); + return result; + } +}; diff --git a/editor/src/ModernizationRPC.h b/editor/src/ModernizationRPC.h new file mode 100644 index 0000000..e9e0a50 --- /dev/null +++ b/editor/src/ModernizationRPC.h @@ -0,0 +1,242 @@ +#pragma once + +// Step 442: Modernization RPC + MCP tool definitions. +// Provides JSON-RPC method handlers and MCP tool metadata for the +// legacy analysis → safety audit → modernization suggestion → workflow pipeline. + +#include "LegacyIdiomDetector.h" +#include "SafetyAuditor.h" +#include "ModernizationSuggester.h" +#include "ModernizationWorkflow.h" + +#include +#include +#include + +using json = nlohmann::json; + +// --- JSON serialization helpers --- + +inline json findingToJson(const LegacyIdiomFinding& f) { + return {{"pattern", f.pattern}, {"detail", f.detail}, {"score", f.score}}; +} + +inline json safetyFindingToJson(const SafetyFinding& f) { + return {{"category", f.category}, {"annotation", f.annotation}, + {"detail", f.detail}, {"cwe", f.cwe}, {"riskLevel", f.riskLevel}}; +} + +inline json suggestionToJson(const ModernizeSuggestion& s) { + std::string effort = "quick_win"; + if (s.effort == ModernizeEffort::Moderate) effort = "moderate"; + if (s.effort == ModernizeEffort::DeepRefactor) effort = "deep_refactor"; + return {{"from", s.fromPattern}, {"to", s.toReplacement}, + {"annotation", s.annotation}, {"risk", s.risk}, + {"effort", effort}, {"rationale", s.rationale}}; +} + +inline json workItemToJson(const ModernizeWorkItem& item) { + std::string routing = "deterministic"; + if (item.routing == WorkItemRouting::LLM) routing = "llm"; + if (item.routing == WorkItemRouting::Human) routing = "human"; + std::string effort = "quick_win"; + if (item.effort == ModernizeEffort::Moderate) effort = "moderate"; + if (item.effort == ModernizeEffort::DeepRefactor) effort = "deep_refactor"; + return {{"id", item.id}, {"title", item.title}, + {"from", item.fromPattern}, {"to", item.toReplacement}, + {"annotation", item.annotation}, {"routing", routing}, + {"effort", effort}, {"priority", item.priority}, + {"dependsOn", item.dependsOn}}; +} + +// --- RPC method handlers --- + +class ModernizationRPCHandler { +public: + // analyzeLegacy: run legacy idiom detection on source + static json handleAnalyzeLegacy(const json& params) { + std::string source = params.value("source", ""); + std::string language = params.value("language", "c"); + std::string filePath = params.value("filePath", "unknown"); + + auto report = LegacyIdiomDetector::analyzeFile(source, language, filePath); + + json findings = json::array(); + for (const auto& f : report.findings) + findings.push_back(findingToJson(f)); + + json fnFindings = json::array(); + for (const auto& ff : report.functionFindings) { + json ffs = json::array(); + for (const auto& f : ff.findings) + ffs.push_back(findingToJson(f)); + fnFindings.push_back({{"functionName", ff.functionName}, + {"findings", ffs}, + {"legacyScore", ff.legacyScore}}); + } + + return {{"filePath", report.filePath}, + {"language", report.language}, + {"languageVersion", report.languageVersion}, + {"legacyScore", report.legacyScore}, + {"findings", findings}, + {"functionFindings", fnFindings}}; + } + + // getSafetyReport: run safety audit on source + static json handleGetSafetyReport(const json& params) { + std::string source = params.value("source", ""); + std::string language = params.value("language", "c"); + std::string filePath = params.value("filePath", "unknown"); + + auto report = SafetyAuditor::audit(source, language, filePath); + + json findings = json::array(); + for (const auto& f : report.findings) + findings.push_back(safetyFindingToJson(f)); + + json fnFindings = json::array(); + for (const auto& ff : report.functionFindings) { + json ffs = json::array(); + for (const auto& f : ff.findings) + ffs.push_back(safetyFindingToJson(f)); + fnFindings.push_back({{"functionName", ff.functionName}, + {"findings", ffs}, + {"maxRisk", ff.maxRisk}}); + } + + return {{"filePath", report.filePath}, + {"language", report.language}, + {"overallRisk", report.overallRisk}, + {"findings", findings}, + {"functionFindings", fnFindings}}; + } + + // suggestModernization: generate modernization suggestions + static json handleSuggestModernization(const json& params) { + std::string source = params.value("source", ""); + std::string language = params.value("language", "c"); + std::string filePath = params.value("filePath", "unknown"); + std::string targetLang = params.value("targetLanguage", ""); + + auto legacy = LegacyIdiomDetector::analyzeFile(source, language, filePath); + auto safety = SafetyAuditor::audit(source, language, filePath); + auto report = ModernizationSuggester::suggest(legacy, safety, targetLang); + + json suggestions = json::array(); + for (const auto& s : report.suggestions) + suggestions.push_back(suggestionToJson(s)); + + return {{"filePath", report.filePath}, + {"sourceLanguage", report.sourceLanguage}, + {"targetLanguage", report.targetLanguage}, + {"quickWinCount", report.countByEffort(ModernizeEffort::QuickWin)}, + {"moderateCount", report.countByEffort(ModernizeEffort::Moderate)}, + {"deepRefactorCount", report.countByEffort(ModernizeEffort::DeepRefactor)}, + {"suggestions", suggestions}}; + } + + // createModernizationWorkflow: full pipeline → workflow + static json handleCreateWorkflow(const json& params) { + std::string source = params.value("source", ""); + std::string language = params.value("language", "c"); + std::string filePath = params.value("filePath", "unknown"); + std::string targetLang = params.value("targetLanguage", ""); + + auto legacy = LegacyIdiomDetector::analyzeFile(source, language, filePath); + auto safety = SafetyAuditor::audit(source, language, filePath); + auto report = ModernizationSuggester::suggest(legacy, safety, targetLang); + auto wf = ModernizationWorkflowGenerator::generate(source, report); + + json items = json::array(); + for (const auto& item : wf.itemsInOrder()) + items.push_back(workItemToJson(item)); + + return {{"filePath", wf.filePath}, + {"sourceLanguage", wf.sourceLanguage}, + {"targetLanguage", wf.targetLanguage}, + {"itemCount", (int)wf.items.size()}, + {"deterministicCount", wf.countByRouting(WorkItemRouting::Deterministic)}, + {"llmCount", wf.countByRouting(WorkItemRouting::LLM)}, + {"humanCount", wf.countByRouting(WorkItemRouting::Human)}, + {"items", items}, + {"skeleton", { + {"language", wf.skeleton.language}, + {"annotations", wf.skeleton.annotations}, + {"source", wf.skeleton.skeletonSource} + }}}; + } + + // Dispatch: route method name to handler + static bool canHandle(const std::string& method) { + return method == "analyzeLegacy" || + method == "getSafetyReport" || + method == "suggestModernization" || + method == "createModernizationWorkflow"; + } + + static json dispatch(const std::string& method, const json& params, const json& id) { + json result; + if (method == "analyzeLegacy") + result = handleAnalyzeLegacy(params); + else if (method == "getSafetyReport") + result = handleGetSafetyReport(params); + else if (method == "suggestModernization") + result = handleSuggestModernization(params); + else if (method == "createModernizationWorkflow") + result = handleCreateWorkflow(params); + else + return {{"jsonrpc", "2.0"}, {"id", id}, + {"error", {{"code", -32601}, {"message", "Unknown method"}}}}; + return {{"jsonrpc", "2.0"}, {"id", id}, {"result", result}}; + } + + // --- MCP Tool Definitions --- + + struct MCPToolDef { + std::string name; + std::string description; + json inputSchema; + }; + + static std::vector toolDefinitions() { + json sourceSchema = { + {"type", "object"}, + {"properties", { + {"source", {{"type", "string"}, {"description", "Source code to analyze"}}}, + {"language", {{"type", "string"}, {"description", "Language: c, cpp, python, etc."}}}, + {"filePath", {{"type", "string"}, {"description", "File path for reporting"}}} + }}, + {"required", json::array({"source"})} + }; + + json modernizeSchema = sourceSchema; + modernizeSchema["properties"]["targetLanguage"] = { + {"type", "string"}, + {"description", "Target language for cross-language modernization (optional)"} + }; + + return { + {"whetstone_analyze_legacy", + "Run legacy idiom detection on source code. Detects K&R declarations, " + "goto flow, deprecated APIs (gets/sprintf/strcpy), pointer arithmetic, " + "manual memory management. Returns legacy score (0-10) and findings.", + sourceSchema}, + {"whetstone_get_safety_report", + "Run structured safety audit on source code. Detects buffer overflow, " + "use-after-free, null dereference, race conditions, integer overflow. " + "Maps findings to CWE codes with risk levels (0-4).", + sourceSchema}, + {"whetstone_suggest_modernization", + "Generate modernization suggestions for legacy code. Maps each legacy " + "pattern to a modern replacement with @Modernize annotations. Groups " + "suggestions by effort: quick wins, moderate, deep refactors.", + modernizeSchema}, + {"whetstone_create_modernization_workflow", + "Full pipeline: analyze legacy code → safety audit → suggest modernization " + "→ generate executable workflow. Returns ordered work items with routing " + "(deterministic/LLM/human) and a modernized skeleton.", + modernizeSchema} + }; + } +}; diff --git a/editor/src/ModernizationSuggester.h b/editor/src/ModernizationSuggester.h new file mode 100644 index 0000000..2343603 --- /dev/null +++ b/editor/src/ModernizationSuggester.h @@ -0,0 +1,192 @@ +#pragma once + +// Step 440: Modernization suggestions — maps legacy patterns to modern replacements +// with @Modernize annotations and effort classification. + +#include "LegacyIdiomDetector.h" +#include "SafetyAuditor.h" + +#include +#include + +enum class ModernizeEffort { QuickWin, Moderate, DeepRefactor }; + +struct ModernizeSuggestion { + std::string fromPattern; + std::string toReplacement; + std::string annotation; // @Modernize(from=..., to=..., risk=...) + std::string risk; // "low", "medium", "high" + ModernizeEffort effort = ModernizeEffort::QuickWin; + std::string rationale; +}; + +struct ModernizationReport { + std::string filePath; + std::string sourceLanguage; + std::string targetLanguage; // may differ for cross-language modernization + std::vector suggestions; + + int countByEffort(ModernizeEffort e) const { + int n = 0; + for (const auto& s : suggestions) + if (s.effort == e) ++n; + return n; + } + + bool hasSuggestionFrom(const std::string& from) const { + for (const auto& s : suggestions) + if (s.fromPattern == from) return true; + return false; + } + + bool hasSuggestionTo(const std::string& to) const { + for (const auto& s : suggestions) + if (s.toReplacement == to) return true; + return false; + } + + std::vector quickWins() const { + std::vector out; + for (const auto& s : suggestions) + if (s.effort == ModernizeEffort::QuickWin) out.push_back(s); + return out; + } + + std::vector deepRefactors() const { + std::vector out; + for (const auto& s : suggestions) + if (s.effort == ModernizeEffort::DeepRefactor) out.push_back(s); + return out; + } +}; + +class ModernizationSuggester { +public: + static ModernizationReport suggest(const LegacyAnalysisReport& legacy, + const SafetyReport& safety, + const std::string& targetLanguage = "") { + ModernizationReport report; + report.filePath = legacy.filePath; + report.sourceLanguage = legacy.language; + report.targetLanguage = targetLanguage.empty() ? legacy.language : targetLanguage; + + suggestFromLegacyFindings(legacy, report); + suggestFromSafetyFindings(safety, report); + suggestCrossLanguage(legacy, report); + return report; + } + +private: + static void suggestFromLegacyFindings(const LegacyAnalysisReport& legacy, + ModernizationReport& r) { + for (const auto& f : legacy.findings) { + if (f.pattern == "manual-memory-management") { + r.suggestions.push_back({ + "malloc/free", "std::unique_ptr / std::shared_ptr", + "@Modernize(from=\"malloc/free\", to=\"smart_pointers\", risk=\"medium\")", + "medium", ModernizeEffort::Moderate, + "Replace manual memory management with RAII smart pointers" + }); + } + if (f.pattern == "deprecated-api-sprintf") { + r.suggestions.push_back({ + "sprintf", "std::format / snprintf", + "@Modernize(from=\"sprintf\", to=\"std::format\", risk=\"low\")", + "low", ModernizeEffort::QuickWin, + "Replace sprintf with bounds-checked snprintf or C++20 std::format" + }); + } + if (f.pattern == "deprecated-api-strcpy") { + r.suggestions.push_back({ + "strcpy", "std::string / strncpy", + "@Modernize(from=\"strcpy\", to=\"std::string\", risk=\"low\")", + "low", ModernizeEffort::QuickWin, + "Replace strcpy with std::string or bounds-checked strncpy" + }); + } + if (f.pattern == "deprecated-api-gets") { + r.suggestions.push_back({ + "gets", "fgets / std::getline", + "@Modernize(from=\"gets\", to=\"fgets\", risk=\"low\")", + "low", ModernizeEffort::QuickWin, + "Replace gets with fgets (C) or std::getline (C++)" + }); + } + if (f.pattern == "goto-control-flow") { + r.suggestions.push_back({ + "goto", "structured control flow", + "@Modernize(from=\"goto\", to=\"structured_control\", risk=\"high\")", + "high", ModernizeEffort::DeepRefactor, + "Refactor goto-based control flow to loops/conditionals/early returns" + }); + } + if (f.pattern == "pointer-arithmetic") { + r.suggestions.push_back({ + "pointer arithmetic", "std::span / iterators", + "@Modernize(from=\"pointer_arithmetic\", to=\"std::span\", risk=\"medium\")", + "medium", ModernizeEffort::Moderate, + "Replace raw pointer arithmetic with std::span or container iterators" + }); + } + if (f.pattern == "knr-declaration") { + r.suggestions.push_back({ + "K&R declarations", "ANSI C prototypes", + "@Modernize(from=\"knr_decl\", to=\"ansi_prototype\", risk=\"low\")", + "low", ModernizeEffort::QuickWin, + "Convert K&R function declarations to ANSI C style" + }); + } + } + } + + static void suggestFromSafetyFindings(const SafetyReport& safety, + ModernizationReport& r) { + for (const auto& f : safety.findings) { + if (f.category == "race-condition" && f.annotation == "@Sync(none)") { + r.suggestions.push_back({ + "unprotected shared state", "std::mutex / std::atomic", + "@Modernize(from=\"unprotected_shared\", to=\"std::mutex\", risk=\"high\")", + "high", ModernizeEffort::DeepRefactor, + "Add synchronization primitives to protect shared mutable state" + }); + } + if (f.category == "integer-overflow") { + r.suggestions.push_back({ + "unchecked arithmetic", "checked arithmetic / SafeInt", + "@Modernize(from=\"unchecked_arithmetic\", to=\"checked_arithmetic\", risk=\"medium\")", + "medium", ModernizeEffort::Moderate, + "Add overflow checks or use safe integer types" + }); + } + } + } + + static void suggestCrossLanguage(const LegacyAnalysisReport& legacy, + ModernizationReport& r) { + if (r.targetLanguage == r.sourceLanguage) return; + + std::string tgt = r.targetLanguage; + if (tgt == "rust") { + // C/C++ → Rust: ownership model replaces manual memory + if (legacy.hasPattern("manual-memory-management") || + legacy.hasPattern("pointer-arithmetic")) { + r.suggestions.push_back({ + "manual memory (C/C++)", "Rust ownership + borrow checker", + "@Modernize(from=\"manual_memory\", to=\"rust_ownership\", risk=\"high\")", + "high", ModernizeEffort::DeepRefactor, + "Rust's ownership model eliminates manual memory management entirely" + }); + } + } + if (tgt == "java" || tgt == "python") { + if (legacy.hasPattern("manual-memory-management")) { + r.suggestions.push_back({ + "manual memory (C/C++)", "garbage collection (" + tgt + ")", + "@Modernize(from=\"manual_memory\", to=\"gc_" + tgt + "\", risk=\"medium\")", + "medium", ModernizeEffort::Moderate, + "Target language uses GC — manual memory management eliminated" + }); + } + } + } +}; diff --git a/editor/src/ModernizationWorkflow.h b/editor/src/ModernizationWorkflow.h new file mode 100644 index 0000000..0b1ea0f --- /dev/null +++ b/editor/src/ModernizationWorkflow.h @@ -0,0 +1,179 @@ +#pragma once + +// Step 441: Modernization Workflow Generation — converts modernization suggestions +// into an ordered workflow with routing (deterministic/LLM/human) and dependencies. + +#include "ModernizationSuggester.h" + +#include +#include + +enum class WorkItemRouting { Deterministic, LLM, Human }; + +struct ModernizeWorkItem { + std::string id; + std::string title; + std::string fromPattern; + std::string toReplacement; + std::string annotation; + WorkItemRouting routing = WorkItemRouting::Deterministic; + ModernizeEffort effort = ModernizeEffort::QuickWin; + int priority = 0; // lower = execute first + std::vector dependsOn; +}; + +struct ModernizationSkeleton { + std::string originalSource; + std::string skeletonSource; // modernized structure with placeholders + std::string language; + std::vector annotations; +}; + +struct ModernizationWorkflow { + std::string filePath; + std::string sourceLanguage; + std::string targetLanguage; + std::vector items; + ModernizationSkeleton skeleton; + + int countByRouting(WorkItemRouting r) const { + int n = 0; + for (const auto& item : items) + if (item.routing == r) ++n; + return n; + } + + bool hasItemFor(const std::string& from) const { + for (const auto& item : items) + if (item.fromPattern == from) return true; + return false; + } + + std::vector itemsInOrder() const { + auto sorted = items; + std::sort(sorted.begin(), sorted.end(), + [](const ModernizeWorkItem& a, const ModernizeWorkItem& b) { + return a.priority < b.priority; + }); + return sorted; + } + + ModernizeWorkItem itemById(const std::string& id) const { + for (const auto& item : items) + if (item.id == id) return item; + return {}; + } +}; + +class ModernizationWorkflowGenerator { +public: + static ModernizationWorkflow generate(const std::string& source, + const ModernizationReport& report) { + ModernizationWorkflow wf; + wf.filePath = report.filePath; + wf.sourceLanguage = report.sourceLanguage; + wf.targetLanguage = report.targetLanguage; + + // Convert suggestions to work items with routing and priority + int idx = 0; + for (const auto& s : report.suggestions) { + ModernizeWorkItem item; + item.id = "mod-" + std::to_string(idx++); + item.title = s.fromPattern + " -> " + s.toReplacement; + item.fromPattern = s.fromPattern; + item.toReplacement = s.toReplacement; + item.annotation = s.annotation; + item.effort = s.effort; + item.routing = routeByEffort(s); + item.priority = priorityForEffort(s.effort); + wf.items.push_back(std::move(item)); + } + + // Set dependencies: deep refactors depend on quick wins completing first + setDependencies(wf); + + // Generate skeleton + wf.skeleton = generateSkeleton(source, report); + + return wf; + } + +private: + static WorkItemRouting routeByEffort(const ModernizeSuggestion& s) { + if (s.effort == ModernizeEffort::QuickWin) + return WorkItemRouting::Deterministic; + if (s.effort == ModernizeEffort::DeepRefactor) + return WorkItemRouting::Human; + // Moderate: depends on risk + if (s.risk == "high") + return WorkItemRouting::Human; + return WorkItemRouting::LLM; + } + + static int priorityForEffort(ModernizeEffort e) { + switch (e) { + case ModernizeEffort::QuickWin: return 0; + case ModernizeEffort::Moderate: return 1; + case ModernizeEffort::DeepRefactor: return 2; + } + return 1; + } + + static void setDependencies(ModernizationWorkflow& wf) { + // Collect quick-win IDs + std::vector quickWinIds; + for (const auto& item : wf.items) + if (item.effort == ModernizeEffort::QuickWin) + quickWinIds.push_back(item.id); + + // Deep refactors depend on all quick wins + for (auto& item : wf.items) { + if (item.effort == ModernizeEffort::DeepRefactor) { + item.dependsOn = quickWinIds; + } + } + } + + static ModernizationSkeleton generateSkeleton(const std::string& source, + const ModernizationReport& report) { + ModernizationSkeleton sk; + sk.originalSource = source; + sk.language = report.targetLanguage; + + // Collect annotations + for (const auto& s : report.suggestions) + sk.annotations.push_back(s.annotation); + + // Build skeleton: replace known patterns with placeholders + std::string skel = source; + for (const auto& s : report.suggestions) { + if (s.fromPattern == "malloc/free") { + replaceAll(skel, "malloc(", "/* @Modernize: smart_ptr */ malloc("); + replaceAll(skel, "free(", "/* @Modernize: remove_free */ free("); + } + if (s.fromPattern == "gets") { + replaceAll(skel, "gets(", "/* @Modernize: fgets */ gets("); + } + if (s.fromPattern == "sprintf") { + replaceAll(skel, "sprintf(", "/* @Modernize: snprintf */ sprintf("); + } + if (s.fromPattern == "strcpy") { + replaceAll(skel, "strcpy(", "/* @Modernize: strncpy */ strcpy("); + } + if (s.fromPattern == "goto") { + replaceAll(skel, "goto ", "/* @Modernize: structured_control */ goto "); + } + } + sk.skeletonSource = skel; + return sk; + } + + static void replaceAll(std::string& str, const std::string& from, + const std::string& to) { + size_t pos = 0; + while ((pos = str.find(from, pos)) != std::string::npos) { + str.replace(pos, from.size(), to); + pos += to.size(); + } + } +}; diff --git a/editor/src/SafetyAuditor.h b/editor/src/SafetyAuditor.h new file mode 100644 index 0000000..a15f2e4 --- /dev/null +++ b/editor/src/SafetyAuditor.h @@ -0,0 +1,289 @@ +#pragma once + +// Step 439: Safety Audit via Annotations — structured safety analysis +// with CWE mapping and risk-level classification. + +#include +#include +#include +#include + +struct SafetyFinding { + std::string category; // e.g. "buffer-overflow", "use-after-free" + std::string annotation; // e.g. "@BoundsCheck(unchecked)" + std::string detail; + std::string cwe; // CWE code, e.g. "CWE-120" + int riskLevel = 0; // 0=info, 1=low, 2=medium, 3=high, 4=critical +}; + +struct FunctionSafetyFinding { + std::string functionName; + std::vector findings; + int maxRisk = 0; +}; + +struct SafetyReport { + std::string filePath; + std::string language; + int overallRisk = 0; // max risk across all findings + std::vector findings; + std::vector functionFindings; + + bool hasCategory(const std::string& cat) const { + for (const auto& f : findings) + if (f.category == cat) return true; + return false; + } + + bool hasCwe(const std::string& code) const { + for (const auto& f : findings) + if (f.cwe == code) return true; + return false; + } + + int countByRisk(int level) const { + int n = 0; + for (const auto& f : findings) + if (f.riskLevel == level) ++n; + return n; + } +}; + +class SafetyAuditor { +public: + static SafetyReport audit(const std::string& source, + const std::string& language, + const std::string& filePath) { + SafetyReport report; + report.filePath = filePath; + report.language = toLower(language); + + detectBufferOverflow(source, report); + detectUseAfterFree(source, report); + detectNullDeref(source, report); + detectRaceCondition(source, report); + detectIntegerOverflow(source, report); + + report.overallRisk = 0; + for (const auto& f : report.findings) + report.overallRisk = std::max(report.overallRisk, f.riskLevel); + + report.functionFindings = buildFunctionFindings(source, report.findings); + return report; + } + +private: + // --- Buffer overflow (CWE-120, CWE-676) --- + static void detectBufferOverflow(const std::string& src, SafetyReport& r) { + // Unchecked array indexing without bounds check + if (hasPattern(src, "gets(")) { + r.findings.push_back({ + "buffer-overflow", "@BoundsCheck(unchecked)", + "gets() has no bounds checking — classic buffer overflow", + "CWE-120", 4 + }); + } + if (hasPattern(src, "strcpy(")) { + r.findings.push_back({ + "buffer-overflow", "@BoundsCheck(unchecked)", + "strcpy() does not check destination buffer size", + "CWE-120", 3 + }); + } + if (hasPattern(src, "sprintf(")) { + r.findings.push_back({ + "buffer-overflow", "@BoundsCheck(unchecked)", + "sprintf() does not check buffer bounds", + "CWE-120", 3 + }); + } + if (hasPattern(src, "strcat(")) { + r.findings.push_back({ + "buffer-overflow", "@BoundsCheck(unchecked)", + "strcat() does not check destination capacity", + "CWE-120", 3 + }); + } + // Array access with no visible bounds check + if (hasUncheckedArrayAccess(src)) { + r.findings.push_back({ + "buffer-overflow", "@BoundsCheck(unchecked)", + "array access without visible bounds check", + "CWE-787", 2 + }); + } + } + + // --- Use-after-free (CWE-416) --- + static void detectUseAfterFree(const std::string& src, SafetyReport& r) { + bool hasFree = hasPattern(src, "free("); + bool hasMalloc = hasPattern(src, "malloc(") || hasPattern(src, "calloc("); + bool hasRaii = hasPattern(src, "std::unique_ptr") || + hasPattern(src, "std::shared_ptr"); + if (hasFree && hasMalloc && !hasRaii) { + r.findings.push_back({ + "use-after-free", "@Owner(Manual)", + "manual malloc/free without RAII — use-after-free risk", + "CWE-416", 3 + }); + } + // Dangling pointer after free — simple heuristic: free() followed by usage + if (hasFreeFollowedByDeref(src)) { + r.findings.push_back({ + "use-after-free", "@Lifetime(dangling)", + "potential dereference after free()", + "CWE-416", 4 + }); + } + } + + // --- Null dereference (CWE-476) --- + static void detectNullDeref(const std::string& src, SafetyReport& r) { + bool hasNull = hasPattern(src, "NULL") || hasPattern(src, "nullptr"); + bool hasDeref = hasPattern(src, "*") || hasPattern(src, "->"); + bool hasCheck = hasPattern(src, "!= NULL") || hasPattern(src, "!= nullptr") || + hasPattern(src, "== NULL") || hasPattern(src, "== nullptr") || + hasPattern(src, "if (p)") || hasPattern(src, "if(p)"); + if (hasNull && hasDeref && !hasCheck) { + r.findings.push_back({ + "null-dereference", "@Nullability(nullable)", + "nullable pointer used without null check", + "CWE-476", 3 + }); + } + // Return value of malloc unchecked + if (hasPattern(src, "malloc(") && !hasPattern(src, "if (") && + !hasPattern(src, "if(") && !hasPattern(src, "assert(")) { + r.findings.push_back({ + "null-dereference", "@Nullability(unchecked-alloc)", + "malloc() return value not checked for NULL", + "CWE-476", 2 + }); + } + } + + // --- Race condition (CWE-362) --- + static void detectRaceCondition(const std::string& src, SafetyReport& r) { + bool hasMutableGlobal = hasGlobalMutableState(src); + bool hasThread = hasPattern(src, "pthread_") || hasPattern(src, "std::thread") || + hasPattern(src, "CreateThread") || hasPattern(src, "thrd_create"); + bool hasSync = hasPattern(src, "mutex") || hasPattern(src, "std::atomic") || + hasPattern(src, "pthread_mutex") || hasPattern(src, "critical_section") || + hasPattern(src, "std::lock_guard") || hasPattern(src, "std::unique_lock"); + + if (hasMutableGlobal && hasThread && !hasSync) { + r.findings.push_back({ + "race-condition", "@Sync(none)", + "shared mutable state with threading but no synchronization", + "CWE-362", 3 + }); + } + if (hasMutableGlobal && !hasSync && !hasThread) { + // Weaker signal — global mutable state without protection + r.findings.push_back({ + "race-condition", "@Sync(unprotected-global)", + "global mutable state without synchronization primitives", + "CWE-362", 1 + }); + } + } + + // --- Integer overflow (CWE-190) --- + static void detectIntegerOverflow(const std::string& src, SafetyReport& r) { + bool hasArith = hasPattern(src, " + ") || hasPattern(src, " * ") || + hasPattern(src, "+=") || hasPattern(src, "*="); + bool hasCheck = hasPattern(src, "INT_MAX") || hasPattern(src, "UINT_MAX") || + hasPattern(src, "std::numeric_limits") || + hasPattern(src, "__builtin_add_overflow") || + hasPattern(src, "__builtin_mul_overflow") || + hasPattern(src, "SafeInt") || hasPattern(src, "CheckedInt"); + bool hasIntType = hasPattern(src, "int ") || hasPattern(src, "size_t ") || + hasPattern(src, "unsigned "); + if (hasArith && hasIntType && !hasCheck) { + r.findings.push_back({ + "integer-overflow", "@Overflow(unchecked)", + "integer arithmetic without overflow check", + "CWE-190", 2 + }); + } + } + + // --- Helpers --- + static bool hasPattern(const std::string& src, const std::string& pat) { + return src.find(pat) != std::string::npos; + } + + static std::string toLower(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) { return (char)std::tolower(c); }); + return s; + } + + static bool hasUncheckedArrayAccess(const std::string& src) { + // Heuristic: `buf[` present but no `if (i <` or `assert` or `size()` + if (src.find('[') == std::string::npos) return false; + // Look for array subscript without bounds checking + bool hasSubscript = false; + for (size_t i = 0; i < src.size(); ++i) { + if (src[i] == '[' && i > 0 && (std::isalnum((unsigned char)src[i-1]) || src[i-1] == '_')) { + hasSubscript = true; + break; + } + } + if (!hasSubscript) return false; + bool hasBoundsCheck = hasPattern(src, ".size()") || + hasPattern(src, ".length()") || + hasPattern(src, "< sizeof") || + hasPattern(src, "assert("); + return !hasBoundsCheck; + } + + static bool hasGlobalMutableState(const std::string& src) { + // Heuristic: line starts with a non-const type followed by identifier at file scope + // Simplified: look for `static int`, `static char*`, `int g_`, or global assignments + return hasPattern(src, "static int ") || + hasPattern(src, "static char ") || + hasPattern(src, "int g_") || + hasPattern(src, "volatile "); + } + + static bool hasFreeFollowedByDeref(const std::string& src) { + size_t freePos = src.find("free("); + if (freePos == std::string::npos) return false; + // Check for pointer usage after free() + size_t afterFree = src.find(';', freePos); + if (afterFree == std::string::npos) return false; + std::string rest = src.substr(afterFree); + // Look for dereference of the freed pointer + return hasPattern(rest, "->") || hasPattern(rest, "*p"); + } + + static std::string detectFirstFunctionName(const std::string& source) { + size_t open = source.find('('); + if (open == std::string::npos) return ""; + size_t i = open; + while (i > 0 && std::isspace((unsigned char)source[i - 1])) --i; + size_t end = i; + while (i > 0 && + (std::isalnum((unsigned char)source[i - 1]) || source[i - 1] == '_')) --i; + if (end <= i) return ""; + return source.substr(i, end - i); + } + + static std::vector buildFunctionFindings( + const std::string& source, + const std::vector& allFindings) { + std::vector out; + if (allFindings.empty()) return out; + std::string fn = detectFirstFunctionName(source); + if (fn.empty()) return out; + FunctionSafetyFinding f; + f.functionName = fn; + f.findings = allFindings; + f.maxRisk = 0; + for (const auto& sf : allFindings) + f.maxRisk = std::max(f.maxRisk, sf.riskLevel); + out.push_back(std::move(f)); + return out; + } +}; diff --git a/editor/tests/step439_test.cpp b/editor/tests/step439_test.cpp new file mode 100644 index 0000000..64fdce1 --- /dev/null +++ b/editor/tests/step439_test.cpp @@ -0,0 +1,178 @@ +// Step 439: Safety Audit via Annotations Tests (12 tests) + +#include "SafetyAuditor.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 {} + +void test_buffer_overflow_gets() { + TEST(buffer_overflow_gets); + const std::string src = "void f(){ char b[32]; gets(b); }"; + auto r = SafetyAuditor::audit(src, "c", "unsafe.c"); + CHECK(r.hasCategory("buffer-overflow"), "expected buffer-overflow category"); + CHECK(r.hasCwe("CWE-120"), "expected CWE-120"); + CHECK(r.overallRisk >= 3, "expected high risk for gets()"); + PASS(); +} + +void test_buffer_overflow_strcpy() { + TEST(buffer_overflow_strcpy); + const std::string src = "void copy(char* d, const char* s){ strcpy(d, s); }"; + auto r = SafetyAuditor::audit(src, "c", "cpy.c"); + CHECK(r.hasCategory("buffer-overflow"), "expected buffer-overflow"); + CHECK(r.hasCwe("CWE-120"), "expected CWE-120"); + PASS(); +} + +void test_use_after_free_manual_malloc() { + TEST(use_after_free_manual_malloc); + const std::string src = "void f(){ int* p=(int*)malloc(sizeof(int)); free(p); }"; + auto r = SafetyAuditor::audit(src, "c", "uaf.c"); + CHECK(r.hasCategory("use-after-free"), "expected use-after-free"); + CHECK(r.hasCwe("CWE-416"), "expected CWE-416"); + PASS(); +} + +void test_use_after_free_dangling_deref() { + TEST(use_after_free_dangling_deref); + const std::string src = "void f(){ int* p=(int*)malloc(4); free(p); int x = *p; }"; + auto r = SafetyAuditor::audit(src, "c", "dangling.c"); + CHECK(r.hasCategory("use-after-free"), "expected use-after-free"); + // Should find the dangling dereference + bool foundDangling = false; + for (const auto& f : r.findings) + if (f.annotation == "@Lifetime(dangling)") foundDangling = true; + CHECK(foundDangling, "expected @Lifetime(dangling) annotation"); + PASS(); +} + +void test_null_deref_unchecked_pointer() { + TEST(null_deref_unchecked_pointer); + const std::string src = "void f(int* p){ *p = 42; p = NULL; *p = 0; }"; + auto r = SafetyAuditor::audit(src, "c", "null.c"); + CHECK(r.hasCategory("null-dereference"), "expected null-dereference"); + CHECK(r.hasCwe("CWE-476"), "expected CWE-476"); + PASS(); +} + +void test_null_deref_unchecked_malloc() { + TEST(null_deref_unchecked_malloc); + const std::string src = "void f(){ int* p = (int*)malloc(4); *p = 7; free(p); }"; + auto r = SafetyAuditor::audit(src, "c", "nullalloc.c"); + CHECK(r.hasCategory("null-dereference"), "expected null-dereference for unchecked malloc"); + bool foundUncheckedAlloc = false; + for (const auto& f : r.findings) + if (f.annotation == "@Nullability(unchecked-alloc)") foundUncheckedAlloc = true; + CHECK(foundUncheckedAlloc, "expected @Nullability(unchecked-alloc)"); + PASS(); +} + +void test_race_condition_thread_without_sync() { + TEST(race_condition_thread_without_sync); + const std::string src = + "static int counter = 0;\n" + "void worker(){ counter++; }\n" + "void start(){ pthread_create(&t, NULL, worker, NULL); }\n"; + auto r = SafetyAuditor::audit(src, "c", "race.c"); + CHECK(r.hasCategory("race-condition"), "expected race-condition"); + CHECK(r.hasCwe("CWE-362"), "expected CWE-362"); + PASS(); +} + +void test_race_condition_with_mutex_not_flagged() { + TEST(race_condition_with_mutex_not_flagged); + const std::string src = + "static int counter = 0;\n" + "pthread_mutex_t mutex;\n" + "void worker(){ pthread_mutex_lock(&mutex); counter++; pthread_mutex_unlock(&mutex); }\n" + "void start(){ pthread_create(&t, NULL, worker, NULL); }\n"; + auto r = SafetyAuditor::audit(src, "c", "safe_race.c"); + // With mutex present, the thread+global finding should NOT appear + bool hasSyncNone = false; + for (const auto& f : r.findings) + if (f.annotation == "@Sync(none)") hasSyncNone = true; + CHECK(!hasSyncNone, "should not flag @Sync(none) when mutex is present"); + PASS(); +} + +void test_integer_overflow_unchecked() { + TEST(integer_overflow_unchecked); + const std::string src = "int f(int a, int b){ unsigned int result = a + b; return result; }"; + auto r = SafetyAuditor::audit(src, "c", "overflow.c"); + CHECK(r.hasCategory("integer-overflow"), "expected integer-overflow"); + CHECK(r.hasCwe("CWE-190"), "expected CWE-190"); + PASS(); +} + +void test_integer_overflow_checked_not_flagged() { + TEST(integer_overflow_checked_not_flagged); + const std::string src = + "int safe_add(int a, int b){\n" + " if (a > INT_MAX - b) return -1;\n" + " int result = a + b;\n" + " return result;\n" + "}\n"; + auto r = SafetyAuditor::audit(src, "c", "safe_overflow.c"); + CHECK(!r.hasCategory("integer-overflow"), + "should not flag integer-overflow when INT_MAX check is present"); + PASS(); +} + +void test_per_function_safety_findings() { + TEST(per_function_safety_findings); + const std::string src = "void vulnerable(){ char b[32]; gets(b); }"; + auto r = SafetyAuditor::audit(src, "c", "fn.c"); + CHECK(!r.functionFindings.empty(), "expected function findings"); + CHECK(r.functionFindings[0].functionName == "vulnerable", + "expected function name 'vulnerable'"); + CHECK(r.functionFindings[0].maxRisk >= 3, "expected high max risk"); + PASS(); +} + +void test_cwe_mapping_comprehensive() { + TEST(cwe_mapping_comprehensive); + const std::string src = + "static int g_count = 0;\n" + "void f(){\n" + " char buf[32];\n" + " int* p = (int*)malloc(4);\n" + " gets(buf);\n" + " free(p);\n" + " *p = 1;\n" // use-after-free + dangling + " unsigned int x = g_count + 1;\n" + " pthread_create(&t, NULL, f, NULL);\n" + "}\n"; + auto r = SafetyAuditor::audit(src, "c", "all.c"); + CHECK(r.hasCwe("CWE-120"), "expected CWE-120 (buffer overflow)"); + CHECK(r.hasCwe("CWE-416"), "expected CWE-416 (use-after-free)"); + CHECK(r.hasCwe("CWE-190"), "expected CWE-190 (integer overflow)"); + CHECK(r.hasCwe("CWE-362"), "expected CWE-362 (race condition)"); + CHECK(r.findings.size() >= 4, "expected at least 4 distinct findings"); + PASS(); +} + +int main() { + std::cout << "Step 439: Safety Audit via Annotations Tests\n"; + + test_buffer_overflow_gets(); // 1 + test_buffer_overflow_strcpy(); // 2 + test_use_after_free_manual_malloc(); // 3 + test_use_after_free_dangling_deref(); // 4 + test_null_deref_unchecked_pointer(); // 5 + test_null_deref_unchecked_malloc(); // 6 + test_race_condition_thread_without_sync(); // 7 + test_race_condition_with_mutex_not_flagged(); // 8 + test_integer_overflow_unchecked(); // 9 + test_integer_overflow_checked_not_flagged(); // 10 + test_per_function_safety_findings(); // 11 + test_cwe_mapping_comprehensive(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/editor/tests/step440_test.cpp b/editor/tests/step440_test.cpp new file mode 100644 index 0000000..b66fe29 --- /dev/null +++ b/editor/tests/step440_test.cpp @@ -0,0 +1,180 @@ +// Step 440: Modernization Suggestions Tests (12 tests) + +#include "ModernizationSuggester.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 {} + +// Helper: run full pipeline on source +static ModernizationReport analyze(const std::string& src, + const std::string& lang, + const std::string& file, + const std::string& target = "") { + auto legacy = LegacyIdiomDetector::analyzeFile(src, lang, file); + auto safety = SafetyAuditor::audit(src, lang, file); + return ModernizationSuggester::suggest(legacy, safety, target); +} + +void test_malloc_to_smart_pointer() { + TEST(malloc_to_smart_pointer); + auto r = analyze("void f(){ int* p=(int*)malloc(4); free(p); }", "cpp", "mem.cpp"); + CHECK(r.hasSuggestionFrom("malloc/free"), "expected malloc/free suggestion"); + CHECK(r.hasSuggestionTo("std::unique_ptr / std::shared_ptr"), + "expected smart pointer target"); + PASS(); +} + +void test_sprintf_to_format() { + TEST(sprintf_to_format); + auto r = analyze("void f(){ char b[32]; sprintf(b, \"%d\", 7); }", "c", "fmt.c"); + CHECK(r.hasSuggestionFrom("sprintf"), "expected sprintf suggestion"); + CHECK(r.hasSuggestionTo("std::format / snprintf"), "expected format target"); + PASS(); +} + +void test_strcpy_to_string() { + TEST(strcpy_to_string); + auto r = analyze("void f(char* d, const char* s){ strcpy(d, s); }", "c", "cpy.c"); + CHECK(r.hasSuggestionFrom("strcpy"), "expected strcpy suggestion"); + PASS(); +} + +void test_gets_to_fgets() { + TEST(gets_to_fgets); + auto r = analyze("void f(){ char b[32]; gets(b); }", "c", "gets.c"); + CHECK(r.hasSuggestionFrom("gets"), "expected gets suggestion"); + CHECK(r.hasSuggestionTo("fgets / std::getline"), "expected fgets target"); + PASS(); +} + +void test_goto_to_structured() { + TEST(goto_to_structured); + auto r = analyze("void f(){ start: goto start; }", "c", "goto.c"); + CHECK(r.hasSuggestionFrom("goto"), "expected goto suggestion"); + CHECK(r.hasSuggestionTo("structured control flow"), "expected structured control"); + PASS(); +} + +void test_global_mutable_to_mutex() { + TEST(global_mutable_to_mutex); + const std::string src = + "static int counter = 0;\n" + "void worker(){ counter++; }\n" + "void start(){ pthread_create(&t, NULL, worker, NULL); }\n"; + auto r = analyze(src, "c", "race.c"); + CHECK(r.hasSuggestionFrom("unprotected shared state"), + "expected shared state suggestion"); + CHECK(r.hasSuggestionTo("std::mutex / std::atomic"), "expected mutex target"); + PASS(); +} + +void test_quick_wins_grouped() { + TEST(quick_wins_grouped); + auto r = analyze( + "void f(){ char b[32]; gets(b); sprintf(b, \"x\"); strcpy(b, \"y\"); }", + "c", "multi.c"); + auto qw = r.quickWins(); + CHECK(qw.size() >= 3, "expected at least 3 quick wins"); + for (const auto& s : qw) + CHECK(s.risk == "low", "quick wins should be low risk"); + PASS(); +} + +void test_deep_refactors_grouped() { + TEST(deep_refactors_grouped); + const std::string src = + "static int g = 0;\n" + "void f(){ start: goto start; }\n" + "void g2(){ pthread_create(&t, NULL, f, NULL); }\n"; + auto r = analyze(src, "c", "deep.c"); + auto dr = r.deepRefactors(); + CHECK(dr.size() >= 1, "expected at least 1 deep refactor"); + for (const auto& s : dr) + CHECK(s.risk == "high", "deep refactors should be high risk"); + PASS(); +} + +void test_annotation_format() { + TEST(annotation_format); + auto r = analyze("void f(){ char b[32]; gets(b); }", "c", "ann.c"); + CHECK(!r.suggestions.empty(), "expected suggestions"); + bool foundAnnotation = false; + for (const auto& s : r.suggestions) { + if (s.annotation.find("@Modernize(") == 0) { + foundAnnotation = true; + CHECK(s.annotation.find("from=") != std::string::npos, "annotation missing from="); + CHECK(s.annotation.find("to=") != std::string::npos, "annotation missing to="); + CHECK(s.annotation.find("risk=") != std::string::npos, "annotation missing risk="); + } + } + CHECK(foundAnnotation, "expected @Modernize annotation"); + PASS(); +} + +void test_cross_language_c_to_rust() { + TEST(cross_language_c_to_rust); + auto r = analyze("void f(){ int* p=(int*)malloc(4); free(p); }", "c", "migrate.c", "rust"); + CHECK(r.targetLanguage == "rust", "expected rust target"); + CHECK(r.hasSuggestionTo("Rust ownership + borrow checker"), + "expected Rust ownership suggestion"); + PASS(); +} + +void test_cross_language_c_to_java() { + TEST(cross_language_c_to_java); + auto r = analyze("void f(){ int* p=(int*)malloc(4); free(p); }", "c", "migrate.c", "java"); + CHECK(r.targetLanguage == "java", "expected java target"); + bool foundGc = false; + for (const auto& s : r.suggestions) + if (s.toReplacement.find("garbage collection") != std::string::npos) foundGc = true; + CHECK(foundGc, "expected GC suggestion for Java target"); + PASS(); +} + +void test_effort_classification_counts() { + TEST(effort_classification_counts); + const std::string src = + "static int g = 0;\n" + "void f(int* p){\n" + " char b[32]; gets(b); sprintf(b,\"x\"); strcpy(b,\"y\");\n" + " int* q=(int*)malloc(4); free(q);\n" + " goto end; end:;\n" + " unsigned int x = g + 1;\n" + " pthread_create(&t, NULL, f, NULL);\n" + "}\n"; + auto r = analyze(src, "c", "effort.c"); + int qw = r.countByEffort(ModernizeEffort::QuickWin); + int mod = r.countByEffort(ModernizeEffort::Moderate); + int deep = r.countByEffort(ModernizeEffort::DeepRefactor); + CHECK(qw >= 2, "expected at least 2 quick wins"); + CHECK(mod >= 1, "expected at least 1 moderate"); + CHECK(deep >= 1, "expected at least 1 deep refactor"); + CHECK(qw + mod + deep == (int)r.suggestions.size(), "effort counts should sum to total"); + PASS(); +} + +int main() { + std::cout << "Step 440: Modernization Suggestions Tests\n"; + + test_malloc_to_smart_pointer(); // 1 + test_sprintf_to_format(); // 2 + test_strcpy_to_string(); // 3 + test_gets_to_fgets(); // 4 + test_goto_to_structured(); // 5 + test_global_mutable_to_mutex(); // 6 + test_quick_wins_grouped(); // 7 + test_deep_refactors_grouped(); // 8 + test_annotation_format(); // 9 + test_cross_language_c_to_rust(); // 10 + test_cross_language_c_to_java(); // 11 + test_effort_classification_counts(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/editor/tests/step441_test.cpp b/editor/tests/step441_test.cpp new file mode 100644 index 0000000..0bc96c3 --- /dev/null +++ b/editor/tests/step441_test.cpp @@ -0,0 +1,168 @@ +// Step 441: Modernization Workflow Generation Tests (12 tests) + +#include "ModernizationWorkflow.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 ModernizationWorkflow buildWorkflow(const std::string& src, + const std::string& lang, + const std::string& file, + const std::string& target = "") { + auto legacy = LegacyIdiomDetector::analyzeFile(src, lang, file); + auto safety = SafetyAuditor::audit(src, lang, file); + auto report = ModernizationSuggester::suggest(legacy, safety, target); + return ModernizationWorkflowGenerator::generate(src, report); +} + +void test_workflow_items_created_from_suggestions() { + TEST(workflow_items_created_from_suggestions); + auto wf = buildWorkflow("void f(){ char b[32]; gets(b); sprintf(b,\"x\"); }", "c", "a.c"); + CHECK(wf.items.size() >= 2, "expected at least 2 work items"); + CHECK(wf.hasItemFor("gets"), "expected gets work item"); + CHECK(wf.hasItemFor("sprintf"), "expected sprintf work item"); + PASS(); +} + +void test_quick_wins_routed_deterministic() { + TEST(quick_wins_routed_deterministic); + auto wf = buildWorkflow("void f(){ char b[32]; gets(b); strcpy(b,\"x\"); }", "c", "b.c"); + int det = wf.countByRouting(WorkItemRouting::Deterministic); + CHECK(det >= 2, "expected at least 2 deterministic items for quick wins"); + PASS(); +} + +void test_deep_refactors_routed_human() { + TEST(deep_refactors_routed_human); + auto wf = buildWorkflow("void f(){ start: goto start; }", "c", "c.c"); + int human = wf.countByRouting(WorkItemRouting::Human); + CHECK(human >= 1, "expected at least 1 human-routed item for goto refactor"); + PASS(); +} + +void test_moderate_routed_llm() { + TEST(moderate_routed_llm); + auto wf = buildWorkflow("void f(){ int* p=(int*)malloc(4); free(p); }", "cpp", "d.cpp"); + int llm = wf.countByRouting(WorkItemRouting::LLM); + CHECK(llm >= 1, "expected at least 1 LLM-routed item for moderate effort"); + PASS(); +} + +void test_ordering_safe_first_risky_last() { + TEST(ordering_safe_first_risky_last); + const std::string src = + "void f(){\n" + " char b[32]; gets(b); strcpy(b,\"x\");\n" + " start: goto start;\n" + "}\n"; + auto wf = buildWorkflow(src, "c", "e.c"); + auto ordered = wf.itemsInOrder(); + CHECK(ordered.size() >= 3, "expected at least 3 items"); + // First items should be priority 0 (quick wins), last should be priority 2 (deep) + CHECK(ordered.front().priority <= ordered.back().priority, + "safe changes should come before risky ones"); + CHECK(ordered.front().effort == ModernizeEffort::QuickWin, + "first item should be a quick win"); + PASS(); +} + +void test_deep_refactors_depend_on_quick_wins() { + TEST(deep_refactors_depend_on_quick_wins); + const std::string src = + "void f(){\n" + " char b[32]; gets(b);\n" + " start: goto start;\n" + "}\n"; + auto wf = buildWorkflow(src, "c", "f.c"); + // Find the goto (deep refactor) item + bool foundDep = false; + for (const auto& item : wf.items) { + if (item.effort == ModernizeEffort::DeepRefactor) { + CHECK(!item.dependsOn.empty(), + "deep refactor should depend on quick wins"); + foundDep = true; + } + } + CHECK(foundDep, "expected a deep refactor item"); + PASS(); +} + +void test_skeleton_generated() { + TEST(skeleton_generated); + auto wf = buildWorkflow("void f(){ char b[32]; gets(b); }", "c", "g.c"); + CHECK(!wf.skeleton.skeletonSource.empty(), "skeleton source should not be empty"); + CHECK(wf.skeleton.originalSource != wf.skeleton.skeletonSource, + "skeleton should differ from original"); + PASS(); +} + +void test_skeleton_has_modernize_comments() { + TEST(skeleton_has_modernize_comments); + auto wf = buildWorkflow("void f(){ char b[32]; gets(b); }", "c", "h.c"); + CHECK(wf.skeleton.skeletonSource.find("@Modernize") != std::string::npos, + "skeleton should contain @Modernize comments"); + PASS(); +} + +void test_skeleton_annotations_collected() { + TEST(skeleton_annotations_collected); + auto wf = buildWorkflow("void f(){ char b[32]; gets(b); sprintf(b,\"x\"); }", "c", "i.c"); + CHECK(wf.skeleton.annotations.size() >= 2, "expected at least 2 annotations"); + PASS(); +} + +void test_work_item_ids_unique() { + TEST(work_item_ids_unique); + const std::string src = + "void f(){\n" + " gets(b); sprintf(b,\"x\"); strcpy(b,\"y\");\n" + " int* p=(int*)malloc(4); free(p);\n" + "}\n"; + auto wf = buildWorkflow(src, "c", "j.c"); + for (size_t i = 0; i < wf.items.size(); ++i) + for (size_t j = i + 1; j < wf.items.size(); ++j) + CHECK(wf.items[i].id != wf.items[j].id, "work item IDs must be unique"); + PASS(); +} + +void test_workflow_file_metadata() { + TEST(workflow_file_metadata); + auto wf = buildWorkflow("void f(){ gets(b); }", "c", "meta.c", "rust"); + CHECK(wf.filePath == "meta.c", "expected filePath"); + CHECK(wf.sourceLanguage == "c", "expected source language"); + CHECK(wf.targetLanguage == "rust", "expected target language"); + PASS(); +} + +void test_empty_source_produces_empty_workflow() { + TEST(empty_source_produces_empty_workflow); + auto wf = buildWorkflow("int main(){ return 0; }", "c", "clean.c"); + CHECK(wf.items.empty(), "clean code should produce no work items"); + PASS(); +} + +int main() { + std::cout << "Step 441: Modernization Workflow Generation Tests\n"; + + test_workflow_items_created_from_suggestions(); // 1 + test_quick_wins_routed_deterministic(); // 2 + test_deep_refactors_routed_human(); // 3 + test_moderate_routed_llm(); // 4 + test_ordering_safe_first_risky_last(); // 5 + test_deep_refactors_depend_on_quick_wins(); // 6 + test_skeleton_generated(); // 7 + test_skeleton_has_modernize_comments(); // 8 + test_skeleton_annotations_collected(); // 9 + test_work_item_ids_unique(); // 10 + test_workflow_file_metadata(); // 11 + test_empty_source_produces_empty_workflow(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/editor/tests/step442_test.cpp b/editor/tests/step442_test.cpp new file mode 100644 index 0000000..93bdd71 --- /dev/null +++ b/editor/tests/step442_test.cpp @@ -0,0 +1,190 @@ +// Step 442: Modernization RPC + MCP Tests (12 tests) + +#include "ModernizationRPC.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 {} + +void test_rpc_analyze_legacy() { + TEST(rpc_analyze_legacy); + json params = {{"source", "void f(){ gets(buf); }"}, {"language", "c"}, {"filePath", "a.c"}}; + auto result = ModernizationRPCHandler::handleAnalyzeLegacy(params); + CHECK(result.contains("legacyScore"), "expected legacyScore"); + CHECK(result["legacyScore"].get() > 0, "expected non-zero legacy score"); + CHECK(result["findings"].is_array(), "expected findings array"); + CHECK(!result["findings"].empty(), "expected at least one finding"); + PASS(); +} + +void test_rpc_safety_report() { + TEST(rpc_safety_report); + json params = {{"source", "void f(){ char b[32]; gets(b); }"}, + {"language", "c"}, {"filePath", "b.c"}}; + auto result = ModernizationRPCHandler::handleGetSafetyReport(params); + CHECK(result.contains("overallRisk"), "expected overallRisk"); + CHECK(result["overallRisk"].get() >= 3, "expected high risk for gets"); + CHECK(result["findings"].is_array(), "expected findings array"); + bool foundCwe = false; + for (const auto& f : result["findings"]) + if (f["cwe"] == "CWE-120") foundCwe = true; + CHECK(foundCwe, "expected CWE-120 in findings"); + PASS(); +} + +void test_rpc_suggest_modernization() { + TEST(rpc_suggest_modernization); + json params = {{"source", "void f(){ int* p=(int*)malloc(4); free(p); }"}, + {"language", "cpp"}, {"filePath", "c.cpp"}}; + auto result = ModernizationRPCHandler::handleSuggestModernization(params); + CHECK(result.contains("suggestions"), "expected suggestions"); + CHECK(!result["suggestions"].empty(), "expected at least one suggestion"); + CHECK(result.contains("quickWinCount"), "expected quickWinCount"); + PASS(); +} + +void test_rpc_suggest_with_target_language() { + TEST(rpc_suggest_with_target_language); + json params = {{"source", "void f(){ int* p=(int*)malloc(4); free(p); }"}, + {"language", "c"}, {"filePath", "d.c"}, + {"targetLanguage", "rust"}}; + auto result = ModernizationRPCHandler::handleSuggestModernization(params); + CHECK(result["targetLanguage"] == "rust", "expected rust target"); + bool foundRust = false; + for (const auto& s : result["suggestions"]) + if (s["to"].get().find("Rust") != std::string::npos) foundRust = true; + CHECK(foundRust, "expected Rust-specific suggestion"); + PASS(); +} + +void test_rpc_create_workflow() { + TEST(rpc_create_workflow); + json params = {{"source", "void f(){ gets(b); sprintf(b,\"x\"); goto end; end:; }"}, + {"language", "c"}, {"filePath", "e.c"}}; + auto result = ModernizationRPCHandler::handleCreateWorkflow(params); + CHECK(result.contains("items"), "expected items"); + CHECK(result["items"].is_array(), "expected items array"); + CHECK(result["itemCount"].get() >= 3, "expected at least 3 items"); + CHECK(result.contains("deterministicCount"), "expected deterministicCount"); + CHECK(result.contains("skeleton"), "expected skeleton"); + PASS(); +} + +void test_rpc_workflow_routing_counts() { + TEST(rpc_workflow_routing_counts); + json params = {{"source", "void f(){ gets(b); goto end; end:; }"}, + {"language", "c"}, {"filePath", "f.c"}}; + auto result = ModernizationRPCHandler::handleCreateWorkflow(params); + int det = result["deterministicCount"].get(); + int human = result["humanCount"].get(); + CHECK(det >= 1, "expected at least 1 deterministic item"); + CHECK(human >= 1, "expected at least 1 human item"); + PASS(); +} + +void test_rpc_dispatch_routes_correctly() { + TEST(rpc_dispatch_routes_correctly); + json params = {{"source", "void f(){ gets(b); }"}, {"language", "c"}}; + json id = 42; + + auto r1 = ModernizationRPCHandler::dispatch("analyzeLegacy", params, id); + CHECK(r1["id"] == 42, "expected id preserved"); + CHECK(r1.contains("result"), "expected result for analyzeLegacy"); + + auto r2 = ModernizationRPCHandler::dispatch("getSafetyReport", params, id); + CHECK(r2.contains("result"), "expected result for getSafetyReport"); + + auto r3 = ModernizationRPCHandler::dispatch("suggestModernization", params, id); + CHECK(r3.contains("result"), "expected result for suggestModernization"); + + auto r4 = ModernizationRPCHandler::dispatch("createModernizationWorkflow", params, id); + CHECK(r4.contains("result"), "expected result for createModernizationWorkflow"); + PASS(); +} + +void test_rpc_dispatch_unknown_method() { + TEST(rpc_dispatch_unknown_method); + auto r = ModernizationRPCHandler::dispatch("nonexistent", {}, 1); + CHECK(r.contains("error"), "expected error for unknown method"); + PASS(); +} + +void test_can_handle() { + TEST(can_handle); + CHECK(ModernizationRPCHandler::canHandle("analyzeLegacy"), "analyzeLegacy"); + CHECK(ModernizationRPCHandler::canHandle("getSafetyReport"), "getSafetyReport"); + CHECK(ModernizationRPCHandler::canHandle("suggestModernization"), "suggestModernization"); + CHECK(ModernizationRPCHandler::canHandle("createModernizationWorkflow"), "createModernizationWorkflow"); + CHECK(!ModernizationRPCHandler::canHandle("getAST"), "getAST should not match"); + PASS(); +} + +void test_tool_definitions_count() { + TEST(tool_definitions_count); + auto tools = ModernizationRPCHandler::toolDefinitions(); + CHECK(tools.size() == 4, "expected 4 tool definitions"); + // Verify names + bool foundAnalyze = false, foundSafety = false, foundSuggest = false, foundWorkflow = false; + for (const auto& t : tools) { + if (t.name == "whetstone_analyze_legacy") foundAnalyze = true; + if (t.name == "whetstone_get_safety_report") foundSafety = true; + if (t.name == "whetstone_suggest_modernization") foundSuggest = true; + if (t.name == "whetstone_create_modernization_workflow") foundWorkflow = true; + } + CHECK(foundAnalyze, "expected whetstone_analyze_legacy"); + CHECK(foundSafety, "expected whetstone_get_safety_report"); + CHECK(foundSuggest, "expected whetstone_suggest_modernization"); + CHECK(foundWorkflow, "expected whetstone_create_modernization_workflow"); + PASS(); +} + +void test_tool_schemas_have_source_property() { + TEST(tool_schemas_have_source_property); + auto tools = ModernizationRPCHandler::toolDefinitions(); + for (const auto& t : tools) { + CHECK(t.inputSchema.contains("properties"), "schema missing properties"); + CHECK(t.inputSchema["properties"].contains("source"), "schema missing source property"); + } + PASS(); +} + +void test_json_serialization_roundtrip() { + TEST(json_serialization_roundtrip); + json params = { + {"source", "void f(){ int* p=(int*)malloc(4); free(p); *p = 1; }"}, + {"language", "c"}, {"filePath", "rt.c"} + }; + auto result = ModernizationRPCHandler::handleCreateWorkflow(params); + // Verify JSON is well-formed by serializing and re-parsing + std::string serialized = result.dump(); + json reparsed = json::parse(serialized); + CHECK(reparsed["filePath"] == "rt.c", "roundtrip filePath"); + CHECK(reparsed["items"].is_array(), "roundtrip items"); + CHECK(reparsed["skeleton"]["source"].is_string(), "roundtrip skeleton source"); + PASS(); +} + +int main() { + std::cout << "Step 442: Modernization RPC + MCP Tests\n"; + + test_rpc_analyze_legacy(); // 1 + test_rpc_safety_report(); // 2 + test_rpc_suggest_modernization(); // 3 + test_rpc_suggest_with_target_language(); // 4 + test_rpc_create_workflow(); // 5 + test_rpc_workflow_routing_counts(); // 6 + test_rpc_dispatch_routes_correctly(); // 7 + test_rpc_dispatch_unknown_method(); // 8 + test_can_handle(); // 9 + test_tool_definitions_count(); // 10 + test_tool_schemas_have_source_property(); // 11 + test_json_serialization_roundtrip(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/editor/tests/step443_test.cpp b/editor/tests/step443_test.cpp new file mode 100644 index 0000000..e18371b --- /dev/null +++ b/editor/tests/step443_test.cpp @@ -0,0 +1,195 @@ +// Step 443: Phase 20a Integration Tests (8 tests) +// Full pipeline: legacy C → analyze → safety audit → suggest → workflow → validate + +#include "ModernizationRPC.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 {} + +// Representative legacy C source with multiple issues +static const std::string LEGACY_C_SOURCE = R"( +static int request_count = 0; + +int process_request(buf, len) +int buf; +int len; +{ + char response[64]; + int* data = (int*)malloc(len * sizeof(int)); + + gets(response); + sprintf(response, "Received: %d", buf); + strcpy(response, "hello"); + + goto cleanup; + + request_count++; + unsigned int total = request_count + len; + +cleanup: + free(data); + *data = 0; + return 0; +} +)"; + +void test_full_pipeline_legacy_to_workflow() { + TEST(full_pipeline_legacy_to_workflow); + auto legacy = LegacyIdiomDetector::analyzeFile(LEGACY_C_SOURCE, "c", "server.c"); + auto safety = SafetyAuditor::audit(LEGACY_C_SOURCE, "c", "server.c"); + auto suggestions = ModernizationSuggester::suggest(legacy, safety); + auto wf = ModernizationWorkflowGenerator::generate(LEGACY_C_SOURCE, suggestions); + + // Legacy analysis should find multiple patterns + CHECK(legacy.legacyScore >= 5, "expected high legacy score for this code"); + CHECK(legacy.hasPattern("knr-declaration"), "expected K&R pattern"); + CHECK(legacy.hasPattern("goto-control-flow"), "expected goto pattern"); + CHECK(legacy.hasPattern("deprecated-api-gets"), "expected gets pattern"); + CHECK(legacy.hasPattern("manual-memory-management"), "expected manual memory"); + + // Workflow should have items covering all findings + CHECK(wf.items.size() >= 5, "expected at least 5 work items"); + PASS(); +} + +void test_safety_report_flags_real_issues() { + TEST(safety_report_flags_real_issues); + auto safety = SafetyAuditor::audit(LEGACY_C_SOURCE, "c", "server.c"); + + CHECK(safety.hasCategory("buffer-overflow"), "expected buffer-overflow (gets/sprintf/strcpy)"); + CHECK(safety.hasCategory("use-after-free"), "expected use-after-free (free then *data)"); + CHECK(safety.overallRisk >= 3, "expected high overall risk"); + + // CWE mappings should be present + CHECK(safety.hasCwe("CWE-120"), "expected CWE-120 for buffer overflow"); + CHECK(safety.hasCwe("CWE-416"), "expected CWE-416 for use-after-free"); + PASS(); +} + +void test_workflow_respects_risk_ordering() { + TEST(workflow_respects_risk_ordering); + auto legacy = LegacyIdiomDetector::analyzeFile(LEGACY_C_SOURCE, "c", "server.c"); + auto safety = SafetyAuditor::audit(LEGACY_C_SOURCE, "c", "server.c"); + auto suggestions = ModernizationSuggester::suggest(legacy, safety); + auto wf = ModernizationWorkflowGenerator::generate(LEGACY_C_SOURCE, suggestions); + + auto ordered = wf.itemsInOrder(); + CHECK(ordered.size() >= 3, "expected items in order"); + + // First items should be safe quick wins (priority 0) + CHECK(ordered.front().priority == 0, "first item should be priority 0 (quick win)"); + // Last items should be risky deep refactors (priority 2) + CHECK(ordered.back().priority >= 1, "last item should be higher priority (risky)"); + + // Quick wins should be deterministic + for (const auto& item : ordered) { + if (item.effort == ModernizeEffort::QuickWin) + CHECK(item.routing == WorkItemRouting::Deterministic, + "quick wins should route deterministic"); + } + PASS(); +} + +void test_cross_language_c_to_rust_modernization() { + TEST(cross_language_c_to_rust_modernization); + auto legacy = LegacyIdiomDetector::analyzeFile(LEGACY_C_SOURCE, "c", "server.c"); + auto safety = SafetyAuditor::audit(LEGACY_C_SOURCE, "c", "server.c"); + auto suggestions = ModernizationSuggester::suggest(legacy, safety, "rust"); + + CHECK(suggestions.targetLanguage == "rust", "expected rust target"); + // Should include Rust-specific ownership suggestion + bool foundRust = false; + for (const auto& s : suggestions.suggestions) + if (s.toReplacement.find("Rust") != std::string::npos) foundRust = true; + CHECK(foundRust, "expected Rust ownership suggestion for C→Rust migration"); + PASS(); +} + +void test_rpc_full_pipeline_via_json() { + TEST(rpc_full_pipeline_via_json); + json params = {{"source", LEGACY_C_SOURCE}, {"language", "c"}, {"filePath", "server.c"}}; + + // Step 1: analyze legacy + auto legacy = ModernizationRPCHandler::handleAnalyzeLegacy(params); + CHECK(legacy["legacyScore"].get() >= 5, "RPC legacy score too low"); + + // Step 2: safety report + auto safety = ModernizationRPCHandler::handleGetSafetyReport(params); + CHECK(safety["overallRisk"].get() >= 3, "RPC safety risk too low"); + + // Step 3: suggest + auto suggest = ModernizationRPCHandler::handleSuggestModernization(params); + CHECK(!suggest["suggestions"].empty(), "RPC suggestions empty"); + + // Step 4: create workflow + auto wf = ModernizationRPCHandler::handleCreateWorkflow(params); + CHECK(wf["itemCount"].get() >= 5, "RPC workflow too few items"); + CHECK(wf["skeleton"]["source"].is_string(), "RPC skeleton missing"); + PASS(); +} + +void test_deterministic_modernizations_identified() { + TEST(deterministic_modernizations_identified); + json params = {{"source", LEGACY_C_SOURCE}, {"language", "c"}, {"filePath", "server.c"}}; + auto wf = ModernizationRPCHandler::handleCreateWorkflow(params); + + int det = wf["deterministicCount"].get(); + CHECK(det >= 3, "expected at least 3 deterministic (quick-win) items"); + + // Verify deterministic items are API replacements + for (const auto& item : wf["items"]) { + if (item["routing"] == "deterministic") { + CHECK(item["effort"] == "quick_win", "deterministic items should be quick wins"); + } + } + PASS(); +} + +void test_complex_items_prepared_for_llm_or_human() { + TEST(complex_items_prepared_for_llm_or_human); + json params = {{"source", LEGACY_C_SOURCE}, {"language", "c"}, {"filePath", "server.c"}}; + auto wf = ModernizationRPCHandler::handleCreateWorkflow(params); + + int llm = wf["llmCount"].get(); + int human = wf["humanCount"].get(); + CHECK(llm + human >= 1, "expected at least 1 LLM or human routed item"); + + // Deep refactors and high-risk items should go to human + for (const auto& item : wf["items"]) { + if (item["effort"] == "deep_refactor") + CHECK(item["routing"] == "human", "deep refactors should route to human"); + } + PASS(); +} + +void test_language_version_detection_in_pipeline() { + TEST(language_version_detection_in_pipeline); + json params = {{"source", LEGACY_C_SOURCE}, {"language", "c"}, {"filePath", "server.c"}}; + auto legacy = ModernizationRPCHandler::handleAnalyzeLegacy(params); + + // K&R declarations → should detect as c89 + CHECK(legacy["languageVersion"] == "c89", "expected c89 for K&R code"); + PASS(); +} + +int main() { + std::cout << "Step 443: Phase 20a Integration Tests\n"; + + test_full_pipeline_legacy_to_workflow(); // 1 + test_safety_report_flags_real_issues(); // 2 + test_workflow_respects_risk_ordering(); // 3 + test_cross_language_c_to_rust_modernization(); // 4 + test_rpc_full_pipeline_via_json(); // 5 + test_deterministic_modernizations_identified(); // 6 + test_complex_items_prepared_for_llm_or_human(); // 7 + test_language_version_detection_in_pipeline(); // 8 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/editor/tests/step444_test.cpp b/editor/tests/step444_test.cpp new file mode 100644 index 0000000..21f00fc --- /dev/null +++ b/editor/tests/step444_test.cpp @@ -0,0 +1,179 @@ +// Step 444: Migration Plan Generator Tests (12 tests) + +#include "MigrationPlanGenerator.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 {} + +// Test project: 3-file C project with dependencies +// utils.c (leaf) -> parser.c (depends on utils) -> main.c (depends on parser, utils) +static std::vector testProject() { + return { + {"src/utils.c", + "int clamp(int x, int lo, int hi){ return xhi?hi:x; }\n" + "int max(int a, int b){ return a>b?a:b; }\n", + {"clamp", "max"}, {}}, + {"src/parser.c", + "// parser module\n" + "#include \"utils.h\"\n" + "typedef struct { int type; char* text; } Token;\n" + "Token* parse(const char* input){ int m = max(1,2); return 0; }\n" + "void free_tokens(Token* t){ }\n" + "int token_count(Token* t){ return clamp(0,0,100); }\n", + {"parse", "free_tokens", "token_count"}, {"max", "clamp"}}, + {"src/main.c", + "// main entry point\n" + "#include \"parser.h\"\n" + "#include \"utils.h\"\n" + "int main(){ Token* t = parse(\"hello\"); free_tokens(t); return 0; }\n", + {"main"}, {"parse", "free_tokens"}} + }; +} + +void test_plan_created_for_project() { + TEST(plan_created_for_project); + auto plan = MigrationPlanGenerator::generate("testproj", testProject(), "c", "rust"); + CHECK(plan.projectName == "testproj", "expected project name"); + CHECK(plan.sourceLanguage == "c", "expected source language"); + CHECK(plan.targetLanguage == "rust", "expected target language"); + CHECK(plan.units.size() == 3, "expected 3 units"); + PASS(); +} + +void test_migration_units_identified() { + TEST(migration_units_identified); + auto plan = MigrationPlanGenerator::generate("testproj", testProject(), "c", "rust"); + CHECK(plan.hasUnit("src/utils.c"), "expected utils.c unit"); + CHECK(plan.hasUnit("src/parser.c"), "expected parser.c unit"); + CHECK(plan.hasUnit("src/main.c"), "expected main.c unit"); + PASS(); +} + +void test_module_names_extracted() { + TEST(module_names_extracted); + auto plan = MigrationPlanGenerator::generate("testproj", testProject(), "c", "rust"); + auto u = plan.unitByPath("src/utils.c"); + CHECK(u.moduleName == "utils", "expected module name 'utils'"); + auto p = plan.unitByPath("src/parser.c"); + CHECK(p.moduleName == "parser", "expected module name 'parser'"); + PASS(); +} + +void test_dependencies_resolved() { + TEST(dependencies_resolved); + auto plan = MigrationPlanGenerator::generate("testproj", testProject(), "c", "rust"); + auto parser = plan.unitByPath("src/parser.c"); + CHECK(!parser.dependsOn.empty(), "parser should depend on utils"); + auto main = plan.unitByPath("src/main.c"); + CHECK(!main.dependsOn.empty(), "main should depend on parser"); + PASS(); +} + +void test_leaf_modules_in_phase_zero() { + TEST(leaf_modules_in_phase_zero); + auto plan = MigrationPlanGenerator::generate("testproj", testProject(), "c", "rust"); + auto utils = plan.unitByPath("src/utils.c"); + CHECK(utils.phase == 0, "leaf module (utils) should be in phase 0"); + PASS(); +} + +void test_dependent_modules_in_later_phases() { + TEST(dependent_modules_in_later_phases); + auto plan = MigrationPlanGenerator::generate("testproj", testProject(), "c", "rust"); + auto utils = plan.unitByPath("src/utils.c"); + auto parser = plan.unitByPath("src/parser.c"); + auto main = plan.unitByPath("src/main.c"); + CHECK(parser.phase > utils.phase, "parser should be after utils"); + CHECK(main.phase > utils.phase, "main should be after utils"); + PASS(); +} + +void test_migration_order_dependencies_first() { + TEST(migration_order_dependencies_first); + auto plan = MigrationPlanGenerator::generate("testproj", testProject(), "c", "rust"); + auto ordered = plan.orderedUnits(); + CHECK(ordered.size() == 3, "expected 3 ordered units"); + CHECK(ordered[0].filePath == "src/utils.c", "utils should be first"); + PASS(); +} + +void test_phase_count() { + TEST(phase_count); + auto plan = MigrationPlanGenerator::generate("testproj", testProject(), "c", "rust"); + CHECK(plan.phaseCount >= 2, "expected at least 2 phases"); + auto phase0 = plan.unitsInPhase(0); + CHECK(!phase0.empty(), "phase 0 should have units"); + PASS(); +} + +void test_effort_classification() { + TEST(effort_classification); + auto plan = MigrationPlanGenerator::generate("testproj", testProject(), "c", "rust"); + auto utils = plan.unitByPath("src/utils.c"); + CHECK(utils.effort >= 1 && utils.effort <= 3, "effort should be 1-3"); + // Small file = low effort + CHECK(utils.effort == 1, "small file should have low effort"); + PASS(); +} + +void test_risk_classification() { + TEST(risk_classification); + auto plan = MigrationPlanGenerator::generate("testproj", testProject(), "c", "rust"); + auto parser = plan.unitByPath("src/parser.c"); + // Parser has 3 exports and dependencies → medium-high risk + CHECK(parser.risk >= 2, "parser with exports + deps should be medium-high risk"); + PASS(); +} + +void test_routing_assigned() { + TEST(routing_assigned); + auto plan = MigrationPlanGenerator::generate("testproj", testProject(), "c", "rust"); + bool hasDetOrLlm = false; + for (const auto& u : plan.units) { + if (u.routing == MigrationRouting::Deterministic || + u.routing == MigrationRouting::LLM) + hasDetOrLlm = true; + } + CHECK(hasDetOrLlm, "expected at least one non-human routing"); + PASS(); +} + +void test_exports_preserved_in_units() { + TEST(exports_preserved_in_units); + auto plan = MigrationPlanGenerator::generate("testproj", testProject(), "c", "rust"); + auto utils = plan.unitByPath("src/utils.c"); + CHECK(utils.exports.size() == 2, "utils should have 2 exports"); + bool foundClamp = false, foundMax = false; + for (const auto& e : utils.exports) { + if (e == "clamp") foundClamp = true; + if (e == "max") foundMax = true; + } + CHECK(foundClamp && foundMax, "expected clamp and max exports"); + PASS(); +} + +int main() { + std::cout << "Step 444: Migration Plan Generator Tests\n"; + + test_plan_created_for_project(); // 1 + test_migration_units_identified(); // 2 + test_module_names_extracted(); // 3 + test_dependencies_resolved(); // 4 + test_leaf_modules_in_phase_zero(); // 5 + test_dependent_modules_in_later_phases(); // 6 + test_migration_order_dependencies_first(); // 7 + test_phase_count(); // 8 + test_effort_classification(); // 9 + test_risk_classification(); // 10 + test_routing_assigned(); // 11 + test_exports_preserved_in_units(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/editor/tests/step445_test.cpp b/editor/tests/step445_test.cpp new file mode 100644 index 0000000..db7484f --- /dev/null +++ b/editor/tests/step445_test.cpp @@ -0,0 +1,182 @@ +// Step 445: API Boundary Preservation Tests (12 tests) + +#include "APIBoundaryPreserver.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 MigrationUnit makeUnit(const std::string& path, const std::string& mod, + const std::vector& exports, + const std::string& target = "rust") { + MigrationUnit u; + u.id = "unit-0"; + u.filePath = path; + u.moduleName = mod; + u.sourceLanguage = "c"; + u.targetLanguage = target; + u.exports = exports; + return u; +} + +static const std::string UTILS_SRC = + "int clamp(int x, int lo, int hi){ return xhi?hi:x; }\n" + "int max(int a, int b){ return a>b?a:b; }\n"; + +static const std::string PARSER_SRC = + "typedef struct { int type; char* text; } Token;\n" + "Token* parse(const char* input){ return 0; }\n" + "void free_tokens(Token* t){ }\n"; + +void test_public_api_extracted() { + TEST(public_api_extracted); + auto unit = makeUnit("utils.c", "utils", {"clamp", "max"}); + auto report = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + CHECK(report.hasFunction("clamp"), "expected clamp in public API"); + CHECK(report.hasFunction("max"), "expected max in public API"); + CHECK(report.publicAPI.size() == 2, "expected 2 public functions"); + PASS(); +} + +void test_function_signatures_captured() { + TEST(function_signatures_captured); + auto unit = makeUnit("utils.c", "utils", {"clamp"}); + auto report = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + CHECK(!report.publicAPI.empty(), "expected public API"); + auto& sig = report.publicAPI[0]; + CHECK(sig.name == "clamp", "expected clamp"); + CHECK(!sig.returnType.empty(), "expected return type"); + CHECK(!sig.paramTypes.empty(), "expected param types"); + PASS(); +} + +void test_type_mappings_c_to_rust() { + TEST(type_mappings_c_to_rust); + auto unit = makeUnit("utils.c", "utils", {"clamp"}, "rust"); + auto report = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + CHECK(report.hasTypeMapping("int"), "expected int type mapping"); + PASS(); +} + +void test_type_mappings_c_to_java() { + TEST(type_mappings_c_to_java); + auto unit = makeUnit("utils.c", "utils", {"clamp"}, "java"); + auto report = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + CHECK(report.hasTypeMapping("int"), "expected int type mapping for java"); + PASS(); +} + +void test_pointer_type_mapping() { + TEST(pointer_type_mapping); + auto unit = makeUnit("parser.c", "parser", {"parse"}, "rust"); + auto report = APIBoundaryPreserver::analyze(PARSER_SRC, unit); + CHECK(report.hasTypeMapping("char*"), "expected char* type mapping"); + PASS(); +} + +void test_contract_for_pointer_params() { + TEST(contract_for_pointer_params); + auto unit = makeUnit("parser.c", "parser", {"free_tokens"}, "rust"); + auto report = APIBoundaryPreserver::analyze(PARSER_SRC, unit); + CHECK(report.hasContract("free_tokens"), "expected contract for free_tokens"); + // Should have non-null precondition for pointer param + for (const auto& c : report.contracts) { + if (c.functionName == "free_tokens") { + CHECK(c.precondition.find("NULL") != std::string::npos, + "expected NULL check precondition"); + } + } + PASS(); +} + +void test_contract_for_pointer_return() { + TEST(contract_for_pointer_return); + auto unit = makeUnit("parser.c", "parser", {"parse"}, "rust"); + auto report = APIBoundaryPreserver::analyze(PARSER_SRC, unit); + CHECK(report.hasContract("parse"), "expected contract for parse"); + for (const auto& c : report.contracts) { + if (c.functionName == "parse") { + CHECK(!c.postcondition.empty(), "expected postcondition for pointer return"); + } + } + PASS(); +} + +void test_ffi_boundary_rust() { + TEST(ffi_boundary_rust); + auto unit = makeUnit("utils.c", "utils", {"clamp", "max"}, "rust"); + auto report = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + CHECK(report.hasFFI("clamp"), "expected FFI for clamp"); + CHECK(report.hasFFI("max"), "expected FFI for max"); + for (const auto& f : report.ffiBoundaries) { + CHECK(f.annotation.find("no_mangle") != std::string::npos, + "Rust FFI should have #[no_mangle]"); + } + PASS(); +} + +void test_ffi_boundary_java() { + TEST(ffi_boundary_java); + auto unit = makeUnit("utils.c", "utils", {"clamp"}, "java"); + auto report = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + CHECK(report.hasFFI("clamp"), "expected FFI for clamp"); + for (const auto& f : report.ffiBoundaries) { + CHECK(f.annotation.find("JNI") != std::string::npos, + "Java FFI should mention JNI"); + } + PASS(); +} + +void test_api_preserved_flag() { + TEST(api_preserved_flag); + auto unit = makeUnit("utils.c", "utils", {"clamp", "max"}); + auto report = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + CHECK(report.apiPreserved, "API should be preserved"); + PASS(); +} + +void test_verify_preservation_pass() { + TEST(verify_preservation_pass); + auto unit = makeUnit("utils.c", "utils", {"clamp", "max"}); + auto original = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + auto migrated = original; // same API = preserved + CHECK(APIBoundaryPreserver::verifyPreservation(original, migrated), + "same API should verify as preserved"); + PASS(); +} + +void test_verify_preservation_fail() { + TEST(verify_preservation_fail); + auto unit = makeUnit("utils.c", "utils", {"clamp", "max"}); + auto original = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + auto migrated = original; + migrated.publicAPI.pop_back(); // remove one function + CHECK(!APIBoundaryPreserver::verifyPreservation(original, migrated), + "missing function should fail verification"); + PASS(); +} + +int main() { + std::cout << "Step 445: API Boundary Preservation Tests\n"; + + test_public_api_extracted(); // 1 + test_function_signatures_captured(); // 2 + test_type_mappings_c_to_rust(); // 3 + test_type_mappings_c_to_java(); // 4 + test_pointer_type_mapping(); // 5 + test_contract_for_pointer_params(); // 6 + test_contract_for_pointer_return(); // 7 + test_ffi_boundary_rust(); // 8 + test_ffi_boundary_java(); // 9 + test_api_preserved_flag(); // 10 + test_verify_preservation_pass(); // 11 + test_verify_preservation_fail(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/editor/tests/step446_test.cpp b/editor/tests/step446_test.cpp new file mode 100644 index 0000000..9a8fb59 --- /dev/null +++ b/editor/tests/step446_test.cpp @@ -0,0 +1,215 @@ +// Step 446: Test Generation for Migration Validation Tests (12 tests) + +#include "MigrationTestGenerator.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 MigrationUnit makeUnit(const std::string& mod, + const std::vector& exports, + const std::string& target = "rust") { + MigrationUnit u; + u.id = "unit-0"; + u.filePath = mod + ".c"; + u.moduleName = mod; + u.sourceLanguage = "c"; + u.targetLanguage = target; + u.exports = exports; + return u; +} + +static const std::string UTILS_SRC = + "int clamp(int x, int lo, int hi){ return xhi?hi:x; }\n" + "int max(int a, int b){ return a>b?a:b; }\n"; + +static const std::string PARSER_SRC = + "typedef struct { int type; char* text; } Token;\n" + "Token* parse(const char* input){ return 0; }\n" + "void free_tokens(Token* t){ }\n"; + +void test_equivalence_tests_generated() { + TEST(equivalence_tests_generated); + auto unit = makeUnit("utils", {"clamp", "max"}); + auto api = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + auto suite = MigrationTestGenerator::generate(api); + int eq = suite.countByKind(MigrationTestKind::Equivalence); + CHECK(eq >= 2, "expected equivalence test for each public function"); + PASS(); +} + +void test_edge_case_tests_from_contracts() { + TEST(edge_case_tests_from_contracts); + auto unit = makeUnit("parser", {"parse", "free_tokens"}); + auto api = APIBoundaryPreserver::analyze(PARSER_SRC, unit); + auto suite = MigrationTestGenerator::generate(api); + int edge = suite.countByKind(MigrationTestKind::EdgeCase); + CHECK(edge >= 1, "expected at least 1 edge case test from contracts"); + PASS(); +} + +void test_performance_tests_generated() { + TEST(performance_tests_generated); + auto unit = makeUnit("utils", {"clamp", "max"}); + auto api = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + auto suite = MigrationTestGenerator::generate(api); + int perf = suite.countByKind(MigrationTestKind::Performance); + CHECK(perf >= 2, "expected performance test for each function"); + PASS(); +} + +void test_tests_generated_for_each_function() { + TEST(tests_generated_for_each_function); + auto unit = makeUnit("utils", {"clamp", "max"}); + auto api = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + auto suite = MigrationTestGenerator::generate(api); + CHECK(suite.hasTestFor("clamp"), "expected tests for clamp"); + CHECK(suite.hasTestFor("max"), "expected tests for max"); + PASS(); +} + +void test_test_language_matches_target_rust() { + TEST(test_language_matches_target_rust); + auto unit = makeUnit("utils", {"clamp"}, "rust"); + auto api = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + auto suite = MigrationTestGenerator::generate(api); + CHECK(suite.targetLanguage == "rust", "suite should target rust"); + for (const auto& t : suite.tests) + CHECK(t.language == "rust", "all tests should be in rust"); + PASS(); +} + +void test_test_language_matches_target_java() { + TEST(test_language_matches_target_java); + auto unit = makeUnit("utils", {"clamp"}, "java"); + auto api = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + auto suite = MigrationTestGenerator::generate(api); + CHECK(suite.targetLanguage == "java", "suite should target java"); + for (const auto& t : suite.tests) + CHECK(t.language == "java", "all tests should be in java"); + PASS(); +} + +void test_rust_test_body_format() { + TEST(rust_test_body_format); + auto unit = makeUnit("utils", {"clamp"}, "rust"); + auto api = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + auto suite = MigrationTestGenerator::generate(api); + auto tests = suite.testsForFunction("clamp"); + bool foundRustTest = false; + for (const auto& t : tests) { + if (t.kind == MigrationTestKind::Equivalence) { + CHECK(t.testBody.find("#[test]") != std::string::npos, + "Rust test should have #[test] attribute"); + CHECK(t.testBody.find("fn test_clamp") != std::string::npos, + "Rust test should have test function name"); + foundRustTest = true; + } + } + CHECK(foundRustTest, "expected Rust equivalence test"); + PASS(); +} + +void test_java_test_body_format() { + TEST(java_test_body_format); + auto unit = makeUnit("utils", {"clamp"}, "java"); + auto api = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + auto suite = MigrationTestGenerator::generate(api); + auto tests = suite.testsForFunction("clamp"); + bool foundJavaTest = false; + for (const auto& t : tests) { + if (t.kind == MigrationTestKind::Equivalence) { + CHECK(t.testBody.find("@Test") != std::string::npos, + "Java test should have @Test annotation"); + foundJavaTest = true; + } + } + CHECK(foundJavaTest, "expected Java equivalence test"); + PASS(); +} + +void test_null_edge_case_for_pointer_params() { + TEST(null_edge_case_for_pointer_params); + auto unit = makeUnit("parser", {"free_tokens"}, "rust"); + auto api = APIBoundaryPreserver::analyze(PARSER_SRC, unit); + auto suite = MigrationTestGenerator::generate(api); + auto tests = suite.testsForFunction("free_tokens"); + bool foundNullTest = false; + for (const auto& t : tests) { + if (t.kind == MigrationTestKind::EdgeCase && + t.title.find("null") != std::string::npos) { + foundNullTest = true; + } + } + CHECK(foundNullTest, "expected null edge case test for pointer param"); + PASS(); +} + +void test_skeleton_work_items_routed() { + TEST(skeleton_work_items_routed); + auto unit = makeUnit("utils", {"clamp"}); + auto api = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + auto suite = MigrationTestGenerator::generate(api); + bool hasSLM = false, hasLLM = false; + for (const auto& t : suite.tests) { + if (t.routing == TestRouting::SLM) hasSLM = true; + if (t.routing == TestRouting::LLM) hasLLM = true; + } + CHECK(hasSLM, "expected SLM-routed tests (equivalence)"); + CHECK(hasLLM, "expected LLM-routed tests (edge/perf)"); + PASS(); +} + +void test_unique_test_ids() { + TEST(unique_test_ids); + auto unit = makeUnit("parser", {"parse", "free_tokens"}); + auto api = APIBoundaryPreserver::analyze(PARSER_SRC, unit); + auto suite = MigrationTestGenerator::generate(api); + for (size_t i = 0; i < suite.tests.size(); ++i) + for (size_t j = i + 1; j < suite.tests.size(); ++j) + CHECK(suite.tests[i].id != suite.tests[j].id, "test IDs must be unique"); + PASS(); +} + +void test_python_target_language() { + TEST(python_target_language); + auto unit = makeUnit("utils", {"clamp"}, "python"); + auto api = APIBoundaryPreserver::analyze(UTILS_SRC, unit); + auto suite = MigrationTestGenerator::generate(api); + auto tests = suite.testsForFunction("clamp"); + bool foundPython = false; + for (const auto& t : tests) { + if (t.kind == MigrationTestKind::Equivalence) { + CHECK(t.testBody.find("def test_") != std::string::npos, + "Python test should use def test_ format"); + foundPython = true; + } + } + CHECK(foundPython, "expected Python test"); + PASS(); +} + +int main() { + std::cout << "Step 446: Test Generation for Migration Validation Tests\n"; + + test_equivalence_tests_generated(); // 1 + test_edge_case_tests_from_contracts(); // 2 + test_performance_tests_generated(); // 3 + test_tests_generated_for_each_function(); // 4 + test_test_language_matches_target_rust(); // 5 + test_test_language_matches_target_java(); // 6 + test_rust_test_body_format(); // 7 + test_java_test_body_format(); // 8 + test_null_edge_case_for_pointer_params(); // 9 + test_skeleton_work_items_routed(); // 10 + test_unique_test_ids(); // 11 + test_python_target_language(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/editor/tests/step447_test.cpp b/editor/tests/step447_test.cpp new file mode 100644 index 0000000..10de771 --- /dev/null +++ b/editor/tests/step447_test.cpp @@ -0,0 +1,202 @@ +// Step 447: Migration Execution Integration Tests (12 tests) + +#include "MigrationExecutor.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 testProject() { + return { + {"src/utils.c", + "int clamp(int x, int lo, int hi){ return xhi?hi:x; }\n" + "int max(int a, int b){ return a>b?a:b; }\n", + {"clamp", "max"}, {}}, + {"src/parser.c", + "typedef struct { int type; } Token;\n" + "Token* parse(const char* input){ int m = max(1,2); return 0; }\n" + "void free_tokens(Token* t){ }\n" + "int token_count(Token* t){ return clamp(0,0,100); }\n", + {"parse", "free_tokens", "token_count"}, {"max", "clamp"}}, + {"src/main.c", + "int main(){ Token* t = parse(\"hello\"); free_tokens(t); return 0; }\n", + {"main"}, {"parse", "free_tokens"}} + }; +} + +static MigrationExecution setupExecution() { + auto files = testProject(); + auto plan = MigrationPlanGenerator::generate("testproj", files, "c", "rust"); + return MigrationExecutor::createExecution(plan, files); +} + +void test_execution_created_from_plan() { + TEST(execution_created_from_plan); + auto exec = setupExecution(); + CHECK(exec.projectName == "testproj", "expected project name"); + CHECK(exec.sourceLanguage == "c", "expected source language"); + CHECK(exec.targetLanguage == "rust", "expected target language"); + CHECK(exec.workItems.size() == 3, "expected 3 work items"); + PASS(); +} + +void test_all_items_start_pending() { + TEST(all_items_start_pending); + auto exec = setupExecution(); + CHECK(exec.countByStatus(MigrationStatus::Pending) == 3, "all should be pending"); + CHECK(exec.countByStatus(MigrationStatus::Completed) == 0, "none completed yet"); + PASS(); +} + +void test_cross_unit_dependencies_respected() { + TEST(cross_unit_dependencies_respected); + auto exec = setupExecution(); + // Only leaf modules (no deps) should be ready initially + auto ready = exec.readyItems(); + CHECK(!ready.empty(), "at least one item should be ready"); + // Utils (leaf) should be ready; parser/main should not be if they depend on utils + for (const auto& r : ready) + CHECK(r.dependsOn.empty() || r.filePath == "src/utils.c", + "ready items should have no unresolved deps"); + PASS(); +} + +void test_complete_unit_updates_status() { + TEST(complete_unit_updates_status); + auto exec = setupExecution(); + auto ready = exec.readyItems(); + CHECK(!ready.empty(), "need ready items"); + MigrationExecutor::completeUnit(exec, ready[0].id); + CHECK(exec.countByStatus(MigrationStatus::Completed) == 1, "one should be completed"); + PASS(); +} + +void test_completing_dependency_unblocks_next() { + TEST(completing_dependency_unblocks_next); + auto exec = setupExecution(); + auto ready1 = exec.readyItems(); + int initialReady = (int)ready1.size(); + // Complete all initially ready items + for (const auto& r : ready1) + MigrationExecutor::completeUnit(exec, r.id); + auto ready2 = exec.readyItems(); + CHECK((int)ready2.size() >= 1, "completing deps should unblock more items"); + PASS(); +} + +void test_rollback_annotations() { + TEST(rollback_annotations); + auto exec = setupExecution(); + for (const auto& w : exec.workItems) { + CHECK(w.rollbackAnnotation.find("@Rollback") != std::string::npos, + "each item should have rollback annotation"); + CHECK(w.rollbackAnnotation.find(w.filePath) != std::string::npos, + "rollback should reference original file"); + } + PASS(); +} + +void test_failed_unit_flagged_for_human_review() { + TEST(failed_unit_flagged_for_human_review); + auto exec = setupExecution(); + auto ready = exec.readyItems(); + CHECK(!ready.empty(), "need ready items"); + MigrationExecutor::failUnit(exec, ready[0].id); + for (const auto& w : exec.workItems) { + if (w.id == ready[0].id) { + CHECK(w.status == MigrationStatus::Failed, "should be failed"); + CHECK(w.requiresHumanReview, "failed should require human review"); + } + } + PASS(); +} + +void test_progressive_migration_partial_state() { + TEST(progressive_migration_partial_state); + auto exec = setupExecution(); + CHECK(!MigrationExecutor::isPartialMigration(exec), "initially not partial"); + // Complete first unit + auto ready = exec.readyItems(); + MigrationExecutor::completeUnit(exec, ready[0].id); + CHECK(MigrationExecutor::isPartialMigration(exec), + "after completing one unit, should be partial"); + PASS(); +} + +void test_ffi_boundary_during_partial_migration() { + TEST(ffi_boundary_during_partial_migration); + auto exec = setupExecution(); + // Complete utils (leaf) + auto ready = exec.readyItems(); + for (const auto& r : ready) { + if (r.filePath == "src/utils.c") + MigrationExecutor::completeUnit(exec, r.id); + } + // Parser depends on utils — now it's a mixed-language boundary + int ffiBoundaries = exec.countFfiBoundaries(); + CHECK(ffiBoundaries >= 1, "expected FFI boundary in partial migration"); + PASS(); +} + +void test_full_migration_no_ffi_boundaries() { + TEST(full_migration_no_ffi_boundaries); + auto exec = setupExecution(); + // Complete all in order + while (true) { + auto ready = exec.readyItems(); + if (ready.empty()) break; + for (const auto& r : ready) + MigrationExecutor::completeUnit(exec, r.id); + } + CHECK(!MigrationExecutor::isPartialMigration(exec), + "fully migrated should not be partial"); + CHECK(exec.countByStatus(MigrationStatus::Completed) == 3, "all should be completed"); + PASS(); +} + +void test_high_risk_units_require_human() { + TEST(high_risk_units_require_human); + auto exec = setupExecution(); + bool foundHumanReview = false; + for (const auto& w : exec.workItems) { + if (w.requiresHumanReview) foundHumanReview = true; + } + // At least parser (high API surface, deps) should require review + CHECK(foundHumanReview, "expected at least one item requiring human review"); + PASS(); +} + +void test_work_item_titles_descriptive() { + TEST(work_item_titles_descriptive); + auto exec = setupExecution(); + for (const auto& w : exec.workItems) { + CHECK(w.title.find("Migrate") != std::string::npos, "title should contain Migrate"); + CHECK(w.title.find("->") != std::string::npos, "title should show language transition"); + } + PASS(); +} + +int main() { + std::cout << "Step 447: Migration Execution Integration Tests\n"; + + test_execution_created_from_plan(); // 1 + test_all_items_start_pending(); // 2 + test_cross_unit_dependencies_respected(); // 3 + test_complete_unit_updates_status(); // 4 + test_completing_dependency_unblocks_next(); // 5 + test_rollback_annotations(); // 6 + test_failed_unit_flagged_for_human_review(); // 7 + test_progressive_migration_partial_state(); // 8 + test_ffi_boundary_during_partial_migration(); // 9 + test_full_migration_no_ffi_boundaries(); // 10 + test_high_risk_units_require_human(); // 11 + test_work_item_titles_descriptive(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/editor/tests/step448_test.cpp b/editor/tests/step448_test.cpp new file mode 100644 index 0000000..e001d26 --- /dev/null +++ b/editor/tests/step448_test.cpp @@ -0,0 +1,236 @@ +// Step 448: Phase 20b Integration + Sprint 20 Summary Tests (8 tests) +// Full migration: 3-file C project → Rust with API preservation, test generation, +// partial migration, and FFI boundary management. + +#include "MigrationExecutor.h" +#include "MigrationTestGenerator.h" +#include "ModernizationRPC.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 testProject() { + return { + {"src/utils.c", + "int clamp(int x, int lo, int hi){ return xhi?hi:x; }\n" + "int max(int a, int b){ return a>b?a:b; }\n", + {"clamp", "max"}, {}}, + {"src/parser.c", + "typedef struct { int type; char* text; } Token;\n" + "Token* parse(const char* input){ int m = max(1,2); return 0; }\n" + "void free_tokens(Token* t){ }\n" + "int token_count(Token* t){ return clamp(0,0,100); }\n", + {"parse", "free_tokens", "token_count"}, {"max", "clamp"}}, + {"src/main.c", + "int main(){ Token* t = parse(\"hello\"); free_tokens(t); return 0; }\n", + {"main"}, {"parse", "free_tokens"}} + }; +} + +void test_full_migration_3_file_c_to_rust() { + TEST(full_migration_3_file_c_to_rust); + auto files = testProject(); + auto plan = MigrationPlanGenerator::generate("myproj", files, "c", "rust"); + auto exec = MigrationExecutor::createExecution(plan, files); + + // Execute all phases in order + int rounds = 0; + while (exec.countByStatus(MigrationStatus::Pending) > 0 && rounds < 10) { + auto ready = exec.readyItems(); + if (ready.empty()) break; + for (const auto& r : ready) + MigrationExecutor::completeUnit(exec, r.id); + ++rounds; + } + + CHECK(exec.countByStatus(MigrationStatus::Completed) == 3, + "all 3 files should be migrated"); + CHECK(!MigrationExecutor::isPartialMigration(exec), + "full migration should not be partial"); + PASS(); +} + +void test_api_boundaries_preserved_across_migration() { + TEST(api_boundaries_preserved_across_migration); + auto files = testProject(); + auto plan = MigrationPlanGenerator::generate("myproj", files, "c", "rust"); + + // Check API boundary for each unit + for (const auto& unit : plan.units) { + const FileInfo* fi = nullptr; + for (const auto& f : files) + if (f.filePath == unit.filePath) { fi = &f; break; } + if (!fi) continue; + + auto api = APIBoundaryPreserver::analyze(fi->source, unit); + CHECK(api.apiPreserved, "API should be preserved for " + unit.filePath); + // Every export should be in the public API + for (const auto& exp : unit.exports) + CHECK(api.hasFunction(exp), "missing function: " + exp); + } + PASS(); +} + +void test_generated_tests_validate_equivalence() { + TEST(generated_tests_validate_equivalence); + auto files = testProject(); + auto plan = MigrationPlanGenerator::generate("myproj", files, "c", "rust"); + + int totalTests = 0; + for (const auto& unit : plan.units) { + const FileInfo* fi = nullptr; + for (const auto& f : files) + if (f.filePath == unit.filePath) { fi = &f; break; } + if (!fi) continue; + + auto api = APIBoundaryPreserver::analyze(fi->source, unit); + auto suite = MigrationTestGenerator::generate(api); + totalTests += (int)suite.tests.size(); + + // Each public function should have at least an equivalence test + for (const auto& exp : unit.exports) + CHECK(suite.hasTestFor(exp), "missing test for " + exp); + + // All tests should be in Rust + for (const auto& t : suite.tests) + CHECK(t.language == "rust", "test language should be rust"); + } + CHECK(totalTests >= 6, "expected at least 6 total tests across all modules"); + PASS(); +} + +void test_partial_migration_2_migrated_1_remaining() { + TEST(partial_migration_2_migrated_1_remaining); + auto files = testProject(); + auto plan = MigrationPlanGenerator::generate("myproj", files, "c", "rust"); + auto exec = MigrationExecutor::createExecution(plan, files); + + // Complete only utils and parser (leaf + first dependent) + int completed = 0; + while (completed < 2) { + auto ready = exec.readyItems(); + if (ready.empty()) break; + MigrationExecutor::completeUnit(exec, ready[0].id); + ++completed; + } + + CHECK(MigrationExecutor::isPartialMigration(exec), + "should be partial with 1 remaining"); + CHECK(exec.countByStatus(MigrationStatus::Completed) == 2, "2 should be completed"); + CHECK(exec.countByStatus(MigrationStatus::Pending) >= 1, "at least 1 pending"); + PASS(); +} + +void test_ffi_annotations_on_unmigrated_boundary() { + TEST(ffi_annotations_on_unmigrated_boundary); + auto files = testProject(); + auto plan = MigrationPlanGenerator::generate("myproj", files, "c", "rust"); + auto exec = MigrationExecutor::createExecution(plan, files); + + // Complete utils only + auto ready = exec.readyItems(); + for (const auto& r : ready) { + if (r.filePath == "src/utils.c") + MigrationExecutor::completeUnit(exec, r.id); + } + + CHECK(MigrationExecutor::isPartialMigration(exec), "should be partial"); + int ffi = exec.countFfiBoundaries(); + CHECK(ffi >= 1, "expected FFI boundary between migrated and unmigrated units"); + PASS(); +} + +void test_legacy_analysis_integrated_with_migration() { + TEST(legacy_analysis_integrated_with_migration); + // Run legacy analysis on one of the project files + auto files = testProject(); + json params = {{"source", files[1].source}, {"language", "c"}, {"filePath", "src/parser.c"}}; + auto legacy = ModernizationRPCHandler::handleAnalyzeLegacy(params); + + // Parser source is clean — should have low legacy score + CHECK(legacy.contains("legacyScore"), "expected legacyScore"); + CHECK(legacy.contains("languageVersion"), "expected languageVersion"); + // Now run safety audit + auto safety = ModernizationRPCHandler::handleGetSafetyReport(params); + CHECK(safety.contains("overallRisk"), "expected overallRisk"); + PASS(); +} + +void test_modernization_and_migration_combined() { + TEST(modernization_and_migration_combined); + // Legacy code gets both modernization suggestions AND migration plan + const std::string legacySrc = + "void process(){ int* p=(int*)malloc(4); gets(buf); free(p); }"; + json params = {{"source", legacySrc}, {"language", "c"}, + {"filePath", "legacy.c"}, {"targetLanguage", "rust"}}; + + auto suggestions = ModernizationRPCHandler::handleSuggestModernization(params); + CHECK(!suggestions["suggestions"].empty(), "should have modernization suggestions"); + + auto workflow = ModernizationRPCHandler::handleCreateWorkflow(params); + CHECK(workflow["itemCount"].get() >= 1, "should have workflow items"); + + // Migration plan for the same file + FileInfo fi{"legacy.c", legacySrc, {"process"}, {}}; + auto plan = MigrationPlanGenerator::generate("legacy", {fi}, "c", "rust"); + CHECK(plan.units.size() == 1, "should have 1 migration unit"); + CHECK(plan.units[0].targetLanguage == "rust", "target should be rust"); + PASS(); +} + +void test_sprint_20_complete_pipeline() { + TEST(sprint_20_complete_pipeline); + // Sprint 20 delivers: legacy analysis + safety audit + modernization + + // migration planning + API preservation + test generation + execution + // Verify all components work together + + auto files = testProject(); + + // Phase 20a: Legacy analysis pipeline + json params = {{"source", files[0].source}, {"language", "c"}, {"filePath", "src/utils.c"}}; + auto legacy = ModernizationRPCHandler::handleAnalyzeLegacy(params); + CHECK(legacy.contains("legacyScore"), "phase 20a: legacy analysis works"); + + auto safety = ModernizationRPCHandler::handleGetSafetyReport(params); + CHECK(safety.contains("overallRisk"), "phase 20a: safety audit works"); + + auto suggest = ModernizationRPCHandler::handleSuggestModernization(params); + CHECK(suggest.contains("suggestions"), "phase 20a: suggestions work"); + + // Phase 20b: Migration pipeline + auto plan = MigrationPlanGenerator::generate("sprint20", files, "c", "rust"); + CHECK(plan.units.size() == 3, "phase 20b: migration plan works"); + + auto api = APIBoundaryPreserver::analyze(files[0].source, plan.units[0]); + CHECK(api.apiPreserved, "phase 20b: API boundary works"); + + auto suite = MigrationTestGenerator::generate(api); + CHECK(!suite.tests.empty(), "phase 20b: test generation works"); + + auto exec = MigrationExecutor::createExecution(plan, files); + CHECK(exec.workItems.size() == 3, "phase 20b: execution works"); + + PASS(); +} + +int main() { + std::cout << "Step 448: Phase 20b Integration + Sprint 20 Summary Tests\n"; + + test_full_migration_3_file_c_to_rust(); // 1 + test_api_boundaries_preserved_across_migration(); // 2 + test_generated_tests_validate_equivalence(); // 3 + test_partial_migration_2_migrated_1_remaining(); // 4 + test_ffi_annotations_on_unmigrated_boundary(); // 5 + test_legacy_analysis_integrated_with_migration(); // 6 + test_modernization_and_migration_combined(); // 7 + test_sprint_20_complete_pipeline(); // 8 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 630abac..9c7cb88 100644 --- a/progress.md +++ b/progress.md @@ -5417,6 +5417,10 @@ reporting, plus language-version heuristics and bounded 0-10 legacy scoring. - Routed queue with `whetstone_route_all_ready`. - Note: workflow state in `whetstone_mcp` is session-scoped; execute/get_work_item must run in the same long-lived MCP session. +- Experiment outcome: MCP task flow is functional, but current execution overhead + (session management + transport framing + state locality) is still higher than + direct text-editing workflow for implementation speed. Keep MCP usage for trace + capture and behavior tuning, while using direct edits as primary path for sprint velocity. **Verification run:** - `step438_test` — PASS (12/12) new step coverage @@ -5428,6 +5432,211 @@ reporting, plus language-version heuristics and bounded 0-10 legacy scoring. - `editor/src/LegacyIdiomDetector.h` within header-size limit (`193` <= `600`) - `editor/tests/step438_test.cpp` within test-file size guidance (`129` lines) +**Step 438 experiment note (follow-up):** +- Human-in-the-loop comparison confirmed that normal direct text editing is + currently faster and easier for sprint delivery than the MCP worker path. +- Decision for now: keep direct editing as the primary implementation path, + and use MCP/task-queue flows selectively for behavior tracing and later + training-data/worker-tuning experiments. + +### Step 439: Safety Audit via Annotations +**Status:** PASS (12/12 tests) + +Implemented structured safety analysis with annotation-based findings, +CWE mapping, and per-function risk classification. + +**Files created:** +- `editor/src/SafetyAuditor.h` — full safety audit model: + - buffer overflow detection (gets, strcpy, sprintf, strcat, unchecked array access) + - use-after-free detection (manual malloc/free without RAII, dangling dereference) + - null dereference detection (nullable pointers without null check, unchecked malloc) + - race condition detection (shared mutable state + threading without synchronization) + - integer overflow detection (unchecked arithmetic on integer types) + - CWE code mapping (CWE-120, CWE-416, CWE-476, CWE-362, CWE-190, CWE-787) + - risk levels: 0=info, 1=low, 2=medium, 3=high, 4=critical + - per-function safety findings with max risk aggregation +- `editor/tests/step439_test.cpp` — 12 tests covering: + 1. buffer overflow via gets() → CWE-120 + 2. buffer overflow via strcpy() → CWE-120 + 3. use-after-free with manual malloc/free → CWE-416 + 4. dangling pointer dereference after free → @Lifetime(dangling) + 5. null dereference with unchecked nullable pointer → CWE-476 + 6. unchecked malloc return value → @Nullability(unchecked-alloc) + 7. race condition: thread + global mutable state without sync → CWE-362 + 8. no false positive when mutex synchronization present + 9. integer overflow without bounds check → CWE-190 + 10. no false positive when INT_MAX check present + 11. per-function findings include function name and max risk + 12. comprehensive CWE mapping across all categories +- `editor/CMakeLists.txt` — `step439_test` target + +**Verification run:** +- `step439_test` — PASS (12/12) new step coverage +- `step438_test` — PASS (12/12) regression coverage +- `step54_test` — PASS (10/10) regression coverage + +**Architecture gate check:** +- `editor/src/SafetyAuditor.h` within header-size limit (`225` <= `600`) +- `editor/tests/step439_test.cpp` within test-file size guidance (`148` lines) + +### Step 440: Modernization Suggestions +**Status:** PASS (12/12 tests) + +Maps legacy patterns to modern replacements with @Modernize annotations, +effort classification (quick win / moderate / deep refactor), and cross-language +modernization support (C→Rust ownership, C→Java/Python GC). + +**Files created:** +- `editor/src/ModernizationSuggester.h` — suggestion model: + - maps 7 legacy patterns to modern replacements (malloc→smart_ptr, sprintf→format, + strcpy→string, gets→fgets, goto→structured, pointer_arith→span, K&R→ANSI) + - safety-derived suggestions (unprotected shared state→mutex, unchecked arith→SafeInt) + - cross-language suggestions (C→Rust ownership, C→Java/Python GC) + - effort enum: QuickWin, Moderate, DeepRefactor + - @Modernize(from=..., to=..., risk=...) annotation format +- `editor/tests/step440_test.cpp` — 12 tests +- `editor/CMakeLists.txt` — `step440_test` target + +### Step 441: Modernization Workflow Generation +**Status:** PASS (12/12 tests) + +Converts modernization suggestions into an ordered workflow with routing +(deterministic/LLM/human), dependency tracking, and skeleton code generation. + +**Files created:** +- `editor/src/ModernizationWorkflow.h` — workflow model: + - work items with routing (QuickWin→Deterministic, Moderate→LLM, DeepRefactor→Human) + - priority ordering (safe changes first, risky last) + - dependency graph (deep refactors depend on quick wins) + - skeleton generation with @Modernize inline comments +- `editor/tests/step441_test.cpp` — 12 tests +- `editor/CMakeLists.txt` — `step441_test` target + +### Step 442: Modernization RPC + MCP +**Status:** PASS (12/12 tests) + +JSON-RPC dispatch and MCP tool definitions for the full modernization pipeline. + +**Files created:** +- `editor/src/ModernizationRPC.h` — RPC handler + MCP tool defs: + - `analyzeLegacy` / `whetstone_analyze_legacy` — legacy idiom detection + - `getSafetyReport` / `whetstone_get_safety_report` — structured safety audit + - `suggestModernization` / `whetstone_suggest_modernization` — modernization suggestions + - `createModernizationWorkflow` / `whetstone_create_modernization_workflow` — full pipeline + - JSON serialization for all report types + - canHandle() + dispatch() for integration with existing RPC chain + - 4 MCP tool definitions with input schemas +- `editor/tests/step442_test.cpp` — 12 tests +- `editor/CMakeLists.txt` — `step442_test` target + +### Step 443: Phase 20a Integration +**Status:** PASS (8/8 tests) + +End-to-end integration: legacy C file → analyze → safety audit → suggest → +workflow → validate routing and risk ordering via both native API and JSON-RPC. + +**Files created:** +- `editor/tests/step443_test.cpp` — 8 integration tests covering: + - full pipeline legacy→workflow, safety report CWE validation, + - risk ordering verification, C→Rust cross-language, RPC pipeline, + - deterministic vs LLM/human routing, language version detection + +### Step 444: Migration Plan Generator +**Status:** PASS (12/12 tests) + +Multi-file project analysis with dependency resolution, phased migration ordering +(leaves first), and effort/risk/routing classification per migration unit. + +**Files created:** +- `editor/src/MigrationPlanGenerator.h` — plan generator: + - FileInfo input with exports/imports for dependency resolution + - topological phase assignment (leaf modules = phase 0) + - effort (file size), risk (API surface + deps), routing classification + - ordered execution plan respecting dependencies +- `editor/tests/step444_test.cpp` — 12 tests +- `editor/CMakeLists.txt` — `step444_test` target + +### Step 445: API Boundary Preservation +**Status:** PASS (12/12 tests) + +Preserves public API surfaces during migration with function signature extraction, +type mappings, @Contract annotations, and FFI boundary generation. + +**Files created:** +- `editor/src/APIBoundaryPreserver.h` — boundary preserver: + - public function signature extraction from source + - type mappings C→Rust/Java/Python (int→i32, char*→&str, etc.) + - @Contract annotations (null preconditions, return postconditions) + - FFI boundaries (#[no_mangle] for Rust, JNI for Java) + - verifyPreservation() for pre/post migration comparison +- `editor/tests/step445_test.cpp` — 12 tests +- `editor/CMakeLists.txt` — `step445_test` target + +### Step 446: Test Generation for Migration Validation +**Status:** PASS (12/12 tests) + +Auto-generates equivalence, edge case, and performance regression tests for +migrated modules in the target language. + +**Files created:** +- `editor/src/MigrationTestGenerator.h` — test generator: + - equivalence tests (same inputs → same outputs) per public function + - edge case tests from @Contract pre/post conditions (null handling, etc.) + - performance regression tests + - target-language-specific test body generation (Rust #[test], Java @Test, Python def test_) + - SLM/LLM routing for generated test skeletons +- `editor/tests/step446_test.cpp` — 12 tests +- `editor/CMakeLists.txt` — `step446_test` target + +### Step 447: Migration Execution Integration +**Status:** PASS (12/12 tests) + +Hooks migration plans into orchestration with dependency-respecting execution, +rollback annotations, progressive (partial) migration, and FFI boundary management. + +**Files created:** +- `editor/src/MigrationExecutor.h` — execution engine: + - work items with cross-unit dependency tracking + - ready-items computation (unblocked pending items) + - completeUnit/failUnit state transitions + - rollback annotations (@Rollback pointing to original files) + - partial migration detection (mixed-language state) + - FFI boundary auto-detection during progressive migration +- `editor/tests/step447_test.cpp` — 12 tests +- `editor/CMakeLists.txt` — `step447_test` target + +### Step 448: Phase 20b Integration + Sprint 20 Summary +**Status:** PASS (8/8 tests) + +Full integration: 3-file C project → Rust migration with API preservation, +test generation, partial migration with FFI boundaries, and combined +modernization + migration pipeline. + +**Files created:** +- `editor/tests/step448_test.cpp` — 8 integration tests covering: + - full 3-file C→Rust migration, API boundary preservation, + - generated test validation, partial migration (2/3 files), + - FFI boundary management, legacy+migration combined pipeline, + - sprint 20 complete pipeline verification + +**Sprint 20 totals:** +- **Steps:** 438-448 (11 steps) +- **Tests:** 124/124 passing +- **Headers created:** 8 (LegacyIdiomDetector, SafetyAuditor, ModernizationSuggester, + ModernizationWorkflow, ModernizationRPC, MigrationPlanGenerator, APIBoundaryPreserver, + MigrationTestGenerator, MigrationExecutor) +- **MCP tools added:** 4 (whetstone_analyze_legacy, whetstone_get_safety_report, + whetstone_suggest_modernization, whetstone_create_modernization_workflow) +- **Capabilities delivered:** + - Legacy code analysis (K&R, goto, deprecated APIs, manual memory, pointer arithmetic) + - Safety audit with CWE mapping (CWE-120, 190, 362, 416, 476, 787) + - Modernization suggestions with effort classification and @Modernize annotations + - Workflow generation with deterministic/LLM/human routing + - Multi-file migration planning with dependency-ordered phases + - API boundary preservation with type mappings, contracts, FFI annotations + - Test generation in target language (Rust, Java, Python) + - Progressive migration with FFI boundary management + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)