From 5ae07b851db6f56e5fcad03f84fdc8345209c490 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Fri, 23 Oct 2026 15:14:49 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=AF=E5=BE=84=E9=87=8C=E7=9A=84=E5=8F=8D?= =?UTF-8?q?=E6=96=9C=E6=9D=A0=E5=9C=A8=20Linux=20=E4=B8=8A=E6=8D=A2?= =?UTF-8?q?=E6=88=90=E6=96=9C=E6=9D=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 教材写 AppDomain.CurrentDomain.BaseDirectory + "Camera\\",Linux 上反斜杠会成为文件名的一部分 - 编译时把传给 System.IO(File、Directory、Path、各种 Stream)和 Image / Bitmap 读写文件方法的 路径参数包上 Sim.Host.WindowsPaths.Fix;只包语义上确实是这些方法里名字带 path / file / dir 的 字符串参数,不换行,编辑器里看到的仍是原代码 Co-Authored-By: Claude Opus 5.5 --- runtime/Sim.Runner/PathRewriter.cs | 78 +++++++++++++++++++++++ runtime/Sim.Runner/StudentCompiler.cs | 2 +- runtime/Sim.WinForms/Host/WindowsPaths.cs | 13 ++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 runtime/Sim.Runner/PathRewriter.cs create mode 100644 runtime/Sim.WinForms/Host/WindowsPaths.cs diff --git a/runtime/Sim.Runner/PathRewriter.cs b/runtime/Sim.Runner/PathRewriter.cs new file mode 100644 index 0000000..d0b75fa --- /dev/null +++ b/runtime/Sim.Runner/PathRewriter.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Text; + +namespace Sim.Runner; + +/// +/// 编译前把传给 System.IO(File、Directory、Path、FileStream、StreamWriter 等)和 Image / Bitmap 读写文件方法的路径参数 +/// 包上 Sim.Host.WindowsPaths.Fix(...):教材按 Windows 写 AppDomain.CurrentDomain.BaseDirectory + "Camera\\", +/// 在 Linux 上反斜杠要换成斜杠。只包语义上确实是这些方法里名字带 path / file / dir 的字符串参数; +/// 改写不换行,报错和异常里的行号不变。学生在编辑器里看到的仍是原来的代码。 +/// +internal static class PathRewriter +{ + private const string Fix = "global::Sim.Host.WindowsPaths.Fix"; + + public static CSharpCompilation Rewrite(CSharpCompilation compilation) + { + var result = compilation; + foreach (var tree in compilation.SyntaxTrees) + { + var model = compilation.GetSemanticModel(tree); + var root = tree.GetRoot(); + var targets = root.DescendantNodes() + .Where(n => n is InvocationExpressionSyntax or BaseObjectCreationExpressionSyntax) + .SelectMany(n => PathArguments(model.GetOperation(n))) + .ToHashSet(); + if (targets.Count == 0) continue; + var rewritten = root.ReplaceNodes(targets, (_, node) => + SyntaxFactory.ParseExpression($"{Fix}({node.WithoutTrivia().ToFullString()})").WithTriviaFrom(node)); + result = result.ReplaceSyntaxTree(tree, + CSharpSyntaxTree.ParseText(SourceText.From(rewritten.ToFullString(), Encoding.UTF8), (CSharpParseOptions)tree.Options, tree.FilePath)); + } + return result; + } + + /// 调用里要包起来的路径参数(只取学生直接写的表达式;不含反斜杠的字符串常量不用包) + private static IEnumerable PathArguments(IOperation? operation) + { + var (method, arguments) = operation switch + { + IInvocationOperation invocation => (invocation.TargetMethod, invocation.Arguments), + IObjectCreationOperation creation when creation.Constructor != null => (creation.Constructor, creation.Arguments), + _ => (null, default), + }; + if (method == null || !HandlesFiles(method.ContainingType)) yield break; + foreach (var argument in arguments) + { + if (argument.Parameter is not { } parameter || !IsPathName(parameter.Name)) continue; + var values = argument.ArgumentKind == ArgumentKind.ParamArray && argument.Value is IArrayCreationOperation { Initializer: { } init } + ? init.ElementValues + : [argument.Value]; + foreach (var value in values) + { + if (value.IsImplicit || value.Type?.SpecialType != SpecialType.System_String) continue; + if (value.ConstantValue is { HasValue: true, Value: string constant } && !constant.Contains('\\')) continue; + if (value.Syntax is ExpressionSyntax expression) yield return expression; + } + } + } + + private static bool HandlesFiles(INamedTypeSymbol type) + { + var ns = type.ContainingNamespace?.ToDisplayString(); + return ns == "System.IO" || (ns == "System.Drawing" && type.Name is "Image" or "Bitmap"); + } + + private static bool IsPathName(string name) => + name.Contains("path", StringComparison.OrdinalIgnoreCase) + || name.Contains("file", StringComparison.OrdinalIgnoreCase) + || name.Contains("dir", StringComparison.OrdinalIgnoreCase); +} diff --git a/runtime/Sim.Runner/StudentCompiler.cs b/runtime/Sim.Runner/StudentCompiler.cs index f9288e9..d138a1d 100644 --- a/runtime/Sim.Runner/StudentCompiler.cs +++ b/runtime/Sim.Runner/StudentCompiler.cs @@ -57,7 +57,7 @@ internal static class StudentCompiler using var peStream = new MemoryStream(); using var pdbStream = new MemoryStream(); // 编译改写后的代码(旧版线程 API 换成平台实现,DateTime.Now 换成虚拟时钟),API 检查仍针对学生写的原始代码 - var emit = ClockRewriter.Rewrite(LegacyThreadRewriter.Rewrite(compilation)).Emit(peStream, pdbStream, + var emit = PathRewriter.Rewrite(ClockRewriter.Rewrite(LegacyThreadRewriter.Rewrite(compilation))).Emit(peStream, pdbStream, options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb), manifestResources: EmptyControlResources(compilation)); var denied = ApiPolicy.Check(compilation).ToList(); diff --git a/runtime/Sim.WinForms/Host/WindowsPaths.cs b/runtime/Sim.WinForms/Host/WindowsPaths.cs new file mode 100644 index 0000000..373e325 --- /dev/null +++ b/runtime/Sim.WinForms/Host/WindowsPaths.cs @@ -0,0 +1,13 @@ +using System.ComponentModel; + +namespace Sim.Host; + +/// +/// Windows 写法的路径(教材里常见 BaseDirectory + "Camera\\")在 Linux 上要把反斜杠换成斜杠,否则反斜杠成了文件名的一部分。 +/// 编译时把学生代码里传给文件、文件夹、图片读写方法的路径参数包上这个方法(见 Sim.Runner/PathRewriter.cs)。 +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class WindowsPaths +{ + public static string Fix(string path) => path?.Replace('\\', '/')!; +}