// 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.Generic;
using System.Collections.Immutable;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines
{
using static MicrosoftCodeQualityAnalyzersResources;
/// <summary>
/// CA1716: <inheritdoc cref="IdentifiersShouldNotMatchKeywordsTitle"/>
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class IdentifiersShouldNotMatchKeywordsAnalyzer : DiagnosticAnalyzer
{
internal const string RuleId = "CA1716";
private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(IdentifiersShouldNotMatchKeywordsTitle));
private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(IdentifiersShouldNotMatchKeywordsDescription));
internal static readonly DiagnosticDescriptor MemberParameterRule = DiagnosticDescriptorHelper.Create(
RuleId,
s_localizableTitle,
CreateLocalizableResourceString(nameof(IdentifiersShouldNotMatchKeywordsMessageMemberParameter)),
DiagnosticCategory.Naming,
RuleLevel.IdeHidden_BulkConfigurable,
description: s_localizableDescription,
isPortedFxCopRule: true,
isDataflowRule: false);
internal static readonly DiagnosticDescriptor MemberRule = DiagnosticDescriptorHelper.Create(
RuleId,
s_localizableTitle,
CreateLocalizableResourceString(nameof(IdentifiersShouldNotMatchKeywordsMessageMember)),
DiagnosticCategory.Naming,
RuleLevel.IdeHidden_BulkConfigurable,
description: s_localizableDescription,
isPortedFxCopRule: true,
isDataflowRule: false);
internal static readonly DiagnosticDescriptor TypeRule = DiagnosticDescriptorHelper.Create(
RuleId,
s_localizableTitle,
CreateLocalizableResourceString(nameof(IdentifiersShouldNotMatchKeywordsMessageType)),
DiagnosticCategory.Naming,
RuleLevel.IdeHidden_BulkConfigurable,
description: s_localizableDescription,
isPortedFxCopRule: true,
isDataflowRule: false);
internal static readonly DiagnosticDescriptor NamespaceRule = DiagnosticDescriptorHelper.Create(
RuleId,
s_localizableTitle,
CreateLocalizableResourceString(nameof(IdentifiersShouldNotMatchKeywordsMessageNamespace)),
DiagnosticCategory.Naming,
RuleLevel.IdeHidden_BulkConfigurable,
description: s_localizableDescription,
isPortedFxCopRule: true,
isDataflowRule: false);
// Define the format in which this rule displays namespace names. The format is chosen to be
// consistent with FxCop's display format for this rule.
private static readonly SymbolDisplayFormat s_namespaceDisplayFormat =
SymbolDisplayFormat.CSharpErrorMessageFormat
// Turn off the EscapeKeywordIdentifiers flag (which is on by default), so that
// a method named "@for" is displayed as "for"
.WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.None);
private static readonly ImmutableHashSet<SymbolKind> s_defaultAnalyzedSymbolKinds =
ImmutableHashSet.Create(
SymbolKind.Namespace,
SymbolKind.NamedType,
SymbolKind.Method,
SymbolKind.Property,
SymbolKind.Event,
SymbolKind.Parameter
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(MemberParameterRule, MemberRule, TypeRule, NamespaceRule);
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(compilationStartAnalysisContext =>
{
var namespaceRuleAnalyzer = new NamespaceRuleAnalyzer();
compilationStartAnalysisContext.RegisterSymbolAction(
symbolAnalysisContext => namespaceRuleAnalyzer.Analyze(symbolAnalysisContext),
SymbolKind.NamedType);
compilationStartAnalysisContext.RegisterSymbolAction(AnalyzeTypeRule, SymbolKind.NamedType);
compilationStartAnalysisContext.RegisterSymbolAction(AnalyzeMemberRule, SymbolKind.Event, SymbolKind.Method, SymbolKind.Property);
compilationStartAnalysisContext.RegisterSymbolAction(AnalyzeMemberParameterRule, SymbolKind.Method);
});
}
private static bool ShouldAnalyze(SymbolAnalysisContext context, DiagnosticDescriptor rule)
{
if (!context.Options.MatchesConfiguredVisibility(rule, context.Symbol, context.Compilation))
{
return false;
}
return GetSymbolKindsToAnalyze(context, rule).Contains(context.Symbol.Kind);
}
private static ImmutableHashSet<SymbolKind> GetSymbolKindsToAnalyze(SymbolAnalysisContext context, DiagnosticDescriptor rule)
=> context.Options.GetAnalyzedSymbolKindsOption(rule, context.Symbol, context.Compilation, s_defaultAnalyzedSymbolKinds);
private sealed class NamespaceRuleAnalyzer
{
private readonly ISet<string> _namespaceWithKeywordSet = new HashSet<string>();
private readonly object _lockGuard = new();
public void Analyze(SymbolAnalysisContext context)
{
INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol;
if (!GetSymbolKindsToAnalyze(context, NamespaceRule).Contains(SymbolKind.Namespace))
{
return;
}
// Don't complain about a namespace unless it contains at least one public type.
if (!context.Options.MatchesConfiguredVisibility(NamespaceRule, type, context.Compilation))
{
return;
}
INamespaceSymbol containingNamespace = type.ContainingNamespace;
if (containingNamespace.IsGlobalNamespace)
{
return;
}
string namespaceDisplayString = containingNamespace.ToDisplayString(s_namespaceDisplayFormat);
IEnumerable<string> namespaceNameComponents = containingNamespace.ToDisplayParts(s_namespaceDisplayFormat)
.Where(dp => dp.Kind == SymbolDisplayPartKind.NamespaceName)
.Select(dp => dp.ToString());
foreach (string component in namespaceNameComponents)
{
if (IsKeyword(component, out string matchingKeyword))
{
bool doReportDiagnostic;
lock (_lockGuard)
{
string namespaceWithKeyword = namespaceDisplayString + "*" + matchingKeyword;
doReportDiagnostic = _namespaceWithKeywordSet.Add(namespaceWithKeyword);
}
if (doReportDiagnostic)
{
var diagnostic = containingNamespace.CreateDiagnostic(NamespaceRule, namespaceDisplayString, matchingKeyword);
context.ReportDiagnostic(diagnostic);
}
}
}
}
}
private static void AnalyzeTypeRule(SymbolAnalysisContext context)
{
INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol;
if (!ShouldAnalyze(context, TypeRule))
{
return;
}
if (IsKeyword(type.Name, out string matchingKeyword))
{
context.ReportDiagnostic(
type.CreateDiagnostic(
TypeRule,
type.FormatMemberName(),
matchingKeyword));
}
}
private static void AnalyzeMemberRule(SymbolAnalysisContext context)
{
ISymbol symbol = context.Symbol;
if (!ShouldAnalyze(context, MemberRule))
{
return;
}
if (!IsKeyword(symbol.Name, out string matchingKeyword))
{
return;
}
// IsAbstract returns true for both abstract class members and interface members.
if (symbol.IsVirtual || symbol.IsAbstract)
{
context.ReportDiagnostic(
symbol.CreateDiagnostic(
MemberRule,
symbol.FormatMemberName(),
matchingKeyword));
}
}
private static void AnalyzeMemberParameterRule(SymbolAnalysisContext context)
{
var method = (IMethodSymbol)context.Symbol;
if (!GetSymbolKindsToAnalyze(context, MemberParameterRule).Contains(SymbolKind.Parameter) ||
!context.Options.MatchesConfiguredVisibility(MemberParameterRule, method, context.Compilation))
{
return;
}
// IsAbstract returns true for both abstract class members and interface members.
if (!method.IsVirtual && !method.IsAbstract)
{
return;
}
foreach (IParameterSymbol parameter in method.Parameters)
{
if (IsKeyword(parameter.Name, out string matchingKeyword))
{
context.ReportDiagnostic(
parameter.CreateDiagnostic(
MemberParameterRule,
method.FormatMemberName(),
parameter.Name,
matchingKeyword));
}
}
}
private static bool IsKeyword(string name, out string keyword)
{
if (s_caseSensitiveKeywords.TryGetValue(name, out keyword))
{
return true;
}
return s_caseInsensitiveKeywords.TryGetKey(name, out keyword);
}
private static readonly ImmutableHashSet<string> s_caseSensitiveKeywords = new[]
{
// C#
"abstract",
"as",
"base",
"bool",
"break",
"byte",
"case",
"catch",
"char",
"checked",
"class",
"const",
"continue",
"decimal",
"default",
"delegate",
"do",
"double",
"else",
"enum",
"event",
"explicit",
"extern",
"false",
"finally",
"fixed",
"float",
"for",
"foreach",
"goto",
"if",
"implicit",
"in",
"int",
"interface",
"internal",
"is",
"lock",
"long",
"namespace",
"new",
"null",
"object",
"operator",
"out",
"override",
"params",
"private",
"protected",
"public",
"readonly",
"ref",
"return",
"sbyte",
"sealed",
"short",
"sizeof",
"static",
"string",
"struct",
"switch",
"this",
"throw",
"true",
"try",
"typeof",
"uint",
"ulong",
"unchecked",
"unsafe",
"ushort",
"using",
"virtual",
"void",
"volatile",
"while",
// Listed as a keywords in Microsoft.CodeAnalysis.CSharp.SyntaxKind, but
// omitted, at least for now, for compatibility with FxCop:
//"__arglist",
//"__makeref",
//"__reftype",
//"__refvalue",
//"stackalloc",
// C++
"__abstract",
"__alignof",
"__asm",
"__assume",
"__based",
"__box",
"__builtin_alignof",
"__cdecl",
"__clrcall",
"__compileBreak",
"__CURSOR__",
"__declspec",
"__delegate",
"__event",
"__except",
"__fastcall",
"__feacp_av",
"__feacpBreak",
"__finally",
"__forceinline",
"__gc",
"__has_assign",
"__has_copy",
"__has_finalizer",
"__has_nothrow_assign",
"__has_nothrow_copy",
"__has_trivial_assign",
"__has_trivial_constructor",
"__has_trivial_copy",
"__has_trivial_destructor",
"__has_user_destructor",
"__has_virtual_destructor",
"__hook",
"__identifier",
"__if_exists",
"__if_not_exists",
"__inline",
"__int128",
"__int16",
"__int32",
"__int64",
"__int8",
"__interface",
"__is_abstract",
"__is_base_of",
"__is_class",
"__is_convertible_to",
"__is_delegate",
"__is_empty",
"__is_enum",
"__is_interface_class",
"__is_pod",
"__is_polymorphic",
"__is_ref_array",
"__is_ref_class",
"__is_sealed",
"__is_simple_value_class",
"__is_union",
"__is_value_class",
"__leave",
"__multiple_inheritance",
"__newslot",
"__nogc",
"__nounwind",
"__nvtordisp",
"__offsetof",
"__pin",
"__pragma",
"__property",
"__ptr32",
"__ptr64",
"__raise",
"__restrict",
"__resume",
"__sealed",
"__single_inheritance",
"__stdcall",
"__super",
"__thiscall",
"__try",
"__try_cast",
"__typeof",
"__unaligned",
"__unhook",
"__uuidof",
"__value",
"__virtual_inheritance",
"__w64",
"__wchar_t",
"and",
"and_eq",
"asm",
"auto",
"bitand",
"bitor",
//"bool",
//"break",
//"case",
//"catch",
"cdecl",
//"char",
//"class",
"compl",
//"const",
"const_cast",
//"continue",
//"default",
"delete",
//"do",
//"double",
"dynamic_cast",
//"else",
//"enum",
//"explicit",
"export",
//"extern",
//"false,
//"float",
//"for",
"friend",
"gcnew",
"generic",
//"goto",
//"if",
"inline",
//"int",
//"long",
"mutable",
//"namespace",
//"new",
"not",
"not_eq",
"nullptr",
//"operator",
"or",
"or_eq",
//"private",
//"protected",
//"public",
"register",
"reinterpret_cast",
//"return",
//"short",
"signed",
//"sizeof",
//"static",
"static_cast",
//"struct",
//"switch",
"template",
//"this",
//"throw",
//"true",
//"try",
"typedef",
"typeid",
"typename",
"union",
"unsigned",
//"using",
//"virtual",
//"void",
//"volatile",
"wchar_t",
//"while",
"xor",
"xor_eq"
}.ToImmutableHashSet(StringComparer.Ordinal);
private static readonly ImmutableDictionary<string, string> s_caseInsensitiveKeywords = new[]
{
"AddHandler",
"AddressOf",
"Alias",
"And",
"AndAlso",
"As",
"Boolean",
"ByRef",
"Byte",
"ByVal",
"Call",
"Case",
"Catch",
"CBool",
"CByte",
"CChar",
"CDate",
"CDbl",
"CDec",
"Char",
"CInt",
"Class",
"CLng",
"CObj",
"Const",
"Continue",
"CSByte",
"CShort",
"CSng",
"CStr",
"CType",
"CUInt",
"CULng",
"CUShort",
"Date",
"Decimal",
"Declare",
"Default",
"Delegate",
"Dim",
"DirectCast",
"Do",
"Double",
"Each",
"Else",
"ElseIf",
"End",
"Enum",
"Erase",
"Error",
"Event",
"Exit",
"False",
"Finally",
"For",
"Friend",
"Function",
"Get",
"GetType",
"Global",
"GoTo",
"Handles",
"If",
"Implements",
"Imports",
"In",
"Inherits",
"Integer",
"Interface",
"Is",
"IsNot",
"Lib",
"Like",
"Long",
"Loop",
"Me",
"Mod",
"Module",
"MustInherit",
"MustOverride",
"MyBase",
"MyClass",
"Namespace",
"Narrowing",
"New",
"Next",
"Not",
"Nothing",
"NotInheritable",
"NotOverridable",
"Object",
"Of",
"On",
"Operator",
"Option",
"Optional",
"Or",
"OrElse",
"Overloads",
"Overridable",
"Overrides",
"ParamArray",
"Partial",
"Private",
"Property",
"Protected",
"Public",
"RaiseEvent",
"ReadOnly",
"ReDim",
"REM",
"RemoveHandler",
"Resume",
"Return",
"SByte",
"Select",
"Set",
"Shadows",
"Shared",
"Short",
"Single",
"Static",
"Step",
"Stop",
"String",
"Structure",
"Sub",
"SyncLock",
"Then",
"Throw",
"To",
"True",
"Try",
"TryCast",
"TypeOf",
"UInteger",
"ULong",
"UShort",
"Using",
"When",
"While",
"Widening",
"With",
"WithEvents",
"WriteOnly",
"Xor"
// Listed as a keywords in Microsoft.CodeAnalysis.VisualBasic.SyntaxKind, but
// omitted, at least for now, for compatibility with FxCop:
//"Aggregate",
//"All",
//"Ansi",
//"Ascending",
//"Assembly",
//"Async",
//"Await",
//"Auto",
//"Binary",
//"By",
//"Compare",
//"Custom",
//"Descending",
//"Disable",
//"Distinct",
//"Enable",
//"EndIf",
//"Equals",
//"Explicit",
//"ExternalChecksum",
//"ExternalSource",
//"From",
//"GetXmlNamespace",
//"Gosub",
//"Group",
//"Infer",
//"Into",
//"IsFalse",
//"IsTrue",
//"Iterator",
//"Yield",
//"Join",
//"Key",
//"Let",
//"Mid",
//"Off",
//"Order",
//"Out",
//"Preserve",
//"Reference",
//"Region",
//"Strict",
//"Take",
//"Text",
//"Type",
//"Unicode",
//"Until",
//"Warning",
//"Variant",
//"Wend",
//"Where",
//"Xml"
}.ToImmutableDictionary(key => key, StringComparer.OrdinalIgnoreCase);
}
}