Files
AX-Copilot-Codex/.decompiledproj/AxCopilot/Services/Agent/CodeReviewTool.cs

576 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace AxCopilot.Services.Agent;
public class CodeReviewTool : IAgentTool
{
private class DiffFile
{
public string Path { get; set; } = "";
public string Status { get; set; } = "modified";
public int Added { get; set; }
public int Removed { get; set; }
public List<string> Hunks { get; } = new List<string>();
}
private record ReviewIssue(int Line, string Severity, string Message);
public string Name => "code_review";
public string Description => "코드 리뷰를 수행합니다. Git diff 분석, 파일 정적 검사, PR 요약을 생성합니다.\naction별 기능:\n- diff_review: git diff 출력을 분석하여 이슈/개선점을 구조화\n- file_review: 특정 파일의 코드 품질을 정적 검사\n- pr_summary: 변경사항을 PR 설명 형식으로 요약";
public ToolParameterSchema Parameters
{
get
{
ToolParameterSchema toolParameterSchema = new ToolParameterSchema();
Dictionary<string, ToolProperty> dictionary = new Dictionary<string, ToolProperty>();
ToolProperty obj = new ToolProperty
{
Type = "string",
Description = "리뷰 유형: diff_review (diff 분석), file_review (파일 검사), pr_summary (PR 요약)"
};
int num = 3;
List<string> list = new List<string>(num);
CollectionsMarshal.SetCount(list, num);
Span<string> span = CollectionsMarshal.AsSpan(list);
span[0] = "diff_review";
span[1] = "file_review";
span[2] = "pr_summary";
obj.Enum = list;
dictionary["action"] = obj;
dictionary["target"] = new ToolProperty
{
Type = "string",
Description = "대상 지정. diff_review: '--staged' 또는 빈값(working tree). file_review: 파일 경로. pr_summary: 브랜치명(선택)."
};
ToolProperty obj2 = new ToolProperty
{
Type = "string",
Description = "리뷰 초점: all (전체), bugs (버그), performance (성능), security (보안), style (스타일). 기본 all."
};
num = 5;
List<string> list2 = new List<string>(num);
CollectionsMarshal.SetCount(list2, num);
Span<string> span2 = CollectionsMarshal.AsSpan(list2);
span2[0] = "all";
span2[1] = "bugs";
span2[2] = "performance";
span2[3] = "security";
span2[4] = "style";
obj2.Enum = list2;
dictionary["focus"] = obj2;
toolParameterSchema.Properties = dictionary;
num = 1;
List<string> list3 = new List<string>(num);
CollectionsMarshal.SetCount(list3, num);
CollectionsMarshal.AsSpan(list3)[0] = "action";
toolParameterSchema.Required = list3;
return toolParameterSchema;
}
}
public async Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default(CancellationToken))
{
if (!((Application.Current as App)?.SettingsService?.Settings.Llm.Code.EnableCodeReview ?? true))
{
return ToolResult.Ok("코드 리뷰가 비활성 상태입니다. 설정 → AX Agent → 코드에서 활성화하세요.");
}
JsonElement a;
string action = (args.TryGetProperty("action", out a) ? (a.GetString() ?? "") : "");
JsonElement t;
string target = (args.TryGetProperty("target", out t) ? (t.GetString() ?? "") : "");
JsonElement f;
string focus = (args.TryGetProperty("focus", out f) ? (f.GetString() ?? "all") : "all");
if (string.IsNullOrEmpty(context.WorkFolder))
{
return ToolResult.Fail("작업 폴더가 설정되어 있지 않습니다.");
}
if (1 == 0)
{
}
ToolResult result = action switch
{
"diff_review" => await DiffReviewAsync(context, target, focus, ct),
"file_review" => await FileReviewAsync(context, target, focus, ct),
"pr_summary" => await PrSummaryAsync(context, target, ct),
_ => ToolResult.Fail("지원하지 않는 action: " + action + ". diff_review, file_review, pr_summary 중 선택하세요."),
};
if (1 == 0)
{
}
return result;
}
private async Task<ToolResult> DiffReviewAsync(AgentContext ctx, string target, string focus, CancellationToken ct)
{
string diffResult = await RunGitAsync(args: string.IsNullOrEmpty(target) ? "diff" : ("diff " + target), workDir: ctx.WorkFolder, ct: ct);
if (diffResult == null)
{
return ToolResult.Fail("Git을 찾을 수 없습니다.");
}
if (string.IsNullOrWhiteSpace(diffResult))
{
return ToolResult.Ok("변경사항이 없습니다. (clean working tree)");
}
List<DiffFile> files = ParseDiffFiles(diffResult);
StringBuilder sb = new StringBuilder();
sb.AppendLine("═══ Code Review Report (diff_review) ═══\n");
int totalAdded = 0;
int totalRemoved = 0;
foreach (DiffFile file in files)
{
totalAdded += file.Added;
totalRemoved += file.Removed;
}
StringBuilder stringBuilder = sb;
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(26, 3, stringBuilder);
handler.AppendLiteral("\ud83d\udcca 파일 ");
handler.AppendFormatted(files.Count);
handler.AppendLiteral("개 변경 | +");
handler.AppendFormatted(totalAdded);
handler.AppendLiteral(" 추가 -");
handler.AppendFormatted(totalRemoved);
handler.AppendLiteral(" 삭제\n");
stringBuilder2.AppendLine(ref handler);
stringBuilder = sb;
StringBuilder stringBuilder3 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder);
handler.AppendLiteral("\ud83d\udd0d 리뷰 초점: ");
handler.AppendFormatted(focus);
handler.AppendLiteral("\n");
stringBuilder3.AppendLine(ref handler);
foreach (DiffFile file2 in files)
{
stringBuilder = sb;
StringBuilder stringBuilder4 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(11, 2, stringBuilder);
handler.AppendLiteral("─── ");
handler.AppendFormatted(file2.Path);
handler.AppendLiteral(" (");
handler.AppendFormatted(file2.Status);
handler.AppendLiteral(") ───");
stringBuilder4.AppendLine(ref handler);
stringBuilder = sb;
StringBuilder stringBuilder5 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(9, 2, stringBuilder);
handler.AppendLiteral(" 변경: +");
handler.AppendFormatted(file2.Added);
handler.AppendLiteral(" -");
handler.AppendFormatted(file2.Removed);
stringBuilder5.AppendLine(ref handler);
List<ReviewIssue> issues = AnalyzeDiffHunks(file2.Hunks, focus);
if (issues.Count > 0)
{
foreach (ReviewIssue issue in issues)
{
stringBuilder = sb;
StringBuilder stringBuilder6 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(12, 3, stringBuilder);
handler.AppendLiteral(" [");
handler.AppendFormatted(issue.Severity);
handler.AppendLiteral("] Line ");
handler.AppendFormatted(issue.Line);
handler.AppendLiteral(": ");
handler.AppendFormatted(issue.Message);
stringBuilder6.AppendLine(ref handler);
}
}
else
{
sb.AppendLine(" [OK] 정적 검사에서 특이사항 없음");
}
sb.AppendLine();
}
sb.AppendLine("═══ 위 분석 결과를 바탕으로 상세한 코드 리뷰를 작성하세요. ═══");
return ToolResult.Ok(sb.ToString());
}
private async Task<ToolResult> FileReviewAsync(AgentContext ctx, string target, string focus, CancellationToken ct)
{
if (string.IsNullOrEmpty(target))
{
return ToolResult.Fail("file_review에는 target(파일 경로)이 필요합니다.");
}
string fullPath = (Path.IsPathRooted(target) ? target : Path.Combine(ctx.WorkFolder, target));
if (!File.Exists(fullPath))
{
return ToolResult.Fail("파일을 찾을 수 없습니다: " + target);
}
if (!ctx.IsPathAllowed(fullPath))
{
return ToolResult.Fail("접근이 차단된 경로입니다: " + target);
}
string[] lines = (await File.ReadAllTextAsync(fullPath, ct)).Split('\n');
StringBuilder sb = new StringBuilder();
sb.AppendLine("═══ Code Review Report (file_review) ═══\n");
StringBuilder stringBuilder = sb;
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(7, 1, stringBuilder);
handler.AppendLiteral("\ud83d\udcc1 파일: ");
handler.AppendFormatted(target);
stringBuilder2.AppendLine(ref handler);
stringBuilder = sb;
StringBuilder stringBuilder3 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(17, 2, stringBuilder);
handler.AppendLiteral("\ud83d\udccf ");
handler.AppendFormatted(lines.Length);
handler.AppendLiteral("줄 | \ud83d\udd0d 초점: ");
handler.AppendFormatted(focus);
handler.AppendLiteral("\n");
stringBuilder3.AppendLine(ref handler);
List<ReviewIssue> issues = AnalyzeFile(lines, focus);
if (issues.Count > 0)
{
stringBuilder = sb;
StringBuilder stringBuilder4 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(13, 1, stringBuilder);
handler.AppendLiteral("⚠\ufe0f 발견된 이슈 ");
handler.AppendFormatted(issues.Count);
handler.AppendLiteral("개:\n");
stringBuilder4.AppendLine(ref handler);
foreach (ReviewIssue issue in issues)
{
stringBuilder = sb;
StringBuilder stringBuilder5 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(12, 3, stringBuilder);
handler.AppendLiteral(" [");
handler.AppendFormatted(issue.Severity);
handler.AppendLiteral("] Line ");
handler.AppendFormatted(issue.Line);
handler.AppendLiteral(": ");
handler.AppendFormatted(issue.Message);
stringBuilder5.AppendLine(ref handler);
}
}
else
{
sb.AppendLine("✅ 정적 검사에서 특이사항 없음");
}
sb.AppendLine("\n─── 파일 내용 (처음 200줄) ───");
string preview = string.Join('\n', lines.Take(200));
sb.AppendLine(preview);
if (lines.Length > 200)
{
stringBuilder = sb;
StringBuilder stringBuilder6 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder);
handler.AppendLiteral("\n... (");
handler.AppendFormatted(lines.Length - 200);
handler.AppendLiteral("줄 생략)");
stringBuilder6.AppendLine(ref handler);
}
sb.AppendLine("\n═══ 위 분석 결과와 코드를 바탕으로 상세한 리뷰를 작성하세요. ═══");
return ToolResult.Ok(sb.ToString());
}
private async Task<ToolResult> PrSummaryAsync(AgentContext ctx, string target, CancellationToken ct)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("═══ PR Summary Data ═══\n");
string log = await RunGitAsync(args: string.IsNullOrEmpty(target) ? "log --oneline -20" : ("log --oneline " + target + "..HEAD"), workDir: ctx.WorkFolder, ct: ct);
if (!string.IsNullOrWhiteSpace(log))
{
sb.AppendLine("\ud83d\udccb 커밋 이력:");
sb.AppendLine(log);
sb.AppendLine();
}
string stat = await RunGitAsync(args: string.IsNullOrEmpty(target) ? "diff --stat" : ("diff --stat " + target + "..HEAD"), workDir: ctx.WorkFolder, ct: ct);
if (!string.IsNullOrWhiteSpace(stat))
{
sb.AppendLine("\ud83d\udcca 변경 통계:");
sb.AppendLine(stat);
sb.AppendLine();
}
string status = await RunGitAsync(ctx.WorkFolder, "status --short", ct);
if (!string.IsNullOrWhiteSpace(status))
{
sb.AppendLine("\ud83d\udcc1 현재 상태:");
sb.AppendLine(status);
sb.AppendLine();
}
sb.AppendLine("═══ 위 데이터를 바탕으로 PR 제목과 설명(Summary, Changes, Test Plan)을 작성하세요. ═══");
return ToolResult.Ok(sb.ToString());
}
private static List<ReviewIssue> AnalyzeDiffHunks(List<string> hunks, string focus)
{
List<ReviewIssue> list = new List<ReviewIssue>();
int num = 0;
foreach (string hunk in hunks)
{
Match match = Regex.Match(hunk, "@@ -\\d+(?:,\\d+)? \\+(\\d+)");
if (match.Success)
{
num = int.Parse(match.Groups[1].Value);
}
else
{
if (!hunk.StartsWith('+') || hunk.StartsWith("+++"))
{
continue;
}
num++;
string text = hunk;
string text2 = text.Substring(1, text.Length - 1);
if ((focus == "all" || focus == "bugs") ? true : false)
{
if (Regex.IsMatch(text2, "catch\\s*\\{?\\s*\\}"))
{
list.Add(new ReviewIssue(num, "WARNING", "빈 catch 블록 — 예외가 무시됩니다"));
}
if (Regex.IsMatch(text2, "\\.Result\\b|\\.Wait\\(\\)"))
{
list.Add(new ReviewIssue(num, "WARNING", "동기 대기 (.Result/.Wait()) — 데드락 위험"));
}
if (Regex.IsMatch(text2, "==\\s*null") && text2.Contains('.'))
{
list.Add(new ReviewIssue(num, "INFO", "null 비교 — null 조건 연산자(?.) 사용 검토"));
}
}
if ((focus == "all" || focus == "security") ? true : false)
{
if (Regex.IsMatch(text2, "(password|secret|token|api_?key)\\s*=\\s*\"[^\"]+\"", RegexOptions.IgnoreCase))
{
list.Add(new ReviewIssue(num, "CRITICAL", "하드코딩된 비밀번호/키 감지"));
}
if (Regex.IsMatch(text2, "(TODO|FIXME|HACK|XXX)\\b", RegexOptions.IgnoreCase))
{
list.Add(new ReviewIssue(num, "INFO", "TODO/FIXME 마커 발견: " + text2.Trim()));
}
}
if ((focus == "all" || focus == "performance") ? true : false)
{
if (Regex.IsMatch(text2, "new\\s+List<.*>\\(\\).*\\.Add\\(") || Regex.IsMatch(text2, "\\.ToList\\(\\).*\\.Where\\("))
{
list.Add(new ReviewIssue(num, "INFO", "불필요한 컬렉션 할당 가능성"));
}
if (Regex.IsMatch(text2, "string\\s*\\+\\s*=|\".*\"\\s*\\+\\s*\""))
{
list.Add(new ReviewIssue(num, "INFO", "문자열 연결 — StringBuilder 사용 검토"));
}
}
bool flag = ((focus == "all" || focus == "style") ? true : false);
if (flag && text2.Length > 150)
{
list.Add(new ReviewIssue(num, "STYLE", $"긴 라인 ({text2.Length}자) — 가독성 저하"));
}
}
}
return list;
}
private static List<ReviewIssue> AnalyzeFile(string[] lines, string focus)
{
List<ReviewIssue> list = new List<ReviewIssue>();
int num = 0;
int num2 = 0;
bool flag = false;
for (int i = 0; i < lines.Length; i++)
{
string text = lines[i];
int num3 = i + 1;
string text2 = text.TrimStart();
if (Regex.IsMatch(text2, "(public|private|protected|internal|static|async|override)\\s+.*\\(.*\\)\\s*\\{?\\s*$") && !text2.Contains(';'))
{
flag = true;
num2 = num3;
}
if (text2.Contains('{'))
{
num++;
}
bool flag4;
if (text2.Contains('}'))
{
num--;
if (flag && num <= 1)
{
int num4 = num3 - num2;
bool flag2 = num4 > 60;
bool flag3 = flag2;
if (flag3)
{
flag4 = ((focus == "all" || focus == "style") ? true : false);
flag3 = flag4;
}
if (flag3)
{
list.Add(new ReviewIssue(num2, "STYLE", $"긴 메서드 ({num4}줄) — 분할 검토"));
}
flag = false;
}
}
if ((focus == "all" || focus == "bugs") ? true : false)
{
if (Regex.IsMatch(text2, "catch\\s*(\\(\\s*Exception)?\\s*\\)?\\s*\\{\\s*\\}"))
{
list.Add(new ReviewIssue(num3, "WARNING", "빈 catch 블록"));
}
if (Regex.IsMatch(text2, "\\.Result\\b|\\.Wait\\(\\)"))
{
list.Add(new ReviewIssue(num3, "WARNING", "동기 대기 (.Result/.Wait()) — 데드락 위험"));
}
}
if ((focus == "all" || focus == "security") ? true : false)
{
if (Regex.IsMatch(text2, "(password|secret|token|api_?key)\\s*=\\s*\"[^\"]+\"", RegexOptions.IgnoreCase))
{
list.Add(new ReviewIssue(num3, "CRITICAL", "하드코딩된 비밀번호/키"));
}
if (Regex.IsMatch(text2, "(TODO|FIXME|HACK|XXX)\\b", RegexOptions.IgnoreCase))
{
list.Add(new ReviewIssue(num3, "INFO", "마커: " + text2.Trim()));
}
}
flag4 = ((focus == "all" || focus == "performance") ? true : false);
if (flag4 && Regex.IsMatch(text2, "string\\s*\\+\\s*="))
{
list.Add(new ReviewIssue(num3, "INFO", "루프 내 문자열 연결 — StringBuilder 검토"));
}
flag4 = ((focus == "all" || focus == "style") ? true : false);
if (flag4 && text.Length > 150)
{
list.Add(new ReviewIssue(num3, "STYLE", $"긴 라인 ({text.Length}자)"));
}
}
bool flag5 = lines.Length > 500;
bool flag6 = flag5;
if (flag6)
{
bool flag4 = ((focus == "all" || focus == "style") ? true : false);
flag6 = flag4;
}
if (flag6)
{
list.Add(new ReviewIssue(1, "STYLE", $"큰 파일 ({lines.Length}줄) — 클래스 분할 검토"));
}
return list;
}
private static async Task<string?> RunGitAsync(string workDir, string args, CancellationToken ct)
{
string gitPath = FindGit();
if (gitPath == null)
{
return null;
}
try
{
ProcessStartInfo psi = new ProcessStartInfo(gitPath, args)
{
WorkingDirectory = workDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(30.0));
using Process proc = Process.Start(psi);
if (proc == null)
{
return null;
}
string stdout = await proc.StandardOutput.ReadToEndAsync(cts.Token);
await proc.WaitForExitAsync(cts.Token);
return (stdout.Length > 12000) ? (stdout.Substring(0, 12000) + "\n... (출력 잘림)") : stdout;
}
catch
{
return null;
}
}
private static string? FindGit()
{
string[] array = new string[3] { "git", "C:\\Program Files\\Git\\bin\\git.exe", "C:\\Program Files (x86)\\Git\\bin\\git.exe" };
string[] array2 = array;
foreach (string text in array2)
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo(text, "--version")
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
process?.WaitForExit(3000);
if (process != null && process.ExitCode == 0)
{
return text;
}
}
catch
{
}
}
return null;
}
private static List<DiffFile> ParseDiffFiles(string diff)
{
List<DiffFile> list = new List<DiffFile>();
DiffFile diffFile = null;
string[] array = diff.Split('\n');
foreach (string text in array)
{
if (text.StartsWith("diff --git"))
{
diffFile = new DiffFile();
list.Add(diffFile);
string[] array2 = text.Split(" b/");
diffFile.Path = ((array2.Length > 1) ? array2[1].Trim() : text);
}
else
{
if (diffFile == null)
{
continue;
}
if (text.StartsWith("new file"))
{
diffFile.Status = "added";
}
else if (text.StartsWith("deleted file"))
{
diffFile.Status = "deleted";
}
else if (text.StartsWith("@@") || text.StartsWith("+") || text.StartsWith("-"))
{
diffFile.Hunks.Add(text);
if (text.StartsWith("+") && !text.StartsWith("+++"))
{
diffFile.Added++;
}
if (text.StartsWith("-") && !text.StartsWith("---"))
{
diffFile.Removed++;
}
}
}
}
return list;
}
}