From 6aa54540a442a5db5ac6821956cf041d1e215618 Mon Sep 17 00:00:00 2001 From: Antonio Aversa Date: Thu, 23 Feb 2023 09:40:30 +0100 Subject: [PATCH] New rule S4545: "DebuggerDisplayAttribute" strings should reference existing members (#6728) --- analyzers/rspec/cs/S4545_c#.html | 23 ++ analyzers/rspec/cs/S4545_c#.json | 15 + analyzers/rspec/cs/Sonar_way_profile.json | 1 + analyzers/rspec/vbnet/S4545_vb.net.html | 25 ++ analyzers/rspec/vbnet/S4545_vb.net.json | 15 + analyzers/rspec/vbnet/Sonar_way_profile.json | 1 + .../Facade/CSharpSyntaxFacade.cs | 3 + .../DebuggerDisplayUsesExistingMembers.cs | 35 +++ .../Common/RegexConstants.cs | 26 ++ .../Facade/SyntaxFacade.cs | 1 + .../DebuggerDisplayUsesExistingMembersBase.cs | 101 +++++++ .../Facade/VisualBasicSyntaxFacade.cs | 3 + .../DebuggerDisplayUsesExistingMembers.cs | 35 +++ .../PackagingTests/RuleTypeMappingCS.cs | 2 +- .../PackagingTests/RuleTypeMappingVB.cs | 2 +- .../DebuggerDisplayUsesExistingMembersTest.cs | 61 ++++ ...ggerDisplayUsesExistingMembers.CSharp10.cs | 50 ++++ ...ggerDisplayUsesExistingMembers.CSharp11.cs | 31 ++ ...uggerDisplayUsesExistingMembers.CSharp8.cs | 19 ++ .../DebuggerDisplayUsesExistingMembers.cs | 267 ++++++++++++++++++ .../DebuggerDisplayUsesExistingMembers.vb | 224 +++++++++++++++ 21 files changed, 938 insertions(+), 2 deletions(-) create mode 100644 analyzers/rspec/cs/S4545_c#.html create mode 100644 analyzers/rspec/cs/S4545_c#.json create mode 100644 analyzers/rspec/vbnet/S4545_vb.net.html create mode 100644 analyzers/rspec/vbnet/S4545_vb.net.json create mode 100644 analyzers/src/SonarAnalyzer.CSharp/Rules/DebuggerDisplayUsesExistingMembers.cs create mode 100644 analyzers/src/SonarAnalyzer.Common/Common/RegexConstants.cs create mode 100644 analyzers/src/SonarAnalyzer.Common/Rules/DebuggerDisplayUsesExistingMembersBase.cs create mode 100644 analyzers/src/SonarAnalyzer.VisualBasic/Rules/DebuggerDisplayUsesExistingMembers.cs create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/Rules/DebuggerDisplayUsesExistingMembersTest.cs create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.CSharp10.cs create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.CSharp11.cs create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.CSharp8.cs create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.cs create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.vb diff --git a/analyzers/rspec/cs/S4545_c#.html b/analyzers/rspec/cs/S4545_c#.html new file mode 100644 index 00000000000..1ddd69d9df0 --- /dev/null +++ b/analyzers/rspec/cs/S4545_c#.html @@ -0,0 +1,23 @@ +

The DebuggerDisplayAttribute is used to determine how an object is displayed in the debugger window.

+

The DebuggerDisplayAttribute constructor takes a single argument: the string to be displayed in the value column for instances of the +type. Any text within curly braces is evaluated as the name of a field, property, or method.

+

Naming a non-existent field, property or method between curly braces will result in a CS0103 error in the debug window when debugging objects. +Although there is no impact on the production code, providing a wrong value can lead to difficulties when debugging the application.

+

This rule raises an issue when text specified between curly braces refers to members that don’t exist in the current context.

+

Noncompliant Code Example

+
+[DebuggerDisplay("Name: {Name}")] // Noncompliant - Name doesn't exist in this context
+public class Person
+{
+    public string FullName { get; private set; }
+}
+
+

Compliant Solution

+
+[DebuggerDisplay("Name: {FullName}")]
+public class Person
+{
+    public string FullName { get; private set; }
+}
+
+ diff --git a/analyzers/rspec/cs/S4545_c#.json b/analyzers/rspec/cs/S4545_c#.json new file mode 100644 index 00000000000..70b4930cf47 --- /dev/null +++ b/analyzers/rspec/cs/S4545_c#.json @@ -0,0 +1,15 @@ +{ + "title": "\"DebuggerDisplayAttribute\" strings should reference existing members", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-4545", + "sqKey": "S4545", + "scope": "All", + "quickfix": "unknown" +} diff --git a/analyzers/rspec/cs/Sonar_way_profile.json b/analyzers/rspec/cs/Sonar_way_profile.json index af947b58cfc..71e5333aef8 100644 --- a/analyzers/rspec/cs/Sonar_way_profile.json +++ b/analyzers/rspec/cs/Sonar_way_profile.json @@ -241,6 +241,7 @@ "S4502", "S4507", "S4524", + "S4545", "S4581", "S4583", "S4586", diff --git a/analyzers/rspec/vbnet/S4545_vb.net.html b/analyzers/rspec/vbnet/S4545_vb.net.html new file mode 100644 index 00000000000..80db2002279 --- /dev/null +++ b/analyzers/rspec/vbnet/S4545_vb.net.html @@ -0,0 +1,25 @@ +

The DebuggerDisplayAttribute is used to determine how an object is displayed in the debugger window.

+

The DebuggerDisplayAttribute constructor takes a single argument: the string to be displayed in the value column for instances of the +type. Any text within curly braces is evaluated as the name of a field, property, or method.

+

Naming a non-existent field, property or method between curly braces will result in a BC30451 error in the debug window when debugging objects. +Although there is no impact on the production code, providing a wrong value can lead to difficulties when debugging the application.

+

This rule raises an issue when text specified between curly braces refers to members that don’t exist in the current context.

+

Noncompliant Code Example

+
+<DebuggerDisplay("Name: {Name}")> ' Noncompliant - Name doesn't exist in this context
+Public Class Person
+
+    Public Property FullName As String
+
+End Class
+
+

Compliant Solution

+
+<DebuggerDisplay("Name: {FullName}")>
+Public Class Person
+
+    Public Property FullName As String
+
+End Class
+
+ diff --git a/analyzers/rspec/vbnet/S4545_vb.net.json b/analyzers/rspec/vbnet/S4545_vb.net.json new file mode 100644 index 00000000000..70b4930cf47 --- /dev/null +++ b/analyzers/rspec/vbnet/S4545_vb.net.json @@ -0,0 +1,15 @@ +{ + "title": "\"DebuggerDisplayAttribute\" strings should reference existing members", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-4545", + "sqKey": "S4545", + "scope": "All", + "quickfix": "unknown" +} diff --git a/analyzers/rspec/vbnet/Sonar_way_profile.json b/analyzers/rspec/vbnet/Sonar_way_profile.json index a54c097cfa1..f0c78503a4d 100644 --- a/analyzers/rspec/vbnet/Sonar_way_profile.json +++ b/analyzers/rspec/vbnet/Sonar_way_profile.json @@ -108,6 +108,7 @@ "S4423", "S4428", "S4507", + "S4545", "S4581", "S4583", "S4586", diff --git a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs index c26432354f0..21f0560d744 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs @@ -50,6 +50,9 @@ internal sealed class CSharpSyntaxFacade : SyntaxFacade public override bool IsNullLiteral(SyntaxNode node) => node.IsNullLiteral(); + public override bool IsKnownAttributeType(SyntaxNode attribute, KnownType knownType, SemanticModel model) => + AttributeSyntaxExtensions.IsKnownType(Cast(attribute), knownType, model); + public override IEnumerable ArgumentExpressions(SyntaxNode node) => node switch { diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/DebuggerDisplayUsesExistingMembers.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/DebuggerDisplayUsesExistingMembers.cs new file mode 100644 index 00000000000..15455c23004 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/DebuggerDisplayUsesExistingMembers.cs @@ -0,0 +1,35 @@ +/* + * 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 DebuggerDisplayUsesExistingMembers : DebuggerDisplayUsesExistingMembersBase +{ + protected override ILanguageFacade Language => CSharpFacade.Instance; + + protected override SyntaxNode AttributeFormatString(AttributeSyntax attribute) => + attribute.ArgumentList.Arguments.FirstOrDefault() is { Expression: LiteralExpressionSyntax { RawKind: (int)SyntaxKind.StringLiteralExpression } formatString } + ? formatString + : null; + + protected override bool IsValidMemberName(string memberName) => + SyntaxFacts.IsValidIdentifier(memberName); +} diff --git a/analyzers/src/SonarAnalyzer.Common/Common/RegexConstants.cs b/analyzers/src/SonarAnalyzer.Common/Common/RegexConstants.cs new file mode 100644 index 00000000000..58c78e05571 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Common/RegexConstants.cs @@ -0,0 +1,26 @@ +/* + * 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.Common; + +public static class RegexConstants +{ + public static TimeSpan DefaultTimeout => TimeSpan.FromMilliseconds(100); +} diff --git a/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs b/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs index 9144a08af14..f08ceed8ff4 100644 --- a/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs +++ b/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs @@ -32,6 +32,7 @@ public abstract class SyntaxFacade 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 bool IsKnownAttributeType(SyntaxNode attribute, KnownType knownType, SemanticModel model); 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/DebuggerDisplayUsesExistingMembersBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/DebuggerDisplayUsesExistingMembersBase.cs new file mode 100644 index 00000000000..1453d0d5150 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Rules/DebuggerDisplayUsesExistingMembersBase.cs @@ -0,0 +1,101 @@ +/* + * 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 Microsoft.CodeAnalysis; + +namespace SonarAnalyzer.Rules; + +public abstract class DebuggerDisplayUsesExistingMembersBase : SonarDiagnosticAnalyzer + where TAttributeSyntax : SyntaxNode + where TSyntaxKind : struct +{ + private const string DiagnosticId = "S4545"; + + private readonly Regex evaluatedExpressionRegex = new(@"\{(?[^}]+)\}", RegexOptions.Multiline, RegexConstants.DefaultTimeout); + + protected abstract SyntaxNode AttributeFormatString(TAttributeSyntax attribute); + protected abstract bool IsValidMemberName(string memberName); + + protected override string MessageFormat => "'{0}' doesn't exist in this context."; + + protected DebuggerDisplayUsesExistingMembersBase() : base(DiagnosticId) { } + + protected override void Initialize(SonarAnalysisContext context) => + context.RegisterNodeAction(Language.GeneratedCodeRecognizer, + c => + { + var attribute = (TAttributeSyntax)c.Node; + if (Language.Syntax.IsKnownAttributeType(attribute, KnownType.System_Diagnostics_DebuggerDisplayAttribute, c.SemanticModel) + && AttributeFormatString(attribute) is { } formatString + && Language.Syntax.StringValue(formatString, c.SemanticModel) is { } formatStringText + && FirstInvalidMemberName(c, formatStringText, attribute) is { } firstInvalidMember) + { + c.ReportIssue(Diagnostic.Create(Rule, formatString.GetLocation(), firstInvalidMember)); + } + }, + Language.SyntaxKind.Attribute); + + private string FirstInvalidMemberName(SonarSyntaxNodeReportingContext context, string formatString, TAttributeSyntax attributeSyntax) + { + try + { + return (attributeSyntax.Parent?.Parent is { } targetSyntax + && context.SemanticModel.GetDeclaredSymbol(targetSyntax) is { } targetSymbol + && TypeContainingReferencedMembers(targetSymbol) is { } typeSymbol) + ? FirstInvalidMemberName(typeSymbol) + : null; + } + catch (RegexMatchTimeoutException) + { + return null; + } + + string FirstInvalidMemberName(ITypeSymbol typeSymbol) + { + var allMembers = typeSymbol + .GetSelfAndBaseTypes() + .SelectMany(x => x.GetMembers()) + .Select(x => x.Name) + .ToHashSet(Language.NameComparer); + + foreach (Match match in evaluatedExpressionRegex.Matches(formatString)) + { + if (match.Groups["EvaluatedExpression"] is { Success: true, Value: var evaluatedExpression } + && ExtractValidMemberName(evaluatedExpression) is { } memberName + && !allMembers.Contains(memberName)) + { + return memberName; + } + } + + return null; + } + + string ExtractValidMemberName(string evaluatedExpression) + { + var sanitizedExpression = evaluatedExpression.Split(',')[0].Trim(); + return IsValidMemberName(sanitizedExpression) ? sanitizedExpression : null; + } + + static ITypeSymbol TypeContainingReferencedMembers(ISymbol symbol) => + symbol is ITypeSymbol typeSymbol ? typeSymbol : symbol.ContainingType; + } +} diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs index c03a1e1f4bd..66a63dfaa0e 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs @@ -50,6 +50,9 @@ internal sealed class VisualBasicSyntaxFacade : SyntaxFacade public override bool IsAnyKind(SyntaxTrivia trivia, params SyntaxKind[] syntaxKinds) => trivia.IsAnyKind(syntaxKinds); + public override bool IsKnownAttributeType(SyntaxNode attribute, KnownType knownType, SemanticModel model) => + AttributeSyntaxExtensions.IsKnownType(Cast(attribute), knownType, model); + public override IEnumerable ArgumentExpressions(SyntaxNode node) => node switch { diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Rules/DebuggerDisplayUsesExistingMembers.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Rules/DebuggerDisplayUsesExistingMembers.cs new file mode 100644 index 00000000000..459649f2609 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Rules/DebuggerDisplayUsesExistingMembers.cs @@ -0,0 +1,35 @@ +/* + * 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 DebuggerDisplayUsesExistingMembers : DebuggerDisplayUsesExistingMembersBase +{ + protected override ILanguageFacade Language => VisualBasicFacade.Instance; + + protected override SyntaxNode AttributeFormatString(AttributeSyntax attribute) => + attribute.ArgumentList.Arguments.FirstOrDefault() is SimpleArgumentSyntax { Expression: LiteralExpressionSyntax { RawKind: (int)SyntaxKind.StringLiteralExpression } formatString } + ? formatString + : null; + + protected override bool IsValidMemberName(string memberName) => + SyntaxFacts.IsValidIdentifier(memberName); +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs index 65ea9f59d34..4167bb5d587 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs @@ -4469,7 +4469,7 @@ internal static class RuleTypeMappingCS // ["S4542"], // ["S4543"], // ["S4544"], - // ["S4545"], + ["S4545"] = "CODE_SMELL", // ["S4546"], // ["S4547"], // ["S4548"], diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs index 626cb1e3a0a..b6d6396c5bc 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs @@ -4469,7 +4469,7 @@ internal static class RuleTypeMappingVB // ["S4542"], // ["S4543"], // ["S4544"], - // ["S4545"], + ["S4545"] = "CODE_SMELL", // ["S4546"], // ["S4547"], // ["S4548"], diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/DebuggerDisplayUsesExistingMembersTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/DebuggerDisplayUsesExistingMembersTest.cs new file mode 100644 index 00000000000..7b98e04ee54 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/DebuggerDisplayUsesExistingMembersTest.cs @@ -0,0 +1,61 @@ +/* + * 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 DebuggerDisplayUsesExistingMembersTest +{ + private readonly VerifierBuilder builderCS = new VerifierBuilder(); + private readonly VerifierBuilder builderVB = new VerifierBuilder(); + + [TestMethod] + public void DebuggerDisplayUsesExistingMembers_CS() => + builderCS.AddPaths("DebuggerDisplayUsesExistingMembers.cs").Verify(); + + [TestMethod] + public void DebuggerDisplayUsesExistingMembers_CSharp8() => + builderCS.AddPaths("DebuggerDisplayUsesExistingMembers.CSharp8.cs") + .WithOptions(ParseOptionsHelper.FromCSharp8) + .Verify(); + +#if NET + + [TestMethod] + public void DebuggerDisplayUsesExistingMembers_CSharp10() => + builderCS.AddPaths("DebuggerDisplayUsesExistingMembers.CSharp10.cs") + .WithOptions(ParseOptionsHelper.FromCSharp10) + .Verify(); + +#endif + + [TestMethod] + public void DebuggerDisplayUsesExistingMembers_CSharp11() => + builderCS.AddPaths("DebuggerDisplayUsesExistingMembers.CSharp11.cs") + .WithOptions(ParseOptionsHelper.FromCSharp11) + .Verify(); + + [TestMethod] + public void DebuggerDisplayUsesExistingMembers_VB() => + builderVB.AddPaths("DebuggerDisplayUsesExistingMembers.vb").Verify(); +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.CSharp10.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.CSharp10.cs new file mode 100644 index 00000000000..baa330e4c1b --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.CSharp10.cs @@ -0,0 +1,50 @@ +using System; +using System.Diagnostics; + +[DebuggerDisplay("{RecordProperty}")] +public record SomeRecord(int RecordProperty) +{ + [DebuggerDisplay("{RecordProperty}")] public record struct RecordStruct1(int RecordStructProperty); // Noncompliant + [DebuggerDisplay("{RecordStructProperty}")] public record struct RecordStruct2(int RecordStructProperty); // Compliant, RecordStructProperty is a property + + [DebuggerDisplay("{RecordProperty}")] public record NestedRecord1(int NestedRecordProperty); // Noncompliant + [DebuggerDisplay("{NestedRecordProperty}")] public record NestedRecord2(int NestedRecordProperty); // Compliant, NestedRecordProperty is a property +} + +[DebuggerDisplay("{RecordProperty1} bla bla {RecordProperty2}")] +public record struct SomeRecordStruct(int RecordProperty1, string RecordProperty2) +{ + [DebuggerDisplay("{RecordProperty}")] // Noncompliant + public class NestedClass1 + { + [DebuggerDisplay("{NestedClassProperty}")] + public int NestedClassProperty => 1; + } + + [DebuggerDisplay("{NestedClassProperty}")] + public class NestedClass2 + { + [DebuggerDisplay("{NestedClassProperty}")] + public int NestedClassProperty => 1; + } +} + +public class ConstantInterpolatedStrings +{ + [DebuggerDisplay($"{{{nameof(SomeProperty)}}}")] + [DebuggerDisplay($"{{{nameof(NotAProperty)}}}")] // FN: constant interpolated strings not supported + public int SomeProperty => 1; + + public class NotAProperty { } +} + +public interface DefaultInterfaceImplementations +{ + [DebuggerDisplay("{OtherProperty}")] + [DebuggerDisplay("{OtherPropertyImplemented}")] + [DebuggerDisplay("{Nonexistent}")] // Noncompliant + int WithNonexistentProperty => 1; + + string OtherProperty { get; } + string OtherPropertyImplemented => "Something"; +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.CSharp11.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.CSharp11.cs new file mode 100644 index 00000000000..c9e078e9019 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.CSharp11.cs @@ -0,0 +1,31 @@ +using System.Diagnostics; + +class RawStringLiterals +{ + int SomeProperty => 1; + int SomeField = 2; + + [DebuggerDisplay("""{SomeProperty}""")] int ExistingMemberTripleQuotes => 1; + [DebuggerDisplay(""""{SomeField}"""")] int ExistingMemberQuadrupleQuotes => 1; + [DebuggerDisplay(""" + Some text{SomeField} + """)] int ExistingMultiLine => 1; + [DebuggerDisplay($$""""" + Some text{SomeField} + """"")] int ExistingMultiLineInterpolated => 1; + + [DebuggerDisplay("""{Nonexistent}""")] int NonexistentTripleQuotes => 1; // Noncompliant + // ^^^^^^^^^^^^^^^^^^^ + [DebuggerDisplay(""""{Nonexistent}"""")] int NonexistentQuadrupleQuotes => 1; // Noncompliant + // ^^^^^^^^^^^^^^^^^^^^^ + [DebuggerDisplay(""" + Some text{Nonexistent} + """)] int NonexistentMultiLine1 => 1; // Noncompliant@-2^22#46 + [DebuggerDisplay(""" + Some text{Some + Property} + """)] int NonexistentMultiLine2 => 1; // FN: the new line char make the expression within braces not a valid identifier + [DebuggerDisplay($$""""" + Some text{Nonexistent} + """"")] int NonexistentMultiLineInterpolated => 1; // FN: interpolated raw string literals strings not supported +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.CSharp8.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.CSharp8.cs new file mode 100644 index 00000000000..96ea4a0d938 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.CSharp8.cs @@ -0,0 +1,19 @@ +using System; +using System.Diagnostics; + +public class AccessModifiers +{ + public class BaseClass + { + private protected int PrivateProtectedProperty => 1; + + [DebuggerDisplay("{PrivateProtectedProperty}")] + public int SomeProperty => 1; + } + + public class SubClass : BaseClass + { + [DebuggerDisplay("{PrivateProtectedProperty}")] + public int OtherProperty => 1; + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.cs new file mode 100644 index 00000000000..2de2f760e2d --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.cs @@ -0,0 +1,267 @@ +using System; +using System.Diagnostics; + +class PropertiesAndFields +{ + const string ConstantWithoutInvalidMembers = "Something"; + const string ConstantWithInvalidMember = "{Nonexistent}"; + const string ConstantFragment1 = "{Non"; + const string ConstantFragment2 = "existent}"; + + int SomeProperty => 1; + int SomeField = 2; + + [DebuggerDisplayAttribute("Hardcoded text")] int WithSuffix => 1; + [System.Diagnostics.DebuggerDisplay("Hardcoded text")] int WithNamespace => 1; + [DebuggerDisplay(value: "Hardcoded text")] int WithExplicitParameterName => 1; + + [DebuggerDisplay(null)] int WithEmptyArgList => 1; + [DebuggerDisplay("")] int WithEmptyFormat => 1; + [DebuggerDisplay(ConstantWithoutInvalidMembers)] int WithFormatAsConstant1 => 1; + [DebuggerDisplay(nameof(ConstantWithoutInvalidMembers))] int WithFormatAsNameOf => 1; + + [DebuggerDisplay("{SomeProperty}")] int WithExistingProperty => 1; + [DebuggerDisplay("{SomeField}")] int WithExistingField => 1; + [DebuggerDisplay(@"{SomeField}")] int WithExistingFieldVerbatim => 1; + [DebuggerDisplay(@"Some text + {SomeField}")] int WithExistingFieldVerbatimMultiLine => 1; + + [DebuggerDisplay("{1 + 1}")] int WithNoMemberReferenced1 => 1; + [DebuggerDisplay(@"{""1"" + ""1""}")] int WithNoMemberReferenced2 => 1; + + [DebuggerDisplay("{Nonexistent}")] int WithNonexistentMember1 => 1; // Noncompliant {{'Nonexistent' doesn't exist in this context.}} + // ^^^^^^^^^^^^^^^ + [DebuggerDisplay("1 + {Nonexistent}")] int WithNonexistentMember2 => 1; // Noncompliant {{'Nonexistent' doesn't exist in this context.}} + // ^^^^^^^^^^^^^^^^^^^ + [DebuggerDisplay("{Nonexistent1} bla bla {Nonexistent2}")] int WithMultipleNonexistent => 1; // Noncompliant {{'Nonexistent1' doesn't exist in this context.}} + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + [DebuggerDisplay(@"{Nonexistent}")] int WithNonexistentMemberVerbatim => 1; // Noncompliant {{'Nonexistent' doesn't exist in this context.}} + [DebuggerDisplay(@"Some text + {Nonexistent}")] int WithNonexistentMemberVerbatimMultiLine1 => 1; // Noncompliant@-1^22#34 {{'Nonexistent' doesn't exist in this context.}} + [DebuggerDisplay(@"Some text {Some + Property}")] int WithNonexistentMemberVerbatimMultiLine2 => 1; // FN@-1: the new line char make the expression within braces not a valid identifier + + [DebuggerDisplay(ConstantWithInvalidMember)] int WithFormatAsConstant2 => 1; // FN: constants are not checked + [DebuggerDisplay("{Non" + "Existent}")] int WithFormatAsConcatenationOfLiterals => 1; // FN: only simple literal supported + [DebuggerDisplay("{Non" + + "Existent}")] int WithFormatAsConcatenationOfLiteralsMultiLine => 1; // FN: only simple literal supported + [DebuggerDisplay(ConstantFragment1 + ConstantFragment2)] int WithFormatAsConcatenationOfConstants => 1; // FN: only simple literal supported + + [DebuggerDisplay("{this.NonexistentProperty}")] int PropertyWithExplicitThis => 1; // FN: "this." not supported (valid when debugging a C# project) + [DebuggerDisplay("{Me.NonexistentField}")] int FieldWithExplicitThis => 1; // FN: "Me." not supported (valid when debugging a VB.NET project) + [DebuggerDisplay("{1 + NonexistentProperty}")] int ContainingInvalidMembers => 1; // FN: expressions not supported +} + +[DebuggerDisplay("{this.ToString()}")] // Compliant, valid when debugging a C# project +[DebuggerDisplay("{Me.ToString()}")] // Compliant, valid when debugging a VB.NET project +[DebuggerDisplay("{Nonexistent}")] // Noncompliant +public enum TopLevelEnum { One, Two, Three } + +[DebuggerDisplay("{SomeProperty}")] +[DebuggerDisplay("{SomeField}")] +[DebuggerDisplay("{Nonexistent}")] // Noncompliant +public class NestedTypes +{ + int SomeProperty => 1; + int SomeField = 1; + + [DebuggerDisplay("{ExistingProperty}")] + [DebuggerDisplay("{ExistingField}")] + [DebuggerDisplay("{SomeProperty}")] // Noncompliant + [DebuggerDisplay("{SomeField}")] // Noncompliant + public class NestedClass + { + int ExistingProperty => 1; + int ExistingField => 1; + } + + [DebuggerDisplay("{ExistingProperty}")] + [DebuggerDisplay("{ExistingField}")] + [DebuggerDisplay("{SomeProperty}")] // Noncompliant + [DebuggerDisplay("{SomeField}")] // Noncompliant + public struct NestedStruct + { + int ExistingProperty => 1; + int ExistingField => 1; + } + + [DebuggerDisplay("{42}")] + [DebuggerDisplay("{SomeProperty}")] // Noncompliant + [DebuggerDisplay("{SomeField}")] // Noncompliant + public enum NestedEnum { One, Two, Three } +} + +public class Delegates +{ + int ExistingProperty => 1; + + [DebuggerDisplay("{ExistingProperty}")] // Noncompliant + [DebuggerDisplay("{42}")] + public delegate void Delegate1(); +} + +public class Indexers +{ + int ExistingProperty => 1; + int ExistingField => 1; + + [DebuggerDisplay("{ExistingProperty}")] + [DebuggerDisplay("{ExistingField}")] + [DebuggerDisplay("{Nonexistent}")] // Noncompliant + int this[int i] => 1; +} + +[DebuggerDisplay("{SomeProperty}"), DebuggerDisplay("{SomeField}"), DebuggerDisplay("{Nonexistent}")] // Noncompliant +// ^^^^^^^^^^^^^^^ +public class MultipleAttributes +{ + int SomeProperty => 1; + int SomeField = 1; + + [DebuggerDisplay("{SomeProperty}"), DebuggerDisplay("{SomeField}"), DebuggerDisplay("{Nonexistent}")] // Noncompliant + // ^^^^^^^^^^^^^^^ + int OtherProperty1 => 1; + + [DebuggerDisplay("{Nonexistent1}"), DebuggerDisplay("{Nonexistent2}")] + // ^^^^^^^^^^^^^^^^ + // ^^^^^^^^^^^^^^^^@-1 + int OtherProperty2 => 1; + + [DebuggerDisplay("{Nonexistent1}")][DebuggerDisplay("{Nonexistent2}")] + // ^^^^^^^^^^^^^^^^ + // ^^^^^^^^^^^^^^^^@-1 + int OtherProperty3 => 1; +} + +public class CaseSensitivity +{ + int SOMEPROPERTY => 1; + int SomeProperty => 1; + + [DebuggerDisplay("{SOMEPROPERTY}")] + [DebuggerDisplay("{SomeProperty}")] + [DebuggerDisplay("{someProperty}")] // Noncompliant + [DebuggerDisplay("{someproperty}")] // Noncompliant + int OtherProperty => 1; +} + +public class NonAlphanumericChars +{ + int Aa1_뿓 => 1; + + [DebuggerDisplay("{Aa1_뿓}")] + [DebuggerDisplay("{Aa1_㤬}")] // Noncompliant {{'Aa1_㤬' doesn't exist in this context.}} + int SomeProperty1 => 1; +} + +public class Whitespaces +{ + [DebuggerDisplay("{ SomeProperty}")] + [DebuggerDisplay("{SomeProperty }")] + [DebuggerDisplay("{\tSomeProperty}")] + [DebuggerDisplay("{\tSomeProperty\t}")] + [DebuggerDisplay("{ Nonexistent}")] // Noncompliant {{'Nonexistent' doesn't exist in this context.}} + [DebuggerDisplay("{Nonexistent }")] // Noncompliant {{'Nonexistent' doesn't exist in this context.}} + [DebuggerDisplay("{\tNonexistent}")] // Noncompliant {{'Nonexistent' doesn't exist in this context.}} + [DebuggerDisplay("{\tNonexistent\t}")] // Noncompliant {{'Nonexistent' doesn't exist in this context.}} + int SomeProperty => 1; +} + +public class NoQuotesModifier +{ + [DebuggerDisplay("{SomeProperty,nq}")] + [DebuggerDisplay("{SomeProperty ,nq}")] + [DebuggerDisplay("{SomeProperty, nq}")] + [DebuggerDisplay("{SomeProperty,nq }")] + [DebuggerDisplay("{Nonexistent,nq}")] // Noncompliant {{'Nonexistent' doesn't exist in this context.}} + [DebuggerDisplay("{Nonexistent ,nq}")] // Noncompliant {{'Nonexistent' doesn't exist in this context.}} + [DebuggerDisplay("{Nonexistent, nq}")] // Noncompliant {{'Nonexistent' doesn't exist in this context.}} + [DebuggerDisplay("{Nonexistent,nq }")] // Noncompliant {{'Nonexistent' doesn't exist in this context.}} + int SomeProperty => 1; +} + +public class InvalidModifier +{ + [DebuggerDisplay("{SomeProperty,asdf}")] + [DebuggerDisplay("{SomeProperty, asdf}")] + [DebuggerDisplay("{Nonexistent,asdf}")] // Noncompliant {{'Nonexistent' doesn't exist in this context.}} + [DebuggerDisplay("{Nonexistent, asdf}")] // Noncompliant {{'Nonexistent' doesn't exist in this context.}} + int SomeProperty => 1; +} + +public class OptionalAttributeParameter +{ + [DebuggerDisplay("{SomeProperty}", Name = "Any name")] + [DebuggerDisplay("{Nonexistent}", Name = "Any name")] // Noncompliant {{'Nonexistent' doesn't exist in this context.}} + // ^^^^^^^^^^^^^^^ + [DebuggerDisplay("{Nonexistent}", Name = "Any name", Type = nameof(OptionalAttributeParameter))] // Noncompliant + // ^^^^^^^^^^^^^^^ + [DebuggerDisplay("{Nonexistent}", Name = "Any name", Target = typeof(OptionalAttributeParameter))] // Noncompliant + // ^^^^^^^^^^^^^^^ + int SomeProperty => 1; +} + +public class Inheritance +{ + public class BaseClass + { + int SomeProperty => 1; + } + + public class SubClass : BaseClass + { + [DebuggerDisplay("{SomeProperty}")] // Compliant, defined in base class + int OtherProperty => 1; + } +} + +public class AccessModifiers +{ + public class BaseClass + { + public int PublicProperty => 1; + internal int InternalProperty => 1; + protected int ProtectedProperty => 1; + private int PrivateProperty => 1; + + [DebuggerDisplay("{PublicProperty}")] + [DebuggerDisplay("{InternalProperty}")] + [DebuggerDisplay("{ProtectedProperty}")] + [DebuggerDisplay("{PrivateProperty}")] + int SomeProperty => 1; + } + + public class SubClass : BaseClass + { + [DebuggerDisplay("{PublicProperty}")] + [DebuggerDisplay("{InternalProperty}")] + [DebuggerDisplay("{ProtectedProperty}")] + [DebuggerDisplay("{PrivateProperty}")] + int OtherProperty => 1; + } +} + +public class AttributeTargets +{ + [global:DebuggerDisplay("{Nonexistent}")] // Noncompliant, attribute at global level, referencing a non-existent member + [assembly: DebuggerDisplay("{Nonexistent}")] // Noncompliant, attribute at assembly level, referencing a non-existent member + [field: DebuggerDisplay("{Nonexistent}")] // Noncompliant, attribute ignored, still referencing a non-existent member + [property: DebuggerDisplay("{Nonexistent}")] // Noncompliant, attribute taken into account + int SomeProperty => 1; +} + +public class InvalidAttributes +{ + [DebuggerDisplay] int NoArgs => 1; // Error [CS7036] + [DebuggerDisplay(Type = nameof(InvalidAttributes))] int MissingValue => 1; // Error [CS0029] +} + +namespace WithTypeAlias +{ + using DebuggerDisplayAlias = System.Diagnostics.DebuggerDisplayAttribute; + + public class Test + { + [DebuggerDisplayAlias("{Nonexistent}")] int WithAlias => 1; // Noncompliant: attribute name also checked at semantic level + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.vb b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.vb new file mode 100644 index 00000000000..c89fcc124dd --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/DebuggerDisplayUsesExistingMembers.vb @@ -0,0 +1,224 @@ +Imports System +Imports System.Diagnostics + +Public Class TestOnPropertiesAndFields + Const ConstantWithoutInvalidMembers As String = "Something" + Const ConstantWithInvalidMember As String = "{Nonexistent}" + Const ConstantFragment1 As String = "{Non" + Const ConstantFragment2 As String = "Existent}" + + Public Property SomeProperty As Integer + Public SomeField As Integer + + Property WithSuffix As Integer + Property WithNamespace As Integer + Property WithGlobal As Integer + + Property WithEmptyArgList As Integer + Property WithEmptyFormat As Integer + Property WithFormatAsConstant1 As Integer + Property WithFormatAsNameOf As Integer + + Property WithExistingProperty As Integer + Property WithExistingField As Integer + Property WithExistingFieldVerbatim As Integer + Property WithNoMemberReferenced1 As Integer + Property WithNoMemberReferenced2 As Integer + + Property WithNonexistentMember1 As Integer ' Noncompliant {{'Nonexistent' doesn't exist in this context.}} + ' ^^^^^^^^^^^^^^^ + Property WithNonexistentMember2 As Integer ' Noncompliant {{'Nonexistent' doesn't exist in this context.}} + ' ^^^^^^^^^^^^^^^^^^^ + Property WithMultipleNonexistent As Integer ' Noncompliant {{'Nonexistent1' doesn't exist in this context.}} + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Property WithNonexistentMemberVerbatim As Integer ' Noncompliant {{'Nonexistent' doesn't exist in this context.}} + ' ^^^^^^^^^^^^^^^ + Property WithNamespaceAndNonexistent As Integer ' Noncompliant {{'Nonexistent' doesn't exist in this context.}} + ' ^^^^^^^^^^^^^^^ + Property WithGlobalAndNonexistent As Integer ' Noncompliant {{'Nonexistent' doesn't exist in this context.}} + ' ^^^^^^^^^^^^^^^ + + Property WithFormatAsConstant2 As Integer ' FN: constants are not checked + Property WithFormatAsConcatenationOfLiterals As Integer ' FN: only simple literal supported + Property WithFormatAsConcatenationOfConstants As Integer ' FN: only simple literal supported + + Property PropertyWithExplicitThis As Integer ' FN: "Me." not supported (valid when debugging a VB.NET project) + Property FieldWithExplicitThis As Integer ' FN: "this." not supported (valid when debugging a C# project) + Property ContainingInvalidMembers As Integer ' FN: expressions not supported +End Class + + ' Compliant, valid when debugging a VB.NET project + ' Compliant, valid when debugging a C# project + ' Noncompliant +Public Enum TopLevelEnum + One + Two + Three +End Enum + + + + ' Noncompliant +Public Class TestOnNestedTypes + Public Property SomeProperty As Integer + Public SomeField As Integer + + + + ' Noncompliant + ' Noncompliant + Public Class NestedClass + Property ExistingProperty As Integer + Property ExistingField As Integer + End Class + + + + ' Noncompliant + ' Noncompliant + Public Structure NestedStruct + Property ExistingProperty As Integer + Property ExistingField As Integer + End Structure + + Public Enum NestedEnum + One + Two + Three + End Enum +End Class + +Class TestOnDelegates + Property ExistingProperty As Integer + + ' Noncompliant + + Delegate Sub Delegate1() +End Class + +Class TestOnIndexers + Public Property ExistingProperty As Integer + Public ExistingField As Integer + + + + ' Noncompliant + Default Property Item(ByVal i As Integer) As Integer + Get + Return 1 + End Get + Set(value As Integer) + End Set + End Property +End Class + + ' Noncompliant +Class TestMultipleAttributes + ' ^^^^^^^^^^^^^^^@-1 + + Public Property SomeProperty As Integer + Public SomeField As Integer = 1 + + Property OtherProperty1 As Integer ' Noncompliant + ' ^^^^^^^^^^^^^^^ + + Property OtherProperty2 As Integer + ' ^^^^^^^^^^^^^^^^ + ' ^^^^^^^^^^^^^^^^@-1 + + Property OtherProperty3 As Integer + ' ^^^^^^^^^^^^^^^^ + ' ^^^^^^^^^^^^^^^^@-1 +End Class + +Class SupportCaseInsensitivity + Property SomeProperty As Integer = 1 + + + + + + Property OtherProperty As Integer +End Class + +Class SupportNonAlphanumericChars + Property Aa1_뿓 As Integer + + + ' Noncompliant {{'Aa1_㤬' doesn't exist in this context.}} + Property SomeProperty1 As Integer +End Class + +Class SupportWhitespaces + + + + + ' Noncompliant {{'Nonexistent' doesn't exist in this context.}} + ' Noncompliant {{'Nonexistent' doesn't exist in this context.}} + ' FN: string concatenation not supported + ' FN: string concatenation not supported + Property SomeProperty As Integer +End Class + +Class SupportNq + + + + + ' Noncompliant + ' Noncompliant + ' Noncompliant + ' Noncompliant + Property SomeProperty As Integer +End Class + +Class SupportOptionalAttributeParameter + + ' Noncompliant + ' Noncompliant + ' Noncompliant + Property SomeProperty As Integer + ' ^^^^^^^^^^^^^^^@-3 + ' ^^^^^^^^^^^^^^^@-3 + ' ^^^^^^^^^^^^^^^@-3 +End Class + +Class SupportInheritance + Class BaseClass + Property SomeProperty As Integer + End Class + + Class SubClass + Inherits BaseClass + + ' Compliant, defined in base class + Property OtherProperty As Integer + End Class +End Class + +Class SupportAccessModifiers + Class BaseClass + Public Property PublicProperty As Integer + Friend Property InternalProperty As Integer + Protected Property ProtectedProperty As Integer + Private Property PrivateProperty As Integer + + + + + + Property SomeProperty As Integer + End Class + + Class SubClass + Inherits BaseClass + + + + + + Property OtherProperty As Integer + End Class +End Class +