|
// 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.Concurrent;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Analyzers.RemoveUnnecessaryNullableDirective;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.RemoveUnnecessaryNullableDirective;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer
: AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer
{
public CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer()
: base(IDEDiagnosticIds.RemoveUnnecessaryNullableDirectiveDiagnosticId,
EnforceOnBuildValues.RemoveUnnecessaryNullableDirective,
option: null,
fadingOption: null,
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Remove_unnecessary_nullable_directive), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)),
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Nullable_directive_is_unnecessary), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
{
}
public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
=> DiagnosticAnalyzerCategory.SemanticDocumentAnalysis;
protected override void InitializeWorker(AnalysisContext context)
{
context.RegisterCompilationStartAction(AnalyzeCompilation);
}
private void AnalyzeCompilation(CompilationStartAnalysisContext context)
{
var analyzer = new AnalyzerImpl(this);
context.RegisterCodeBlockAction(analyzer.AnalyzeCodeBlock);
context.RegisterSemanticModelAction(analyzer.AnalyzeSemanticModel);
}
/// <summary>
/// Determine if a code block is eligible for analysis by <see cref="AnalyzeCodeBlock"/>.
/// </summary>
/// <param name="codeBlock">The syntax node provided via <see cref="CodeBlockAnalysisContext.CodeBlock"/>.</param>
/// <returns><see langword="true"/> if the code block should be analyzed by <see cref="AnalyzeCodeBlock"/>;
/// otherwise, <see langword="false"/> to skip analysis of the block. If a block is skipped, one or more child
/// blocks may be analyzed by <see cref="AnalyzeCodeBlock"/>, and any remaining spans can be analyzed by
/// <see cref="AnalyzeSemanticModel"/>.</returns>
private static bool IsIgnoredCodeBlock(SyntaxNode codeBlock)
{
// Avoid analysis of compilation units and types in AnalyzeCodeBlock. These nodes appear in code block
// callbacks when they include attributes, but analysis of the node at this level would block more efficient
// analysis of descendant members.
return codeBlock.Kind() is
SyntaxKind.CompilationUnit or
SyntaxKind.ClassDeclaration or
SyntaxKind.RecordDeclaration or
SyntaxKind.StructDeclaration or
SyntaxKind.RecordStructDeclaration or
SyntaxKind.InterfaceDeclaration or
SyntaxKind.DelegateDeclaration or
SyntaxKind.EnumDeclaration;
}
private static bool IsReducing([NotNullWhen(true)] NullableContextOptions? oldOptions, [NotNullWhen(true)] NullableContextOptions? newOptions)
{
return oldOptions is { } oldOptionsValue
&& newOptions is { } newOptionsValue
&& newOptionsValue != oldOptionsValue
&& (oldOptionsValue & newOptionsValue) == newOptionsValue;
}
private static ImmutableArray<TextSpan> AnalyzeCodeBlock(CodeBlockAnalysisContext context, int positionOfFirstReducingNullableDirective)
{
using var simplifier = new NullableImpactingSpanWalker(context.SemanticModel, positionOfFirstReducingNullableDirective, ignoredSpans: null, context.CancellationToken);
simplifier.Visit(context.CodeBlock);
return simplifier.Spans;
}
private ImmutableArray<Diagnostic> AnalyzeSemanticModel(SemanticModelAnalysisContext context, int positionOfFirstReducingNullableDirective, TextSpanMutableIntervalTree? codeBlockIntervalTree, TextSpanMutableIntervalTree? possibleNullableImpactIntervalTree)
{
var root = context.SemanticModel.SyntaxTree.GetCompilationUnitRoot(context.CancellationToken);
using (var simplifier = new NullableImpactingSpanWalker(context.SemanticModel, positionOfFirstReducingNullableDirective, ignoredSpans: codeBlockIntervalTree, context.CancellationToken))
{
simplifier.Visit(root);
possibleNullableImpactIntervalTree ??= new TextSpanMutableIntervalTree();
foreach (var interval in simplifier.Spans)
{
possibleNullableImpactIntervalTree.AddIntervalInPlace(interval);
}
}
using var diagnostics = TemporaryArray<Diagnostic>.Empty;
var compilationOptions = ((CSharpCompilationOptions)context.SemanticModel.Compilation.Options).NullableContextOptions;
NullableDirectiveTriviaSyntax? previousRetainedDirective = null;
NullableContextOptions? retainedOptions = compilationOptions;
NullableDirectiveTriviaSyntax? currentOptionsDirective = null;
var currentOptions = retainedOptions;
for (var directive = root.GetFirstDirective(); directive is not null; directive = directive.GetNextDirective())
{
context.CancellationToken.ThrowIfCancellationRequested();
if (directive is NullableDirectiveTriviaSyntax nullableDirectiveTrivia)
{
// Once we reach a new directive, check to see if we can remove the previous directive
var removedCurrent = false;
if (IsReducing(retainedOptions, currentOptions))
{
// We can't have found a reducing directive and not know which directive it was
Contract.ThrowIfNull(currentOptionsDirective);
if (possibleNullableImpactIntervalTree is null
|| !possibleNullableImpactIntervalTree.HasIntervalThatOverlapsWith(currentOptionsDirective.Span.End, nullableDirectiveTrivia.SpanStart - currentOptionsDirective.Span.End))
{
diagnostics.Add(Diagnostic.Create(Descriptor, currentOptionsDirective.GetLocation()));
}
}
if (!removedCurrent)
{
previousRetainedDirective = currentOptionsDirective;
retainedOptions = currentOptions;
}
currentOptionsDirective = nullableDirectiveTrivia;
currentOptions = CSharpRemoveRedundantNullableDirectiveDiagnosticAnalyzer.GetNullableContextOptions(compilationOptions, currentOptions, nullableDirectiveTrivia);
}
else if (directive.Kind() is
SyntaxKind.IfDirectiveTrivia or
SyntaxKind.ElifDirectiveTrivia or
SyntaxKind.ElseDirectiveTrivia)
{
possibleNullableImpactIntervalTree ??= new TextSpanMutableIntervalTree();
possibleNullableImpactIntervalTree.AddIntervalInPlace(directive.Span);
}
}
// Once we reach the end of the file, check to see if we can remove the last directive
if (IsReducing(retainedOptions, currentOptions))
{
// We can't have found a reducing directive and not know which directive it was
Contract.ThrowIfNull(currentOptionsDirective);
if (possibleNullableImpactIntervalTree is null
|| !possibleNullableImpactIntervalTree.HasIntervalThatOverlapsWith(currentOptionsDirective.Span.End, root.Span.End - currentOptionsDirective.Span.End))
{
diagnostics.Add(Diagnostic.Create(Descriptor, currentOptionsDirective.GetLocation()));
}
}
return diagnostics.ToImmutableAndClear();
}
private sealed class SyntaxTreeState
{
private SyntaxTreeState(bool completed, int? positionOfFirstReducingNullableDirective)
{
Completed = completed;
PositionOfFirstReducingNullableDirective = positionOfFirstReducingNullableDirective;
if (!completed)
{
IntervalTree = new TextSpanMutableIntervalTree();
PossibleNullableImpactIntervalTree = new TextSpanMutableIntervalTree();
}
}
[MemberNotNullWhen(false, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))]
public bool Completed { get; private set; }
public int? PositionOfFirstReducingNullableDirective { get; }
public TextSpanMutableIntervalTree? IntervalTree { get; }
public TextSpanMutableIntervalTree? PossibleNullableImpactIntervalTree { get; }
public static SyntaxTreeState Create(bool defaultCompleted, NullableContextOptions compilationOptions, SyntaxTree tree, CancellationToken cancellationToken)
{
var root = tree.GetCompilationUnitRoot(cancellationToken);
// This analyzer only needs to process syntax trees that contain at least one #nullable directive that
// reduces the nullable analysis scope.
int? positionOfFirstReducingNullableDirective = null;
NullableContextOptions? currentOptions = compilationOptions;
for (var directive = root.GetFirstDirective(); directive is not null; directive = directive.GetNextDirective())
{
cancellationToken.ThrowIfCancellationRequested();
if (directive is NullableDirectiveTriviaSyntax nullableDirectiveTrivia)
{
var newOptions = CSharpRemoveRedundantNullableDirectiveDiagnosticAnalyzer.GetNullableContextOptions(compilationOptions, currentOptions, nullableDirectiveTrivia);
if (IsReducing(currentOptions, newOptions))
{
positionOfFirstReducingNullableDirective = directive.SpanStart;
break;
}
currentOptions = newOptions;
}
}
return new SyntaxTreeState(completed: defaultCompleted || positionOfFirstReducingNullableDirective is null, positionOfFirstReducingNullableDirective);
}
[MemberNotNullWhen(true, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))]
public bool TryProceedWithInterval(TextSpan span)
=> TryProceedOrReportNullableImpactingSpans(span, nullableImpactingSpans: null);
[MemberNotNullWhen(true, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))]
public bool TryReportNullableImpactingSpans(TextSpan span, ImmutableArray<TextSpan> nullableImpactingSpans)
=> TryProceedOrReportNullableImpactingSpans(span, nullableImpactingSpans);
[MemberNotNullWhen(true, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))]
private bool TryProceedOrReportNullableImpactingSpans(TextSpan span, ImmutableArray<TextSpan>? nullableImpactingSpans)
{
if (Completed)
return false;
lock (this)
{
if (Completed)
return false;
if (IntervalTree.HasIntervalThatOverlapsWith(span.Start, span.End))
return false;
if (nullableImpactingSpans is { } spans)
{
foreach (var nullableImpactingSpan in spans)
PossibleNullableImpactIntervalTree.AddIntervalInPlace(nullableImpactingSpan);
}
return true;
}
}
internal void MarkComplete()
{
if (Completed)
return;
lock (this)
{
Completed = true;
}
}
}
private sealed class AnalyzerImpl(CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer analyzer)
{
private readonly CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer _analyzer = analyzer;
/// <summary>
/// Tracks the analysis state of syntax trees in a compilation.
/// </summary>
private readonly ConcurrentDictionary<SyntaxTree, SyntaxTreeState> _codeBlockIntervals = [];
public void AnalyzeCodeBlock(CodeBlockAnalysisContext context)
{
if (_analyzer.ShouldSkipAnalysis(context, notification: null)
|| IsIgnoredCodeBlock(context.CodeBlock))
{
return;
}
var root = context.GetAnalysisRoot(findInTrivia: true);
// Bail out if the root contains no nullable directives.
if (!root.ContainsDirective(SyntaxKind.NullableDirectiveTrivia))
return;
var syntaxTreeState = GetOrCreateSyntaxTreeState(context.CodeBlock.SyntaxTree, defaultCompleted: false, context.SemanticModel, context.CancellationToken);
if (!syntaxTreeState.TryProceedWithInterval(context.CodeBlock.FullSpan))
return;
var nullableImpactingSpans = CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer.AnalyzeCodeBlock(context, syntaxTreeState.PositionOfFirstReducingNullableDirective.Value);
// After this point, cancellation is not allowed due to possible state alteration
syntaxTreeState.TryReportNullableImpactingSpans(context.CodeBlock.FullSpan, nullableImpactingSpans);
}
public void AnalyzeSemanticModel(SemanticModelAnalysisContext context)
{
if (_analyzer.ShouldSkipAnalysis(context, notification: null))
return;
var root = context.GetAnalysisRoot(findInTrivia: true);
// Bail out if the root contains no nullable directives.
if (!root.ContainsDirective(SyntaxKind.NullableDirectiveTrivia))
return;
// Get the state information for the syntax tree. If the state information is not available, it is
// initialized directly to a completed state, ensuring that concurrent (or future) calls to
// AnalyzeCodeBlock will always read completed==true, and intervalTree does not need to be initialized
// to a non-null value.
var syntaxTreeState = GetOrCreateSyntaxTreeState(context.SemanticModel.SyntaxTree, defaultCompleted: true, context.SemanticModel, context.CancellationToken);
syntaxTreeState.MarkComplete();
if (syntaxTreeState.PositionOfFirstReducingNullableDirective is not { } positionOfFirstReducingNullableDirective)
return;
var diagnostics = _analyzer.AnalyzeSemanticModel(context, positionOfFirstReducingNullableDirective, syntaxTreeState.IntervalTree, syntaxTreeState.PossibleNullableImpactIntervalTree);
// After this point, cancellation is not allowed due to possible state alteration
foreach (var diagnostic in diagnostics)
{
context.ReportDiagnostic(diagnostic);
}
}
private SyntaxTreeState GetOrCreateSyntaxTreeState(SyntaxTree tree, bool defaultCompleted, SemanticModel semanticModel, CancellationToken cancellationToken)
{
return _codeBlockIntervals.GetOrAdd(
tree,
static (tree, arg) => SyntaxTreeState.Create(arg.defaultCompleted, arg.options, tree, arg.cancellationToken),
(defaultCompleted, options: ((CSharpCompilationOptions)semanticModel.Compilation.Options).NullableContextOptions, cancellationToken));
}
}
}
|