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
+'''
+'''