Implement context and risk analysis for merge conflicts

- Add context analyzer to extract function/class names and imports
- Add risk analyzer to assess resolution strategies (ours/theirs/both)
- Integrate analysis into three-way merge conflicts
- Update MergeController to include analysis in API responses
- Add comprehensive test coverage for both analyzers

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-27 02:44:40 +00:00
parent 95e19968c9
commit 7c489b5c55
10 changed files with 1153 additions and 1 deletions
+232
View File
@@ -0,0 +1,232 @@
/**
* @file context_analyzer.cpp
* @brief Implementation of context analysis for merge conflicts
*/
#include "wizardmerge/analysis/context_analyzer.h"
#include <algorithm>
#include <regex>
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<std::regex> 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<std::regex> 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<std::string>& 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<std::string>& 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<int>(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<std::string>& 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<int>(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<std::string> extract_imports(
const std::vector<std::string>& lines
) {
std::vector<std::string> 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
+343
View File
@@ -0,0 +1,343 @@
/**
* @file risk_analyzer.cpp
* @brief Implementation of risk analysis for merge conflict resolutions
*/
#include "wizardmerge/analysis/risk_analyzer.h"
#include <algorithm>
#include <regex>
#include <cmath>
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<std::string>& lines1,
const std::vector<std::string>& 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<double>(common_lines) / total_unique : 0.0;
}
/**
* @brief Count number of changed lines between two versions.
*/
size_t count_changes(
const std::vector<std::string>& base,
const std::vector<std::string>& 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<std::regex> 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<std::string>& lines) {
std::vector<std::regex> 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<std::string>& base,
const std::vector<std::string>& 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<std::string>& base,
const std::vector<std::string>& ours,
const std::vector<std::string>& 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<double>(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<std::string>& base,
const std::vector<std::string>& ours,
const std::vector<std::string>& 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<double>(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<std::string>& base,
const std::vector<std::string>& ours,
const std::vector<std::string>& 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
@@ -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;
+19
View File
@@ -4,6 +4,8 @@
*/
#include "wizardmerge/merge/three_way_merge.h"
#include "wizardmerge/analysis/context_analyzer.h"
#include "wizardmerge/analysis/risk_analyzer.h"
#include <algorithm>
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<std::string> 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<std::string> base_vec = {base_line};
std::vector<std::string> ours_vec = {our_line};
std::vector<std::string> 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