diff --git a/analyzers/its/expected/Ember-MM/EmberAPI-{208AA35E-C6AE-4D2D-A9DD-B6EFD19A4279}-S4663.json b/analyzers/its/expected/Ember-MM/EmberAPI-{208AA35E-C6AE-4D2D-A9DD-B6EFD19A4279}-S4663.json new file mode 100644 index 00000000000..936136f3497 --- /dev/null +++ b/analyzers/its/expected/Ember-MM/EmberAPI-{208AA35E-C6AE-4D2D-A9DD-B6EFD19A4279}-S4663.json @@ -0,0 +1,17 @@ +{ +"issues": [ +{ +"id": "S4663", +"message": "Remove this empty comment", +"location": { +"uri": "sources\Ember-MM\EmberAPI\clsAPICommon.vb", +"region": { +"startLine": 577, +"startColumn": 19, +"endLine": 577, +"endColumn": 21 +} +} +} +] +} diff --git a/analyzers/its/expected/Ember-MM/scraper.EmberCore-{EF6A550E-DD76-4F4D-8250-8598140F828B}-S4663.json b/analyzers/its/expected/Ember-MM/scraper.EmberCore-{EF6A550E-DD76-4F4D-8250-8598140F828B}-S4663.json new file mode 100644 index 00000000000..dac912b5aff --- /dev/null +++ b/analyzers/its/expected/Ember-MM/scraper.EmberCore-{EF6A550E-DD76-4F4D-8250-8598140F828B}-S4663.json @@ -0,0 +1,17 @@ +{ +"issues": [ +{ +"id": "S4663", +"message": "Remove this empty comment", +"location": { +"uri": "sources\Ember-MM\Addons\scraper.EmberCore\scraperMovieNativeModule.vb", +"region": { +"startLine": 364, +"startColumn": 104, +"endLine": 364, +"endColumn": 105 +} +} +} +] +} diff --git a/analyzers/rspec/cs/S4663_c#.html b/analyzers/rspec/cs/S4663_c#.html new file mode 100644 index 00000000000..a250d368623 --- /dev/null +++ b/analyzers/rspec/cs/S4663_c#.html @@ -0,0 +1,16 @@ +

An empty comment is likely to be a mistake and doesn’t help to improve the readability of the code. For these reasons, it should be removed.

+

Noncompliant Code Example

+
+//
+
+///
+
+/**
+
+ */
+
+/*
+
+ */
+
+ diff --git a/analyzers/rspec/cs/S4663_c#.json b/analyzers/rspec/cs/S4663_c#.json new file mode 100644 index 00000000000..05e82bc91e7 --- /dev/null +++ b/analyzers/rspec/cs/S4663_c#.json @@ -0,0 +1,15 @@ +{ + "title": "Comments should not be empty", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "1min" + }, + "tags": [], + "defaultSeverity": "Minor", + "ruleSpecification": "RSPEC-4663", + "sqKey": "S4663", + "scope": "Main", + "quickfix": "unknown" +} diff --git a/analyzers/rspec/cs/Sonar_way_profile.json b/analyzers/rspec/cs/Sonar_way_profile.json index 444cf4210f6..5879a1c7933 100644 --- a/analyzers/rspec/cs/Sonar_way_profile.json +++ b/analyzers/rspec/cs/Sonar_way_profile.json @@ -241,6 +241,7 @@ "S4583", "S4586", "S4635", + "S4663", "S4790", "S4792", "S4830", diff --git a/analyzers/rspec/vbnet/S4663_vb.net.html b/analyzers/rspec/vbnet/S4663_vb.net.html new file mode 100644 index 00000000000..eac2d8bb6d4 --- /dev/null +++ b/analyzers/rspec/vbnet/S4663_vb.net.html @@ -0,0 +1,8 @@ +

An empty comment is likely to be a mistake and doesn’t help to improve the readability of the code. For these reasons, it should be removed.

+

Noncompliant Code Example

+
+'
+
+'''
+
+ diff --git a/analyzers/rspec/vbnet/S4663_vb.net.json b/analyzers/rspec/vbnet/S4663_vb.net.json new file mode 100644 index 00000000000..05e82bc91e7 --- /dev/null +++ b/analyzers/rspec/vbnet/S4663_vb.net.json @@ -0,0 +1,15 @@ +{ + "title": "Comments should not be empty", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "1min" + }, + "tags": [], + "defaultSeverity": "Minor", + "ruleSpecification": "RSPEC-4663", + "sqKey": "S4663", + "scope": "Main", + "quickfix": "unknown" +} diff --git a/analyzers/rspec/vbnet/Sonar_way_profile.json b/analyzers/rspec/vbnet/Sonar_way_profile.json index 11efa1e3eb7..46bcb99963e 100644 --- a/analyzers/rspec/vbnet/Sonar_way_profile.json +++ b/analyzers/rspec/vbnet/Sonar_way_profile.json @@ -108,6 +108,7 @@ "S4581", "S4583", "S4586", + "S4663", "S4790", "S4792", "S4830", diff --git a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs index c57298bf631..c26432354f0 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs @@ -40,10 +40,14 @@ internal sealed class CSharpSyntaxFacade : SyntaxFacade public override bool IsKind(SyntaxToken token, SyntaxKind kind) => token.IsKind(kind); + public override bool IsKind(SyntaxTrivia trivia, SyntaxKind kind) => trivia.IsKind(kind); + public override bool IsAnyKind(SyntaxNode node, ISet syntaxKinds) => node.IsAnyKind(syntaxKinds); public override bool IsAnyKind(SyntaxNode node, params SyntaxKind[] syntaxKinds) => node.IsAnyKind(syntaxKinds); + public override bool IsAnyKind(SyntaxTrivia trivia, params SyntaxKind[] syntaxKinds) => trivia.IsAnyKind(syntaxKinds); + public override bool IsNullLiteral(SyntaxNode node) => node.IsNullLiteral(); public override IEnumerable ArgumentExpressions(SyntaxNode node) => diff --git a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxKindFacade.cs b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxKindFacade.cs index 0a97a8d62a8..83954886377 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxKindFacade.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxKindFacade.cs @@ -29,6 +29,13 @@ internal sealed class CSharpSyntaxKindFacade : ISyntaxKindFacade SyntaxKindEx.RecordClassDeclaration, }; public SyntaxKind ClassDeclaration => SyntaxKind.ClassDeclaration; + public SyntaxKind[] CommentTrivia => new[] + { + SyntaxKind.SingleLineCommentTrivia, + SyntaxKind.MultiLineCommentTrivia, + SyntaxKind.SingleLineDocumentationCommentTrivia, + SyntaxKind.MultiLineDocumentationCommentTrivia, + }; public SyntaxKind[] ComparisonKinds => new[] { SyntaxKind.GreaterThanExpression, @@ -41,11 +48,14 @@ internal sealed class CSharpSyntaxKindFacade : ISyntaxKindFacade public SyntaxKind ConstructorDeclaration => SyntaxKind.ConstructorDeclaration; public SyntaxKind[] DefaultExpressions => new[] { SyntaxKind.DefaultExpression, SyntaxKindEx.DefaultLiteralExpression }; public SyntaxKind EnumDeclaration => SyntaxKind.EnumDeclaration; + public SyntaxKind EndOfLineTrivia => SyntaxKind.EndOfLineTrivia; public SyntaxKind FieldDeclaration => SyntaxKind.FieldDeclaration; public SyntaxKind IdentifierName => SyntaxKind.IdentifierName; public SyntaxKind IdentifierToken => SyntaxKind.IdentifierToken; public SyntaxKind InvocationExpression => SyntaxKind.InvocationExpression; public SyntaxKind InterpolatedStringExpression => SyntaxKind.InterpolatedStringExpression; + public SyntaxKind LeftShiftAssignmentStatement => SyntaxKind.LeftShiftAssignmentExpression; + public SyntaxKind LeftShiftExpression => SyntaxKind.LeftShiftExpression; public SyntaxKind LocalDeclaration => SyntaxKind.LocalDeclarationStatement; public SyntaxKind[] MethodDeclarations => new[] { SyntaxKind.MethodDeclaration }; public SyntaxKind[] ObjectCreationExpressions => new[] { SyntaxKind.ObjectCreationExpression, SyntaxKindEx.ImplicitObjectCreationExpression }; @@ -53,7 +63,10 @@ internal sealed class CSharpSyntaxKindFacade : ISyntaxKindFacade public SyntaxKind ParameterList => SyntaxKind.ParameterList; public SyntaxKind RefKeyword => SyntaxKind.RefKeyword; public SyntaxKind ReturnStatement => SyntaxKind.ReturnStatement; + public SyntaxKind RightShiftAssignmentStatement => SyntaxKind.RightShiftAssignmentExpression; + public SyntaxKind RightShiftExpression => SyntaxKind.RightShiftExpression; public SyntaxKind SimpleAssignment => SyntaxKind.SimpleAssignmentExpression; + public SyntaxKind SimpleCommentTrivia => SyntaxKind.SingleLineCommentTrivia; public SyntaxKind SimpleMemberAccessExpression => SyntaxKind.SimpleMemberAccessExpression; public SyntaxKind[] StringLiteralExpressions => new[] { SyntaxKind.StringLiteralExpression, SyntaxKindEx.Utf8StringLiteralExpression }; public SyntaxKind StructDeclaration => SyntaxKind.StructDeclaration; @@ -66,8 +79,5 @@ internal sealed class CSharpSyntaxKindFacade : ISyntaxKindFacade SyntaxKindEx.RecordClassDeclaration, SyntaxKindEx.RecordStructDeclaration, }; - public SyntaxKind LeftShiftExpression => SyntaxKind.LeftShiftExpression; - public SyntaxKind RightShiftExpression => SyntaxKind.RightShiftExpression; - public SyntaxKind LeftShiftAssignmentStatement => SyntaxKind.LeftShiftAssignmentExpression; - public SyntaxKind RightShiftAssignmentStatement => SyntaxKind.RightShiftAssignmentExpression; + public SyntaxKind WhitespaceTrivia => SyntaxKind.WhitespaceTrivia; } diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/CommentsShouldNotBeEmpty.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/CommentsShouldNotBeEmpty.cs new file mode 100644 index 00000000000..4339b95afae --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/CommentsShouldNotBeEmpty.cs @@ -0,0 +1,87 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 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. + */ + +using System.Text; + +namespace SonarAnalyzer.Rules.CSharp; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class CommentsShouldNotBeEmpty : CommentsShouldNotBeEmptyBase +{ + protected override ILanguageFacade Language => CSharpFacade.Instance; + + protected override string GetCommentText(SyntaxTrivia trivia) => + trivia.Kind() switch + { + SyntaxKind.SingleLineCommentTrivia => GetSingleLineText(trivia), + SyntaxKind.MultiLineCommentTrivia => GetMultiLineText(trivia), + SyntaxKind.SingleLineDocumentationCommentTrivia => GetSingleLineDocumentationText(trivia), + SyntaxKind.MultiLineDocumentationCommentTrivia => GetMultiLineDocumentationText(trivia), + }; + + // // + private static string GetSingleLineText(SyntaxTrivia trivia) => + trivia.ToString().Trim().Substring(2); + + // /// + private static string GetSingleLineDocumentationText(SyntaxTrivia trivia) + { + var stringBuilder = new StringBuilder(); + foreach (var line in trivia.ToFullString().Split(MetricsBase.LineTerminators, StringSplitOptions.None)) + { + var trimmedLine = line.TrimStart(null); + trimmedLine = trimmedLine.StartsWith("///") + ? trimmedLine.Substring(3).Trim() + : trimmedLine.TrimEnd(null); + stringBuilder.Append(trimmedLine); + } + return stringBuilder.ToString(); + } + + // /* */ + private static string GetMultiLineText(SyntaxTrivia trivia) => + ParseMultiLine(trivia.ToString(), 2); // Length of "/*" + + // /** */ + private static string GetMultiLineDocumentationText(SyntaxTrivia trivia) => + ParseMultiLine(trivia.ToFullString(), 3); // Length of "/**" + + private static string ParseMultiLine(string commentText, int initialTrimSize) + { + commentText = commentText.Trim().Substring(initialTrimSize); + if (commentText.EndsWith("*/", StringComparison.Ordinal)) // Might be unclosed, still reported + { + commentText = commentText.Substring(0, commentText.Length - 2); + } + + var stringBuilder = new StringBuilder(); + foreach (var line in commentText.Split(MetricsBase.LineTerminators, StringSplitOptions.None)) + { + var trimmedLine = line.TrimStart(null); + if (trimmedLine.StartsWith("*", StringComparison.Ordinal)) + { + trimmedLine = trimmedLine.TrimStart('*'); + } + + stringBuilder.Append(trimmedLine.Trim()); + } + return stringBuilder.ToString(); + } +} diff --git a/analyzers/src/SonarAnalyzer.Common/Facade/ISyntaxKindFacade.cs b/analyzers/src/SonarAnalyzer.Common/Facade/ISyntaxKindFacade.cs index 3d8a0052fc4..9394383d9f8 100644 --- a/analyzers/src/SonarAnalyzer.Common/Facade/ISyntaxKindFacade.cs +++ b/analyzers/src/SonarAnalyzer.Common/Facade/ISyntaxKindFacade.cs @@ -26,29 +26,33 @@ public interface ISyntaxKindFacade abstract TSyntaxKind Attribute { get; } abstract TSyntaxKind[] ClassAndRecordDeclaration { get; } abstract TSyntaxKind ClassDeclaration { get; } + abstract TSyntaxKind[] CommentTrivia { get; } abstract TSyntaxKind[] ComparisonKinds { get; } abstract TSyntaxKind ConstructorDeclaration { get; } abstract TSyntaxKind[] DefaultExpressions { get; } + abstract TSyntaxKind EndOfLineTrivia { get; } abstract TSyntaxKind EnumDeclaration { get; } abstract TSyntaxKind FieldDeclaration { get; } abstract TSyntaxKind IdentifierName { get; } abstract TSyntaxKind IdentifierToken { get; } abstract TSyntaxKind InvocationExpression { get; } abstract TSyntaxKind InterpolatedStringExpression { get; } + abstract TSyntaxKind LeftShiftAssignmentStatement { get; } + abstract TSyntaxKind LeftShiftExpression { get; } abstract TSyntaxKind LocalDeclaration { get; } abstract TSyntaxKind[] MethodDeclarations { get; } abstract TSyntaxKind[] ObjectCreationExpressions { get; } abstract TSyntaxKind Parameter { get; } abstract TSyntaxKind RefKeyword { get; } + abstract TSyntaxKind RightShiftExpression { get; } + abstract TSyntaxKind RightShiftAssignmentStatement { get; } abstract TSyntaxKind ParameterList { get; } abstract TSyntaxKind ReturnStatement { get; } abstract TSyntaxKind SimpleAssignment { get; } + abstract TSyntaxKind SimpleCommentTrivia { get; } abstract TSyntaxKind SimpleMemberAccessExpression { get; } abstract TSyntaxKind[] StringLiteralExpressions { get; } abstract TSyntaxKind StructDeclaration { get; } abstract TSyntaxKind[] TypeDeclaration { get; } - abstract TSyntaxKind LeftShiftExpression { get; } - abstract TSyntaxKind RightShiftExpression { get; } - abstract TSyntaxKind LeftShiftAssignmentStatement { get; } - abstract TSyntaxKind RightShiftAssignmentStatement { get; } + abstract TSyntaxKind WhitespaceTrivia { get; } } diff --git a/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs b/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs index d97f5cfa666..9144a08af14 100644 --- a/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs +++ b/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs @@ -28,8 +28,10 @@ public abstract class SyntaxFacade public abstract bool IsNullLiteral(SyntaxNode node); public abstract bool IsKind(SyntaxNode node, TSyntaxKind kind); public abstract bool IsKind(SyntaxToken token, TSyntaxKind kind); + public abstract bool IsKind(SyntaxTrivia trivia, TSyntaxKind kind); public abstract bool IsAnyKind(SyntaxNode node, ISet syntaxKinds); public abstract bool IsAnyKind(SyntaxNode node, params TSyntaxKind[] syntaxKinds); + public abstract bool IsAnyKind(SyntaxTrivia trivia, params TSyntaxKind[] syntaxKinds); public abstract IEnumerable ArgumentExpressions(SyntaxNode node); public abstract ImmutableArray AssignmentTargets(SyntaxNode assignment); public abstract SyntaxNode AssignmentLeft(SyntaxNode assignment); diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/CommentsShouldNotBeEmptyBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/CommentsShouldNotBeEmptyBase.cs new file mode 100644 index 00000000000..2acea99fd14 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Rules/CommentsShouldNotBeEmptyBase.cs @@ -0,0 +1,152 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 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. + */ + +using System.Linq; +using Microsoft.CodeAnalysis.Text; + +namespace SonarAnalyzer.Rules; + +public abstract class CommentsShouldNotBeEmptyBase : SonarDiagnosticAnalyzer + where TSyntaxKind : struct +{ + private const string DiagnosticId = "S4663"; + + protected abstract string GetCommentText(SyntaxTrivia trivia); + + protected override string MessageFormat => "Remove this empty comment"; + + protected CommentsShouldNotBeEmptyBase() : base(DiagnosticId) { } + + protected override void Initialize(SonarAnalysisContext context) => + context.RegisterTreeAction(Language.GeneratedCodeRecognizer, c => + { + foreach (var token in c.Tree.GetRoot().DescendantTokens()) + { + // Hotpath: Don't allocate the trivia enumerable if not needed + if (token.HasLeadingTrivia) + { + CheckTrivia(c, token.LeadingTrivia); + } + + if (token.HasTrailingTrivia) + { + CheckTrivia(c, token.TrailingTrivia); + } + } + }); + + private void CheckTrivia(SonarSyntaxTreeReportingContext context, IEnumerable trivia) + { + var partitions = PartitionComments(trivia); + if (partitions is null) + { + return; + } + + foreach (var partition in partitions.Where(trivia => trivia.Any() && trivia.All(x => string.IsNullOrWhiteSpace(GetCommentText(x))))) + { + var start = partition.First().GetLocation().SourceSpan.Start; + var end = partition.Last().GetLocation().SourceSpan.End; + var location = Location.Create(context.Tree, TextSpan.FromBounds(start, end)); + context.ReportIssue(Diagnostic.Create(Rule, location)); + } + } + + private List> PartitionComments(IEnumerable trivia) + { + // Hotpath: avoid unnecessary allocations + List> partitions = null; + List current = null; + var firstEndOfLineFound = false; + + foreach (var trivium in trivia) + { + if (IsSimpleComment(trivium)) + { + AddTriviaToPartition(ref current, trivium, ref firstEndOfLineFound); + } + // This is for the case, of two different comment types, for example: + // // + // /// + else if (IsValidTriviaType(trivium)) // valid but not "//", because of the upper if + { + CloseCurrentPartition(ref current, ref partitions, ref firstEndOfLineFound); + // all comments except single-line comments are parsed as a block already. + AddTriviaToPartition(ref current, trivium, ref firstEndOfLineFound); + CloseCurrentPartition(ref current, ref partitions, ref firstEndOfLineFound); + } + // This handles an empty line, for example: + // // some comment \n <- EOL found, firstEndOfLineFound set to true + // // \n <- EOL is set to false at CommentTrivia, set to true after it + // // some other comment <- EOL is set to false at CommentTrivia, set to true after it + // \n <- EOL found, is already true, closes current partition + else if (IsEndOfLine(trivium)) + { + if (firstEndOfLineFound) + { + CloseCurrentPartition(ref current, ref partitions, ref firstEndOfLineFound); + } + else + { + firstEndOfLineFound = true; + } + } + else if (!IsWhitespace(trivium)) + { + CloseCurrentPartition(ref current, ref partitions, ref firstEndOfLineFound); + } + } + + CloseCurrentPartition(ref current, ref partitions, ref firstEndOfLineFound); + return partitions; + + // Hotpath: Don't capture variables + static void AddTriviaToPartition(ref List current, SyntaxTrivia trivia, ref bool firstEndOfLineFound) + { + current ??= new(); + current.Add(trivia); + firstEndOfLineFound = false; + } + + static void CloseCurrentPartition(ref List current, ref List> partitions, ref bool firstEndOfLineFound) + { + if (current is { Count: > 0 }) + { + partitions ??= new(); + partitions.Add(current); + current = null; + } + + firstEndOfLineFound = false; + } + } + + private bool IsValidTriviaType(SyntaxTrivia trivia) => + Language.Syntax.IsAnyKind(trivia, Language.SyntaxKind.CommentTrivia); + + private bool IsSimpleComment(SyntaxTrivia trivia) => + Language.Syntax.IsKind(trivia, Language.SyntaxKind.SimpleCommentTrivia); + + private bool IsEndOfLine(SyntaxTrivia trivia) => + Language.Syntax.IsKind(trivia, Language.SyntaxKind.EndOfLineTrivia); + + private bool IsWhitespace(SyntaxTrivia trivia) => + Language.Syntax.IsKind(trivia, Language.SyntaxKind.WhitespaceTrivia); +} diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs index b49dea3e7b7..c03a1e1f4bd 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs @@ -42,10 +42,14 @@ internal sealed class VisualBasicSyntaxFacade : SyntaxFacade public override bool IsKind(SyntaxToken token, SyntaxKind kind) => token.IsKind(kind); + public override bool IsKind(SyntaxTrivia trivia, SyntaxKind kind) => trivia.IsKind(kind); + public override bool IsAnyKind(SyntaxNode node, ISet syntaxKinds) => node.IsAnyKind(syntaxKinds); public override bool IsAnyKind(SyntaxNode node, params SyntaxKind[] syntaxKinds) => node.IsAnyKind(syntaxKinds); + public override bool IsAnyKind(SyntaxTrivia trivia, params SyntaxKind[] syntaxKinds) => trivia.IsAnyKind(syntaxKinds); + public override IEnumerable ArgumentExpressions(SyntaxNode node) => node switch { diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxKindFacade.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxKindFacade.cs index 602fd70132b..2e855745d74 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxKindFacade.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxKindFacade.cs @@ -25,6 +25,11 @@ internal sealed class VisualBasicSyntaxKindFacade : ISyntaxKindFacade SyntaxKind.Attribute; public SyntaxKind[] ClassAndRecordDeclaration => new[] { SyntaxKind.ClassBlock }; public SyntaxKind ClassDeclaration => SyntaxKind.ClassBlock; + public SyntaxKind[] CommentTrivia => new[] + { + SyntaxKind.CommentTrivia, + SyntaxKind.DocumentationCommentTrivia, + }; public SyntaxKind[] ComparisonKinds => new[] { SyntaxKind.GreaterThanExpression, @@ -36,12 +41,15 @@ internal sealed class VisualBasicSyntaxKindFacade : ISyntaxKindFacade SyntaxKind.ConstructorBlock; public SyntaxKind[] DefaultExpressions => new[] { SyntaxKind.NothingLiteralExpression }; + public SyntaxKind EndOfLineTrivia => SyntaxKind.EndOfLineTrivia; public SyntaxKind EnumDeclaration => SyntaxKind.EnumStatement; public SyntaxKind FieldDeclaration => SyntaxKind.FieldDeclaration; public SyntaxKind IdentifierName => SyntaxKind.IdentifierName; public SyntaxKind IdentifierToken => SyntaxKind.IdentifierToken; public SyntaxKind InvocationExpression => SyntaxKind.InvocationExpression; public SyntaxKind InterpolatedStringExpression => SyntaxKind.InterpolatedStringExpression; + public SyntaxKind LeftShiftAssignmentStatement => SyntaxKind.LeftShiftAssignmentStatement; + public SyntaxKind LeftShiftExpression => SyntaxKind.LeftShiftExpression; public SyntaxKind LocalDeclaration => SyntaxKind.LocalDeclarationStatement; public SyntaxKind[] MethodDeclarations => new[] { SyntaxKind.FunctionStatement, SyntaxKind.SubStatement }; public SyntaxKind[] ObjectCreationExpressions => new[] { SyntaxKind.ObjectCreationExpression }; @@ -49,13 +57,13 @@ internal sealed class VisualBasicSyntaxKindFacade : ISyntaxKindFacade SyntaxKind.ParameterList; public SyntaxKind RefKeyword => SyntaxKind.ByRefKeyword; public SyntaxKind ReturnStatement => SyntaxKind.ReturnStatement; + public SyntaxKind RightShiftAssignmentStatement => SyntaxKind.RightShiftAssignmentStatement; + public SyntaxKind RightShiftExpression => SyntaxKind.RightShiftExpression; public SyntaxKind SimpleAssignment => SyntaxKind.SimpleAssignmentStatement; + public SyntaxKind SimpleCommentTrivia => SyntaxKind.CommentTrivia; public SyntaxKind SimpleMemberAccessExpression => SyntaxKind.SimpleMemberAccessExpression; public SyntaxKind[] StringLiteralExpressions => new[] { SyntaxKind.StringLiteralExpression }; public SyntaxKind StructDeclaration => SyntaxKind.StructureBlock; public SyntaxKind[] TypeDeclaration => new[] { SyntaxKind.ClassBlock, SyntaxKind.StructureBlock, SyntaxKind.InterfaceBlock, SyntaxKind.EnumBlock }; - public SyntaxKind LeftShiftExpression => SyntaxKind.LeftShiftExpression; - public SyntaxKind RightShiftExpression => SyntaxKind.RightShiftExpression; - public SyntaxKind LeftShiftAssignmentStatement => SyntaxKind.LeftShiftAssignmentStatement; - public SyntaxKind RightShiftAssignmentStatement => SyntaxKind.RightShiftAssignmentStatement; + public SyntaxKind WhitespaceTrivia => SyntaxKind.WhitespaceTrivia; } diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicSyntaxHelper.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicSyntaxHelper.cs index b9ceda82773..d392971b41c 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicSyntaxHelper.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicSyntaxHelper.cs @@ -98,6 +98,9 @@ public static StatementSyntax GetSucceedingStatement(this StatementSyntax curren public static bool IsAnyKind(this SyntaxToken syntaxToken, params SyntaxKind[] syntaxKinds) => syntaxKinds.Contains((SyntaxKind)syntaxToken.RawKind); + public static bool IsAnyKind(this SyntaxTrivia syntaxTrivia, params SyntaxKind[] syntaxKinds) => + syntaxKinds.Contains((SyntaxKind)syntaxTrivia.RawKind); + public static bool AnyOfKind(this IEnumerable nodes, SyntaxKind kind) => nodes.Any(n => n.RawKind == (int)kind); diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Rules/CommentsShouldNotBeEmpty.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Rules/CommentsShouldNotBeEmpty.cs new file mode 100644 index 00000000000..773f222a0cd --- /dev/null +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Rules/CommentsShouldNotBeEmpty.cs @@ -0,0 +1,55 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 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. + */ + +using System.Text; + +namespace SonarAnalyzer.Rules.VisualBasic; + +[DiagnosticAnalyzer(LanguageNames.VisualBasic)] +public sealed class CommentsShouldNotBeEmpty : CommentsShouldNotBeEmptyBase +{ + protected override ILanguageFacade Language => VisualBasicFacade.Instance; + + protected override string GetCommentText(SyntaxTrivia trivia) => + trivia.Kind() switch + { + SyntaxKind.CommentTrivia => GetText(trivia), + SyntaxKind.DocumentationCommentTrivia => GetDocumentationText(trivia), + }; + + // ' + private static string GetText(SyntaxTrivia trivia) + => trivia.ToString().Trim().Substring(1); + + // ''' + private static string GetDocumentationText(SyntaxTrivia trivia) + { + var stringBuilder = new StringBuilder(); + foreach (var line in trivia.ToFullString().Split(MetricsBase.LineTerminators, StringSplitOptions.None)) + { + var trimmedLine = line.TrimStart(null); + trimmedLine = trimmedLine.StartsWith("'''") + ? trimmedLine.Substring(3).Trim() + : trimmedLine.TrimEnd(null); + stringBuilder.Append(trimmedLine); + } + return stringBuilder.ToString(); + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs index 0cb1259d864..1f44dcb4ef6 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs @@ -4587,7 +4587,7 @@ internal static class RuleTypeMappingCS // ["S4660"], // ["S4661"], // ["S4662"], - // ["S4663"], + ["S4663"] = "CODE_SMELL", // ["S4664"], // ["S4665"], // ["S4666"], diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs index 755f2ef4aa9..54584469d62 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs @@ -4587,7 +4587,7 @@ internal static class RuleTypeMappingVB // ["S4660"], // ["S4661"], // ["S4662"], - // ["S4663"], + ["S4663"] = "CODE_SMELL", // ["S4664"], // ["S4665"], // ["S4666"], diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/CommentsShouldNotBeEmptyTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/CommentsShouldNotBeEmptyTest.cs new file mode 100644 index 00000000000..6dea699cc0a --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/CommentsShouldNotBeEmptyTest.cs @@ -0,0 +1,39 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 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. + */ + +using CS = SonarAnalyzer.Rules.CSharp; +using VB = SonarAnalyzer.Rules.VisualBasic; + +namespace SonarAnalyzer.UnitTest.Rules; + +[TestClass] +public class CommentsShouldNotBeEmptyTest +{ + private readonly VerifierBuilder builderCS = new VerifierBuilder(); + private readonly VerifierBuilder builderVB = new VerifierBuilder(); + + [TestMethod] + public void CommentsShouldNotBeEmpty_CS() => + builderCS.AddPaths("CommentsShouldNotBeEmpty.cs").Verify(); + + [TestMethod] + public void CommentsShouldNotBeEmpty_VB() => + builderVB.AddPaths("CommentsShouldNotBeEmpty.vb").Verify(); +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/CommentsShouldNotBeEmpty.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/CommentsShouldNotBeEmpty.cs new file mode 100644 index 00000000000..095eb9350f8 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/CommentsShouldNotBeEmpty.cs @@ -0,0 +1,392 @@ +using System; + +[Obsolete] // Ipsem Lorum +public class SingleLine // +// hey + +// Noncompliant@-3 (inline comment) +{ // Ipsem Lorum + // + + // Noncompliant@-2 (a lot of whitespace before) + + // + + // Noncompliant@-2 (a lot of whitespace after) + + // * + // Ipsem Lorum + // + + // hey + // + // + // + + // + // + // hey + // + // + // + + // + // + // + // hey + // + // + // + + + // + + // Noncompliant @-2 + + // \r + + // \n + + // \r\n + + // \t + + // z̶̤͚̅̍a̷͈̤̪͌͛̈ļ̷̈͐͝g̸̰̈́͂̆o̴̓̏͜ + + // /**/ + + // /// + + // // + + // /** */ + + // + // + // hey + // + // + // there + // + // + + // + // + // + /// text + // Noncompliant@-4 + + // + // + // + /* + * text + */ + // Noncompliant@-6 + + // + // + // + /** + * text + */ + // Noncompliant@-6 + + void Method() + { + // Noncompliant@+2 + + // + // + // + var x = 42; // + // + // + // + + // Noncompliant@-5 + // Noncompliant@-5 + + // + // + // hello + // + // + var y = 42; // + // + // + // + // there + + // Noncompliant@-6 + + // + } // + // + + // Noncompliant@-4 + // Noncompliant@-4 + // Noncompliant@-4 +} + +[Obsolete] /// Ipsem Lorum +public class SingleLineDocumentation /// +// Noncompliant@-1 (inline comment) +{ + /// + // Noncompliant@-1 (a lot of whitespace before) + + /// + // Noncompliant@-1 (a lot of whitespace after) + + /// + /// * + /// Ipsem Lorum + /// \\n + /// + + /// + /// hey there + + /// hey there + /// text + /// + + /// /// + + /// // + + /// /* */ + + /// /** */ + + /// \r + + /// \n + + /// \r\n + + /// \t + + /// z̶̤͚̅̍a̷͈̤̪͌͛̈ļ̷̈͐͝g̸̰̈́͂̆o̴̓̏͜ + + /// + /// + /// + // Noncompliant @-3 +} + +[Obsolete] /* Ipsem Lorum */ +[Serializable] /**/ +// Noncompliant @-1 (inline comment) +public class MultiLine /* */ +// Noncompliant @-1 (inline comment) +{ + /* */ + // Noncompliant@-1 (a lot of whitespace before) + + /* */ + // Noncompliant@-1 (a lot of whitespace inside) + + /**/ + // Noncompliant@-1 (a lot of whitespace after) + + // Noncompliant@+1 + /* + * + */ + + // Noncompliant@+1 + /* + * + */ + + // Noncompliant@+1 + /* + * + * + * + */ + + + // Noncompliant@+1 + /* + + */ + + // Noncompliant@+1 + /* + + + + */ + + /* hey + */ + + /* + there */ + + /* hey + * + */ + + /* + * + there */ + + /* + + Ipsem Lorum + \n + */ + + /* + hey there + */ + + /* + // + */ + + /* + /// + */ + + /* + \r + */ + + /* + \n + */ + + /* + \r\n + */ + + /* + \t + */ + + /* + z̶̤͚̅̍a̷͈̤̪͌͛̈ļ̷̈͐͝g̸̰̈́͂̆o̴̓̏͜ + */ +} + +[Obsolete] /** Ipsem Lorum */ +[Serializable] /***/ +// Noncompliant @-1 (inline comment) +public class MultiLineDocumentation /** */ +// Noncompliant @-1 (inline comment) +{ + /** */ + // Noncompliant@-1 (a lot of whitespace before) + + /** */ + // Noncompliant@-1 (a lot of whitespace inside) + + /***/ + // Noncompliant@-1 (a lot of whitespace after) + + // Noncompliant@+1 + /** + * + */ + + // Noncompliant@+1 + /** + * + * + * + */ + + + // Noncompliant@+1 + /** + + */ + + // Noncompliant@+1 + /** + + + + */ + + /** hey + */ + + /** + there */ + + /** hey + * + */ + + /** + * + there */ + + /** + + Ipsem Lorum + \n + */ + + /** + hey there + */ + + /** + // + */ + + /** + /// + */ + + /** + \r + */ + + /** + \n + */ + + /** + \r\n + */ + + /** + \t + */ + + /** + z̶̤͚̅̍a̷͈̤̪͌͛̈ļ̷̈͐͝g̸̰̈́͂̆o̴̓̏͜ + */ +} + +// +// hey +// +// +// there +// + +/// +/// hey +/// +/// there +/// + + +// Noncompliant@+2 + +// +// +// + +// Noncompliant@+1 +/// +/// diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/CommentsShouldNotBeEmpty.vb b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/CommentsShouldNotBeEmpty.vb new file mode 100644 index 00000000000..8d5000fd227 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/CommentsShouldNotBeEmpty.vb @@ -0,0 +1,144 @@ +Public Class Testcases + + Public Sub Comment() ' + + ' Noncompliant@-2 (inline comment) + + Dim x = 42 'Ipsem Lorum + Dim y = 42 ' Ipsem Lorum + Dim z = 42 ' + + ' Noncompliant@-2 + + Dim a = 42 '''Ipsem Lorum + Dim b = 42 ''' Ipsem Lorum + Dim c = 42 ''' + + ' + + ' Noncompliant@-2 (whitespace) + + + ' + ' + ' + ' hey + + ' hey + ' + ' + ' + + ' Noncompliant@+2 + + ' + ' + ' + ' + + ' Noncompliant@+2 + + ' + + ' + ' + ' + + ' Noncompliant@-4 + + ' * + ' Ipsem Lorum + 'Ipsem Lorum + ' + + ' + + ' Noncompliant @-2 + + ' \r + + ' \n + + ' \r\n + + ' \t + + ' z̶̤͚̅̍a̷͈̤̪͌͛̈ļ̷̈͐͝g̸̰̈́͂̆o̴̓̏͜ + + ' ''' + + '''' + + ' ' + + '' + + '''''' + + ' + ' + ' + ''' Ipsem Lorum + ' Noncompliant@-4 + End Sub + + Public Sub DocumentationComment() + ''' + ' Noncompliant@-1 (whitespace) + + ''' * + ''' Ipsem Lorum + '''Ipsem Lorum + ''' + + ''' Ipsem Lorum + + '''Ipsem Lorum + + ''' + ' Noncompliant @-1 + + ' Noncompliant @+1 + ''' + ''' + ''' + + ''' \r + + ''' \n + + ''' \r\n + + ''' \t + + ''' z̶̤͚̅̍a̷͈̤̪͌͛̈ļ̷̈͐͝g̸̰̈́͂̆o̴̓̏͜ + + ''' ''' + + ''' ' + End Sub + +End Class +' +' hey +' +' +' there +' + +''' +''' hey +''' +''' there +''' + + +' Noncompliant@+2 + +' +' +' + +' Noncompliant@+1 +''' +'''