From 370f241eb90b29a21345cebaa0a74af55d6c2a76 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 03:10:17 +0000 Subject: [PATCH] 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> --- backend/CMakeLists.txt | 4 + .../wizardmerge/analysis/risk_analyzer.h | 20 +++ backend/src/analysis/context_analyzer.cpp | 35 ++++- backend/src/analysis/risk_analyzer.cpp | 97 +++++++++++++ backend/tests/test_context_analyzer.cpp | 91 +++++++++++++ backend/tests/test_risk_analyzer.cpp | 128 ++++++++++++++++++ 6 files changed, 369 insertions(+), 6 deletions(-) diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt index 4de0cc2..3b0cb66 100644 --- a/backend/CMakeLists.txt +++ b/backend/CMakeLists.txt @@ -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 diff --git a/backend/include/wizardmerge/analysis/risk_analyzer.h b/backend/include/wizardmerge/analysis/risk_analyzer.h index 402c2fe..614a909 100644 --- a/backend/include/wizardmerge/analysis/risk_analyzer.h +++ b/backend/include/wizardmerge/analysis/risk_analyzer.h @@ -112,6 +112,26 @@ bool has_api_signature_changes( const std::vector& 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& base, + const std::vector& 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 diff --git a/backend/src/analysis/context_analyzer.cpp b/backend/src/analysis/context_analyzer.cpp index e77549a..d260aac 100644 --- a/backend/src/analysis/context_analyzer.cpp +++ b/backend/src/analysis/context_analyzer.cpp @@ -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 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 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); } } diff --git a/backend/src/analysis/risk_analyzer.cpp b/backend/src/analysis/risk_analyzer.cpp index dc439c6..3b2a41c 100644 --- a/backend/src/analysis/risk_analyzer.cpp +++ b/backend/src/analysis/risk_analyzer.cpp @@ -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& 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& base, + const std::vector& modified +) { + // Check for interface, type, or enum changes + std::vector 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& base, const std::vector& 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"); diff --git a/backend/tests/test_context_analyzer.cpp b/backend/tests/test_context_analyzer.cpp index 58d1ee5..c3e66d7 100644 --- a/backend/tests/test_context_analyzer.cpp +++ b/backend/tests/test_context_analyzer.cpp @@ -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 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 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 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 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 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 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); +} diff --git a/backend/tests/test_risk_analyzer.cpp b/backend/tests/test_risk_analyzer.cpp index c085cbe..28cfd8d 100644 --- a/backend/tests/test_risk_analyzer.cpp +++ b/backend/tests/test_risk_analyzer.cpp @@ -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 base = { + "interface User {", + " name: string;", + "}" + }; + std::vector 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 base = { + "type Status = 'pending' | 'approved';" + }; + std::vector modified = { + "type Status = 'pending' | 'approved' | 'rejected';" + }; + + EXPECT_TRUE(has_typescript_interface_changes(base, modified)); +} + +/** + * Test TypeScript enum change detection + */ +TEST(RiskAnalyzerTest, TypeScriptEnumChangesDetected) { + std::vector base = { + "enum Color {", + " Red,", + " Green", + "}" + }; + std::vector 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 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 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 base = { + "interface User {", + " name: string;", + "}" + }; + std::vector ours = { + "interface User {", + " name: string;", + " email: string;", + "}" + }; + std::vector 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); +}