// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Analyzer.Utilities; using System; using System.Diagnostics; using Analyzer.Utilities.Extensions; namespace Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines { using static MicrosoftCodeQualityAnalyzersResources; /// <summary> /// CA1030: <inheritdoc cref="UseEventsWhereAppropriateTitle"/> /// <para> /// This rule detects methods that have names that ordinarily would be used for events. /// Events follow the Observer or Publish-Subscribe design pattern; they are used when a state change in one object must be communicated to other objects. /// If a method gets called in response to a clearly defined state change, the method should be invoked by an event handler. /// Objects that call the method should raise events instead of calling the method directly. /// </para> /// </summary> [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] public sealed class UseEventsWhereAppropriateAnalyzer : DiagnosticAnalyzer { internal const string RuleId = "CA1030"; internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( RuleId, CreateLocalizableResourceString(nameof(UseEventsWhereAppropriateTitle)), CreateLocalizableResourceString(nameof(UseEventsWhereAppropriateMessage)), DiagnosticCategory.Design, RuleLevel.CandidateForRemoval, description: CreateLocalizableResourceString(nameof(UseEventsWhereAppropriateDescription)), isPortedFxCopRule: true, isDataflowRule: false); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.RegisterSymbolAction(symbolContext => { var method = (IMethodSymbol)symbolContext.Symbol; if (!IsEventLikeNameCandidate(method.Name)) { Debug.Assert(!HasEventLikeName(method), "fast check failed but eventual check succeeds?"); return; } // FxCop compat: bail out for implicitly declared methods, overridden methods, interface implementations, // constructors and finalizers and non-externally visible methods by default. if (method.IsImplicitlyDeclared || method.IsOverride || method.IsImplementationOfAnyInterfaceMember() || method.IsConstructor() || method.IsFinalizer() || !symbolContext.Options.MatchesConfiguredVisibility(Rule, method, symbolContext.Compilation)) { return; } if (HasEventLikeName(method)) { // Consider making '{0}' an event. var diagnostic = method.CreateDiagnostic(Rule, method.Name); symbolContext.ReportDiagnostic(diagnostic); } }, SymbolKind.Method); } private static bool IsEventLikeNameCandidate(string name) { return name.StartsWith("fire", StringComparison.OrdinalIgnoreCase) || name.StartsWith("raise", StringComparison.OrdinalIgnoreCase) || name.StartsWith("add", StringComparison.OrdinalIgnoreCase) || name.StartsWith("remove", StringComparison.OrdinalIgnoreCase); } private static bool HasEventLikeName(IMethodSymbol method) { WordParser parser = new WordParser(method.Name, WordParserOptions.SplitCompoundWords); string? word = parser.NextWord(); // Check for 'FireXXX', 'RaiseXXX' if (string.Equals(word, "fire", StringComparison.OrdinalIgnoreCase) || string.Equals(word, "raise", StringComparison.OrdinalIgnoreCase)) { return true; } // Check for 'AddOnXXX', 'RemoveOnXXX' if (string.Equals(word, "add", StringComparison.OrdinalIgnoreCase) || string.Equals(word, "remove", StringComparison.OrdinalIgnoreCase)) { return string.Equals(parser.NextWord(), "on", StringComparison.OrdinalIgnoreCase); } return false; } } }