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