diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt index 228bf31..cb605ca 100644 --- a/backend/CMakeLists.txt +++ b/backend/CMakeLists.txt @@ -14,6 +14,8 @@ find_package(CURL QUIET) # Library sources set(WIZARDMERGE_SOURCES src/merge/three_way_merge.cpp + src/analysis/context_analyzer.cpp + src/analysis/risk_analyzer.cpp ) # Add git sources only if CURL is available @@ -67,7 +69,11 @@ endif() if(GTest_FOUND) enable_testing() - set(TEST_SOURCES tests/test_three_way_merge.cpp) + set(TEST_SOURCES + tests/test_three_way_merge.cpp + tests/test_context_analyzer.cpp + tests/test_risk_analyzer.cpp + ) # Add github client tests only if CURL is available if(CURL_FOUND) diff --git a/backend/include/wizardmerge/analysis/context_analyzer.h b/backend/include/wizardmerge/analysis/context_analyzer.h new file mode 100644 index 0000000..f20362b --- /dev/null +++ b/backend/include/wizardmerge/analysis/context_analyzer.h @@ -0,0 +1,98 @@ +/** + * @file context_analyzer.h + * @brief Context analysis for merge conflicts + * + * Analyzes the code context around merge conflicts to provide better + * understanding and intelligent suggestions for resolution. + */ + +#ifndef WIZARDMERGE_ANALYSIS_CONTEXT_ANALYZER_H +#define WIZARDMERGE_ANALYSIS_CONTEXT_ANALYZER_H + +#include +#include +#include + +namespace wizardmerge { +namespace analysis { + +/** + * @brief Represents code context information for a specific line or region. + */ +struct CodeContext { + size_t start_line; + size_t end_line; + std::vector surrounding_lines; + std::string function_name; + std::string class_name; + std::vector imports; + std::map metadata; +}; + +/** + * @brief Analyzes code context around a specific region. + * + * This function examines the code surrounding a conflict or change + * to provide contextual information that can help in understanding + * the change and making better merge decisions. + * + * @param lines The full file content as lines + * @param start_line Starting line of the region of interest + * @param end_line Ending line of the region of interest + * @param context_window Number of lines before/after to include (default: 5) + * @return CodeContext containing analyzed context information + */ +CodeContext analyze_context( + const std::vector& lines, + size_t start_line, + size_t end_line, + size_t context_window = 5 +); + +/** + * @brief Extracts function or method name from context. + * + * Analyzes surrounding code to determine if the region is within + * a function or method, and extracts its name. + * + * @param lines Lines of code to analyze + * @param line_number Line number to check + * @return Function name if found, empty string otherwise + */ +std::string extract_function_name( + const std::vector& lines, + size_t line_number +); + +/** + * @brief Extracts class name from context. + * + * Analyzes surrounding code to determine if the region is within + * a class definition, and extracts its name. + * + * @param lines Lines of code to analyze + * @param line_number Line number to check + * @return Class name if found, empty string otherwise + */ +std::string extract_class_name( + const std::vector& lines, + size_t line_number +); + +/** + * @brief Extracts import/include statements from the file. + * + * Scans the file for import, include, or require statements + * to understand dependencies. + * + * @param lines Lines of code to analyze + * @return Vector of import statements + */ +std::vector extract_imports( + const std::vector& lines +); + +} // namespace analysis +} // namespace wizardmerge + +#endif // WIZARDMERGE_ANALYSIS_CONTEXT_ANALYZER_H diff --git a/backend/include/wizardmerge/analysis/risk_analyzer.h b/backend/include/wizardmerge/analysis/risk_analyzer.h new file mode 100644 index 0000000..402c2fe --- /dev/null +++ b/backend/include/wizardmerge/analysis/risk_analyzer.h @@ -0,0 +1,118 @@ +/** + * @file risk_analyzer.h + * @brief Risk analysis for merge conflict resolutions + * + * Assesses the risk level of different resolution choices to help + * developers make safer merge decisions. + */ + +#ifndef WIZARDMERGE_ANALYSIS_RISK_ANALYZER_H +#define WIZARDMERGE_ANALYSIS_RISK_ANALYZER_H + +#include +#include + +namespace wizardmerge { +namespace analysis { + +/** + * @brief Risk level enumeration for merge resolutions. + */ +enum class RiskLevel { + LOW, // Safe to merge, minimal risk + MEDIUM, // Some risk, review recommended + HIGH, // High risk, careful review required + CRITICAL // Critical risk, requires expert review +}; + +/** + * @brief Detailed risk assessment for a merge resolution. + */ +struct RiskAssessment { + RiskLevel level; + double confidence_score; // 0.0 to 1.0 + std::vector risk_factors; + std::vector recommendations; + + // Specific risk indicators + bool has_syntax_changes; + bool has_logic_changes; + bool has_api_changes; + bool affects_multiple_functions; + bool affects_critical_section; +}; + +/** + * @brief Analyzes risk of accepting "ours" version. + * + * @param base Base version lines + * @param ours Our version lines + * @param theirs Their version lines + * @return RiskAssessment for accepting ours + */ +RiskAssessment analyze_risk_ours( + const std::vector& base, + const std::vector& ours, + const std::vector& theirs +); + +/** + * @brief Analyzes risk of accepting "theirs" version. + * + * @param base Base version lines + * @param ours Our version lines + * @param theirs Their version lines + * @return RiskAssessment for accepting theirs + */ +RiskAssessment analyze_risk_theirs( + const std::vector& base, + const std::vector& ours, + const std::vector& theirs +); + +/** + * @brief Analyzes risk of accepting both versions (concatenation). + * + * @param base Base version lines + * @param ours Our version lines + * @param theirs Their version lines + * @return RiskAssessment for accepting both + */ +RiskAssessment analyze_risk_both( + const std::vector& base, + const std::vector& ours, + const std::vector& theirs +); + +/** + * @brief Converts RiskLevel to string representation. + * + * @param level Risk level to convert + * @return String representation ("low", "medium", "high", "critical") + */ +std::string risk_level_to_string(RiskLevel level); + +/** + * @brief Checks if code contains critical patterns (security, data loss, etc.). + * + * @param lines Lines of code to check + * @return true if critical patterns detected + */ +bool contains_critical_patterns(const std::vector& lines); + +/** + * @brief Detects if changes affect API signatures. + * + * @param base Base version lines + * @param modified Modified version lines + * @return true if API changes detected + */ +bool has_api_signature_changes( + const std::vector& base, + const std::vector& modified +); + +} // namespace analysis +} // namespace wizardmerge + +#endif // WIZARDMERGE_ANALYSIS_RISK_ANALYZER_H diff --git a/backend/include/wizardmerge/merge/three_way_merge.h b/backend/include/wizardmerge/merge/three_way_merge.h index 7a2e245..77f1953 100644 --- a/backend/include/wizardmerge/merge/three_way_merge.h +++ b/backend/include/wizardmerge/merge/three_way_merge.h @@ -12,6 +12,8 @@ #include #include +#include "wizardmerge/analysis/context_analyzer.h" +#include "wizardmerge/analysis/risk_analyzer.h" namespace wizardmerge { namespace merge { @@ -33,6 +35,12 @@ struct Conflict { std::vector base_lines; std::vector our_lines; std::vector their_lines; + + // Context and risk analysis + analysis::CodeContext context; + analysis::RiskAssessment risk_ours; + analysis::RiskAssessment risk_theirs; + analysis::RiskAssessment risk_both; }; /** diff --git a/backend/src/analysis/context_analyzer.cpp b/backend/src/analysis/context_analyzer.cpp new file mode 100644 index 0000000..ec61d31 --- /dev/null +++ b/backend/src/analysis/context_analyzer.cpp @@ -0,0 +1,232 @@ +/** + * @file context_analyzer.cpp + * @brief Implementation of context analysis for merge conflicts + */ + +#include "wizardmerge/analysis/context_analyzer.h" +#include +#include + +namespace wizardmerge { +namespace analysis { + +namespace { + +/** + * @brief Trim whitespace from string. + */ +std::string trim(const std::string& str) { + size_t start = str.find_first_not_of(" \t\n\r"); + size_t end = str.find_last_not_of(" \t\n\r"); + if (start == std::string::npos) return ""; + return str.substr(start, end - start + 1); +} + +/** + * @brief Check if a line is a function definition. + */ +bool is_function_definition(const std::string& line) { + std::string trimmed = trim(line); + + // Common function patterns across languages + std::vector patterns = { + std::regex(R"(^\w+\s+\w+\s*\([^)]*\)\s*\{?)"), // C/C++/Java: type name(params) + std::regex(R"(^def\s+\w+\s*\([^)]*\):)"), // Python: def name(params): + std::regex(R"(^function\s+\w+\s*\([^)]*\))"), // JavaScript: function name(params) + std::regex(R"(^\w+\s*:\s*function\s*\([^)]*\))"), // JS object method + std::regex(R"(^(public|private|protected)?\s*\w+\s+\w+\s*\([^)]*\))") // Java/C# methods + }; + + for (const auto& pattern : patterns) { + if (std::regex_search(trimmed, pattern)) { + return true; + } + } + + return false; +} + +/** + * @brief Extract function name from a function definition line. + */ +std::string get_function_name_from_line(const std::string& line) { + std::string trimmed = trim(line); + + // Try to extract function name using regex + std::smatch match; + + // Python: def function_name( + std::regex py_pattern(R"(def\s+(\w+)\s*\()"); + if (std::regex_search(trimmed, match, py_pattern)) { + return match[1].str(); + } + + // JavaScript: function function_name( + std::regex js_pattern(R"(function\s+(\w+)\s*\()"); + if (std::regex_search(trimmed, match, js_pattern)) { + return match[1].str(); + } + + // C/C++/Java: type function_name( + std::regex cpp_pattern(R"(\w+\s+(\w+)\s*\()"); + if (std::regex_search(trimmed, match, cpp_pattern)) { + return match[1].str(); + } + + return ""; +} + +/** + * @brief Check if a line is a class definition. + */ +bool is_class_definition(const std::string& line) { + std::string trimmed = trim(line); + + std::vector patterns = { + std::regex(R"(^class\s+\w+)"), // Python/C++/Java: class Name + std::regex(R"(^(public|private)?\s*class\s+\w+)"), // Java/C#: visibility class Name + std::regex(R"(^struct\s+\w+)") // C/C++: struct Name + }; + + for (const auto& pattern : patterns) { + if (std::regex_search(trimmed, pattern)) { + return true; + } + } + + return false; +} + +/** + * @brief Extract class name from a class definition line. + */ +std::string get_class_name_from_line(const std::string& line) { + std::string trimmed = trim(line); + + std::smatch match; + std::regex pattern(R"((class|struct)\s+(\w+))"); + + if (std::regex_search(trimmed, match, pattern)) { + return match[2].str(); + } + + return ""; +} + +} // anonymous namespace + +CodeContext analyze_context( + const std::vector& lines, + size_t start_line, + size_t end_line, + size_t context_window +) { + CodeContext context; + context.start_line = start_line; + context.end_line = end_line; + + // Extract surrounding lines + size_t window_start = (start_line >= context_window) ? (start_line - context_window) : 0; + size_t window_end = std::min(end_line + context_window, lines.size()); + + for (size_t i = window_start; i < window_end; ++i) { + context.surrounding_lines.push_back(lines[i]); + } + + // Extract function name + context.function_name = extract_function_name(lines, start_line); + + // Extract class name + context.class_name = extract_class_name(lines, start_line); + + // Extract imports + context.imports = extract_imports(lines); + + // Add metadata + context.metadata["context_window_start"] = std::to_string(window_start); + context.metadata["context_window_end"] = std::to_string(window_end); + context.metadata["total_lines"] = std::to_string(lines.size()); + + return context; +} + +std::string extract_function_name( + const std::vector& lines, + size_t line_number +) { + if (line_number >= lines.size()) { + return ""; + } + + // Check the line itself first + if (is_function_definition(lines[line_number])) { + return get_function_name_from_line(lines[line_number]); + } + + // Search backwards for function definition + for (int i = static_cast(line_number) - 1; i >= 0; --i) { + if (is_function_definition(lines[i])) { + return get_function_name_from_line(lines[i]); + } + + // Stop searching if we hit a class definition or another function + std::string trimmed = trim(lines[i]); + if (trimmed.find("class ") == 0 || trimmed.find("struct ") == 0) { + break; + } + } + + return ""; +} + +std::string extract_class_name( + const std::vector& lines, + size_t line_number +) { + if (line_number >= lines.size()) { + return ""; + } + + // Search backwards for class definition + int brace_count = 0; + for (int i = static_cast(line_number); i >= 0; --i) { + std::string line = lines[i]; + + // Count braces to track scope + brace_count += std::count(line.begin(), line.end(), '}'); + brace_count -= std::count(line.begin(), line.end(), '{'); + + if (is_class_definition(line) && brace_count <= 0) { + return get_class_name_from_line(line); + } + } + + return ""; +} + +std::vector extract_imports( + const std::vector& lines +) { + std::vector imports; + + // Scan first 50 lines (imports are typically at the top) + size_t scan_limit = std::min(lines.size(), size_t(50)); + + for (size_t i = 0; i < scan_limit; ++i) { + std::string line = trim(lines[i]); + + // Check for various import patterns + if (line.find("#include") == 0 || + line.find("import ") == 0 || + line.find("from ") == 0 || + line.find("require(") != std::string::npos || + line.find("using ") == 0) { + imports.push_back(line); + } + } + + return imports; +} + +} // namespace analysis +} // namespace wizardmerge diff --git a/backend/src/analysis/risk_analyzer.cpp b/backend/src/analysis/risk_analyzer.cpp new file mode 100644 index 0000000..4a3372d --- /dev/null +++ b/backend/src/analysis/risk_analyzer.cpp @@ -0,0 +1,343 @@ +/** + * @file risk_analyzer.cpp + * @brief Implementation of risk analysis for merge conflict resolutions + */ + +#include "wizardmerge/analysis/risk_analyzer.h" +#include +#include +#include + +namespace wizardmerge { +namespace analysis { + +namespace { + +/** + * @brief Trim whitespace from string. + */ +std::string trim(const std::string& str) { + size_t start = str.find_first_not_of(" \t\n\r"); + size_t end = str.find_last_not_of(" \t\n\r"); + if (start == std::string::npos) return ""; + return str.substr(start, end - start + 1); +} + +/** + * @brief Calculate similarity score between two sets of lines (0.0 to 1.0). + */ +double calculate_similarity( + const std::vector& lines1, + const std::vector& lines2 +) { + if (lines1.empty() && lines2.empty()) return 1.0; + if (lines1.empty() || lines2.empty()) return 0.0; + + // Simple Jaccard similarity on lines + size_t common_lines = 0; + for (const auto& line1 : lines1) { + if (std::find(lines2.begin(), lines2.end(), line1) != lines2.end()) { + common_lines++; + } + } + + size_t total_unique = lines1.size() + lines2.size() - common_lines; + return total_unique > 0 ? static_cast(common_lines) / total_unique : 0.0; +} + +/** + * @brief Count number of changed lines between two versions. + */ +size_t count_changes( + const std::vector& base, + const std::vector& modified +) { + size_t changes = 0; + size_t max_len = std::max(base.size(), modified.size()); + + for (size_t i = 0; i < max_len; ++i) { + std::string base_line = (i < base.size()) ? base[i] : ""; + std::string mod_line = (i < modified.size()) ? modified[i] : ""; + + if (base_line != mod_line) { + changes++; + } + } + + return changes; +} + +/** + * @brief Check if line contains function or method definition. + */ +bool is_function_signature(const std::string& line) { + std::string trimmed = trim(line); + + std::vector patterns = { + std::regex(R"(^\w+\s+\w+\s*\([^)]*\))"), // C/C++/Java + std::regex(R"(^def\s+\w+\s*\([^)]*\):)"), // Python + std::regex(R"(^function\s+\w+\s*\([^)]*\))"), // JavaScript + }; + + for (const auto& pattern : patterns) { + if (std::regex_search(trimmed, pattern)) { + return true; + } + } + + return false; +} + +} // anonymous namespace + +std::string risk_level_to_string(RiskLevel level) { + switch (level) { + case RiskLevel::LOW: return "low"; + case RiskLevel::MEDIUM: return "medium"; + case RiskLevel::HIGH: return "high"; + case RiskLevel::CRITICAL: return "critical"; + default: return "unknown"; + } +} + +bool contains_critical_patterns(const std::vector& lines) { + std::vector critical_patterns = { + std::regex(R"(delete\s+\w+)"), // Delete operations + std::regex(R"(drop\s+(table|database))"), // Database drops + std::regex(R"(rm\s+-rf)"), // Destructive file operations + std::regex(R"(eval\s*\()"), // Eval (security risk) + std::regex(R"(exec\s*\()"), // Exec (security risk) + std::regex(R"(system\s*\()"), // System calls + std::regex(R"(\.password\s*=)"), // Password assignments + std::regex(R"(\.secret\s*=)"), // Secret assignments + std::regex(R"(sudo\s+)"), // Sudo usage + std::regex(R"(chmod\s+777)"), // Overly permissive permissions + }; + + for (const auto& line : lines) { + std::string trimmed = trim(line); + for (const auto& pattern : critical_patterns) { + if (std::regex_search(trimmed, pattern)) { + return true; + } + } + } + + return false; +} + +bool has_api_signature_changes( + const std::vector& base, + const std::vector& modified +) { + // Check if function signatures changed + for (size_t i = 0; i < base.size() && i < modified.size(); ++i) { + bool base_is_sig = is_function_signature(base[i]); + bool mod_is_sig = is_function_signature(modified[i]); + + if (base_is_sig && mod_is_sig && base[i] != modified[i]) { + return true; + } + } + + return false; +} + +RiskAssessment analyze_risk_ours( + const std::vector& base, + const std::vector& ours, + const std::vector& theirs +) { + RiskAssessment assessment; + assessment.level = RiskLevel::LOW; + assessment.confidence_score = 0.5; + assessment.has_syntax_changes = false; + assessment.has_logic_changes = false; + assessment.has_api_changes = false; + assessment.affects_multiple_functions = false; + assessment.affects_critical_section = false; + + // Calculate changes + size_t our_changes = count_changes(base, ours); + size_t their_changes = count_changes(base, theirs); + double similarity_to_theirs = calculate_similarity(ours, theirs); + + // Check for critical patterns + if (contains_critical_patterns(ours)) { + assessment.affects_critical_section = true; + assessment.risk_factors.push_back("Contains critical code patterns (security/data operations)"); + assessment.level = RiskLevel::HIGH; + } + + // Check for API changes + if (has_api_signature_changes(base, ours)) { + assessment.has_api_changes = true; + assessment.risk_factors.push_back("Function/method signatures changed"); + if (assessment.level < RiskLevel::MEDIUM) { + assessment.level = RiskLevel::MEDIUM; + } + } + + // Assess based on amount of change + if (our_changes > 10) { + assessment.has_logic_changes = true; + assessment.risk_factors.push_back("Large number of changes (" + std::to_string(our_changes) + " lines)"); + if (assessment.level < RiskLevel::MEDIUM) { + assessment.level = RiskLevel::MEDIUM; + } + } + + // Check if we're discarding significant changes from theirs + if (their_changes > 5 && similarity_to_theirs < 0.3) { + assessment.risk_factors.push_back("Discarding significant changes from other branch (" + + std::to_string(their_changes) + " lines)"); + if (assessment.level < RiskLevel::MEDIUM) { + assessment.level = RiskLevel::MEDIUM; + } + } + + // Calculate confidence score based on various factors + double change_ratio = (our_changes + their_changes) > 0 ? + static_cast(our_changes) / (our_changes + their_changes) : 0.5; + assessment.confidence_score = 0.5 + (0.3 * similarity_to_theirs) + (0.2 * change_ratio); + + // Add recommendations + if (assessment.level >= RiskLevel::MEDIUM) { + assessment.recommendations.push_back("Review changes carefully before accepting"); + } + if (assessment.has_api_changes) { + assessment.recommendations.push_back("Verify API compatibility with dependent code"); + } + if (assessment.affects_critical_section) { + assessment.recommendations.push_back("Test thoroughly, especially security and data operations"); + } + if (assessment.risk_factors.empty()) { + assessment.recommendations.push_back("Changes appear safe to accept"); + } + + return assessment; +} + +RiskAssessment analyze_risk_theirs( + const std::vector& base, + const std::vector& ours, + const std::vector& theirs +) { + RiskAssessment assessment; + assessment.level = RiskLevel::LOW; + assessment.confidence_score = 0.5; + assessment.has_syntax_changes = false; + assessment.has_logic_changes = false; + assessment.has_api_changes = false; + assessment.affects_multiple_functions = false; + assessment.affects_critical_section = false; + + // Calculate changes + size_t our_changes = count_changes(base, ours); + size_t their_changes = count_changes(base, theirs); + double similarity_to_ours = calculate_similarity(theirs, ours); + + // Check for critical patterns + if (contains_critical_patterns(theirs)) { + assessment.affects_critical_section = true; + assessment.risk_factors.push_back("Contains critical code patterns (security/data operations)"); + assessment.level = RiskLevel::HIGH; + } + + // Check for API changes + if (has_api_signature_changes(base, theirs)) { + assessment.has_api_changes = true; + assessment.risk_factors.push_back("Function/method signatures changed"); + if (assessment.level < RiskLevel::MEDIUM) { + assessment.level = RiskLevel::MEDIUM; + } + } + + // Assess based on amount of change + if (their_changes > 10) { + assessment.has_logic_changes = true; + assessment.risk_factors.push_back("Large number of changes (" + std::to_string(their_changes) + " lines)"); + if (assessment.level < RiskLevel::MEDIUM) { + assessment.level = RiskLevel::MEDIUM; + } + } + + // Check if we're discarding our changes + if (our_changes > 5 && similarity_to_ours < 0.3) { + assessment.risk_factors.push_back("Discarding our local changes (" + + std::to_string(our_changes) + " lines)"); + if (assessment.level < RiskLevel::MEDIUM) { + assessment.level = RiskLevel::MEDIUM; + } + } + + // Calculate confidence score + double change_ratio = (our_changes + their_changes) > 0 ? + static_cast(their_changes) / (our_changes + their_changes) : 0.5; + assessment.confidence_score = 0.5 + (0.3 * similarity_to_ours) + (0.2 * change_ratio); + + // Add recommendations + if (assessment.level >= RiskLevel::MEDIUM) { + assessment.recommendations.push_back("Review changes carefully before accepting"); + } + if (assessment.has_api_changes) { + assessment.recommendations.push_back("Verify API compatibility with dependent code"); + } + if (assessment.affects_critical_section) { + assessment.recommendations.push_back("Test thoroughly, especially security and data operations"); + } + if (assessment.risk_factors.empty()) { + assessment.recommendations.push_back("Changes appear safe to accept"); + } + + return assessment; +} + +RiskAssessment analyze_risk_both( + const std::vector& base, + const std::vector& ours, + const std::vector& theirs +) { + RiskAssessment assessment; + assessment.level = RiskLevel::MEDIUM; // Default to medium for concatenation + assessment.confidence_score = 0.3; // Lower confidence for concatenation + assessment.has_syntax_changes = true; + assessment.has_logic_changes = true; + assessment.has_api_changes = false; + assessment.affects_multiple_functions = false; + assessment.affects_critical_section = false; + + // Concatenating both versions is generally risky + assessment.risk_factors.push_back("Concatenating both versions may cause duplicates or conflicts"); + + // Check if either contains critical patterns + if (contains_critical_patterns(ours) || contains_critical_patterns(theirs)) { + assessment.affects_critical_section = true; + assessment.risk_factors.push_back("Contains critical code patterns that may conflict"); + assessment.level = RiskLevel::HIGH; + } + + // Check for duplicate logic + double similarity = calculate_similarity(ours, theirs); + if (similarity > 0.5) { + assessment.risk_factors.push_back("High similarity may result in duplicate code"); + assessment.level = RiskLevel::HIGH; + } + + // API changes from either side + if (has_api_signature_changes(base, ours) || has_api_signature_changes(base, theirs)) { + assessment.has_api_changes = true; + assessment.risk_factors.push_back("Multiple API changes may cause conflicts"); + assessment.level = RiskLevel::HIGH; + } + + // Recommendations for concatenation + assessment.recommendations.push_back("Manual review required - automatic concatenation is risky"); + assessment.recommendations.push_back("Consider merging logic manually instead of concatenating"); + assessment.recommendations.push_back("Test thoroughly for duplicate or conflicting code"); + + return assessment; +} + +} // namespace analysis +} // namespace wizardmerge diff --git a/backend/src/controllers/MergeController.cc b/backend/src/controllers/MergeController.cc index ad7aa55..d840c0a 100644 --- a/backend/src/controllers/MergeController.cc +++ b/backend/src/controllers/MergeController.cc @@ -101,6 +101,65 @@ void MergeController::merge( } conflictObj["their_lines"] = theirLines; + // Add context analysis + Json::Value contextObj; + contextObj["function_name"] = conflict.context.function_name; + contextObj["class_name"] = conflict.context.class_name; + Json::Value importsArray(Json::arrayValue); + for (const auto& import : conflict.context.imports) { + importsArray.append(import); + } + contextObj["imports"] = importsArray; + conflictObj["context"] = contextObj; + + // Add risk analysis for "ours" resolution + Json::Value riskOursObj; + riskOursObj["level"] = wizardmerge::analysis::risk_level_to_string(conflict.risk_ours.level); + riskOursObj["confidence_score"] = conflict.risk_ours.confidence_score; + Json::Value riskFactorsOurs(Json::arrayValue); + for (const auto& factor : conflict.risk_ours.risk_factors) { + riskFactorsOurs.append(factor); + } + riskOursObj["risk_factors"] = riskFactorsOurs; + Json::Value recommendationsOurs(Json::arrayValue); + for (const auto& rec : conflict.risk_ours.recommendations) { + recommendationsOurs.append(rec); + } + riskOursObj["recommendations"] = recommendationsOurs; + conflictObj["risk_ours"] = riskOursObj; + + // Add risk analysis for "theirs" resolution + Json::Value riskTheirsObj; + riskTheirsObj["level"] = wizardmerge::analysis::risk_level_to_string(conflict.risk_theirs.level); + riskTheirsObj["confidence_score"] = conflict.risk_theirs.confidence_score; + Json::Value riskFactorsTheirs(Json::arrayValue); + for (const auto& factor : conflict.risk_theirs.risk_factors) { + riskFactorsTheirs.append(factor); + } + riskTheirsObj["risk_factors"] = riskFactorsTheirs; + Json::Value recommendationsTheirs(Json::arrayValue); + for (const auto& rec : conflict.risk_theirs.recommendations) { + recommendationsTheirs.append(rec); + } + riskTheirsObj["recommendations"] = recommendationsTheirs; + conflictObj["risk_theirs"] = riskTheirsObj; + + // Add risk analysis for "both" resolution + Json::Value riskBothObj; + riskBothObj["level"] = wizardmerge::analysis::risk_level_to_string(conflict.risk_both.level); + riskBothObj["confidence_score"] = conflict.risk_both.confidence_score; + Json::Value riskFactorsBoth(Json::arrayValue); + for (const auto& factor : conflict.risk_both.risk_factors) { + riskFactorsBoth.append(factor); + } + riskBothObj["risk_factors"] = riskFactorsBoth; + Json::Value recommendationsBoth(Json::arrayValue); + for (const auto& rec : conflict.risk_both.recommendations) { + recommendationsBoth.append(rec); + } + riskBothObj["recommendations"] = recommendationsBoth; + conflictObj["risk_both"] = riskBothObj; + conflictsArray.append(conflictObj); } response["conflicts"] = conflictsArray; diff --git a/backend/src/merge/three_way_merge.cpp b/backend/src/merge/three_way_merge.cpp index 8cac4b1..466944d 100644 --- a/backend/src/merge/three_way_merge.cpp +++ b/backend/src/merge/three_way_merge.cpp @@ -4,6 +4,8 @@ */ #include "wizardmerge/merge/three_way_merge.h" +#include "wizardmerge/analysis/context_analyzer.h" +#include "wizardmerge/analysis/risk_analyzer.h" #include namespace wizardmerge { @@ -68,6 +70,23 @@ MergeResult three_way_merge( conflict.their_lines.push_back({their_line, Line::THEIRS}); conflict.end_line = result.merged_lines.size(); + // Perform context analysis + // Use the merged lines we have so far as context + std::vector context_lines; + for (const auto& line : result.merged_lines) { + context_lines.push_back(line.content); + } + conflict.context = analysis::analyze_context(context_lines, i, i); + + // Perform risk analysis for different resolution strategies + std::vector base_vec = {base_line}; + std::vector ours_vec = {our_line}; + std::vector theirs_vec = {their_line}; + + conflict.risk_ours = analysis::analyze_risk_ours(base_vec, ours_vec, theirs_vec); + conflict.risk_theirs = analysis::analyze_risk_theirs(base_vec, ours_vec, theirs_vec); + conflict.risk_both = analysis::analyze_risk_both(base_vec, ours_vec, theirs_vec); + result.conflicts.push_back(conflict); // Add conflict markers diff --git a/backend/tests/test_context_analyzer.cpp b/backend/tests/test_context_analyzer.cpp new file mode 100644 index 0000000..58d1ee5 --- /dev/null +++ b/backend/tests/test_context_analyzer.cpp @@ -0,0 +1,129 @@ +/** + * @file test_context_analyzer.cpp + * @brief Unit tests for context analysis module + */ + +#include "wizardmerge/analysis/context_analyzer.h" +#include + +using namespace wizardmerge::analysis; + +/** + * Test basic context analysis + */ +TEST(ContextAnalyzerTest, BasicContextAnalysis) { + std::vector lines = { + "#include ", + "", + "class MyClass {", + "public:", + " void myMethod() {", + " int x = 42;", + " int y = 100;", + " return;", + " }", + "};" + }; + + auto context = analyze_context(lines, 5, 7); + + EXPECT_EQ(context.start_line, 5); + EXPECT_EQ(context.end_line, 7); + EXPECT_FALSE(context.surrounding_lines.empty()); +} + +/** + * Test function name extraction + */ +TEST(ContextAnalyzerTest, ExtractFunctionName) { + std::vector lines = { + "void testFunction() {", + " int x = 10;", + " return;", + "}" + }; + + std::string func_name = extract_function_name(lines, 1); + EXPECT_EQ(func_name, "testFunction"); +} + +/** + * Test Python function name extraction + */ +TEST(ContextAnalyzerTest, ExtractPythonFunctionName) { + std::vector lines = { + "def my_python_function():", + " x = 10", + " return x" + }; + + std::string func_name = extract_function_name(lines, 1); + EXPECT_EQ(func_name, "my_python_function"); +} + +/** + * Test class name extraction + */ +TEST(ContextAnalyzerTest, ExtractClassName) { + std::vector lines = { + "class TestClass {", + " int member;", + "};" + }; + + std::string class_name = extract_class_name(lines, 1); + EXPECT_EQ(class_name, "TestClass"); +} + +/** + * Test import extraction + */ +TEST(ContextAnalyzerTest, ExtractImports) { + std::vector lines = { + "#include ", + "#include ", + "", + "int main() {", + " return 0;", + "}" + }; + + auto imports = extract_imports(lines); + EXPECT_EQ(imports.size(), 2); + EXPECT_EQ(imports[0], "#include "); + EXPECT_EQ(imports[1], "#include "); +} + +/** + * Test context with no function + */ +TEST(ContextAnalyzerTest, NoFunctionContext) { + std::vector lines = { + "int x = 10;", + "int y = 20;" + }; + + std::string func_name = extract_function_name(lines, 0); + EXPECT_EQ(func_name, ""); +} + +/** + * Test context window boundaries + */ +TEST(ContextAnalyzerTest, ContextWindowBoundaries) { + std::vector lines = { + "line1", + "line2", + "line3", + "line4", + "line5" + }; + + // Test with small context window at beginning of file + auto context = analyze_context(lines, 0, 0, 2); + EXPECT_GE(context.surrounding_lines.size(), 1); + + // Test with context window at end of file + context = analyze_context(lines, 4, 4, 2); + EXPECT_GE(context.surrounding_lines.size(), 1); +} diff --git a/backend/tests/test_risk_analyzer.cpp b/backend/tests/test_risk_analyzer.cpp new file mode 100644 index 0000000..c085cbe --- /dev/null +++ b/backend/tests/test_risk_analyzer.cpp @@ -0,0 +1,140 @@ +/** + * @file test_risk_analyzer.cpp + * @brief Unit tests for risk analysis module + */ + +#include "wizardmerge/analysis/risk_analyzer.h" +#include + +using namespace wizardmerge::analysis; + +/** + * Test risk level to string conversion + */ +TEST(RiskAnalyzerTest, RiskLevelToString) { + EXPECT_EQ(risk_level_to_string(RiskLevel::LOW), "low"); + EXPECT_EQ(risk_level_to_string(RiskLevel::MEDIUM), "medium"); + EXPECT_EQ(risk_level_to_string(RiskLevel::HIGH), "high"); + EXPECT_EQ(risk_level_to_string(RiskLevel::CRITICAL), "critical"); +} + +/** + * Test basic risk analysis for "ours" + */ +TEST(RiskAnalyzerTest, BasicRiskAnalysisOurs) { + std::vector base = {"int x = 10;"}; + std::vector ours = {"int x = 20;"}; + std::vector theirs = {"int x = 30;"}; + + auto risk = analyze_risk_ours(base, ours, theirs); + + EXPECT_TRUE(risk.level == RiskLevel::LOW || risk.level == RiskLevel::MEDIUM); + EXPECT_GE(risk.confidence_score, 0.0); + EXPECT_LE(risk.confidence_score, 1.0); + EXPECT_FALSE(risk.recommendations.empty()); +} + +/** + * Test basic risk analysis for "theirs" + */ +TEST(RiskAnalyzerTest, BasicRiskAnalysisTheirs) { + std::vector base = {"int x = 10;"}; + std::vector ours = {"int x = 20;"}; + std::vector theirs = {"int x = 30;"}; + + auto risk = analyze_risk_theirs(base, ours, theirs); + + EXPECT_TRUE(risk.level == RiskLevel::LOW || risk.level == RiskLevel::MEDIUM); + EXPECT_GE(risk.confidence_score, 0.0); + EXPECT_LE(risk.confidence_score, 1.0); + EXPECT_FALSE(risk.recommendations.empty()); +} + +/** + * Test risk analysis for "both" (concatenation) + */ +TEST(RiskAnalyzerTest, RiskAnalysisBoth) { + std::vector base = {"int x = 10;"}; + std::vector ours = {"int x = 20;"}; + std::vector theirs = {"int x = 30;"}; + + auto risk = analyze_risk_both(base, ours, theirs); + + // "Both" strategy should typically have medium or higher risk + EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM); + EXPECT_GE(risk.confidence_score, 0.0); + EXPECT_LE(risk.confidence_score, 1.0); + EXPECT_FALSE(risk.recommendations.empty()); +} + +/** + * Test critical pattern detection + */ +TEST(RiskAnalyzerTest, DetectCriticalPatterns) { + std::vector safe_code = {"int x = 10;", "return x;"}; + std::vector unsafe_code = {"delete ptr;", "system(\"rm -rf /\");"}; + + EXPECT_FALSE(contains_critical_patterns(safe_code)); + EXPECT_TRUE(contains_critical_patterns(unsafe_code)); +} + +/** + * Test API signature change detection + */ +TEST(RiskAnalyzerTest, DetectAPISignatureChanges) { + std::vector base_sig = {"void myFunction(int x) {"}; + std::vector modified_sig = {"void myFunction(int x, int y) {"}; + std::vector same_sig = {"void myFunction(int x) {"}; + + EXPECT_TRUE(has_api_signature_changes(base_sig, modified_sig)); + EXPECT_FALSE(has_api_signature_changes(base_sig, same_sig)); +} + +/** + * Test high risk for large changes + */ +TEST(RiskAnalyzerTest, HighRiskForLargeChanges) { + std::vector base = {"line1"}; + std::vector ours; + std::vector theirs = {"line1"}; + + // Create large change in ours + for (int i = 0; i < 15; ++i) { + ours.push_back("changed_line_" + std::to_string(i)); + } + + auto risk = analyze_risk_ours(base, ours, theirs); + + // Should detect significant changes + EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM); + EXPECT_FALSE(risk.risk_factors.empty()); +} + +/** + * Test risk with critical patterns + */ +TEST(RiskAnalyzerTest, CriticalPatternsIncreaseRisk) { + std::vector base = {"int x = 10;"}; + std::vector ours = {"delete database;", "eval(user_input);"}; + std::vector theirs = {"int x = 10;"}; + + auto risk = analyze_risk_ours(base, ours, theirs); + + EXPECT_TRUE(risk.level >= RiskLevel::HIGH); + EXPECT_TRUE(risk.affects_critical_section); + EXPECT_FALSE(risk.risk_factors.empty()); +} + +/** + * Test risk factors are populated + */ +TEST(RiskAnalyzerTest, RiskFactorsPopulated) { + std::vector base = {"line1", "line2", "line3"}; + std::vector ours = {"changed1", "changed2", "changed3"}; + std::vector theirs = {"line1", "line2", "line3"}; + + auto risk = analyze_risk_ours(base, ours, theirs); + + // Should have some analysis results + EXPECT_TRUE(!risk.recommendations.empty() || !risk.risk_factors.empty()); +}