From 42ba372db201ab91ffcdac8e7c0736e6fc93a515 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Tue, 5 Nov 2024 14:20:20 -0800 Subject: [PATCH] Have the naming style fixer understand the 'I' pattern for interfaces better --- .../Tests/NamingStyles/NamingStylesTests.cs | 40 ++++++++++++++----- .../Compiler/Core/NamingStyles/NamingStyle.cs | 21 ++++++---- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/Analyzers/CSharp/Tests/NamingStyles/NamingStylesTests.cs b/src/Analyzers/CSharp/Tests/NamingStyles/NamingStylesTests.cs index ee726f9e1485d..c184e762edd05 100644 --- a/src/Analyzers/CSharp/Tests/NamingStyles/NamingStylesTests.cs +++ b/src/Analyzers/CSharp/Tests/NamingStyles/NamingStylesTests.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; @@ -21,14 +19,10 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics.NamingStyles; [Trait(Traits.Feature, Traits.Features.NamingStyle)] -public class NamingStylesTests : AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest_NoEditor +public sealed class NamingStylesTests(ITestOutputHelper logger) + : AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest_NoEditor(logger) { - public NamingStylesTests(ITestOutputHelper logger) - : base(logger) - { - } - - private static readonly NamingStylesTestOptionSets s_options = new NamingStylesTestOptionSets(LanguageNames.CSharp); + private static readonly NamingStylesTestOptionSets s_options = new(LanguageNames.CSharp); internal override (DiagnosticAnalyzer, CodeFixProvider) CreateDiagnosticProviderAndFixer(Workspace workspace) => (new CSharpNamingStyleDiagnosticAnalyzer(), new NamingStyleCodeFixProvider()); @@ -1402,6 +1396,34 @@ internal interface """, new TestParameters(options: s_options.InterfaceNamesStartWithI)); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/17656")] + public async Task TestInterfacesStartWithIOnTypeThatAlreadyStartsWithI1() + { + await TestInRegularAndScript1Async(""" + interface [|InputStream|] { } + """, """ + interface IInputStream { } + """, new TestParameters(options: s_options.InterfaceNamesStartWithI)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/17656")] + public async Task TestInterfacesStartWithIOnTypeThatAlreadyStartsWithI2() + { + await TestInRegularAndScript1Async(""" + interface [|Stream|] { } + """, """ + interface IStream { } + """, new TestParameters(options: s_options.InterfaceNamesStartWithI)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/17656")] + public async Task TestInterfacesStartWithIOnTypeThatAlreadyStartsWithI3() + { + await TestMissingInRegularAndScriptAsync(""" + interface [|IInputStream|] { } + """, new TestParameters(options: s_options.InterfaceNamesStartWithI)); + } + #if CODE_STYLE [Fact(Skip = "https://github.com/dotnet/roslyn/issues/42218")] #else diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/NamingStyle.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/NamingStyle.cs index 9e64f37819f53..57d4d16d1a82e 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/NamingStyle.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/NamingStyle.cs @@ -427,9 +427,7 @@ private string EnsureSuffix(string name) for (var i = Suffix.Length; i > 0; i--) { if (name.EndsWith(Suffix[..i])) - { return name + Suffix[i..]; - } } return name + Suffix; @@ -437,15 +435,26 @@ private string EnsureSuffix(string name) private string EnsurePrefix(string name) { + // Exceptional cases. If the name is some interface name (like `InputStream`) and the rule is to have a single + // character prefix like "Add `I` for interfaces" don't consider the existing 'I' to be a match of the prefix. + + if (Prefix is [var prefixChar] && + char.IsUpper(prefixChar) && + name is [var nameChar1, var nameChar2, ..] && + prefixChar == nameChar1 && + char.IsLower(nameChar2)) + { + // return IInputStream here, even though InputStream already starts with 'I'. + return Prefix + name; + } + // If the name already starts with any suffix of the Prefix, only prepend the prefix of // the Prefix not contained in the longest such Prefix suffix. For example, if the // required prefix is "catdog_" and the name is "dog_test", then only prepend "cat". for (var i = 0; i < Prefix.Length; i++) { if (name.StartsWith(Prefix[i..])) - { return Prefix[..i] + name; - } } return Prefix + name; @@ -480,13 +489,11 @@ public void WriteTo(ObjectWriter writer) } public static NamingStyle ReadFrom(ObjectReader reader) - { - return new NamingStyle( + => new( reader.ReadGuid(), reader.ReadString(), reader.ReadString(), reader.ReadString(), reader.ReadString(), (Capitalization)reader.ReadInt32()); - } }