- 虚拟时钟:实训台时钟面板可拨时间、1×/10×/60× 倍速、恢复真实时间;编译时把 DateTime.Now / Today / UtcNow 改写成 SimClock;评分规则加 clock 步骤 - 项目资源(Properties.Resources):平台素材库(自绘开关图片、合成门铃声),"项目资源"对话框勾选, 平台生成 Properties/Resources.Designer.cs;设计器图片框的"图片"属性可选资源;程序目录下的 Resources 指向素材库;未添加资源时给中文提示 - Windows Media Player(AxWMPLib):设置 URL 自动播放、Ctlcontrols.play/stop,浏览器里播放声音, 实训台加广播喇叭;编译时给窗体嵌空资源,OcxState 取到 null 不报错;图片和声音统一按内容缓存(blob) - TrackBar:拖动、点轨道、方向键,先 ValueChanged 再 Scroll;设计器加"滑动条""媒体播放器" - 评分规则加 value、media 检查;示例项目 7 任务 1–3(修正书上开关灯判断写反、漏填串口列表) - 修正:Properties/ 下的资源类不当作窗体;带命名空间的 System.AppDomain.CurrentDomain.BaseDirectory 放行 - 测试:虚拟时钟、TrackBar、项目 7 示例评分、作业起点带资源类 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
112 lines
5.6 KiB
C#
112 lines
5.6 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using Microsoft.CodeAnalysis;
|
||
using Microsoft.CodeAnalysis.CSharp;
|
||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||
|
||
namespace Sim.Runner;
|
||
|
||
/// <summary>
|
||
/// 编译时检查学生代码用到的 API:网络、启动外部程序、反射、调用本机代码等课程用不到、只会带来风险的功能,
|
||
/// 直接给出中文报错。真正的隔离靠运行时沙箱,这里是第二道防线,也让学生看到明确的原因。
|
||
/// </summary>
|
||
internal static class ApiPolicy
|
||
{
|
||
#pragma warning disable RS2008 // 不是分析器,不需要发布跟踪
|
||
private static readonly DiagnosticDescriptor NotAllowed = new(
|
||
"SIM001", "平台不允许使用的功能", "平台不允许使用 {0}({1})", "Usage", DiagnosticSeverity.Error, isEnabledByDefault: true);
|
||
#pragma warning restore RS2008
|
||
|
||
/// <summary>整个命名空间(含子命名空间)都不允许</summary>
|
||
private static readonly (string Namespace, string Reason)[] DeniedNamespaces =
|
||
{
|
||
("System.Net", "网络访问"),
|
||
("System.Reflection", "反射"),
|
||
("System.Runtime.Loader", "加载程序集"),
|
||
("System.IO.Pipes", "进程间通信"),
|
||
("System.IO.MemoryMappedFiles", "进程间通信"),
|
||
};
|
||
|
||
/// <summary>单个类型不允许(所在命名空间的其他类型可以用)</summary>
|
||
private static readonly Dictionary<string, string> DeniedTypes = new()
|
||
{
|
||
["System.Diagnostics.Process"] = "启动其他程序",
|
||
["System.Diagnostics.ProcessStartInfo"] = "启动其他程序",
|
||
["System.AppDomain"] = "加载程序集",
|
||
["System.Runtime.InteropServices.Marshal"] = "调用本机代码",
|
||
["System.Runtime.InteropServices.DllImportAttribute"] = "调用本机代码",
|
||
["System.Runtime.InteropServices.LibraryImportAttribute"] = "调用本机代码",
|
||
["System.Runtime.InteropServices.NativeLibrary"] = "调用本机代码",
|
||
["System.Runtime.InteropServices.NativeMemory"] = "调用本机代码",
|
||
};
|
||
|
||
/// <summary>
|
||
/// 不允许的类型里单独放行的成员:教材用 AppDomain.CurrentDomain.BaseDirectory 拼出保存照片的路径;
|
||
/// 类型的名字(GetType().Name、DataColumn.DataType.Name)只是读名字,没有风险
|
||
/// </summary>
|
||
private static readonly HashSet<string> AllowedMembers = new()
|
||
{
|
||
"System.AppDomain.CurrentDomain",
|
||
"System.AppDomain.BaseDirectory",
|
||
"System.Reflection.MemberInfo.Name",
|
||
"System.Type.FullName",
|
||
};
|
||
|
||
public static IEnumerable<Diagnostic> Check(CSharpCompilation compilation)
|
||
{
|
||
foreach (var tree in compilation.SyntaxTrees)
|
||
{
|
||
var model = compilation.GetSemanticModel(tree);
|
||
foreach (var name in tree.GetRoot().DescendantNodes().OfType<SimpleNameSyntax>())
|
||
{
|
||
// A.B.C 只检查最右边的名字,避免同一处报好几次
|
||
if (name.Parent is QualifiedNameSyntax q && q.Left == name) continue;
|
||
// var 推断出的类型在初始化表达式里已经检查过
|
||
if (name is IdentifierNameSyntax { IsVar: true }) continue;
|
||
// 类型名只是用来限定成员(AppDomain.CurrentDomain、System.AppDomain.CurrentDomain):检查后面的成员就够了
|
||
ExpressionSyntax qualifier = name.Parent is MemberAccessExpressionSyntax inner && inner.Name == name ? inner : name;
|
||
if (qualifier.Parent is MemberAccessExpressionSyntax m && m.Expression == qualifier && model.GetSymbolInfo(name).Symbol is INamespaceOrTypeSymbol) continue;
|
||
|
||
var symbol = model.GetSymbolInfo(name).Symbol;
|
||
if (symbol == null) continue;
|
||
if (Reason(symbol) is { } reason)
|
||
{
|
||
yield return Diagnostic.Create(NotAllowed, name.GetLocation(), Display(symbol), reason);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private static string? Reason(ISymbol symbol)
|
||
{
|
||
if (symbol is IDynamicTypeSymbol) return "dynamic 动态调用";
|
||
// using 指令里的命名空间不算:VS 生成的 AssemblyInfo.cs 会 using System.Runtime.InteropServices
|
||
if (symbol is INamespaceSymbol) return null;
|
||
if (symbol is IPropertySymbol && AllowedMembers.Contains($"{symbol.ContainingType.ToDisplayString()}.{symbol.Name}")) return null;
|
||
var type = symbol as INamedTypeSymbol ?? symbol.ContainingType;
|
||
// 方法的返回值、参数类型由调用处检查;这里看符号本身属于哪个类型
|
||
for (var t = type; t != null; t = t.ContainingType)
|
||
{
|
||
var fullName = t.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "");
|
||
if (DeniedTypes.TryGetValue(fullName, out var reason)) return reason;
|
||
}
|
||
if (type == null) return null;
|
||
// AssemblyInfo.cs 里的 [assembly: AssemblyTitle(...)] 等特性可以用
|
||
if (type.Name.EndsWith("Attribute") && type.ContainingNamespace.ToDisplayString() == "System.Reflection") return null;
|
||
var ns = type.ContainingNamespace.ToDisplayString();
|
||
foreach (var (denied, reason) in DeniedNamespaces)
|
||
{
|
||
if (ns == denied || ns.StartsWith(denied + ".")) return reason;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static string Display(ISymbol symbol) => symbol switch
|
||
{
|
||
IDynamicTypeSymbol => "dynamic",
|
||
INamedTypeSymbol type => type.ToDisplayString(),
|
||
_ => $"{symbol.ContainingType.ToDisplayString()}.{symbol.Name}",
|
||
};
|
||
}
|