File: Microsoft.CodeQuality.Analyzers\ApiDesignGuidelines\UsePropertiesWhereAppropriate.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Immutable;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines
{
    using static MicrosoftCodeQualityAnalyzersResources;

    /// <summary>
    /// CA1024: <inheritdoc cref="UsePropertiesWhereAppropriateTitle"/>
    ///
    /// Cause:
    /// A public or protected method has a name that starts with Get, takes no parameters, and returns a value that is not an array.
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class UsePropertiesWhereAppropriateAnalyzer : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA1024";

        internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
            RuleId,
            CreateLocalizableResourceString(nameof(UsePropertiesWhereAppropriateTitle)),
            CreateLocalizableResourceString(nameof(UsePropertiesWhereAppropriateMessage)),
            DiagnosticCategory.Design,
            RuleLevel.Disabled,    // Heuristic based rule.
            description: CreateLocalizableResourceString(nameof(UsePropertiesWhereAppropriateDescription)),
            isPortedFxCopRule: true,
            isDataflowRule: false);

        private const string GetHashCodeName = "GetHashCode";
        private const string GetEnumeratorName = "GetEnumerator";
        private const string GetPinnableReferenceName = "GetPinnableReference";

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

        public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

            context.RegisterCompilationStartAction(context =>
            {
                var taskTypesBuilder = ImmutableHashSet.CreateBuilder<ITypeSymbol>();

                taskTypesBuilder.AddIfNotNull(
                    context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask));
                taskTypesBuilder.AddIfNotNull(
                    context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask1));
                taskTypesBuilder.AddIfNotNull(
                    context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask));
                taskTypesBuilder.AddIfNotNull(
                    context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask1));

                var taskTypes = taskTypesBuilder.ToImmutable();

                var inotifyCompletionType = context.Compilation.GetOrCreateTypeByMetadataName(
                    WellKnownTypeNames.SystemRuntimeCompilerServicesINotifyCompletion);
                var icriticalNotifyCompletionType = context.Compilation.GetOrCreateTypeByMetadataName(
                    WellKnownTypeNames.SystemRuntimeCompilerServicesICriticalNotifyCompletion);

                context.RegisterOperationBlockStartAction(context =>
                {
                    if (context.OwningSymbol is not IMethodSymbol methodSymbol ||
                        methodSymbol.ReturnsVoid ||
                        methodSymbol.ReturnType.Kind == SymbolKind.ArrayType ||
                        !methodSymbol.Parameters.IsEmpty ||
                        !context.Options.MatchesConfiguredVisibility(Rule, methodSymbol, context.Compilation) ||
                        methodSymbol.IsAccessorMethod() ||
                        !IsPropertyLikeName(methodSymbol.Name))
                    {
                        return;
                    }

                    // A few additional checks to reduce the noise for this diagnostic:
                    // Ensure that the method is non-generic, non-virtual/override, has no overloads and doesn't have special names: 'GetHashCode' or 'GetEnumerator'.
                    // Also avoid generating this diagnostic if the method body has any invocation expressions.
                    // Also avoid implicit interface implementation (explicit are handled through the member accessibility)
                    // Also avoid GetAwaiter and GetResult from awaitable-awaiter patterns.
                    if (methodSymbol.IsGenericMethod ||
                        methodSymbol.IsVirtual ||
                        methodSymbol.IsOverride ||
                        methodSymbol.IsExtern ||
                        methodSymbol.Name is GetHashCodeName or GetEnumeratorName ||
                        (methodSymbol.Name == GetPinnableReferenceName && (methodSymbol.ReturnsByRef || methodSymbol.ReturnsByRefReadonly)) ||
                        methodSymbol.ContainingType.GetMembers(methodSymbol.Name).Length > 1 ||
                        taskTypes.Contains(methodSymbol.ReturnType.OriginalDefinition) ||
                        methodSymbol.IsImplementationOfAnyImplicitInterfaceMember() ||
                        methodSymbol.IsGetAwaiterFromAwaitablePattern(inotifyCompletionType, icriticalNotifyCompletionType) ||
                        methodSymbol.IsGetResultFromAwaiterPattern(inotifyCompletionType, icriticalNotifyCompletionType))
                    {
                        return;
                    }

                    bool hasInvocations = false;
                    context.RegisterOperationAction(operationContext =>
                    {
                        hasInvocations = true;
                    }, OperationKind.Invocation);

                    context.RegisterOperationBlockEndAction(endContext =>
                    {
                        if (!hasInvocations)
                        {
                            endContext.ReportDiagnostic(endContext.OwningSymbol.CreateDiagnostic(Rule));
                        }
                    });
                });
            });
        }

        private static bool IsPropertyLikeName(string methodName)
        {
            return methodName.Length > 3 &&
                methodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase) &&
                !char.IsLower(methodName[3]);
        }
    }
}