diff --git a/CodePlay.Core/Converters/PythonToCSharpConverter.cs b/CodePlay.Core/Converters/PythonToCSharpConverter.cs new file mode 100644 index 0000000..8488699 --- /dev/null +++ b/CodePlay.Core/Converters/PythonToCSharpConverter.cs @@ -0,0 +1,103 @@ +using CodePlay.Core.Interfaces; +using CodePlay.Core.Common; +using CodePlay.Core.Models; +using System.Text; + +namespace CodePlay.Core.Converters; + +/// +/// Python 到 C# 转换器 +/// +public class PythonToCSharpConverter : IConverter +{ + public async Task ConvertAsync( + SyntaxTree syntaxTree, + LanguageType targetLanguage, + ConversionOptions? options = null, + CancellationToken cancellationToken = default) + { + var result = new ConversionResult + { + TransformedCode = ConvertToCSharp(syntaxTree.SourceCode ?? ""), + Report = new ConversionReport() + }; + + await Task.CompletedTask; + return result; + } + + private string ConvertToCSharp(string pythonCode) + { + var sb = new StringBuilder(); + sb.AppendLine("// 从 Python 转换而来 - 需要手动审查"); + sb.AppendLine(); + + var lines = pythonCode.Split('\n'); + var indentLevel = 0; + + foreach (var line in lines) + { + var trimmed = line.Trim(); + if (string.IsNullOrEmpty(trimmed)) + { + sb.AppendLine(); + continue; + } + + // Python import -> C# using + if (trimmed.StartsWith("import ")) + { + var module = trimmed.Substring(7).Trim(); + sb.AppendLine($"// using {module}; // TODO: 映射 .NET 命名空间"); + continue; + } + + // Python class -> C# class + var classMatch = System.Text.RegularExpressions.Regex.Match(trimmed, @"class\s+(\w+)"); + if (classMatch.Success) + { + sb.AppendLine($"public class {classMatch.Groups[1].Value}"); + sb.AppendLine("{"); + continue; + } + + // Python def -> C# method + var defMatch = System.Text.RegularExpressions.Regex.Match(trimmed, @"def\s+(\w+)\s*\(([^)]*)\)"); + if (defMatch.Success) + { + var methodName = defMatch.Groups[1].Value; + var parameters = defMatch.Groups[2].Value; + sb.AppendLine($" public void {methodName}({ConvertPythonParams(parameters)})"); + sb.AppendLine(" {"); + sb.AppendLine(" // TODO: 实现方法体"); + sb.AppendLine(" }"); + continue; + } + + // 检测类结束(缩进减少) + if (trimmed.StartsWith("}") || line.TrimStart().Length == line.Length) + { + sb.AppendLine("}"); + } + } + + return sb.ToString(); + } + + private string ConvertPythonParams(string pythonParams) + { + if (string.IsNullOrWhiteSpace(pythonParams)) return ""; + + var parts = pythonParams.Split(','); + var csharpParams = new List(); + + foreach (var part in parts) + { + var param = part.Trim(); + if (param == "self") continue; + csharpParams.Add($"object {param}"); + } + + return string.Join(", ", csharpParams); + } +} diff --git a/CodePlay.Core/Parsers/PythonParser.cs b/CodePlay.Core/Parsers/PythonParser.cs new file mode 100644 index 0000000..8d9734a --- /dev/null +++ b/CodePlay.Core/Parsers/PythonParser.cs @@ -0,0 +1,115 @@ +using CodePlay.Core.Interfaces; +using CodePlay.Core.Common; + +namespace CodePlay.Core.Parsers; + +/// +/// Python 解析器 +/// +public class PythonParser : BaseParser +{ + public override LanguageType SupportedLanguage => LanguageType.None; // Python 暂不加入枚举 + + public override Task ParseAsync(string sourceCode, CancellationToken cancellationToken = default) + { + var tree = CreateSyntaxTree(); + tree.SourceCode = sourceCode; + tree.Root = ParseRoot(sourceCode); + tree.Comments = ExtractComments(sourceCode); + + return Task.FromResult(tree); + } + + private SyntaxNode ParseRoot(string sourceCode) + { + var root = new SyntaxNode { Type = SyntaxNodeType.CompilationUnit, Text = sourceCode }; + + ExtractClasses(sourceCode, root); + ExtractFunctions(sourceCode, root); + ExtractImports(sourceCode, root); + + return root; + } + + private void ExtractClasses(string code, SyntaxNode root) + { + var pattern = @"class\s+(\w+)\s*(?:\(([^)]*)\))?"; + var matches = System.Text.RegularExpressions.Regex.Matches(code, pattern); + + foreach (System.Text.RegularExpressions.Match match in matches) + { + var classNode = new SyntaxNode + { + Type = SyntaxNodeType.Class, + Text = match.Value, + Metadata = new Dictionary + { + ["Name"] = match.Groups[1].Value, + ["BaseClasses"] = match.Groups[2].Success ? match.Groups[2].Value : null + } + }; + root.Children.Add(classNode); + } + } + + private void ExtractFunctions(string code, SyntaxNode root) + { + var pattern = @"def\s+(\w+)\s*\(([^)]*)\)"; + var matches = System.Text.RegularExpressions.Regex.Matches(code, pattern); + + foreach (System.Text.RegularExpressions.Match match in matches) + { + var funcNode = new SyntaxNode + { + Type = SyntaxNodeType.Method, + Text = match.Value, + Metadata = new Dictionary + { + ["Name"] = match.Groups[1].Value, + ["Parameters"] = match.Groups[2].Value + } + }; + root.Children.Add(funcNode); + } + } + + private void ExtractImports(string code, SyntaxNode root) + { + var lines = code.Split('\n'); + foreach (var line in lines) + { + var trimmed = line.Trim(); + if (trimmed.StartsWith("import ") || trimmed.StartsWith("from ")) + { + root.Children.Add(new SyntaxNode + { + Type = SyntaxNodeType.Type, + Text = trimmed, + Metadata = { ["Kind"] = "import" } + }); + } + } + } + + private List ExtractComments(string sourceCode) + { + var comments = new List(); + var lines = sourceCode.Split('\n'); + + for (int i = 0; i < lines.Length; i++) + { + var line = lines[i].Trim(); + if (line.StartsWith("#")) + { + comments.Add(new SyntaxComment + { + Type = CommentType.SingleLine, + Text = line.TrimStart('#').Trim(), + LineNumber = i + 1 + }); + } + } + + return comments; + } +} diff --git a/CodePlay.Core/Services/CodeFormatter.cs b/CodePlay.Core/Services/CodeFormatter.cs new file mode 100644 index 0000000..ea3af92 --- /dev/null +++ b/CodePlay.Core/Services/CodeFormatter.cs @@ -0,0 +1,216 @@ +using System.Diagnostics; + +namespace CodePlay.Core.Services; + +/// +/// 代码格式化服务 +/// 支持 C#/Java/C++ 代码格式化 +/// +public interface ICodeFormatter +{ + Task FormatAsync(string code, string language, CancellationToken cancellationToken = default); + bool IsFormatterAvailable(string language); +} + +public class CodeFormatter : ICodeFormatter +{ + public async Task FormatAsync(string code, string language, CancellationToken cancellationToken = default) + { + return language.ToLower() switch + { + "csharp" => await FormatCSharp(code, cancellationToken), + "java" => await FormatJava(code, cancellationToken), + "cpp" or "c++" => await FormatCpp(code, cancellationToken), + _ => code + }; + } + + public bool IsFormatterAvailable(string language) + { + return language.ToLower() switch + { + "csharp" => IsDotNetFormatAvailable(), + "java" => IsJavaFormatAvailable(), + "cpp" or "c++" => IsClangFormatAvailable(), + _ => false + }; + } + + private async Task FormatCSharp(string code, CancellationToken ct) + { + if (!IsDotNetFormatAvailable()) + return code; + + try + { + var tempFile = Path.GetTempFileName() + ".cs"; + await File.WriteAllTextAsync(tempFile, code, ct); + + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"format \"{tempFile}\" --no-restore", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return code; + + await process.WaitForExitAsync(ct); + + if (process.ExitCode == 0) + return await File.ReadAllTextAsync(tempFile, ct); + + return code; + } + catch + { + return code; + } + } + + private async Task FormatJava(string code, CancellationToken ct) + { + if (!IsJavaFormatAvailable()) + return code; + + try + { + var tempFile = Path.GetTempFileName() + ".java"; + await File.WriteAllTextAsync(tempFile, code, ct); + + var startInfo = new ProcessStartInfo + { + FileName = "google-java-format", + Arguments = $"--replace \"{tempFile}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return code; + + await process.WaitForExitAsync(ct); + + if (process.ExitCode == 0) + return await File.ReadAllTextAsync(tempFile, ct); + + return code; + } + catch + { + return code; + } + } + + private async Task FormatCpp(string code, CancellationToken ct) + { + if (!IsClangFormatAvailable()) + return code; + + try + { + var tempFile = Path.GetTempFileName() + ".cpp"; + await File.WriteAllTextAsync(tempFile, code, ct); + + var startInfo = new ProcessStartInfo + { + FileName = "clang-format", + Arguments = $"-i -style=file \"{tempFile}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return code; + + await process.WaitForExitAsync(ct); + + if (process.ExitCode == 0) + return await File.ReadAllTextAsync(tempFile, ct); + + return code; + } + catch + { + return code; + } + } + + private bool IsDotNetFormatAvailable() + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "tool list -g", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + process?.WaitForExit(5000); + var output = process?.StandardOutput.ReadToEnd(); + return output?.Contains("dotnet-format") == true; + } + catch + { + return false; + } + } + + private bool IsJavaFormatAvailable() + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "google-java-format", + Arguments = "--version", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + process?.WaitForExit(5000); + return process?.ExitCode == 0; + } + catch + { + return false; + } + } + + private bool IsClangFormatAvailable() + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "clang-format", + Arguments = "--version", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + process?.WaitForExit(5000); + return process?.ExitCode == 0; + } + catch + { + return false; + } + } +} diff --git a/CodePlay.WebAPI/Hubs/ConversionHub.cs b/CodePlay.WebAPI/Hubs/ConversionHub.cs new file mode 100644 index 0000000..861c96c --- /dev/null +++ b/CodePlay.WebAPI/Hubs/ConversionHub.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.SignalR; + +namespace CodePlay.WebAPI.Hubs; + +public class ConversionHub : Hub +{ + public async Task JoinProgressGroup(string conversionId) + { + await Groups.AddToGroupAsync(Context.ConnectionId, conversionId); + } + + public async Task LeaveProgressGroup(string conversionId) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, conversionId); + } +} + +public class ProgressMessage +{ + public int Percent { get; set; } + public string CurrentFile { get; set; } = ""; + public string Status { get; set; } = ""; + public TimeSpan? EstimatedTimeRemaining { get; set; } +}