File: SimplifyTypeNamesDiagnosticAnalyzerBase.cs
Web Access
Project: ..\..\..\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.
 
// #define LOG
 
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
#if LOG
using System.IO;
using System.Text.RegularExpressions;
#endif
 
namespace Microsoft.CodeAnalysis.SimplifyTypeNames
{
    internal abstract class SimplifyTypeNamesDiagnosticAnalyzerBase<TLanguageKindEnum, TSimplifierOptions>
        : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer
        where TLanguageKindEnum : struct
        where TSimplifierOptions : SimplifierOptions
    {
#if LOG
        private static string _logFile = @"c:\temp\simplifytypenames.txt";
        private static object _logGate = new object();
        private static readonly Regex s_newlinePattern = new Regex(@"[\r\n]+");
#endif
 
        private static readonly LocalizableString s_localizableMessage = new LocalizableResourceString(nameof(AnalyzersResources.Name_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources));
 
        private static readonly LocalizableString s_localizableTitleSimplifyNames = new LocalizableResourceString(nameof(AnalyzersResources.Simplify_Names), AnalyzersResources.ResourceManager, typeof(AnalyzersResources));
        private static readonly DiagnosticDescriptor s_descriptorSimplifyNames = CreateDescriptorWithId(IDEDiagnosticIds.SimplifyNamesDiagnosticId,
                                                                    EnforceOnBuildValues.SimplifyNames,
                                                                    s_localizableTitleSimplifyNames,
                                                                    s_localizableMessage,
                                                                    isUnnecessary: true);
 
        private static readonly LocalizableString s_localizableTitleSimplifyMemberAccess = new LocalizableResourceString(nameof(AnalyzersResources.Simplify_Member_Access), AnalyzersResources.ResourceManager, typeof(AnalyzersResources));
        private static readonly DiagnosticDescriptor s_descriptorSimplifyMemberAccess = CreateDescriptorWithId(IDEDiagnosticIds.SimplifyMemberAccessDiagnosticId,
                                                                    EnforceOnBuildValues.SimplifyMemberAccess,
                                                                    s_localizableTitleSimplifyMemberAccess,
                                                                    s_localizableMessage,
                                                                    isUnnecessary: true);
 
        private static readonly DiagnosticDescriptor s_descriptorPreferBuiltinOrFrameworkType = CreateDescriptorWithId(IDEDiagnosticIds.PreferBuiltInOrFrameworkTypeDiagnosticId,
            EnforceOnBuildValues.PreferBuiltInOrFrameworkType,
            s_localizableTitleSimplifyNames,
            s_localizableMessage,
            isUnnecessary: true);
 
        protected SimplifyTypeNamesDiagnosticAnalyzerBase()
            : base(ImmutableDictionary<DiagnosticDescriptor, ImmutableHashSet<IOption2>>.Empty
                  .Add(s_descriptorSimplifyNames, ImmutableHashSet<IOption2>.Empty)
                  .Add(s_descriptorSimplifyMemberAccess, ImmutableHashSet<IOption2>.Empty)
                  .Add(s_descriptorPreferBuiltinOrFrameworkType, ImmutableHashSet.Create<IOption2>(CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInDeclaration, CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInMemberAccess)),
                  fadingOption: null)
        {
        }
 
        internal abstract bool IsCandidate(SyntaxNode node);
        internal abstract bool CanSimplifyTypeNameExpression(
            SemanticModel model, SyntaxNode node, TSimplifierOptions options,
            out TextSpan issueSpan, out string diagnosticId, out bool inDeclaration,
            CancellationToken cancellationToken);
 
        public override bool OpenFileOnly(SimplifierOptions? options)
        {
            // analyzer is only active in C# and VB projects
            Contract.ThrowIfNull(options);
 
            return
                !(options.PreferPredefinedTypeKeywordInDeclaration.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error ||
                  options.PreferPredefinedTypeKeywordInMemberAccess.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error);
        }
 
        protected sealed 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>
        protected abstract bool IsIgnoredCodeBlock(SyntaxNode codeBlock);
        protected abstract ImmutableArray<Diagnostic> AnalyzeCodeBlock(CodeBlockAnalysisContext context);
        protected abstract ImmutableArray<Diagnostic> AnalyzeSemanticModel(SemanticModelAnalysisContext context, SimpleIntervalTree<TextSpan, TextSpanIntervalIntrospector>? codeBlockIntervalTree);
 
        public bool TrySimplify(SemanticModel model, SyntaxNode node, [NotNullWhen(true)] out Diagnostic? diagnostic, TSimplifierOptions options, CancellationToken cancellationToken)
        {
            if (!CanSimplifyTypeNameExpression(
                    model, node, options,
                    out var issueSpan, out var diagnosticId, out var inDeclaration,
                    cancellationToken))
            {
                diagnostic = null;
                return false;
            }
 
            if (model.SyntaxTree.OverlapsHiddenPosition(issueSpan, cancellationToken))
            {
                diagnostic = null;
                return false;
            }
 
            diagnostic = CreateDiagnostic(model, options, issueSpan, diagnosticId, inDeclaration);
            return true;
        }
 
        internal static Diagnostic CreateDiagnostic(SemanticModel model, TSimplifierOptions options, TextSpan issueSpan, string diagnosticId, bool inDeclaration)
        {
            DiagnosticDescriptor descriptor;
            ReportDiagnostic severity;
            switch (diagnosticId)
            {
                case IDEDiagnosticIds.SimplifyNamesDiagnosticId:
                    descriptor = s_descriptorSimplifyNames;
                    severity = descriptor.DefaultSeverity.ToReportDiagnostic();
                    break;
 
                case IDEDiagnosticIds.SimplifyMemberAccessDiagnosticId:
                    descriptor = s_descriptorSimplifyMemberAccess;
                    severity = descriptor.DefaultSeverity.ToReportDiagnostic();
                    break;
 
                case IDEDiagnosticIds.PreferBuiltInOrFrameworkTypeDiagnosticId:
                    var optionValue = inDeclaration
                        ? options.PreferPredefinedTypeKeywordInDeclaration
                        : options.PreferPredefinedTypeKeywordInMemberAccess;
 
                    descriptor = s_descriptorPreferBuiltinOrFrameworkType;
                    severity = optionValue.Notification.Severity;
                    break;
                default:
                    throw ExceptionUtilities.UnexpectedValue(diagnosticId);
            }
 
            var tree = model.SyntaxTree;
            var builder = ImmutableDictionary.CreateBuilder<string, string?>();
            builder["OptionName"] = nameof(CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInMemberAccess); // TODO: need the actual one
            builder["OptionLanguage"] = model.Language;
            var diagnostic = DiagnosticHelper.Create(descriptor, tree.GetLocation(issueSpan), severity, additionalLocations: null, builder.ToImmutable());
 
#if LOG
            var sourceText = tree.GetText();
            sourceText.GetLineAndOffset(issueSpan.Start, out var startLineNumber, out var startOffset);
            sourceText.GetLineAndOffset(issueSpan.End, out var endLineNumber, out var endOffset);
            var logLine = tree.FilePath + "," + startLineNumber + "\t" + diagnosticId + "\t" + inDeclaration + "\t";
 
            var leading = sourceText.ToString(TextSpan.FromBounds(
                sourceText.Lines[startLineNumber].Start, issueSpan.Start));
            var mid = sourceText.ToString(issueSpan);
            var trailing = sourceText.ToString(TextSpan.FromBounds(
                issueSpan.End, sourceText.Lines[endLineNumber].End));
 
            var contents = leading + "[|" + s_newlinePattern.Replace(mid, " ") + "|]" + trailing;
            logLine += contents + "\r\n";
 
            lock (_logGate)
            {
                File.AppendAllText(_logFile, logLine);
            }
#endif
 
            return diagnostic;
        }
 
        public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
            => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
 
        private class AnalyzerImpl
        {
            private readonly SimplifyTypeNamesDiagnosticAnalyzerBase<TLanguageKindEnum, TSimplifierOptions> _analyzer;
 
            /// <summary>
            /// Tracks the analysis state of syntax trees in a compilation. Each syntax tree has the properties:
            /// <list type="bullet">
            /// <item><description>
            /// <para><c>completed</c>: <see langword="true"/> to indicate that <c>intervalTree</c> has been obtained
            /// for use in a <see cref="SemanticModelAnalysisContext"/> callback; otherwise, <see langword="false"/> to
            /// indicate that <c>intervalTree</c> may be updated by adding a new non-overlapping <see cref="TextSpan"/>
            /// for analysis performed by a <see cref="CodeBlockAnalysisContext"/> callback.</para>
            ///
            /// <para>This field also serves as the lock object for updating both <c>completed</c> and
            /// <c>intervalTree</c>.</para>
            /// </description></item>
            /// <item><description>
            /// <para><c>intervalTree</c>: the set of intervals analyzed by <see cref="CodeBlockAnalysisContext"/>
            /// callbacks, and therefore do not need to be analyzed again by a
            /// <see cref="SemanticModelAnalysisContext"/> callback.</para>
            ///
            /// <para>This field may only be accessed while <c>completed</c> is locked, and is not valid after
            /// <c>completed</c> is <see langword="true"/>.</para>
            /// </description></item>
            /// </list>
            /// </summary>
            private readonly ConcurrentDictionary<SyntaxTree, (StrongBox<bool> completed, SimpleIntervalTree<TextSpan, TextSpanIntervalIntrospector>? intervalTree)> _codeBlockIntervals
                = new();
 
            public AnalyzerImpl(SimplifyTypeNamesDiagnosticAnalyzerBase<TLanguageKindEnum, TSimplifierOptions> analyzer)
                => _analyzer = analyzer;
 
            public void AnalyzeCodeBlock(CodeBlockAnalysisContext context)
            {
                if (_analyzer.IsIgnoredCodeBlock(context.CodeBlock))
                    return;
 
                var (completed, intervalTree) = _codeBlockIntervals.GetOrAdd(context.CodeBlock.SyntaxTree, _ => (new StrongBox<bool>(false), SimpleIntervalTree.Create(new TextSpanIntervalIntrospector(), Array.Empty<TextSpan>())));
                if (completed.Value)
                    return;
 
                RoslynDebug.AssertNotNull(intervalTree);
                if (!TryProceedWithInterval(addIfAvailable: false, context.CodeBlock.FullSpan, completed, intervalTree))
                    return;
 
                var diagnostics = _analyzer.AnalyzeCodeBlock(context);
 
                // After this point, cancellation is not allowed due to possible state alteration
                if (!TryProceedWithInterval(addIfAvailable: true, context.CodeBlock.FullSpan, completed, intervalTree))
                    return;
 
                foreach (var diagnostic in diagnostics)
                {
                    context.ReportDiagnostic(diagnostic);
                }
 
                static bool TryProceedWithInterval(bool addIfAvailable, TextSpan span, StrongBox<bool> completed, SimpleIntervalTree<TextSpan, TextSpanIntervalIntrospector> intervalTree)
                {
                    lock (completed)
                    {
                        if (completed.Value)
                            return false;
 
                        if (intervalTree.HasIntervalThatOverlapsWith(span.Start, span.End))
                            return false;
 
                        if (addIfAvailable)
                            intervalTree.AddIntervalInPlace(span);
 
                        return true;
                    }
                }
            }
 
            public void AnalyzeSemanticModel(SemanticModelAnalysisContext context)
            {
                // 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 (completed, intervalTree) = _codeBlockIntervals.GetOrAdd(context.SemanticModel.SyntaxTree, syntaxTree => (new StrongBox<bool>(true), null));
 
                // Since SemanticModel callbacks only occur once per syntax tree, the completed state can be safely read
                // here. It will have one of the values:
                //
                //   false: the state was initialized in AnalyzeCodeBlock, and intervalTree will be a non-null tree.
                //   true: the state was initialized on the previous line, and either intervalTree will be null, or
                //         a previous call to AnalyzeSemanticModel was cancelled and the new one will operate on the
                //         same interval tree presented during the previous call.
                if (!completed.Value)
                {
                    // This lock ensures we do not use intervalTree while it is being updated by a concurrent call to
                    // AnalyzeCodeBlock.
                    lock (completed)
                    {
                        // Prevent future code block callbacks from analyzing more spans within this tree
                        completed.Value = true;
                    }
                }
 
                var diagnostics = _analyzer.AnalyzeSemanticModel(context, intervalTree);
 
                // After this point, cancellation is not allowed due to possible state alteration
                foreach (var diagnostic in diagnostics)
                {
                    context.ReportDiagnostic(diagnostic);
                }
            }
        }
    }
}