|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.LanguageService;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;
namespace Microsoft.CodeAnalysis.CSharp.Analyzers.RemoveUnnecessaryNullableDirective;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class CSharpRemoveRedundantNullableDirectiveDiagnosticAnalyzer
: AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer
{
public CSharpRemoveRedundantNullableDirectiveDiagnosticAnalyzer()
: base(
IDEDiagnosticIds.RemoveRedundantNullableDirectiveDiagnosticId,
EnforceOnBuildValues.RemoveRedundantNullableDirective,
option: null,
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Remove_redundant_nullable_directive), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)),
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Nullable_directive_is_redundant), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
{
}
public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
=> DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis;
protected override void InitializeWorker(AnalysisContext context)
=> context.RegisterCompilationStartAction(context =>
{
var compilation = (CSharpCompilation)context.Compilation;
if (compilation.LanguageVersion < LanguageVersion.CSharp8)
{
// Compilation does not support nullable directives
return;
}
var compilationOptions = compilation.Options;
context.RegisterSyntaxTreeAction(context => ProcessSyntaxTree(compilationOptions, context));
});
private void ProcessSyntaxTree(
CSharpCompilationOptions compilationOptions,
SyntaxTreeAnalysisContext context)
{
if (ShouldSkipAnalysis(context, compilationOptions, notification: null))
return;
var root = context.GetAnalysisRoot(findInTrivia: true);
// Bail out if the root contains no nullable directives.
if (!root.ContainsDirective(SyntaxKind.NullableDirectiveTrivia))
return;
var defaultNullableContext = compilationOptions.NullableContextOptions;
NullableContextOptions? currentState = context.Tree.IsGeneratedCode(context.Options, CSharpSyntaxFacts.Instance, context.CancellationToken)
? NullableContextOptions.Disable
: defaultNullableContext;
using var pooledStack = SharedPools.Default<Stack<SyntaxNodeOrToken>>().GetPooledObject();
var stack = pooledStack.Object;
stack.Push(root);
while (stack.TryPop(out var current))
{
if (!current.ContainsDirectives)
continue;
if (current.AsNode(out var childNode))
{
// Add the nodes in reverse so we continue walking in a depth-first fashion.
foreach (var child in childNode.ChildNodesAndTokens().Reverse())
stack.Push(child);
}
else if (current.IsToken)
{
foreach (var trivia in current.AsToken().LeadingTrivia)
ProcessTrivia(trivia);
}
}
return;
void ProcessTrivia(SyntaxTrivia trivia)
{
if (!trivia.IsDirective)
return;
var directive = trivia.GetStructure()!;
switch (directive.Kind())
{
case SyntaxKind.NullableDirectiveTrivia:
{
// Report if the nullable directive puts us in the same state we're already in.
var newState = GetNullableContextOptions(defaultNullableContext, currentState, (NullableDirectiveTriviaSyntax)directive);
if (newState == currentState)
context.ReportDiagnostic(Diagnostic.Create(Descriptor, directive.GetLocation()));
currentState = newState;
return;
}
case SyntaxKind.IfDirectiveTrivia or
SyntaxKind.ElifDirectiveTrivia or
SyntaxKind.ElseDirectiveTrivia or
SyntaxKind.EndIfDirectiveTrivia:
// Reset the known nullable state when crossing a conditional compilation boundary
currentState = null;
return;
}
}
}
internal static NullableContextOptions? GetNullableContextOptions(NullableContextOptions compilationOptions, NullableContextOptions? options, NullableDirectiveTriviaSyntax directive)
{
if (!directive.TargetToken.IsKind(SyntaxKind.None))
{
if (options is not { } knownState)
{
return null;
}
NullableContextOptions flagToChange;
if (directive.TargetToken.IsKind(SyntaxKind.AnnotationsKeyword))
{
flagToChange = NullableContextOptions.Annotations;
}
else if (directive.TargetToken.IsKind(SyntaxKind.WarningsKeyword))
{
flagToChange = NullableContextOptions.Warnings;
}
else
{
return null;
}
if (directive.SettingToken.IsKind(SyntaxKind.EnableKeyword))
{
return knownState | flagToChange;
}
else if (directive.SettingToken.IsKind(SyntaxKind.DisableKeyword))
{
return knownState & (~flagToChange);
}
else
{
return null;
}
}
if (directive.SettingToken.IsKind(SyntaxKind.EnableKeyword))
{
return NullableContextOptions.Annotations | NullableContextOptions.Warnings;
}
else if (directive.SettingToken.IsKind(SyntaxKind.DisableKeyword))
{
return NullableContextOptions.Disable;
}
else if (directive.SettingToken.IsKind(SyntaxKind.RestoreKeyword))
{
return compilationOptions;
}
else
{
return null;
}
}
}
|