// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Immutable; using Analyzer.Utilities.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using MSTest.Analyzers.Helpers; namespace MSTest.Analyzers; /// /// MSTEST0073: . /// [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] public sealed class PreferConstantForResourceLockAnalyzer : DiagnosticAnalyzer { private static readonly LocalizableResourceString Title = new(nameof(Resources.PreferConstantForResourceLockTitle), Resources.ResourceManager, typeof(Resources)); private static readonly LocalizableResourceString MessageFormat = new(nameof(Resources.PreferConstantForResourceLockMessageFormat), Resources.ResourceManager, typeof(Resources)); private static readonly LocalizableResourceString Description = new(nameof(Resources.PreferConstantForResourceLockDescription), Resources.ResourceManager, typeof(Resources)); internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( DiagnosticIds.PreferConstantForResourceLockRuleId, Title, MessageFormat, Description, Category.Usage, DiagnosticSeverity.Info, isEnabledByDefault: true); /// public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); /// public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(context => { if (!context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingResourceLockAttribute, out INamedTypeSymbol? resourceLockAttributeSymbol)) { return; } context.RegisterSymbolAction( context => AnalyzeSymbol(context, resourceLockAttributeSymbol), SymbolKind.Method, SymbolKind.NamedType); }); } private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbol resourceLockAttributeSymbol) { foreach (AttributeData attribute in context.Symbol.GetAttributes()) { if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, resourceLockAttributeSymbol)) { continue; } AnalyzeAttribute(context, attribute); } } private static void AnalyzeAttribute(SymbolAnalysisContext context, AttributeData attribute) { // The only 'string'-typed argument of '[ResourceLock]' is the 'resource' key (the 'Mode' // named argument is an enum). So the presence of a string-literal token anywhere in the // attribute application means the resource key was written as a bare literal (or a literal // concatenation) rather than as a reference to a shared constant. if (attribute.ConstructorArguments.Length == 0 || attribute.ConstructorArguments[0].Value is not string resourceKey) { return; } if (attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken) is not { } attributeSyntax) { return; } // A string-literal token always contains a double-quote character, while an identifier or // member-access (i.e. a 'const' reference) never does. This distinction is language-neutral // and avoids creating a semantic model (RS1030) or depending on C#/VB-specific syntax nodes. foreach (SyntaxToken token in attributeSyntax.DescendantTokens()) { if (token.Text.IndexOf('"') >= 0) { context.ReportDiagnostic(Diagnostic.Create(Rule, token.GetLocation(), resourceKey)); return; } } } }