using System; using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Sim.Runner; /// /// 编译时检查学生代码用到的 API:网络、启动外部程序、反射、调用本机代码等课程用不到、只会带来风险的功能, /// 直接给出中文报错。真正的隔离靠运行时沙箱,这里是第二道防线,也让学生看到明确的原因。 /// 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 /// 整个命名空间(含子命名空间)都不允许 private static readonly (string Namespace, string Reason)[] DeniedNamespaces = { ("System.Net", "网络访问"), ("System.Reflection", "反射"), ("System.Runtime.Loader", "加载程序集"), ("System.IO.Pipes", "进程间通信"), ("System.IO.MemoryMappedFiles", "进程间通信"), }; /// 单个类型不允许(所在命名空间的其他类型可以用) private static readonly Dictionary 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"] = "调用本机代码", }; /// /// 不允许的类型里单独放行的成员:教材用 AppDomain.CurrentDomain.BaseDirectory 拼出保存照片的路径; /// 类型的名字(GetType().Name、DataColumn.DataType.Name)只是读名字,没有风险 /// private static readonly HashSet AllowedMembers = new() { "System.AppDomain.CurrentDomain", "System.AppDomain.BaseDirectory", "System.Reflection.MemberInfo.Name", "System.Type.FullName", }; public static IEnumerable Check(CSharpCompilation compilation) { foreach (var tree in compilation.SyntaxTrees) { var model = compilation.GetSemanticModel(tree); foreach (var name in tree.GetRoot().DescendantNodes().OfType()) { // 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}", }; }