diff --git a/analyzers/NuGet.Config b/analyzers/NuGet.Config index 0118683ee77..2b0930d2017 100644 --- a/analyzers/NuGet.Config +++ b/analyzers/NuGet.Config @@ -17,27 +17,24 @@ - + - + - - + - - protobuf-packages;Microsoft;sharwell;meirb;dotnetfoundation;castleproject;jonorossi;onovotny;fluentassertions;SteveGilham;jamesnk;commandlineparser;grpc-packages;Fody;NSubstitute + protobuf-packages;Microsoft;sharwell;meirb;kzu;dotnetfoundation;castleproject;jonorossi;onovotny;fluentassertions;SteveGilham;jamesnk;commandlineparser;grpc-packages - - + diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/RegularExpressions/RegexShouldNotContainMultipleSpaces.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/RegularExpressions/RegexShouldNotContainMultipleSpaces.cs new file mode 100644 index 00000000000..472b2a317d8 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/RegularExpressions/RegexShouldNotContainMultipleSpaces.cs @@ -0,0 +1,27 @@ +/* + * 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. + */ + +namespace SonarAnalyzer.Rules.CSharp; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class RegexShouldNotContainMultipleSpaces : RegexShouldNotContainMultipleSpacesBase +{ + protected override ILanguageFacade Language => CSharpFacade.Instance; +} diff --git a/analyzers/src/SonarAnalyzer.Common/RegularExpressions/RegexContext.cs b/analyzers/src/SonarAnalyzer.Common/RegularExpressions/RegexContext.cs index 6bbb84be2a9..6863b275afd 100644 --- a/analyzers/src/SonarAnalyzer.Common/RegularExpressions/RegexContext.cs +++ b/analyzers/src/SonarAnalyzer.Common/RegularExpressions/RegexContext.cs @@ -22,18 +22,19 @@ namespace SonarAnalyzer.RegularExpressions; -internal sealed class RegexContext +[DebuggerDisplay("Pattern = {Pattern}, Options = {Options}")] +public sealed class RegexContext { private static readonly RegexOptions ValidationMask = (RegexOptions)int.MaxValue ^ RegexOptions.Compiled; - private static readonly string[] MatchMethods = new[] - { + private static readonly string[] MatchMethods = + [ nameof(Regex.IsMatch), nameof(Regex.Match), nameof(Regex.Matches), nameof(Regex.Replace), nameof(Regex.Split), - }; + ]; public SyntaxNode PatternNode { get; } public string Pattern { get; } @@ -102,9 +103,14 @@ private static RegexContext FromMethod(ILanguageFacade pattern, language.FindConstantValue(model, pattern) as string, options, - language.FindConstantValue(model, options) is RegexOptions value ? value : null); + FindRegexOptions(language, model, options)); } + private static RegexOptions? FindRegexOptions(ILanguageFacade language, SemanticModel model, SyntaxNode options) where TSyntaxKind : struct => + language.FindConstantValue(model, options) is int constant + ? (RegexOptions)constant + : null; + private static SyntaxNode TryGetNonParamsSyntax(IMethodSymbol method, IMethodParameterLookup parameters, string paramName) => method.Parameters.SingleOrDefault(x => x.Name == paramName) is { } param && parameters.TryGetNonParamsSyntax(param, out var node) diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/RegularExpressions/RegexAnalyzerBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/RegularExpressions/RegexAnalyzerBase.cs new file mode 100644 index 00000000000..e9245352f01 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Rules/RegularExpressions/RegexAnalyzerBase.cs @@ -0,0 +1,49 @@ +/* + * 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 SonarAnalyzer.RegularExpressions; + +namespace SonarAnalyzer.Rules; + +public abstract class RegexAnalyzerBase : SonarDiagnosticAnalyzer + where TSyntaxKind : struct +{ + protected RegexAnalyzerBase(string diagnosticId) : base(diagnosticId) { } + + protected abstract void Analyze(SonarSyntaxNodeReportingContext context, RegexContext regexContext); + + protected sealed override void Initialize(SonarAnalysisContext context) + { + context.RegisterNodeAction( + Language.GeneratedCodeRecognizer, + c => Analyze(c, RegexContext.FromCtor(Language, c.SemanticModel, c.Node)), + Language.SyntaxKind.ObjectCreationExpressions); + + context.RegisterNodeAction( + Language.GeneratedCodeRecognizer, + c => Analyze(c, RegexContext.FromMethod(Language, c.SemanticModel, c.Node)), + Language.SyntaxKind.InvocationExpression); + + context.RegisterNodeAction( + Language.GeneratedCodeRecognizer, + c => Analyze(c, RegexContext.FromAttribute(Language, c.SemanticModel, c.Node)), + Language.SyntaxKind.Attribute); + } +} diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/RegularExpressions/RegexMustHaveValidSyntaxBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/RegularExpressions/RegexMustHaveValidSyntaxBase.cs index 1f44b0ce84d..b0d562f992e 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/RegularExpressions/RegexMustHaveValidSyntaxBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/RegularExpressions/RegexMustHaveValidSyntaxBase.cs @@ -22,7 +22,7 @@ namespace SonarAnalyzer.Rules; -public abstract class RegexMustHaveValidSyntaxBase : SonarDiagnosticAnalyzer +public abstract class RegexMustHaveValidSyntaxBase : RegexAnalyzerBase where TSyntaxKind : struct { private const string DiagnosticId = "S5856"; @@ -31,29 +31,11 @@ public abstract class RegexMustHaveValidSyntaxBase : SonarDiagnosti protected RegexMustHaveValidSyntaxBase() : base(DiagnosticId) { } - protected override void Initialize(SonarAnalysisContext context) + protected sealed override void Analyze(SonarSyntaxNodeReportingContext context, RegexContext regexContext) { - context.RegisterNodeAction( - Language.GeneratedCodeRecognizer, - c => Analyze(c, RegexContext.FromCtor(Language, c.SemanticModel, c.Node)), - Language.SyntaxKind.ObjectCreationExpressions); - - context.RegisterNodeAction( - Language.GeneratedCodeRecognizer, - c => Analyze(c, RegexContext.FromMethod(Language, c.SemanticModel, c.Node)), - Language.SyntaxKind.InvocationExpression); - - context.RegisterNodeAction( - Language.GeneratedCodeRecognizer, - c => Analyze(c, RegexContext.FromAttribute(Language, c.SemanticModel, c.Node)), - Language.SyntaxKind.Attribute); - } - - private void Analyze(SonarSyntaxNodeReportingContext c, RegexContext context) - { - if (context?.ParseError is { } error) + if (regexContext?.ParseError is { } error) { - c.ReportIssue(Rule, context.PatternNode, error.Message); + context.ReportIssue(Rule, regexContext.PatternNode, error.Message); } } } diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/RegularExpressions/RegexShouldNotContainMultipleSpacesBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/RegularExpressions/RegexShouldNotContainMultipleSpacesBase.cs new file mode 100644 index 00000000000..21fb9e914c4 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Rules/RegularExpressions/RegexShouldNotContainMultipleSpacesBase.cs @@ -0,0 +1,48 @@ +/* + * 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.RegularExpressions; +using SonarAnalyzer.RegularExpressions; + +namespace SonarAnalyzer.Rules; + +public abstract class RegexShouldNotContainMultipleSpacesBase : RegexAnalyzerBase + where TSyntaxKind : struct +{ + private const string DiagnosticId = "S101"; + + protected sealed override string MessageFormat => "Regular expressions should not contain multiple spaces."; + + protected RegexShouldNotContainMultipleSpacesBase() : base(DiagnosticId) { } + + protected sealed override void Analyze(SonarSyntaxNodeReportingContext context, RegexContext regexContext) + { + if (regexContext?.Regex is not null + && !IgnoresPatternWhitespace(regexContext) + && regexContext.Pattern.Contains(" ")) + { + context.ReportIssue(Diagnostic.Create(Rule, regexContext.PatternNode.GetLocation())); + } + } + + private bool IgnoresPatternWhitespace(RegexContext context) => + context.Options is { } options + && options.HasFlag(RegexOptions.IgnorePatternWhitespace); +} diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Rules/RegularExpressions/RegexShouldNotContainMultipleSpaces.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Rules/RegularExpressions/RegexShouldNotContainMultipleSpaces.cs new file mode 100644 index 00000000000..82fafb23c78 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Rules/RegularExpressions/RegexShouldNotContainMultipleSpaces.cs @@ -0,0 +1,27 @@ +/* + * 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. + */ + +namespace SonarAnalyzer.Rules.VisualBasic; + +[DiagnosticAnalyzer(LanguageNames.VisualBasic)] +public sealed class RegexShouldNotContainMultipleSpaces : RegexShouldNotContainMultipleSpacesBase +{ + protected override ILanguageFacade Language => VisualBasicFacade.Instance; +} diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/RegularExpressions/RegexMustHaveValidSyntaxTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/RegularExpressions/RegexMustHaveValidSyntaxTest.cs index 60642591652..496a9420915 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/RegularExpressions/RegexMustHaveValidSyntaxTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/RegularExpressions/RegexMustHaveValidSyntaxTest.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Text.RegularExpressions; -using SonarAnalyzer.RegularExpressions; using CS = SonarAnalyzer.Rules.CSharp; using VB = SonarAnalyzer.Rules.VisualBasic; diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/RegularExpressions/RegexShouldNotContainMultipleSpacesTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/RegularExpressions/RegexShouldNotContainMultipleSpacesTest.cs new file mode 100644 index 00000000000..d7feedf3c37 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/RegularExpressions/RegexShouldNotContainMultipleSpacesTest.cs @@ -0,0 +1,46 @@ +/* + * 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. + */ + +using CS = SonarAnalyzer.Rules.CSharp; +using VB = SonarAnalyzer.Rules.VisualBasic; + +namespace SonarAnalyzer.Test.Rules; + +[TestClass] +public class RegexShouldNotContainMultipleSpacesTest +{ + private readonly VerifierBuilder builderCS = new VerifierBuilder() + .WithBasePath("RegularExpressions") + .AddReferences(MetadataReferenceFacade.RegularExpressions) + .AddReferences(NuGetMetadataReference.SystemComponentModelAnnotations()); + + private readonly VerifierBuilder builderVB = new VerifierBuilder() + .WithBasePath("RegularExpressions") + .AddReferences(MetadataReferenceFacade.RegularExpressions) + .AddReferences(NuGetMetadataReference.SystemComponentModelAnnotations()); + + [TestMethod] + public void RegexShouldNotContainMultipleSpaces_CS() => + builderCS.AddPaths("RegexShouldNotContainMultipleSpaces.cs").Verify(); + + [TestMethod] + public void RegexShouldNotContainMultipleSpaces_VB() => + builderVB.AddPaths("RegexShouldNotContainMultipleSpaces.vb").Verify(); +} diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/RegularExpressions/RegexShouldNotContainMultipleSpaces.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/RegularExpressions/RegexShouldNotContainMultipleSpaces.cs new file mode 100644 index 00000000000..7ad84d16f0c --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/RegularExpressions/RegexShouldNotContainMultipleSpaces.cs @@ -0,0 +1,139 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Text.RegularExpressions; + +class Compliant +{ + void Ctor() + { + var defaultOrder = new Regex("single space"); // Compliant + + var namedArgs = new Regex( + pattern: "single space"); + + var noRegex = new NoRegex("single space"); // Compliant + + var singleOption = new Regex("ignore pattern white space", RegexOptions.IgnorePatternWhitespace); // Compliant + var mixedOptions = new Regex("ignore pattern white space", RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); // Compliant + } + + void Static() + { + var isMatch = Regex.IsMatch("some input", "single space"); // Compliant + var noRegex = NoRegex.IsMatch("some input", "multiple white spaces"); // Compliant + } + + bool ConcatanationMultiline(string input) + { + return Regex.IsMatch(input, "single space" + + "|b" + + "|c" + + "|d]"); // Compliant + } + + bool ConcatanationSingleIne(string input) + { + return Regex.IsMatch(input, "a" + "|b" + "|c" + "|single white space"); // Compliant + } + + [RegularExpression("single space")] // Compliant + public string Attribute { get; set; } + + bool WhiteSpaceVariations(string input) + { + return Regex.IsMatch(input, " with multple single spaces ") + || Regex.IsMatch(input, "without_spaces") + || Regex.IsMatch(input, "with\ttab") + || Regex.IsMatch(input, "") + || Regex.IsMatch(input, "\x09 character tabulation") + || Regex.IsMatch(input, "\x0A line feed") + || Regex.IsMatch(input, "\x0B line tabulation") + || Regex.IsMatch(input, "\x0C form feed") + || Regex.IsMatch(input, "\x0D carriage feed") + || Regex.IsMatch(input, "\x85 next line") + || Regex.IsMatch(input, "\xA0 non-break space") + || Regex.IsMatch(input, "ignore pattern white space", RegexOptions.IgnorePatternWhitespace); + } +} + +class Noncompliant +{ + private const string Prefix = ".*"; + + void Ctor() + { + var patternOnly = new Regex("multiple white spaces"); // Noncompliant {{Regular expressions should not contain multiple spaces.}} + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + var withConst = new Regex(Prefix + "multiple white spaces"); // Noncompliant + } + + void Static() + { + var isMatch = Regex.IsMatch("some input", "multiple white spaces"); // Noncompliant + // ^^^^^^^^^^^^^^^^^^^^^^^^^ + var match = Regex.Match("some input", "multiple white spaces"); // Noncompliant + var matches = Regex.Matches("some input", "multiple white spaces"); // Noncompliant + var split = Regex.Split("some input", "multiple white spaces"); // Noncompliant + + var replace = Regex.Replace("some input", "multiple white spaces", "some replacement"); // Noncompliant + } + + bool Multiline(string input) + { + return Regex.IsMatch(input, + @"|b + |c + |multiple white spaces"); // Noncompliant @-2 + } + + bool ConcatanationMultiline(string input) + { + return Regex.IsMatch(input, "a" // Noncompliant + + "|b" + + "|c" + + "|multiple white spaces"); + } + + bool ConcatanationSingleIne(string input) + { + return Regex.IsMatch(input, "a" + "|b" + "|c" + "|multiple white spaces"); // Noncompliant + } + + [RegularExpression("multiple white spaces")] // Noncompliant + public string Attribute { get; set; } + + [System.ComponentModel.DataAnnotations.RegularExpression("multiple white spaces")] // Noncompliant + public string AttributeFullySpecified { get; set; } + + [global::System.ComponentModel.DataAnnotations.RegularExpression("multiple white spaces")] // Noncompliant + public string AttributeGloballySpecified { get; set; } +} + +class Invalid +{ + void FalseNegative(string unknown) + { + var regex = new NoRegex(unknown + "multiple white spaces"); // FN + } + + void FalsePositive() + { + var withComment = new Regex("(?# comment with spaces)"); // Noncompliant, FP + } +} + +class DoesNotCrash +{ + bool UnknownVariable(string input) + { + return Regex.IsMatch(input, "a" + undefined); // Error [CS0103] The name 'undefined' does not exist in the current context + } +} + +public class NoRegex +{ + public NoRegex(string pattern) { } + + public static bool IsMatch(string input, string pattern) { return true; } +} diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/RegularExpressions/RegexShouldNotContainMultipleSpaces.vb b/analyzers/tests/SonarAnalyzer.Test/TestCases/RegularExpressions/RegexShouldNotContainMultipleSpaces.vb new file mode 100644 index 00000000000..06c9a59c770 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/RegularExpressions/RegexShouldNotContainMultipleSpaces.vb @@ -0,0 +1,34 @@ +Imports System.ComponentModel.DataAnnotations +Imports System.Text.RegularExpressions + +Class Compliant + Private Sub Ctor() + Dim defaultOrder = New Regex("single space") + Dim namedArgs = New Regex(options:=RegexOptions.IgnorePatternWhitespace, pattern:="ignore pattern white space") + End Sub + + Private Sub [Static]() + Dim isMatch = Regex.IsMatch("some input", "single space") + End Sub + + + Public Property Attribute As String +End Class + +Class Noncompliant + Private Sub Ctor() + Dim patternOnly = New Regex("multiple white spaces") ' Noncompliant {{Regular expressions should not contain multiple spaces.}} + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + End Sub + + Private Sub [Static]() + Dim isMatch = Regex.IsMatch("some input", "multiple white spaces") ' Noncompliant + Dim match = Regex.Match("some input", "multiple white spaces") ' Noncompliant + Dim matches = Regex.Matches("some input", "multiple white spaces") ' Noncompliant + Dim split = Regex.Split("some input", "multiple white spaces") ' Noncompliant + Dim replace = Regex.Replace("some input", "multiple white spaces", "some replacement") ' Noncompliant + End Sub + + ' Noncompliant + Public Property Attribute As String +End Class