mirror of
https://github.com/johndoe6345789/WizardMerge.git
synced 2026-04-24 13:44:55 +00:00
Add TypeScript support to context and risk analyzers
- Enhanced context_analyzer to detect TypeScript patterns (interfaces, types, enums, arrow functions, async functions) - Updated risk_analyzer with TypeScript-specific critical patterns (as any, @ts-ignore, dangerouslySetInnerHTML, etc.) - Added has_typescript_interface_changes() to detect type definition changes - Added is_package_lock_file() to identify lock files - Created comprehensive tests for TypeScript functionality - All 46 tests passing Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
@@ -15,6 +15,8 @@ find_package(CURL QUIET)
|
||||
set(WIZARDMERGE_SOURCES
|
||||
src/merge/three_way_merge.cpp
|
||||
src/git/git_cli.cpp
|
||||
src/analysis/context_analyzer.cpp
|
||||
src/analysis/risk_analyzer.cpp
|
||||
)
|
||||
|
||||
# Add git sources only if CURL is available
|
||||
@@ -71,6 +73,8 @@ if(GTest_FOUND)
|
||||
set(TEST_SOURCES
|
||||
tests/test_three_way_merge.cpp
|
||||
tests/test_git_cli.cpp
|
||||
tests/test_context_analyzer.cpp
|
||||
tests/test_risk_analyzer.cpp
|
||||
)
|
||||
|
||||
# Add github client tests only if CURL is available
|
||||
|
||||
@@ -112,6 +112,26 @@ bool has_api_signature_changes(
|
||||
const std::vector<std::string>& modified
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Detects if TypeScript interface or type definitions changed.
|
||||
*
|
||||
* @param base Base version lines
|
||||
* @param modified Modified version lines
|
||||
* @return true if interface/type changes detected
|
||||
*/
|
||||
bool has_typescript_interface_changes(
|
||||
const std::vector<std::string>& base,
|
||||
const std::vector<std::string>& modified
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Checks if file is a package-lock.json file.
|
||||
*
|
||||
* @param filename Name of the file
|
||||
* @return true if file is package-lock.json
|
||||
*/
|
||||
bool is_package_lock_file(const std::string& filename);
|
||||
|
||||
} // namespace analysis
|
||||
} // namespace wizardmerge
|
||||
|
||||
|
||||
@@ -37,7 +37,11 @@ bool is_function_definition(const std::string& line) {
|
||||
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
|
||||
std::regex(R"(^(public|private|protected)?\s*\w+\s+\w+\s*\([^)]*\))"), // Java/C# methods
|
||||
// TypeScript patterns
|
||||
std::regex(R"(^(export\s+)?(async\s+)?function\s+\w+)"), // TS: export/async function
|
||||
std::regex(R"(^(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>)"), // TS: arrow functions
|
||||
std::regex(R"(^(public|private|protected|readonly)?\s*\w+\s*\([^)]*\)\s*:\s*\w+)") // TS: typed methods
|
||||
};
|
||||
|
||||
for (const auto& pattern : patterns) {
|
||||
@@ -64,12 +68,18 @@ std::string get_function_name_from_line(const std::string& line) {
|
||||
return match[1].str();
|
||||
}
|
||||
|
||||
// JavaScript: function function_name(
|
||||
std::regex js_pattern(R"(function\s+(\w+)\s*\()");
|
||||
// JavaScript/TypeScript: function function_name( or export function function_name(
|
||||
std::regex js_pattern(R"((?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\()");
|
||||
if (std::regex_search(trimmed, match, js_pattern)) {
|
||||
return match[1].str();
|
||||
}
|
||||
|
||||
// TypeScript: const/let/var function_name = (params) =>
|
||||
std::regex arrow_pattern(R"((?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>)");
|
||||
if (std::regex_search(trimmed, match, arrow_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)) {
|
||||
@@ -88,7 +98,12 @@ bool is_class_definition(const std::string& 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
|
||||
std::regex(R"(^struct\s+\w+)"), // C/C++: struct Name
|
||||
// TypeScript patterns
|
||||
std::regex(R"(^(export\s+)?(abstract\s+)?class\s+\w+)"), // TS: export class Name
|
||||
std::regex(R"(^(export\s+)?interface\s+\w+)"), // TS: interface Name
|
||||
std::regex(R"(^(export\s+)?type\s+\w+\s*=)"), // TS: type Name =
|
||||
std::regex(R"(^(export\s+)?enum\s+\w+)") // TS: enum Name
|
||||
};
|
||||
|
||||
for (const auto& pattern : patterns) {
|
||||
@@ -107,7 +122,9 @@ 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+))");
|
||||
|
||||
// Match class, struct, interface, type, or enum
|
||||
std::regex pattern(R"((?:export\s+)?(?:abstract\s+)?(class|struct|interface|type|enum)\s+(\w+))");
|
||||
|
||||
if (std::regex_search(trimmed, match, pattern)) {
|
||||
return match[2].str();
|
||||
@@ -223,7 +240,13 @@ std::vector<std::string> extract_imports(
|
||||
line.find("import ") == 0 ||
|
||||
line.find("from ") == 0 ||
|
||||
line.find("require(") != std::string::npos ||
|
||||
line.find("using ") == 0) {
|
||||
line.find("using ") == 0 ||
|
||||
// TypeScript/ES6 specific patterns
|
||||
line.find("import{") == 0 ||
|
||||
line.find("import *") == 0 ||
|
||||
line.find("import type") == 0 ||
|
||||
line.find("export {") == 0 ||
|
||||
line.find("export *") == 0) {
|
||||
imports.push_back(line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,10 @@ bool is_function_signature(const std::string& line) {
|
||||
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
|
||||
// TypeScript patterns
|
||||
std::regex(R"(^(export\s+)?(async\s+)?function\s+\w+\s*\([^)]*\))"), // TS function
|
||||
std::regex(R"(^(const|let|var)\s+\w+\s*=\s*\([^)]*\)\s*=>)"), // Arrow function
|
||||
std::regex(R"(^\w+\s*\([^)]*\)\s*:\s*\w+)"), // TS: method with return type
|
||||
};
|
||||
|
||||
for (const auto& pattern : patterns) {
|
||||
@@ -117,6 +121,13 @@ bool contains_critical_patterns(const std::vector<std::string>& lines) {
|
||||
std::regex(R"(\.secret\s*=)"), // Secret assignments
|
||||
std::regex(R"(sudo\s+)"), // Sudo usage
|
||||
std::regex(R"(chmod\s+777)"), // Overly permissive permissions
|
||||
// TypeScript specific critical patterns
|
||||
std::regex(R"(dangerouslySetInnerHTML)"), // React XSS risk
|
||||
std::regex(R"(\bas\s+any\b)"), // TypeScript: type safety bypass
|
||||
std::regex(R"(@ts-ignore)"), // TypeScript: error suppression
|
||||
std::regex(R"(@ts-nocheck)"), // TypeScript: file-level error suppression
|
||||
std::regex(R"(localStorage\.setItem.*password)"), // Storing passwords in localStorage
|
||||
std::regex(R"(innerHTML\s*=)"), // XSS risk
|
||||
};
|
||||
|
||||
for (const auto& line : lines) {
|
||||
@@ -148,6 +159,67 @@ bool has_api_signature_changes(
|
||||
return false;
|
||||
}
|
||||
|
||||
bool has_typescript_interface_changes(
|
||||
const std::vector<std::string>& base,
|
||||
const std::vector<std::string>& modified
|
||||
) {
|
||||
// Check for interface, type, or enum changes
|
||||
std::vector<std::regex> ts_definition_patterns = {
|
||||
std::regex(R"(\binterface\s+\w+)"),
|
||||
std::regex(R"(\btype\s+\w+\s*=)"),
|
||||
std::regex(R"(\benum\s+\w+)"),
|
||||
};
|
||||
|
||||
// Check if any TypeScript definition exists in base
|
||||
bool base_has_ts_def = false;
|
||||
for (const auto& line : base) {
|
||||
std::string trimmed = trim(line);
|
||||
for (const auto& pattern : ts_definition_patterns) {
|
||||
if (std::regex_search(trimmed, pattern)) {
|
||||
base_has_ts_def = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (base_has_ts_def) break;
|
||||
}
|
||||
|
||||
// Check if any TypeScript definition exists in modified
|
||||
bool modified_has_ts_def = false;
|
||||
for (const auto& line : modified) {
|
||||
std::string trimmed = trim(line);
|
||||
for (const auto& pattern : ts_definition_patterns) {
|
||||
if (std::regex_search(trimmed, pattern)) {
|
||||
modified_has_ts_def = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (modified_has_ts_def) break;
|
||||
}
|
||||
|
||||
// If either has TS definitions and content differs, it's a TS change
|
||||
if (base_has_ts_def || modified_has_ts_def) {
|
||||
// Check if the actual content changed
|
||||
if (base.size() != modified.size()) {
|
||||
return true;
|
||||
}
|
||||
for (size_t i = 0; i < base.size(); ++i) {
|
||||
if (trim(base[i]) != trim(modified[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_package_lock_file(const std::string& filename) {
|
||||
// Check for package-lock.json, yarn.lock, pnpm-lock.yaml, etc.
|
||||
return filename.find("package-lock.json") != std::string::npos ||
|
||||
filename.find("yarn.lock") != std::string::npos ||
|
||||
filename.find("pnpm-lock.yaml") != std::string::npos ||
|
||||
filename.find("bun.lockb") != std::string::npos;
|
||||
}
|
||||
|
||||
RiskAssessment analyze_risk_ours(
|
||||
const std::vector<std::string>& base,
|
||||
const std::vector<std::string>& ours,
|
||||
@@ -183,6 +255,15 @@ RiskAssessment analyze_risk_ours(
|
||||
}
|
||||
}
|
||||
|
||||
// Check for TypeScript interface/type changes
|
||||
if (has_typescript_interface_changes(base, ours)) {
|
||||
assessment.has_api_changes = true;
|
||||
assessment.risk_factors.push_back("TypeScript interface or type definitions 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;
|
||||
@@ -260,6 +341,15 @@ RiskAssessment analyze_risk_theirs(
|
||||
}
|
||||
}
|
||||
|
||||
// Check for TypeScript interface/type changes
|
||||
if (has_typescript_interface_changes(base, theirs)) {
|
||||
assessment.has_api_changes = true;
|
||||
assessment.risk_factors.push_back("TypeScript interface or type definitions 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;
|
||||
@@ -340,6 +430,13 @@ RiskAssessment analyze_risk_both(
|
||||
assessment.level = RiskLevel::HIGH;
|
||||
}
|
||||
|
||||
// TypeScript interface/type changes from either side
|
||||
if (has_typescript_interface_changes(base, ours) || has_typescript_interface_changes(base, theirs)) {
|
||||
assessment.has_api_changes = true;
|
||||
assessment.risk_factors.push_back("Multiple TypeScript interface/type 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");
|
||||
|
||||
@@ -127,3 +127,94 @@ TEST(ContextAnalyzerTest, ContextWindowBoundaries) {
|
||||
context = analyze_context(lines, 4, 4, 2);
|
||||
EXPECT_GE(context.surrounding_lines.size(), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TypeScript function detection
|
||||
*/
|
||||
TEST(ContextAnalyzerTest, TypeScriptFunctionDetection) {
|
||||
std::vector<std::string> lines = {
|
||||
"export async function fetchData() {",
|
||||
" const data = await api.get();",
|
||||
" return data;",
|
||||
"}"
|
||||
};
|
||||
|
||||
std::string func_name = extract_function_name(lines, 1);
|
||||
EXPECT_EQ(func_name, "fetchData");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TypeScript arrow function detection
|
||||
*/
|
||||
TEST(ContextAnalyzerTest, TypeScriptArrowFunctionDetection) {
|
||||
std::vector<std::string> lines = {
|
||||
"const handleClick = (event: MouseEvent) => {",
|
||||
" console.log(event);",
|
||||
"};"
|
||||
};
|
||||
|
||||
std::string func_name = extract_function_name(lines, 0);
|
||||
EXPECT_EQ(func_name, "handleClick");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TypeScript interface detection
|
||||
*/
|
||||
TEST(ContextAnalyzerTest, TypeScriptInterfaceDetection) {
|
||||
std::vector<std::string> lines = {
|
||||
"export interface User {",
|
||||
" id: number;",
|
||||
" name: string;",
|
||||
"}"
|
||||
};
|
||||
|
||||
std::string class_name = extract_class_name(lines, 1);
|
||||
EXPECT_EQ(class_name, "User");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TypeScript type alias detection
|
||||
*/
|
||||
TEST(ContextAnalyzerTest, TypeScriptTypeAliasDetection) {
|
||||
std::vector<std::string> lines = {
|
||||
"export type Status = 'pending' | 'approved' | 'rejected';",
|
||||
"const status: Status = 'pending';"
|
||||
};
|
||||
|
||||
std::string type_name = extract_class_name(lines, 0);
|
||||
EXPECT_EQ(type_name, "Status");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TypeScript enum detection
|
||||
*/
|
||||
TEST(ContextAnalyzerTest, TypeScriptEnumDetection) {
|
||||
std::vector<std::string> lines = {
|
||||
"enum Color {",
|
||||
" Red,",
|
||||
" Green,",
|
||||
" Blue",
|
||||
"}"
|
||||
};
|
||||
|
||||
std::string enum_name = extract_class_name(lines, 1);
|
||||
EXPECT_EQ(enum_name, "Color");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TypeScript import extraction
|
||||
*/
|
||||
TEST(ContextAnalyzerTest, TypeScriptImportExtraction) {
|
||||
std::vector<std::string> lines = {
|
||||
"import { Component } from 'react';",
|
||||
"import type { User } from './types';",
|
||||
"import * as utils from './utils';",
|
||||
"",
|
||||
"function MyComponent() {",
|
||||
" return null;",
|
||||
"}"
|
||||
};
|
||||
|
||||
auto imports = extract_imports(lines);
|
||||
EXPECT_GE(imports.size(), 3);
|
||||
}
|
||||
|
||||
@@ -138,3 +138,131 @@ TEST(RiskAnalyzerTest, RiskFactorsPopulated) {
|
||||
// Should have some analysis results
|
||||
EXPECT_TRUE(!risk.recommendations.empty() || !risk.risk_factors.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TypeScript interface change detection
|
||||
*/
|
||||
TEST(RiskAnalyzerTest, TypeScriptInterfaceChangesDetected) {
|
||||
std::vector<std::string> base = {
|
||||
"interface User {",
|
||||
" name: string;",
|
||||
"}"
|
||||
};
|
||||
std::vector<std::string> modified = {
|
||||
"interface User {",
|
||||
" name: string;",
|
||||
" age: number;",
|
||||
"}"
|
||||
};
|
||||
|
||||
EXPECT_TRUE(has_typescript_interface_changes(base, modified));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TypeScript type alias change detection
|
||||
*/
|
||||
TEST(RiskAnalyzerTest, TypeScriptTypeChangesDetected) {
|
||||
std::vector<std::string> base = {
|
||||
"type Status = 'pending' | 'approved';"
|
||||
};
|
||||
std::vector<std::string> modified = {
|
||||
"type Status = 'pending' | 'approved' | 'rejected';"
|
||||
};
|
||||
|
||||
EXPECT_TRUE(has_typescript_interface_changes(base, modified));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TypeScript enum change detection
|
||||
*/
|
||||
TEST(RiskAnalyzerTest, TypeScriptEnumChangesDetected) {
|
||||
std::vector<std::string> base = {
|
||||
"enum Color {",
|
||||
" Red,",
|
||||
" Green",
|
||||
"}"
|
||||
};
|
||||
std::vector<std::string> modified = {
|
||||
"enum Color {",
|
||||
" Red,",
|
||||
" Green,",
|
||||
" Blue",
|
||||
"}"
|
||||
};
|
||||
|
||||
EXPECT_TRUE(has_typescript_interface_changes(base, modified));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test package-lock.json file detection
|
||||
*/
|
||||
TEST(RiskAnalyzerTest, PackageLockFileDetection) {
|
||||
EXPECT_TRUE(is_package_lock_file("package-lock.json"));
|
||||
EXPECT_TRUE(is_package_lock_file("path/to/package-lock.json"));
|
||||
EXPECT_TRUE(is_package_lock_file("yarn.lock"));
|
||||
EXPECT_TRUE(is_package_lock_file("pnpm-lock.yaml"));
|
||||
EXPECT_TRUE(is_package_lock_file("bun.lockb"));
|
||||
EXPECT_FALSE(is_package_lock_file("package.json"));
|
||||
EXPECT_FALSE(is_package_lock_file("src/index.ts"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TypeScript critical patterns detection
|
||||
*/
|
||||
TEST(RiskAnalyzerTest, TypeScriptCriticalPatternsDetected) {
|
||||
std::vector<std::string> code_with_ts_issues = {
|
||||
"const user = data as any;",
|
||||
"// @ts-ignore",
|
||||
"element.innerHTML = userInput;",
|
||||
"localStorage.setItem('password', pwd);"
|
||||
};
|
||||
|
||||
EXPECT_TRUE(contains_critical_patterns(code_with_ts_issues));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TypeScript safe code doesn't trigger false positives
|
||||
*/
|
||||
TEST(RiskAnalyzerTest, TypeScriptSafeCodeNoFalsePositives) {
|
||||
std::vector<std::string> safe_code = {
|
||||
"const user: User = { name: 'John', age: 30 };",
|
||||
"function greet(name: string): string {",
|
||||
" return `Hello, ${name}`;",
|
||||
"}"
|
||||
};
|
||||
|
||||
EXPECT_FALSE(contains_critical_patterns(safe_code));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test risk analysis includes TypeScript interface changes
|
||||
*/
|
||||
TEST(RiskAnalyzerTest, RiskAnalysisIncludesTypeScriptChanges) {
|
||||
std::vector<std::string> base = {
|
||||
"interface User {",
|
||||
" name: string;",
|
||||
"}"
|
||||
};
|
||||
std::vector<std::string> ours = {
|
||||
"interface User {",
|
||||
" name: string;",
|
||||
" email: string;",
|
||||
"}"
|
||||
};
|
||||
std::vector<std::string> theirs = base;
|
||||
|
||||
auto risk = analyze_risk_ours(base, ours, theirs);
|
||||
|
||||
EXPECT_TRUE(risk.has_api_changes);
|
||||
EXPECT_TRUE(risk.level >= RiskLevel::MEDIUM);
|
||||
|
||||
// Check if TypeScript-related risk factor is mentioned
|
||||
bool has_ts_risk = false;
|
||||
for (const auto& factor : risk.risk_factors) {
|
||||
if (factor.find("TypeScript") != std::string::npos) {
|
||||
has_ts_risk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(has_ts_risk);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user