如何在C#中当secureCheck设为false时生成编译器警告(可借助Roslyn)
Absolutely feasible! You can build a custom Roslyn Analyzer to exactly meet this requirement—this is exactly the kind of scenario Roslyn was designed for, and it works perfectly with your .NET Standard 2.0 library (C# 7.3 or 8.0 both work).
Here's a step-by-step guide to implement this:
1. Create a Roslyn Analyzer Project
In Visual Studio, create a new Analyzer with Code Fix (.NET Standard) project. This template provides the basic structure you need, including diagnostic rules, analysis logic, and optional code fix support.
2. Implement the Diagnostic Analyzer
This analyzer will scan your code for calls to CheckThing where secureCheck is explicitly set to false, and trigger a custom warning.
Here's the core analyzer code:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using System.Collections.Immutable; using System.Linq; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class InsecureCheckAnalyzer : DiagnosticAnalyzer { // Your custom warning ID (matches what you'll use in #pragma) public const string DiagnosticId = "InsecureCheckWarning"; // Localized messages (use the auto-generated Resources.resx file for these) private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AnalyzerTitle), Resources.ResourceManager, typeof(Resources)); private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources)); private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AnalyzerDescription), Resources.ResourceManager, typeof(Resources)); private const string Category = "Security"; // Define the diagnostic rule (warning level, enabled by default) private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); // Register to analyze method invocation syntax nodes context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); } private void AnalyzeInvocation(SyntaxNodeAnalysisContext context) { var invocationExpr = (InvocationExpressionSyntax)context.Node; // Get the symbol for the called method var methodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpr).Symbol as IMethodSymbol; if (methodSymbol == null) return; // Verify this is your target method (update namespace/class name to match your code) if (methodSymbol.Name != "CheckThing" || methodSymbol.ContainingType.Name != "YourClassName" || methodSymbol.ContainingNamespace.ToString() != "Your.Project.Namespace") return; // Locate the secureCheck parameter var secureCheckParam = methodSymbol.Parameters.FirstOrDefault(p => p.Name == "secureCheck"); if (secureCheckParam == null) return; bool isSecureCheckFalse = false; int paramIndex = methodSymbol.Parameters.IndexOf(secureCheckParam); // Check for named arguments first var namedArg = invocationExpr.ArgumentList.Arguments .OfType<NamedArgumentSyntax>() .FirstOrDefault(a => a.NameEquals.Name.Identifier.Text == "secureCheck"); if (namedArg != null) { var constantValue = context.SemanticModel.GetConstantValue(namedArg.Expression); isSecureCheckFalse = constantValue.HasValue && constantValue.Value is bool b && !b; } else { // Check positional arguments (only if the parameter is explicitly provided) if (invocationExpr.ArgumentList.Arguments.Count > paramIndex) { var positionalArg = invocationExpr.ArgumentList.Arguments[paramIndex]; var constantValue = context.SemanticModel.GetConstantValue(positionalArg.Expression); isSecureCheckFalse = constantValue.HasValue && constantValue.Value is bool b && !b; } } // Trigger the warning if secureCheck is set to false if (isSecureCheckFalse) { var diagnostic = Diagnostic.Create(Rule, invocationExpr.GetLocation(), methodSymbol.Name); context.ReportDiagnostic(diagnostic); } } }
Update the Resources.resx file with these localized strings:
- AnalyzerTitle: Insecure Security Check Usage
- AnalyzerMessageFormat: Calling '{0}' with secureCheck=false bypasses critical security checks. Ensure this has been reviewed by the team lead.
- AnalyzerDescription: Warns when the CheckThing method is called with secureCheck=false, indicating potential security risks that require team approval.
3. Optional: Add a Code Fix for #pragma Directives
To make it easier for your team to suppress the warning with #pragma, add a code fix provider that automatically inserts the disable/restore directives:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting; using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Threading; using System.Threading.Tasks; [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(InsecureCheckCodeFixProvider)), Shared] public class InsecureCheckCodeFixProvider : CodeFixProvider { public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(InsecureCheckAnalyzer.DiagnosticId); public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); var diagnostic = context.Diagnostics.First(); var invocationExpr = root.FindToken(diagnostic.Location.SourceSpan.Start) .Parent.AncestorsAndSelf() .OfType<InvocationExpressionSyntax>() .First(); context.RegisterCodeFix( CodeAction.Create( title: "Add #pragma warning disable for insecure check", createChangedDocument: c => AddPragmaDirectivesAsync(context.Document, invocationExpr, c), equivalenceKey: nameof(InsecureCheckCodeFixProvider)), diagnostic); } private async Task<Document> AddPragmaDirectivesAsync(Document document, InvocationExpressionSyntax invocationExpr, CancellationToken cancellationToken) { var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var statement = invocationExpr.AncestorsAndSelf().OfType<StatementSyntax>().First(); // Create #pragma disable directive var disableDirective = SyntaxFactory.PragmaWarningDirectiveTrivia( SyntaxFactory.Token(SyntaxKind.DisableKeyword), SyntaxFactory.SingletonSeparatedList(SyntaxFactory.IdentifierName(InsecureCheckAnalyzer.DiagnosticId)), false); // Create #pragma restore directive var restoreDirective = SyntaxFactory.PragmaWarningDirectiveTrivia( SyntaxFactory.Token(SyntaxKind.RestoreKeyword), SyntaxFactory.SingletonSeparatedList(SyntaxFactory.IdentifierName(InsecureCheckAnalyzer.DiagnosticId)), false); // Add disable before the invocation statement var newStatement = statement.WithLeadingTrivia( SyntaxFactory.TriviaList(disableDirective).AddRange(statement.GetLeadingTrivia())); root = root.ReplaceNode(statement, newStatement); // Add restore after the invocation statement var nextSibling = statement.GetNextSibling(); if (nextSibling != null) { nextSibling = nextSibling.WithLeadingTrivia( SyntaxFactory.TriviaList(restoreDirective).AddRange(nextSibling.GetLeadingTrivia())); root = root.ReplaceNode(nextSibling, nextSibling); } else { // If it's the last statement, add an empty statement with the restore directive var restoreStatement = SyntaxFactory.EmptyStatement().WithLeadingTrivia(SyntaxFactory.TriviaList(restoreDirective)); root = root.InsertAfter(statement, restoreStatement); } // Format the modified code root = Formatter.Format(root, Formatter.Annotation, document.Project.Solution.Workspace); return document.WithSyntaxRoot(root); } }
4. Integrate the Analyzer with Your Class Library
You have two options to use the analyzer in your .NET Standard 2.0 project:
- Package as a NuGet package: Build the analyzer project, pack it into a NuGet package (using the
.nuspecfile generated by the template), and install it in your class library. This is the cleanest approach for shared projects. - Direct reference: Right-click your class library project → Add → Reference → Browse to the analyzer project's
bin/Debugorbin/Releasefolder, and select the analyzer DLL.
5. Test the Workflow
Once integrated:
- When you call
CheckThing(a, b, secureCheck: false), you'll see your customInsecureCheckWarningin the Error List. - Use the code fix (Ctrl+. or right-click the warning) to auto-add the
#pragmadirectives, or manually write them as you described. - The warning will be suppressed only where you've added the
#pragma disableand restored afterward.
This setup ensures that any use of the insecure mode is explicitly flagged, requiring team review and documentation via the #pragma comments.
内容的提问来源于stack exchange,提问作者TC Fox

