diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/RenameHelper.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/RenameHelper.cs index 626fbbd05..f6c2c3380 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/RenameHelper.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/RenameHelper.cs @@ -52,6 +52,20 @@ public static async Task IsValidNewMemberNameAsync(SemanticModel semanticM var containingSymbol = symbol.ContainingSymbol; + if (symbol.Kind == SymbolKind.TypeParameter) + { + // If the symbol is a type parameter, the name can't be the same as any type parameters of the containing type. + var parentSymbol = containingSymbol?.ContainingSymbol as INamedTypeSymbol; + if (parentSymbol != null + && parentSymbol.TypeParameters.Any(t => t.Name == name)) + { + return false; + } + + // Move up one level for the next validation step. + containingSymbol = containingSymbol?.ContainingSymbol; + } + var containingNamespaceOrTypeSymbol = containingSymbol as INamespaceOrTypeSymbol; if (containingNamespaceOrTypeSymbol != null) { diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/NamingRules/SA1314CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/NamingRules/SA1314CodeFixProvider.cs new file mode 100644 index 000000000..0f0bc61d1 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/NamingRules/SA1314CodeFixProvider.cs @@ -0,0 +1,64 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +namespace StyleCop.Analyzers.NamingRules +{ + using System.Collections.Immutable; + using System.Composition; + using System.Threading; + using System.Threading.Tasks; + using Helpers; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CodeActions; + using Microsoft.CodeAnalysis.CodeFixes; + + /// + /// Implements a code fix for . + /// + /// + /// To fix a violation of this rule, add the capital letter T to the front of the type parameter name. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SA1314CodeFixProvider))] + [Shared] + internal class SA1314CodeFixProvider : CodeFixProvider + { + /// + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(SA1314TypeParameterNamesMustBeginWithT.DiagnosticId); + + /// + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (var diagnostic in context.Diagnostics) + { + context.RegisterCodeFix( + CodeAction.Create( + NamingResources.SA1314CodeFix, + cancellationToken => CreateChangedSolutionAsync(context.Document, diagnostic, cancellationToken), + nameof(SA1314CodeFixProvider)), + diagnostic); + } + + return SpecializedTasks.CompletedTask; + } + + private static async Task CreateChangedSolutionAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(diagnostic.Location.SourceSpan.Start); + var baseName = "T" + token.ValueText; + var index = 0; + var newName = baseName; + + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var declaredSymbol = semanticModel.GetDeclaredSymbol(token.Parent, cancellationToken); + while (!await RenameHelper.IsValidNewMemberNameAsync(semanticModel, declaredSymbol, newName, cancellationToken).ConfigureAwait(false)) + { + index++; + newName = baseName + index; + } + + return await RenameHelper.RenameSymbolAsync(document, root, token, newName, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/StyleCop.Analyzers.CodeFixes.csproj b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/StyleCop.Analyzers.CodeFixes.csproj index 9357577d7..84d8779a4 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/StyleCop.Analyzers.CodeFixes.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/StyleCop.Analyzers.CodeFixes.csproj @@ -102,6 +102,7 @@ + diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/NamingRules/SA1314UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/NamingRules/SA1314UnitTests.cs new file mode 100644 index 000000000..c38899b1d --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/NamingRules/SA1314UnitTests.cs @@ -0,0 +1,274 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +namespace StyleCop.Analyzers.Test.NamingRules +{ + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.Diagnostics; + using StyleCop.Analyzers.NamingRules; + using TestHelper; + using Xunit; + + public class SA1314UnitTests : CodeFixVerifier + { + [Fact] + public async Task TestTypeParameterDoesNotStartWithTAsync() + { + var testCode = @" +public interface IFoo +{ +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(2, 23); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + + var fixedCode = @" +public interface IFoo +{ +}"; + + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + [Fact] + public async Task TestTypeParameterDoesNotStartWithTPlusParameterUsedAsync() + { + var testCode = @" +public class Foo +{ + void Test() + { + var key = typeof(Key); + } +} +"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(2, 18); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + + var fixedCode = @" +public class Foo +{ + void Test() + { + var key = typeof(TKey); + } +} +"; + + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + [Fact] + public async Task TestTypeParameterStartsWithLowerTAsync() + { + var testCode = @" +public interface IFoo +{ +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(2, 23); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + + var fixedCode = @" +public interface IFoo +{ +}"; + + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + [Fact] + public async Task TestInnerTypeParameterDoesNotStartWithTAsync() + { + var testCode = @" +public class Bar +{ + public class Foo + { + } +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, 22); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + + var fixedCode = @" +public class Bar +{ + public class Foo + { + } +}"; + + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + [Fact] + public async Task TestTypeParameterDoesStartWithTAsync() + { + var testCode = @"public interface IFoo +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestInnerTypeParameterDoesStartWithTAsync() + { + var testCode = @" +public class Bar +{ + public class Foo + { + } +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestTypeParameterDoesNotStartWithTWithMemberMatchingTargetTypeAsync() + { + string testCode = @" +public class Foo +{ + Key Bar { get; } +}"; + + string fixedCode = @" +public class Foo +{ + TKey Bar { get; } +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(2, 18); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestNestedTypeParameterDoesNotStartWithTWithConflictAsync() + { + string testCode = @" +public class Outer +{ + public class Foo + { + } +}"; + string fixedCode = @" +public class Outer +{ + public class Foo + { + } +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, 22); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestNestedTypeParameterDoesNotStartWithTWithMemberConflictAsync() + { + string testCode = @" +public class Outer +{ + public class Foo + { + Key Bar { get; } + } +}"; + string fixedCode = @" +public class Outer +{ + public class Foo + { + TKey1 Bar { get; } + } +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, 22); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestTypeParameterDoesNotStartWithTAndTypeConflictAsync() + { + string testCode = @" +public class TFoo +{ +} + +public class Bar +{ +}"; + string fixedCode = @" +public class TFoo +{ +} + +public class Bar +{ +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(6, 18); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestTypeParameterInMethodSignatureDoesNotStartWithTAsync() + { + var testCode = @" +public class Foo +{ + public void Bar() + { + } +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, 21); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + + var fixedCode = @" +public class Foo +{ + public void Bar() + { + } +}"; + + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + protected override IEnumerable GetCSharpDiagnosticAnalyzers() + { + yield return new SA1314TypeParameterNamesMustBeginWithT(); + } + + protected override CodeFixProvider GetCSharpCodeFixProvider() + { + return new SA1314CodeFixProvider(); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj b/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj index fb821a09a..2a369620d 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj @@ -278,6 +278,7 @@ + diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/NamingRules/NamingResources.Designer.cs b/StyleCop.Analyzers/StyleCop.Analyzers/NamingRules/NamingResources.Designer.cs index fb46a7fff..c5f2625ba 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/NamingRules/NamingResources.Designer.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/NamingRules/NamingResources.Designer.cs @@ -132,5 +132,41 @@ internal static string SA1313Title { return ResourceManager.GetString("SA1313Title", resourceCulture); } } + + /// + /// Looks up a localized string similar to Prefix type parameter name with 'T'. + /// + internal static string SA1314CodeFix { + get { + return ResourceManager.GetString("SA1314CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The name of a C# type parameter does not begin with the capital letter T.. + /// + internal static string SA1314Description { + get { + return ResourceManager.GetString("SA1314Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type parameter names must begin with T. + /// + internal static string SA1314MessageFormat { + get { + return ResourceManager.GetString("SA1314MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type parameter names must begin with T. + /// + internal static string SA1314Title { + get { + return ResourceManager.GetString("SA1314Title", resourceCulture); + } + } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/NamingRules/NamingResources.resx b/StyleCop.Analyzers/StyleCop.Analyzers/NamingRules/NamingResources.resx index 3e857f5d3..b872c582f 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/NamingRules/NamingResources.resx +++ b/StyleCop.Analyzers/StyleCop.Analyzers/NamingRules/NamingResources.resx @@ -141,4 +141,16 @@ Parameter names must begin with lower-case letter + + Prefix type parameter name with 'T' + + + The name of a C# type parameter does not begin with the capital letter T. + + + Type parameter names must begin with T + + + Type parameter names must begin with T + \ No newline at end of file diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/NamingRules/SA1314TypeParameterNamesMustBeginWithT.cs b/StyleCop.Analyzers/StyleCop.Analyzers/NamingRules/SA1314TypeParameterNamesMustBeginWithT.cs new file mode 100644 index 000000000..a3426c4c3 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers/NamingRules/SA1314TypeParameterNamesMustBeginWithT.cs @@ -0,0 +1,65 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +namespace StyleCop.Analyzers.NamingRules +{ + using System; + using System.Collections.Immutable; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + + /// + /// The name of a C# type parameter does not begin with the capital letter T. + /// + /// + /// A violation of this rule occurs when the name of a C# type parameter does not begin with the capital letter T. + /// Type parameter names should always begin with T. For example, T or TKey. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class SA1314TypeParameterNamesMustBeginWithT : DiagnosticAnalyzer + { + /// + /// The ID for diagnostics produced by the analyzer. + /// + public const string DiagnosticId = "SA1314"; + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(NamingResources.SA1314Title), NamingResources.ResourceManager, typeof(NamingResources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(NamingResources.SA1314MessageFormat), NamingResources.ResourceManager, typeof(NamingResources)); + private static readonly LocalizableString Description = new LocalizableResourceString(nameof(NamingResources.SA1314Description), NamingResources.ResourceManager, typeof(NamingResources)); + private static readonly string HelpLink = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1314.md"; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.NamingRules, DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, HelpLink); + + private static readonly Action TypeParameterAction = HandleTypeParameter; + + /// + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(Descriptor); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(TypeParameterAction, SyntaxKind.TypeParameter); + } + + private static void HandleTypeParameter(SyntaxNodeAnalysisContext context) + { + var typeParameter = (TypeParameterSyntax)context.Node; + if (typeParameter.Identifier.IsMissing) + { + return; + } + + string name = typeParameter.Identifier.ValueText; + if (name != null && !name.StartsWith("T", StringComparison.Ordinal)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptor, typeParameter.Identifier.GetLocation())); + } + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj b/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj index 3ce374355..cf233cb24 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj @@ -206,6 +206,7 @@ + diff --git a/StyleCopAnalyzers.sln b/StyleCopAnalyzers.sln index ec595caaa..fb8e40b9c 100644 --- a/StyleCopAnalyzers.sln +++ b/StyleCopAnalyzers.sln @@ -146,6 +146,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "documentation", "documentat documentation\SA1311.md = documentation\SA1311.md documentation\SA1312.md = documentation\SA1312.md documentation\SA1313.md = documentation\SA1313.md + documentation\SA1314.md = documentation\SA1314.md documentation\SA1400.md = documentation\SA1400.md documentation\SA1401.md = documentation\SA1401.md documentation\SA1402.md = documentation\SA1402.md diff --git a/documentation/NamingRules.md b/documentation/NamingRules.md index d5c5159f2..f5da6e985 100644 --- a/documentation/NamingRules.md +++ b/documentation/NamingRules.md @@ -17,3 +17,4 @@ Identifier | Name | Description [SA1311](SA1311.md) | StaticReadonlyFieldsMustBeginWithUpperCaseLetter | The name of a static readonly field does not begin with an upper-case letter. [SA1312](SA1312.md) | VariableNamesMustBeginWithLowerCaseLetter | The name of a variable in C# does not begin with a lower-case letter. [SA1313](SA1313.md) | ParameterNamesMustBeginWithLowerCaseLetter | The name of a parameter in C# does not begin with a lower-case letter. +[SA1314](SA1314.md) | TypeParameterNamesMustBeginWithT | The name of a C# type parameter does not begin with the capital letter T diff --git a/documentation/SA1314.md b/documentation/SA1314.md new file mode 100644 index 000000000..8a1c8e1a9 --- /dev/null +++ b/documentation/SA1314.md @@ -0,0 +1,41 @@ +## SA1314 + + + + + + + + + + + + + + +
TypeNameSA1314TypeParameterNamesMustBeginWithT
CheckIdSA1314
CategoryNaming Rules
+ +:memo: This rule is new for StyleCop Analyzers, and was not present in StyleCop Classic. + +## Cause + +The name of a C# type parameter does not begin with the capital letter T. + +## Rule description + +A violation of this rule occurs when the name of a C# type parameter does not begin with the capital letter T. Type parameter names should always begin with T. For example, *T* or *TKey*. + +## How to fix violations + +To fix a violation of this rule, add the capital letter T to the front of the type parameter name. + +## How to suppress violations + +```csharp +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1314:TypeParameterNamesMustBeginWithT", Justification = "Reviewed.")] +``` + +```csharp +#pragma warning disable SA1314 // Type parameter names must begin with T +#pragma warning restore SA1314 // Type parameter names must begin with T +```