Complete Sprint 20: Legacy Code Ingestion (steps 439-448, 124/124 tests)
Phase 20a delivers legacy analysis, safety audit with CWE mapping, modernization suggestions with effort routing, and RPC/MCP tools. Phase 20b adds multi-file migration planning, API boundary preservation, target-language test generation, and progressive migration execution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
253
editor/src/APIBoundaryPreserver.h
Normal file
253
editor/src/APIBoundaryPreserver.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
|
||||
struct FunctionSignature {
|
||||
std::string name;
|
||||
std::string returnType;
|
||||
std::vector<std::string> paramTypes;
|
||||
std::vector<std::string> 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<FunctionSignature> publicAPI;
|
||||
std::vector<TypeMapping> typeMappings;
|
||||
std::vector<ContractAnnotation> contracts;
|
||||
std::vector<FFIBoundary> 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<FunctionSignature> extractPublicFunctions(
|
||||
const std::string& source, const std::vector<std::string>& exports) {
|
||||
std::vector<FunctionSignature> 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<TypeMapping> generateTypeMappings(const std::string& source,
|
||||
const MigrationUnit& unit) {
|
||||
std::vector<TypeMapping> 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<ContractAnnotation> generateContracts(
|
||||
const std::vector<FunctionSignature>& api, const std::string& source) {
|
||||
std::vector<ContractAnnotation> 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<FFIBoundary> generateFFIBoundaries(
|
||||
const std::vector<FunctionSignature>& api, const MigrationUnit& unit) {
|
||||
std::vector<FFIBoundary> 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);
|
||||
}
|
||||
};
|
||||
199
editor/src/MigrationExecutor.h
Normal file
199
editor/src/MigrationExecutor.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
|
||||
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<std::string> 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<MigrationWorkItem> workItems;
|
||||
std::vector<PartialMigrationState> 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<MigrationWorkItem> readyItems() const {
|
||||
std::vector<MigrationWorkItem> 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<FileInfo>& 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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
205
editor/src/MigrationPlanGenerator.h
Normal file
205
editor/src/MigrationPlanGenerator.h
Normal file
@@ -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 <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<std::string> dependsOn; // unit IDs this depends on
|
||||
std::vector<std::string> exports; // public API symbols
|
||||
std::vector<std::string> 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<MigrationUnit> 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<MigrationUnit> unitsInPhase(int phase) const {
|
||||
std::vector<MigrationUnit> out;
|
||||
for (const auto& u : units)
|
||||
if (u.phase == phase) out.push_back(u);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<MigrationUnit> 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<std::string> exports; // public function/type names
|
||||
std::vector<std::string> imports; // referenced external symbols
|
||||
};
|
||||
|
||||
class MigrationPlanGenerator {
|
||||
public:
|
||||
static MigrationPlan generate(const std::string& projectName,
|
||||
const std::vector<FileInfo>& 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<FileInfo>& 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;
|
||||
}
|
||||
};
|
||||
213
editor/src/MigrationTestGenerator.h
Normal file
213
editor/src/MigrationTestGenerator.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
|
||||
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<MigrationTest> 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<MigrationTest> testsForFunction(const std::string& funcName) const {
|
||||
std::vector<MigrationTest> 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;
|
||||
}
|
||||
};
|
||||
242
editor/src/ModernizationRPC.h
Normal file
242
editor/src/ModernizationRPC.h
Normal file
@@ -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 <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<MCPToolDef> 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}
|
||||
};
|
||||
}
|
||||
};
|
||||
192
editor/src/ModernizationSuggester.h
Normal file
192
editor/src/ModernizationSuggester.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
|
||||
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<ModernizeSuggestion> 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<ModernizeSuggestion> quickWins() const {
|
||||
std::vector<ModernizeSuggestion> out;
|
||||
for (const auto& s : suggestions)
|
||||
if (s.effort == ModernizeEffort::QuickWin) out.push_back(s);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<ModernizeSuggestion> deepRefactors() const {
|
||||
std::vector<ModernizeSuggestion> 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"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
179
editor/src/ModernizationWorkflow.h
Normal file
179
editor/src/ModernizationWorkflow.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
|
||||
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<std::string> dependsOn;
|
||||
};
|
||||
|
||||
struct ModernizationSkeleton {
|
||||
std::string originalSource;
|
||||
std::string skeletonSource; // modernized structure with placeholders
|
||||
std::string language;
|
||||
std::vector<std::string> annotations;
|
||||
};
|
||||
|
||||
struct ModernizationWorkflow {
|
||||
std::string filePath;
|
||||
std::string sourceLanguage;
|
||||
std::string targetLanguage;
|
||||
std::vector<ModernizeWorkItem> 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<ModernizeWorkItem> 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<std::string> 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();
|
||||
}
|
||||
}
|
||||
};
|
||||
289
editor/src/SafetyAuditor.h
Normal file
289
editor/src/SafetyAuditor.h
Normal file
@@ -0,0 +1,289 @@
|
||||
#pragma once
|
||||
|
||||
// Step 439: Safety Audit via Annotations — structured safety analysis
|
||||
// with CWE mapping and risk-level classification.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<SafetyFinding> findings;
|
||||
int maxRisk = 0;
|
||||
};
|
||||
|
||||
struct SafetyReport {
|
||||
std::string filePath;
|
||||
std::string language;
|
||||
int overallRisk = 0; // max risk across all findings
|
||||
std::vector<SafetyFinding> findings;
|
||||
std::vector<FunctionSafetyFinding> 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<FunctionSafetyFinding> buildFunctionFindings(
|
||||
const std::string& source,
|
||||
const std::vector<SafetyFinding>& allFindings) {
|
||||
std::vector<FunctionSafetyFinding> 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;
|
||||
}
|
||||
};
|
||||
178
editor/tests/step439_test.cpp
Normal file
178
editor/tests/step439_test.cpp
Normal file
@@ -0,0 +1,178 @@
|
||||
// Step 439: Safety Audit via Annotations Tests (12 tests)
|
||||
|
||||
#include "SafetyAuditor.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
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;
|
||||
}
|
||||
180
editor/tests/step440_test.cpp
Normal file
180
editor/tests/step440_test.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
// Step 440: Modernization Suggestions Tests (12 tests)
|
||||
|
||||
#include "ModernizationSuggester.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
// 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;
|
||||
}
|
||||
168
editor/tests/step441_test.cpp
Normal file
168
editor/tests/step441_test.cpp
Normal file
@@ -0,0 +1,168 @@
|
||||
// Step 441: Modernization Workflow Generation Tests (12 tests)
|
||||
|
||||
#include "ModernizationWorkflow.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
static 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;
|
||||
}
|
||||
190
editor/tests/step442_test.cpp
Normal file
190
editor/tests/step442_test.cpp
Normal file
@@ -0,0 +1,190 @@
|
||||
// Step 442: Modernization RPC + MCP Tests (12 tests)
|
||||
|
||||
#include "ModernizationRPC.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
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<int>() > 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<int>() >= 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<std::string>().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<int>() >= 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>();
|
||||
int human = result["humanCount"].get<int>();
|
||||
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;
|
||||
}
|
||||
195
editor/tests/step443_test.cpp
Normal file
195
editor/tests/step443_test.cpp
Normal file
@@ -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 <iostream>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
// 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<int>() >= 5, "RPC legacy score too low");
|
||||
|
||||
// Step 2: safety report
|
||||
auto safety = ModernizationRPCHandler::handleGetSafetyReport(params);
|
||||
CHECK(safety["overallRisk"].get<int>() >= 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<int>() >= 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<int>();
|
||||
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>();
|
||||
int human = wf["humanCount"].get<int>();
|
||||
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;
|
||||
}
|
||||
179
editor/tests/step444_test.cpp
Normal file
179
editor/tests/step444_test.cpp
Normal file
@@ -0,0 +1,179 @@
|
||||
// Step 444: Migration Plan Generator Tests (12 tests)
|
||||
|
||||
#include "MigrationPlanGenerator.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
// 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<FileInfo> testProject() {
|
||||
return {
|
||||
{"src/utils.c",
|
||||
"int clamp(int x, int lo, int hi){ return x<lo?lo:x>hi?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;
|
||||
}
|
||||
182
editor/tests/step445_test.cpp
Normal file
182
editor/tests/step445_test.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
// Step 445: API Boundary Preservation Tests (12 tests)
|
||||
|
||||
#include "APIBoundaryPreserver.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
static MigrationUnit makeUnit(const std::string& path, const std::string& mod,
|
||||
const std::vector<std::string>& 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 x<lo?lo:x>hi?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;
|
||||
}
|
||||
215
editor/tests/step446_test.cpp
Normal file
215
editor/tests/step446_test.cpp
Normal file
@@ -0,0 +1,215 @@
|
||||
// Step 446: Test Generation for Migration Validation Tests (12 tests)
|
||||
|
||||
#include "MigrationTestGenerator.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
static MigrationUnit makeUnit(const std::string& mod,
|
||||
const std::vector<std::string>& 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 x<lo?lo:x>hi?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;
|
||||
}
|
||||
202
editor/tests/step447_test.cpp
Normal file
202
editor/tests/step447_test.cpp
Normal file
@@ -0,0 +1,202 @@
|
||||
// Step 447: Migration Execution Integration Tests (12 tests)
|
||||
|
||||
#include "MigrationExecutor.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
static std::vector<FileInfo> testProject() {
|
||||
return {
|
||||
{"src/utils.c",
|
||||
"int clamp(int x, int lo, int hi){ return x<lo?lo:x>hi?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;
|
||||
}
|
||||
236
editor/tests/step448_test.cpp
Normal file
236
editor/tests/step448_test.cpp
Normal file
@@ -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 <iostream>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
static std::vector<FileInfo> testProject() {
|
||||
return {
|
||||
{"src/utils.c",
|
||||
"int clamp(int x, int lo, int hi){ return x<lo?lo:x>hi?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<int>() >= 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;
|
||||
}
|
||||
209
progress.md
209
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)
|
||||
|
||||
Reference in New Issue
Block a user