Files
IOT_SIM/runtime/Sim.Runner/StudentCompiler.cs
T
xuyueandClaude Opus 5.5 302eae4f13 代码编辑器:代码补全、悬停提示、参数提示、边写边检查
- 运行器新增 language-server 模式:常驻语言服务,和编译服务分开;用 Roslyn 的
  CompletionService、QuickInfoService(和 VS 的 IntelliSense 同一套引擎),参数提示用语义模型实现;
  每个浏览器标签页一份工程,只更新有变化的文件;去掉仿真类库的可空注解(string? → string)
- 编译和语言服务共用同一套编译选项和引用;检查结果带上禁止 API 的报错和中文提示
- 服务端 POST /api/language 转发请求(按用户区分会话,10 秒超时)
- 编辑器:补全("." 触发,选中时取说明)、悬停、参数提示("(" "," 触发);停止输入 1 秒后检查整个工程,
  和"运行"时的编译结果以较新的为准
- 实测(2 核):50 个标签页同时检查 1.2 秒内完成,同时补全 0.7 秒;内存约 260 MB

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:23:55 +08:00

121 lines
5.0 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json.Nodes;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
namespace Sim.Runner;
/// <summary>用 Roslyn 在内存里编译学生工程,报错信息用简体中文</summary>
internal static class StudentCompiler
{
private static readonly CultureInfo Chinese = CultureInfo.GetCultureInfo("zh-Hans");
/// <summary>
/// 框架里不能给学生代码引用的程序集:System.Drawing.dll 门面把 Font、Image 等转发到 Linux 不支持的
/// System.Drawing.Common,会和仿真库里的同名类型冲突。
/// </summary>
private static readonly HashSet<string> ExcludedFrameworkAssemblies = new(StringComparer.OrdinalIgnoreCase)
{
"System.Drawing.dll",
};
private static readonly string[] SimAssemblies = { "Sim.WinForms.dll", "Sim.Devices.dll" };
public sealed record Result(bool Success, byte[] Assembly, byte[] Pdb, JsonArray Diagnostics);
/// <summary>编译和语言服务(代码补全等)共用同一套选项</summary>
internal static readonly CSharpParseOptions ParseOptions = new(LanguageVersion.Latest);
internal static readonly CSharpCompilationOptions CompilationOptions = new CSharpCompilationOptions(OutputKind.WindowsApplication)
.WithOptimizationLevel(OptimizationLevel.Debug)
.WithNullableContextOptions(NullableContextOptions.Disable)
.WithAllowUnsafe(false)
.WithConcurrentBuild(true);
public static Result Compile(string projectDir)
{
var parseOptions = ParseOptions;
var trees = Directory.EnumerateFiles(projectDir, "*.cs", SearchOption.AllDirectories)
.Where(path => !IsBuildOutput(projectDir, path))
.OrderBy(path => path, StringComparer.Ordinal)
.Select(path => CSharpSyntaxTree.ParseText(
SourceText.From(File.ReadAllText(path), Encoding.UTF8),
parseOptions,
path: Path.GetRelativePath(projectDir, path)))
.ToList();
var compilation = CSharpCompilation.Create("StudentApp", trees, References.Value, CompilationOptions);
using var peStream = new MemoryStream();
using var pdbStream = new MemoryStream();
var emit = compilation.Emit(peStream, pdbStream,
options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb));
var denied = ApiPolicy.Check(compilation).ToList();
var diagnostics = new JsonArray();
foreach (var diagnostic in emit.Diagnostics.Concat(denied)
.Where(d => d.Severity is DiagnosticSeverity.Error or DiagnosticSeverity.Warning)
.OrderBy(d => d.Severity == DiagnosticSeverity.Error ? 0 : 1))
{
diagnostics.Add(ToJson(diagnostic));
}
return new Result(emit.Success && denied.Count == 0, peStream.ToArray(), pdbStream.ToArray(), diagnostics);
}
internal static JsonObject ToJson(Diagnostic diagnostic)
{
var span = diagnostic.Location.GetMappedLineSpan();
var message = diagnostic.GetMessage(Chinese);
var json = new JsonObject
{
["file"] = span.Path,
["line"] = span.StartLinePosition.Line + 1,
["column"] = span.StartLinePosition.Character + 1,
["severity"] = diagnostic.Severity == DiagnosticSeverity.Error ? "error" : "warning",
["code"] = diagnostic.Id,
["message"] = message,
};
var hint = CompileHints.For(diagnostic);
if (hint != null) json["hint"] = hint;
return json;
}
private static bool IsBuildOutput(string projectDir, string path)
{
var relative = Path.GetRelativePath(projectDir, path).Replace('\\', '/');
return relative.StartsWith("bin/") || relative.StartsWith("obj/") || relative.Contains("/bin/") || relative.Contains("/obj/");
}
/// <summary>学生代码可引用的程序集:.NET 框架(去掉冲突的门面)+ 仿真库</summary>
internal static readonly Lazy<IReadOnlyList<MetadataReference>> References = new(() =>
{
var frameworkDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
var framework = Directory.EnumerateFiles(frameworkDir, "*.dll")
.Where(path => !ExcludedFrameworkAssemblies.Contains(Path.GetFileName(path)))
.Where(IsManagedAssembly);
var sim = SimAssemblies.Select(name => Path.Combine(AppContext.BaseDirectory, name));
return framework.Concat(sim).Select(path => (MetadataReference)MetadataReference.CreateFromFile(path)).ToList();
});
private static bool IsManagedAssembly(string path)
{
try
{
AssemblyName.GetAssemblyName(path);
return true;
}
catch (BadImageFormatException)
{
return false; // 框架目录里的原生库
}
}
}