diff --git a/analyzers/src/SonarAnalyzer.CSharp.Styling/Rules/UseShortName.cs b/analyzers/src/SonarAnalyzer.CSharp.Styling/Rules/UseShortName.cs new file mode 100644 index 00000000000..b839f446ca8 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp.Styling/Rules/UseShortName.cs @@ -0,0 +1,78 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarAnalyzer.Rules.CSharp.Styling; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseShortName : StylingAnalyzer +{ + private static readonly RenameInfo[] RenameCandidates = + [ + new("CancellationToken", "cancellationToken", "cancel"), + new("CancellationToken", "CancellationToken", "Cancel"), + new("DiagnosticDescriptor", "diagnosticDescriptor", "descriptor"), + new("DiagnosticDescriptor", "DiagnosticDescriptor", "Descriptor"), + new("SyntaxNode", "syntaxNode", "node"), + new("SyntaxNode", "SyntaxNode", "Node"), + new("SyntaxToken", "syntaxToken", "token"), + new("SyntaxToken", "SyntaxToken", "Token"), + new("SyntaxTree", "syntaxTree", "tree"), + new("SyntaxTree", "SyntaxTree", "Tree"), + new("SyntaxTrivia", "syntaxTrivia", "trivia"), + new("SyntaxTrivia", "SyntaxTrivia", "Trivia"), + new("SemanticModel", "semanticModel", "model"), + new("SemanticModel", "SemanticModel", "Model") + ]; + + public UseShortName() : base("T0017", "Use short name '{0}'.") { } + + protected override void Initialize(SonarAnalysisContext context) + { + context.RegisterNodeAction(c => ValidateDeclaration(c, ((VariableDeclaratorSyntax)c.Node).Identifier), SyntaxKind.VariableDeclarator); + context.RegisterNodeAction(c => ValidateDeclaration(c, ((PropertyDeclarationSyntax)c.Node).Identifier), SyntaxKind.PropertyDeclaration); + context.RegisterNodeAction(c => + { + if (!FollowsPredefinedName(c.ContainingSymbol)) + { + ValidateDeclaration(c, ((ParameterSyntax)c.Node).Identifier); + } + }, + SyntaxKind.Parameter); + } + + private void ValidateDeclaration(SonarSyntaxNodeReportingContext context, SyntaxToken identifier) + { + if (FindRename(identifier.ValueText) is { } name + && context.SemanticModel.GetDeclaredSymbol(context.Node).GetSymbolType() is { } type + && type.Name == name.TypeName) + { + context.ReportIssue(Rule, identifier, identifier.ValueText.Replace(name.UsedName, name.SuggestedName)); + } + } + + private static RenameInfo FindRename(string name) => + Array.Find(RenameCandidates, x => name.Contains(x.UsedName)); + + private static bool FollowsPredefinedName(ISymbol symbol) => + symbol is IMethodSymbol method + && (symbol.IsOverride || symbol.GetInterfaceMember() is not null || method.PartialDefinitionPart is not null); + + private sealed record RenameInfo(string TypeName, string UsedName, string SuggestedName); +} diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/SymbolHelper.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/SymbolHelper.cs index 92edd49dd36..52b9b2347c3 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/SymbolHelper.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/SymbolHelper.cs @@ -22,7 +22,7 @@ namespace SonarAnalyzer.Helpers { - internal static class SymbolHelper + public static class SymbolHelper { private static readonly PropertyInfo ITypeSymbolIsRecord = typeof(ITypeSymbol).GetProperty("IsRecord"); diff --git a/analyzers/tests/SonarAnalyzer.CSharp.Styling.Test/Rules/UseShortNameTest.cs b/analyzers/tests/SonarAnalyzer.CSharp.Styling.Test/Rules/UseShortNameTest.cs new file mode 100644 index 00000000000..d32b516662f --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.CSharp.Styling.Test/Rules/UseShortNameTest.cs @@ -0,0 +1,29 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarAnalyzer.CSharp.Styling.Test.Rules; + +[TestClass] +public class UseShortNameTest +{ + [TestMethod] + public void UseShortName() => + StylingVerifierBuilder.Create().AddPaths("UseShortName.cs").Verify(); +} diff --git a/analyzers/tests/SonarAnalyzer.CSharp.Styling.Test/TestCases/UseShortName.cs b/analyzers/tests/SonarAnalyzer.CSharp.Styling.Test/TestCases/UseShortName.cs new file mode 100644 index 00000000000..adc39f1a3bd --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.CSharp.Styling.Test/TestCases/UseShortName.cs @@ -0,0 +1,147 @@ +using System.Threading; + +public class Sample +{ + public void SuggestedNames(SyntaxNode node, SyntaxTree tree, SemanticModel model, CancellationToken cancel) { } + public void OtherNames(SyntaxNode song, SyntaxTree wood, SemanticModel sculpture, CancellationToken nuke) { } + + public void LongName1(SyntaxNode syntaxNode) { } // Noncompliant {{Use short name 'node'.}} + // ^^^^^^^^^^ + public void LongName2(SyntaxNode prefixedSyntaxNode) { } // Noncompliant {{Use short name 'prefixedNode'.}} + public void LongName3(SyntaxNode syntaxNodeCount) { } // Noncompliant {{Use short name 'nodeCount'.}} + public void LongName4(SyntaxTree syntaxTree) { } // Noncompliant {{Use short name 'tree'.}} + public void LongName5(SyntaxTree firstSyntaxTreeCount) { } // Noncompliant {{Use short name 'firstTreeCount'.}} + public void LongName6(CancellationToken cancellationToken) { } // Noncompliant {{Use short name 'cancel'.}} + + private SyntaxNode node; + private SyntaxNode otherNode, syntaxNode; // Noncompliant {{Use short name 'node'.}} + // ^^^^^^^^^^ + private SyntaxTree syntaxTree; // Noncompliant + private SemanticModel semanticModel; // Noncompliant + + public SyntaxNode SyntaxNode { get; } // Noncompliant {{Use short name 'Node'.}} + // ^^^^^^^^^^ + + private SyntaxToken syntaxToken; // Noncompliant {{Use short name 'token'.}} + private SyntaxToken SyntaxToken { get; } // Noncompliant {{Use short name 'Token'.}} + + private SyntaxTrivia syntaxTrivia; // Noncompliant {{Use short name 'trivia'.}} + private SyntaxTrivia SyntaxTrivia{ get; } // Noncompliant {{Use short name 'Trivia'.}} + + private DiagnosticDescriptor diagnosticDescriptor; // Noncompliant {{Use short name 'descriptor'.}} + private DiagnosticDescriptor DiagnosticDescriptor { get; } // Noncompliant {{Use short name 'Descriptor'.}} + + public void TypedDeclarations() + { + SyntaxNode nodeNode; + SyntaxNode otherNode; + SyntaxNode node; + SyntaxNode syntaxNode; // Noncompliant {{Use short name 'node'.}} If there exist 'node' and 'syntaxNode' in the same scope, both need a rename. + + SyntaxTree syntaxTree; // Noncompliant + SemanticModel semanticModel; // Noncompliant + CancellationToken cancellationToken; // Noncompliant + + void SyntaxNode() { } // Not in the scope (for now) + } + + public void VarDeclarations() + { + var nodeNode = CreateNode(); + var otherNode = CreateNode(); + var node = CreateNode(); + var syntaxNode = CreateNode(); // Noncompliant {{Use short name 'node'.}} If there exist 'node' and 'syntaxNode' in the same scope, both need a rename. + // ^^^^^^^^^^ + + var syntaxTree = CreateTree(); // Noncompliant + var semanticModel = CreateModel(); // Noncompliant + var cancellationToken = CreateCancel(); // Noncompliant + + void SyntaxNode() { } // Not in the scope (for now) + } + + public void UnexpectedType(SyntaxNode syntaxTree) // Wrong, but compliant + { + var semanticModel = CreateNode(); // Wrong, but compliant + SemanticModel syntaxNode = null; // Wrong, but compliant + } + + private SyntaxNode CreateNode() => null; + private SyntaxTree CreateTree() => null; + private SemanticModel CreateModel() => null; + private CancellationToken CreateCancel() => default; + + private class NestedWithPublicFields + { + public SyntaxNode SyntaxNode; // Noncompliant {{Use short name 'Node'.}} + public SyntaxTree SyntaxTree; // Noncompliant + public SemanticModel SemanticModel; // Noncompliant + } +} + +public class ArrowProperty +{ + public SyntaxNode SyntaxNode => null; // Noncompliant +} + +public class BodyProperty +{ + public SyntaxNode SyntaxNode // Noncompliant + { + get => null; + set { } + } +} + +public class Methods +{ + // It does not appy to method names + public void SyntaxNode() { } + public void SyntaxTree() { } + public void SemanticModel() { } + public void CancellationToken() { } +} + +public abstract class Base +{ + protected abstract void DoSomething(SyntaxNode syntaxNode); // Noncompliant {{Use short name 'node'.}} +} + +public class Inherited : Base +{ + protected override void DoSomething(SyntaxNode syntaxNode) // Compliant so we don't contradict S927 + { + } +} + +public interface IInterface +{ + void DoSomething(SyntaxNode syntaxNode); // Noncompliant +} + +public class Implemented : IInterface +{ + public void DoSomething(SyntaxNode syntaxNode) // Compliant so we don't contradict S927 + { + } +} + +public partial class Partial +{ + public partial void DoSomething(SyntaxNode syntaxNode); // Noncompliant +} + +public partial class Partial +{ + public partial void DoSomething(SyntaxNode syntaxNode) // Compliant so we don't contradict S927 + { + // Implementation + } +} + +public class SyntaxNode { } +public class SyntaxToken { } +public class SyntaxTree { } +public class SyntaxTrivia { } +public class SemanticModel { } +public class DiagnosticDescriptor { }