From 1a54f21eadeb86b0773553ca1a4bf6e898544898 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Mon, 23 Feb 2026 23:40:45 -0800 Subject: [PATCH 1/3] fix: Add unit tests and improve analyzers for handling generated code and diagnostics --- .../AsyncVoidTests.cs | 22 +++ .../AttributesOnSeparateLinesTests.cs | 27 ++++ .../DateTimeConversionTests.cs | 25 +++ .../DiagnosticUriBuilderTests.cs | 36 +++++ .../FavorEnumeratorDirectoryCallsTests.cs | 145 ++++++++++++++++++ .../NamingFieldPascalUnderscoreTests.cs | 55 +++++++ .../UnusedLocalVariableTests.cs | 39 ++++- .../Analyzers/AttributesOnSeparateLines.cs | 2 +- .../FavorDirectoryEnumerationCalls.cs | 60 +++++--- .../Analyzers/NamingFieldPascalUnderscore.cs | 6 +- .../Analyzers/NamingMethodPascal.cs | 5 +- .../Analyzers/NamingPropertyPascal.cs | 5 +- .../Analyzers/UnusedLocalVariable.cs | 6 +- .../DiagnosticUrlBuilder.cs | 8 +- 14 files changed, 408 insertions(+), 33 deletions(-) diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AsyncVoidTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AsyncVoidTests.cs index 2ac43b5f..7b5f7387 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AsyncVoidTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AsyncVoidTests.cs @@ -101,6 +101,28 @@ public async Task Sample() { } await VerifyCSharpFix(test, fixTest, allowNewCompilerDiagnostics: true); } + [TestMethod] + [Description("Analyzer should not crash when encountering non-async non-void methods")] + public void NonAsyncNonVoidMethod_NoDiagnosticAndNoCrash() + { + // The analyzer uses 'as IMethodSymbol' then dereferences without null check. + // This test ensures it handles the method symbol safely. + string test = @" + using System; + + namespace ConsoleApplication1 + { + class TypeName + { + public void SyncMethod() { } + public int GetValue() => 42; + public static void StaticMethod() { } + } + }"; + + VerifyCSharpDiagnostic(test); + } + protected override CodeFixProvider GetCSharpCodeFixProvider() { return new CodeFixes.AsyncVoid(); diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AttributesOnSeparateLinesTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AttributesOnSeparateLinesTests.cs index cd7a750a..1d25ea0a 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AttributesOnSeparateLinesTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AttributesOnSeparateLinesTests.cs @@ -390,6 +390,33 @@ static void Main() await VerifyCSharpFix(test, fixTest); } + [TestMethod] + [Description("Analyzer should not report on generated code")] + public void AttributesOnSameLine_InGeneratedCode_NoDiagnostic() + { + // AttributesOnSeparateLines uses GeneratedCodeAnalysisFlags.Analyze | ReportDiagnostics, + // meaning it reports inside generated code. It should skip generated code. + string test = @"using System; +using System.CodeDom.Compiler; + +namespace ConsoleApp +{ + class AAttribute : Attribute { } + class BAttribute : Attribute { } + + [GeneratedCode(""tool"", ""1.0"")] + class Program + { + [A][B] + static void Main() + { + } + } +}"; + // Should NOT produce a diagnostic for attributes on same line inside generated code + VerifyCSharpDiagnostic(test); + } + private static DiagnosticResult GetExpectedDiagnosticResult(int line, int col) { return new DiagnosticResult diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs index 7938ef5b..b37be19a 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs @@ -339,6 +339,31 @@ static void Main(string[] args) }); } + [TestMethod] + [Description("Analyzer should not throw when DateTimeOffset/DateTime types are unresolvable")] + public void AnyDateTimeOffsetConstructorWithUnresolvableTypes_DoesNotThrow() + { + // AnalyzeObjectCreation throws InvalidOperationException if GetTypeByMetadataName returns null. + // Analyzer callbacks must never throw. This test uses code that won't resolve System types. + string source = @" +namespace ConsoleApp +{ + class DateTimeOffset + { + public DateTimeOffset(object arg) { } + } + + class Program + { + static void Main() + { + var dto = new DateTimeOffset(null); + } + } +}"; + VerifyCSharpDiagnostic(source); + } + protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() { return new Analyzers.BanImplicitDateTimeToDateTimeOffsetConversion(); diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DiagnosticUriBuilderTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DiagnosticUriBuilderTests.cs index 6b930487..5d59c84d 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DiagnosticUriBuilderTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DiagnosticUriBuilderTests.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace IntelliTect.Analyzer.Tests @@ -19,5 +20,40 @@ public void GetUrl_GivenBlock00XXCode_ProperlyBuildsUrl(string title, string dia Assert.IsTrue(string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase), $"'{expected}' does not equal '{actual}'"); } + + [TestMethod] + [Description("GetUrl allocates a new Regex on every call — performance regression test")] + public void GetUrl_CalledRepeatedly_DoesNotAllocateExcessively() + { + // Warm up + DiagnosticUrlBuilder.GetUrl("Test Title", "INTL0001"); + + long before = GC.GetAllocatedBytesForCurrentThread(); + + const int iterations = 1000; + for (int i = 0; i < iterations; i++) + { + DiagnosticUrlBuilder.GetUrl("Test Title", "INTL0001"); + } + + long after = GC.GetAllocatedBytesForCurrentThread(); + long bytesPerCall = (after - before) / iterations; + + // A cached Regex or simple string.Replace should allocate ~200-400 bytes per call (strings only). + // A new Regex() per call allocates ~2000+ bytes. Threshold at 500 to catch the Regex allocation. + Assert.IsTrue(bytesPerCall < 500, + $"GetUrl allocated ~{bytesPerCall} bytes/call, suggesting a new Regex is created each time. " + + $"Expected < 500 bytes/call with a cached approach."); + } + + [TestMethod] + [Description("Titles with multiple whitespace types should be hyphenated correctly")] + public void GetUrl_TitleWithTabsAndMultipleSpaces_HyphenatesCorrectly() + { + string actual = DiagnosticUrlBuilder.GetUrl("Fields Multiple\tSpaces", "INTL9999"); + + Assert.IsTrue(actual.Contains("FIELDS--MULTIPLE-SPACES", StringComparison.OrdinalIgnoreCase), + $"Expected all whitespace replaced with hyphens but got: '{actual}'"); + } } } diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/FavorEnumeratorDirectoryCallsTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/FavorEnumeratorDirectoryCallsTests.cs index 97af362d..be495b7b 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/FavorEnumeratorDirectoryCallsTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/FavorEnumeratorDirectoryCallsTests.cs @@ -161,5 +161,150 @@ static void Main(string[] args) "; VerifyCSharpDiagnostic(source); } + + [TestMethod] + [Description("Cast() throws InvalidCastException on generic method calls")] + public void GenericMethodCallOnDirectory_DoesNotThrow() + { + // memberAccess.ChildNodes().Cast() will throw + // if any child node is not IdentifierNameSyntax (e.g. GenericNameSyntax). + // This test uses a generic method call on a class named Directory. + string source = @" +using System; +using System.Collections.Generic; + +namespace ConsoleApp +{ + public static class Directory + { + public static List GetItems() => new List(); + } + + class Program + { + static void Main(string[] args) + { + var items = Directory.GetItems(); + } + } +}"; + VerifyCSharpDiagnostic(source); + } + + [TestMethod] + [Description("Analyzer should not report when symbol is unresolved (compile error)")] + public void UnresolvableDirectoryType_NoDiagnostic() + { + // When symbol.Symbol is null it means the code has a compile error, + // not that it's System.IO.Directory. Should not produce a false positive. + string source = @" +namespace ConsoleApp +{ + class Program + { + static void Main(string[] args) + { + var files = Directory.GetFiles("".""); + } + } +}"; + // No 'using System.IO' so Directory is unresolvable — should NOT produce diagnostic + VerifyCSharpDiagnostic(source); + } + + [TestMethod] + [Description("Identifier comparison should use OrdinalIgnoreCase, not CurrentCultureIgnoreCase")] + public void DirectoryIdentifier_CaseInsensitiveOrdinal_ProducesInfoMessage() + { + // Verifies that an oddly-cased but valid Directory.GetFiles call is still caught. + // CurrentCultureIgnoreCase could fail in Turkish locale for identifiers with 'I'. + string source = @" +using System; +using System.IO; + +namespace ConsoleApp +{ + class Program + { + static void Main(string[] args) + { + string[] files = Directory.GetFiles("".""); + } + } +}"; + VerifyCSharpDiagnostic(source, + new DiagnosticResult + { + Id = "INTL0301", + Severity = DiagnosticSeverity.Info, + Message = "Favor using the method `EnumerateFiles` over the `GetFiles` method", + Locations = + [ + new DiagnosticResultLocation("Test0.cs", 11, 30) + ] + }); + } + + [TestMethod] + [Description("Analyzer misses fully-qualified System.IO.Directory.GetFiles()")] + public void FullyQualifiedDirectoryGetFiles_ProducesInfoMessage() + { + // The analyzer only checks IdentifierNameSyntax, so System.IO.Directory.GetFiles() + // is missed because the expression is a MemberAccessExpressionSyntax, not IdentifierNameSyntax. + string source = @" +using System; + +namespace ConsoleApp +{ + class Program + { + static void Main(string[] args) + { + string[] files = System.IO.Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory); + } + } +}"; + VerifyCSharpDiagnostic(source, + new DiagnosticResult + { + Id = "INTL0301", + Severity = DiagnosticSeverity.Info, + Message = "Favor using the method `EnumerateFiles` over the `GetFiles` method", + Locations = + [ + new DiagnosticResultLocation("Test0.cs", 10, 30) + ] + }); + } + + [TestMethod] + [Description("Analyzer misses fully-qualified System.IO.Directory.GetDirectories()")] + public void FullyQualifiedDirectoryGetDirectories_ProducesInfoMessage() + { + string source = @" +using System; + +namespace ConsoleApp +{ + class Program + { + static void Main(string[] args) + { + string[] dirs = System.IO.Directory.GetDirectories(AppDomain.CurrentDomain.BaseDirectory); + } + } +}"; + VerifyCSharpDiagnostic(source, + new DiagnosticResult + { + Id = "INTL0302", + Severity = DiagnosticSeverity.Info, + Message = "Favor using the method `EnumerateDirectories` over the `GetDirectories` method", + Locations = + [ + new DiagnosticResultLocation("Test0.cs", 10, 29) + ] + }); + } } } diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingFieldPascalUnderscoreTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingFieldPascalUnderscoreTests.cs index 324803e5..cedb3d15 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingFieldPascalUnderscoreTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingFieldPascalUnderscoreTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeFixes; @@ -451,6 +452,60 @@ class TypeName VerifyCSharpDiagnostic(test, expected); } + [TestMethod] + [Description("GeneratedCodeAttribute is checked by Name only, not full type — custom attribute with same name suppresses incorrectly")] + public void FieldWithNamingViolation_CustomGeneratedCodeAttribute_ShouldStillWarn() + { + // A user-defined GeneratedCodeAttribute (different namespace) should NOT + // suppress the naming diagnostic, but the current code checks by Name only. + string test = @" + using System; + + namespace MyNamespace + { + class GeneratedCodeAttribute : Attribute { } + + [GeneratedCode] + class TypeName + { + public string myfield; + } + }"; + + var expected = new DiagnosticResult + { + Id = "INTL0001", + Message = "Field 'myfield' should be named _PascalCase", + Severity = DiagnosticSeverity.Warning, + Locations = + [ + new DiagnosticResultLocation("Test0.cs", 11, 27) + ] + }; + + VerifyCSharpDiagnostic(test, expected); + } + + [TestMethod] + [Description("Verify real GeneratedCodeAttribute still suppresses correctly")] + public void FieldWithNamingViolation_RealGeneratedCodeAttribute_NoDiagnostic() + { + string test = @" + using System; + using System.CodeDom.Compiler; + + namespace ConsoleApplication1 + { + [GeneratedCode(""tool"", ""1.0"")] + class TypeName + { + public string myfield; + } + }"; + + VerifyCSharpDiagnostic(test); + } + protected override CodeFixProvider GetCSharpCodeFixProvider() { diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/UnusedLocalVariableTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/UnusedLocalVariableTests.cs index 70f6e81f..70358b58 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/UnusedLocalVariableTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/UnusedLocalVariableTests.cs @@ -9,6 +9,18 @@ namespace IntelliTect.Analyzer.Tests [TestClass] public class UnusedLocalVariableTests : CodeFixVerifier { + [TestMethod] + [Description("HelpLinkUri should use DiagnosticUrlBuilder, not a hardcoded generic URL")] + public void Descriptor_HelpLinkUri_ShouldBeSpecific() + { + DiagnosticAnalyzer analyzer = GetCSharpDiagnosticAnalyzer(); + DiagnosticDescriptor diagnostic = analyzer.SupportedDiagnostics.Single(); + + string expectedUrl = DiagnosticUrlBuilder.GetUrl("Local variable unused", "INTL0303"); + Assert.AreEqual(expectedUrl, diagnostic.HelpLinkUri, + $"HelpLinkUri should use DiagnosticUrlBuilder but was '{diagnostic.HelpLinkUri}'"); + } + [TestMethod] public void InstanceMemberAccessedOnLocalVariable_NoDiagnosticInformationReturned() { @@ -141,7 +153,7 @@ public void Descriptor_ContainsExpectedValues() Assert.AreEqual(DiagnosticSeverity.Info, diagnostic.DefaultSeverity); Assert.IsTrue(diagnostic.IsEnabledByDefault); Assert.AreEqual("All local variables should be accessed, or named with underscores to indicate they are unused.", diagnostic.Description); - Assert.AreEqual("https://github.com/IntelliTect/CodingGuidelines", diagnostic.HelpLinkUri); + Assert.AreEqual(DiagnosticUrlBuilder.GetUrl("Local variable unused", "INTL0303"), diagnostic.HelpLinkUri); } [TestMethod] @@ -249,6 +261,31 @@ bool Bar(Func func) VerifyCSharpDiagnostic(test, result); } + [TestMethod] + [Description("Analyzer reports on generated code but should skip it")] + public void UnusedLocalVariable_InGeneratedCode_NoDiagnostic() + { + // UnusedLocalVariable uses GeneratedCodeAnalysisFlags.Analyze | ReportDiagnostics, + // meaning it reports inside generated code. It should use None to skip generated code. + string test = @" +using System; +using System.CodeDom.Compiler; + +namespace ConsoleApplication1 +{ + class TypeName + { + [GeneratedCode(""tool"", ""1.0"")] + public void GeneratedMethod() + { + object foo = new object(); + } + } +}"; + // Should NOT produce a diagnostic for unused variable inside generated code + VerifyCSharpDiagnostic(test); + } + protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() { return new Analyzers.UnusedLocalVariable(); diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/AttributesOnSeparateLines.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/AttributesOnSeparateLines.cs index 7a9e6422..55d2c457 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/AttributesOnSeparateLines.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/AttributesOnSeparateLines.cs @@ -29,7 +29,7 @@ public override void Initialize(AnalysisContext context) throw new System.ArgumentNullException(nameof(context)); } - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Property, SymbolKind.NamedType, SymbolKind.Method, SymbolKind.Field); } diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/FavorDirectoryEnumerationCalls.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/FavorDirectoryEnumerationCalls.cs index 9d4939f4..441cb536 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/FavorDirectoryEnumerationCalls.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/FavorDirectoryEnumerationCalls.cs @@ -40,7 +40,7 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); } - private void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) { var expression = (InvocationExpressionSyntax)context.Node; @@ -49,37 +49,55 @@ private void AnalyzeInvocation(SyntaxNodeAnalysisContext context) return; } - if (memberAccess.Expression is not IdentifierNameSyntax nameSyntax) + string methodName = memberAccess.Name.Identifier.Text; + + if (!IsDirectoryExpression(memberAccess.Expression, context)) { return; } - if (string.Equals(nameSyntax.Identifier.Text, "Directory", StringComparison.CurrentCultureIgnoreCase)) + if (string.Equals(methodName, "GetFiles", StringComparison.OrdinalIgnoreCase)) + { + Location loc = memberAccess.GetLocation(); + context.ReportDiagnostic(Diagnostic.Create(_Rule301, loc, memberAccess.Name)); + } + else if (string.Equals(methodName, "GetDirectories", StringComparison.OrdinalIgnoreCase)) + { + Location loc = memberAccess.GetLocation(); + context.ReportDiagnostic(Diagnostic.Create(_Rule302, loc, memberAccess.Name)); + } + } + + private static bool IsDirectoryExpression(ExpressionSyntax expression, SyntaxNodeAnalysisContext context) + { + SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(expression); + + // For simple identifiers like 'Directory' + if (expression is IdentifierNameSyntax) { - if (memberAccess.ChildNodes().Cast().Any(x => - string.Equals(x.Identifier.Text, "GetFiles", StringComparison.CurrentCultureIgnoreCase))) + if (symbolInfo.Symbol is null) { - // Unsure if this is the best way to determine if member was defined in the project. - SymbolInfo symbol = context.SemanticModel.GetSymbolInfo(nameSyntax); - if (symbol.Symbol == null || symbol.Symbol.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::System.IO.Directory") - { - Location loc = memberAccess.GetLocation(); - context.ReportDiagnostic(Diagnostic.Create(_Rule301, loc, memberAccess.Name)); - } + return false; } - if (memberAccess.ChildNodes().Cast().Any(x => - string.Equals(x.Identifier.Text, "GetDirectories", StringComparison.CurrentCultureIgnoreCase))) + return symbolInfo.Symbol.OriginalDefinition + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::System.IO.Directory"; + } + + // For fully-qualified expressions like 'System.IO.Directory' + if (expression is MemberAccessExpressionSyntax) + { + ISymbol resolvedSymbol = symbolInfo.Symbol + ?? (symbolInfo.CandidateSymbols.Length > 0 ? symbolInfo.CandidateSymbols[0] : null); + + if (resolvedSymbol is INamedTypeSymbol namedType) { - // Unsure if this is the best way to determine if member was defined in the project. - SymbolInfo symbol = context.SemanticModel.GetSymbolInfo(nameSyntax); - if (symbol.Symbol is null || symbol.Symbol.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::System.IO.Directory") - { - Location loc = memberAccess.GetLocation(); - context.ReportDiagnostic(Diagnostic.Create(_Rule302, loc, memberAccess.Name)); - } + return namedType.OriginalDefinition + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::System.IO.Directory"; } } + + return false; } private static class Rule301 diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingFieldPascalUnderscore.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingFieldPascalUnderscore.cs index f48e023c..32e48a3f 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingFieldPascalUnderscore.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingFieldPascalUnderscore.cs @@ -39,9 +39,11 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) { ISymbol namedTypeSymbol = context.Symbol; - // ignore GeneratedCodeAttribute on field and first containing type + INamedTypeSymbol generatedCodeAttribute = context.Compilation + .GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute"); ImmutableArray attributes = namedTypeSymbol.GetAttributes().AddRange(namedTypeSymbol.ContainingType.GetAttributes()); - if (attributes.Any(attribute => attribute.AttributeClass?.Name == nameof(System.CodeDom.Compiler.GeneratedCodeAttribute))) + if (generatedCodeAttribute is not null && + attributes.Any(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, generatedCodeAttribute))) { return; } diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingMethodPascal.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingMethodPascal.cs index b572f48c..bf6ecabd 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingMethodPascal.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingMethodPascal.cs @@ -89,8 +89,11 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) return; } + INamedTypeSymbol generatedCodeAttribute = context.Compilation + .GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute"); ImmutableArray attributes = namedTypeSymbol.GetAttributes().AddRange(namedTypeSymbol.ContainingType.GetAttributes()); - if (attributes.Any(attribute => attribute.AttributeClass?.Name == nameof(System.CodeDom.Compiler.GeneratedCodeAttribute))) + if (generatedCodeAttribute is not null && + attributes.Any(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, generatedCodeAttribute))) { return; } diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingPropertyPascal.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingPropertyPascal.cs index 71410e6c..3832f6c3 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingPropertyPascal.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingPropertyPascal.cs @@ -39,8 +39,11 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) { var namedTypeSymbol = (IPropertySymbol)context.Symbol; + INamedTypeSymbol generatedCodeAttribute = context.Compilation + .GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute"); ImmutableArray attributes = namedTypeSymbol.GetAttributes().AddRange(namedTypeSymbol.ContainingType.GetAttributes()); - if (attributes.Any(attribute => attribute.AttributeClass?.Name == nameof(System.CodeDom.Compiler.GeneratedCodeAttribute))) + if (generatedCodeAttribute is not null && + attributes.Any(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, generatedCodeAttribute))) { return; } diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/UnusedLocalVariable.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/UnusedLocalVariable.cs index dfffffcc..74b96aff 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/UnusedLocalVariable.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/UnusedLocalVariable.cs @@ -16,10 +16,10 @@ public class UnusedLocalVariable : DiagnosticAnalyzer private const string MessageFormat = "Local variable '{0}' should be used"; private const string Description = "All local variables should be accessed, or named with underscores to indicate they are unused."; private const string Category = "Flow"; - private const string HelpLinkUri = "https://github.com/IntelliTect/CodingGuidelines"; + private static readonly string _HelpLinkUri = DiagnosticUrlBuilder.GetUrl(Title, DiagnosticId); private static readonly DiagnosticDescriptor _Rule = new(DiagnosticId, Title, MessageFormat, - Category, DiagnosticSeverity.Info, isEnabledByDefault: true, description: Description, HelpLinkUri); + Category, DiagnosticSeverity.Info, isEnabledByDefault: true, description: Description, _HelpLinkUri); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(_Rule); @@ -30,7 +30,7 @@ public override void Initialize(AnalysisContext context) throw new System.ArgumentNullException(nameof(context)); } - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterSyntaxNodeAction(AnalyzeMethod, SyntaxKind.MethodDeclaration); } diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer/DiagnosticUrlBuilder.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer/DiagnosticUrlBuilder.cs index 13bdebfb..175e652e 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer/DiagnosticUrlBuilder.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer/DiagnosticUrlBuilder.cs @@ -3,7 +3,7 @@ namespace IntelliTect.Analyzer { - public static class DiagnosticUrlBuilder + public static partial class DiagnosticUrlBuilder { private const string BaseUrl = "https://github.com/IntelliTect/CodingGuidelines"; @@ -21,10 +21,12 @@ public static string GetUrl(string title, string diagnosticId) if (string.IsNullOrWhiteSpace(diagnosticId)) throw new System.ArgumentException("diagnostic ID cannot be empty", nameof(diagnosticId)); - Regex hyphenateRegex = new Regex(@"\s"); - string hyphenatedTitle = hyphenateRegex.Replace(title, "-"); + string hyphenatedTitle = HyphenateRegex().Replace(title, "-"); return BaseUrl + $"#{diagnosticId.ToUpperInvariant()}" + $"---{hyphenatedTitle.ToUpperInvariant()}"; } + + [GeneratedRegex(@"\s", RegexOptions.Compiled)] + private static partial Regex HyphenateRegex(); } } From ddb114599c871274b407b658d33998aafe204c64 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Tue, 24 Feb 2026 00:05:16 -0800 Subject: [PATCH 2/3] PR Feedback --- .../AttributesOnSeparateLinesTests.cs | 2 -- .../DiagnosticUriBuilderTests.cs | 4 ++-- .../FavorEnumeratorDirectoryCallsTests.cs | 9 ++------- .../NamingFieldPascalUnderscoreTests.cs | 5 +---- .../UnusedLocalVariableTests.cs | 16 +--------------- .../Analyzers/FavorDirectoryEnumerationCalls.cs | 5 +---- .../IntelliTect.Analyzer/DiagnosticUrlBuilder.cs | 8 +++----- 7 files changed, 10 insertions(+), 39 deletions(-) diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AttributesOnSeparateLinesTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AttributesOnSeparateLinesTests.cs index 1d25ea0a..7d8293bc 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AttributesOnSeparateLinesTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AttributesOnSeparateLinesTests.cs @@ -394,8 +394,6 @@ static void Main() [Description("Analyzer should not report on generated code")] public void AttributesOnSameLine_InGeneratedCode_NoDiagnostic() { - // AttributesOnSeparateLines uses GeneratedCodeAnalysisFlags.Analyze | ReportDiagnostics, - // meaning it reports inside generated code. It should skip generated code. string test = @"using System; using System.CodeDom.Compiler; diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DiagnosticUriBuilderTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DiagnosticUriBuilderTests.cs index 5d59c84d..15d83acd 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DiagnosticUriBuilderTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DiagnosticUriBuilderTests.cs @@ -52,8 +52,8 @@ public void GetUrl_TitleWithTabsAndMultipleSpaces_HyphenatesCorrectly() { string actual = DiagnosticUrlBuilder.GetUrl("Fields Multiple\tSpaces", "INTL9999"); - Assert.IsTrue(actual.Contains("FIELDS--MULTIPLE-SPACES", StringComparison.OrdinalIgnoreCase), - $"Expected all whitespace replaced with hyphens but got: '{actual}'"); + Assert.IsTrue(actual.Contains("FIELDS-MULTIPLE-SPACES", StringComparison.OrdinalIgnoreCase), + $"Expected consecutive whitespace collapsed to a single hyphen but got: '{actual}'"); } } } diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/FavorEnumeratorDirectoryCallsTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/FavorEnumeratorDirectoryCallsTests.cs index be495b7b..75c570f4 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/FavorEnumeratorDirectoryCallsTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/FavorEnumeratorDirectoryCallsTests.cs @@ -213,11 +213,8 @@ static void Main(string[] args) } [TestMethod] - [Description("Identifier comparison should use OrdinalIgnoreCase, not CurrentCultureIgnoreCase")] public void DirectoryIdentifier_CaseInsensitiveOrdinal_ProducesInfoMessage() { - // Verifies that an oddly-cased but valid Directory.GetFiles call is still caught. - // CurrentCultureIgnoreCase could fail in Turkish locale for identifiers with 'I'. string source = @" using System; using System.IO; @@ -246,11 +243,9 @@ static void Main(string[] args) } [TestMethod] - [Description("Analyzer misses fully-qualified System.IO.Directory.GetFiles()")] + [Description("Detect fully-qualified System.IO.Directory.GetFiles()")] public void FullyQualifiedDirectoryGetFiles_ProducesInfoMessage() { - // The analyzer only checks IdentifierNameSyntax, so System.IO.Directory.GetFiles() - // is missed because the expression is a MemberAccessExpressionSyntax, not IdentifierNameSyntax. string source = @" using System; @@ -278,7 +273,7 @@ static void Main(string[] args) } [TestMethod] - [Description("Analyzer misses fully-qualified System.IO.Directory.GetDirectories()")] + [Description("Detect fully-qualified System.IO.Directory.GetDirectories()")] public void FullyQualifiedDirectoryGetDirectories_ProducesInfoMessage() { string source = @" diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingFieldPascalUnderscoreTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingFieldPascalUnderscoreTests.cs index cedb3d15..b60d46d2 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingFieldPascalUnderscoreTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingFieldPascalUnderscoreTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeFixes; @@ -453,11 +452,9 @@ class TypeName } [TestMethod] - [Description("GeneratedCodeAttribute is checked by Name only, not full type — custom attribute with same name suppresses incorrectly")] + [Description("Custom GeneratedCodeAttribute in different namespace should not suppress diagnostics")] public void FieldWithNamingViolation_CustomGeneratedCodeAttribute_ShouldStillWarn() { - // A user-defined GeneratedCodeAttribute (different namespace) should NOT - // suppress the naming diagnostic, but the current code checks by Name only. string test = @" using System; diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/UnusedLocalVariableTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/UnusedLocalVariableTests.cs index 70358b58..185d69ba 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/UnusedLocalVariableTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/UnusedLocalVariableTests.cs @@ -9,18 +9,6 @@ namespace IntelliTect.Analyzer.Tests [TestClass] public class UnusedLocalVariableTests : CodeFixVerifier { - [TestMethod] - [Description("HelpLinkUri should use DiagnosticUrlBuilder, not a hardcoded generic URL")] - public void Descriptor_HelpLinkUri_ShouldBeSpecific() - { - DiagnosticAnalyzer analyzer = GetCSharpDiagnosticAnalyzer(); - DiagnosticDescriptor diagnostic = analyzer.SupportedDiagnostics.Single(); - - string expectedUrl = DiagnosticUrlBuilder.GetUrl("Local variable unused", "INTL0303"); - Assert.AreEqual(expectedUrl, diagnostic.HelpLinkUri, - $"HelpLinkUri should use DiagnosticUrlBuilder but was '{diagnostic.HelpLinkUri}'"); - } - [TestMethod] public void InstanceMemberAccessedOnLocalVariable_NoDiagnosticInformationReturned() { @@ -262,11 +250,9 @@ bool Bar(Func func) } [TestMethod] - [Description("Analyzer reports on generated code but should skip it")] + [Description("Analyzer should skip generated code")] public void UnusedLocalVariable_InGeneratedCode_NoDiagnostic() { - // UnusedLocalVariable uses GeneratedCodeAnalysisFlags.Analyze | ReportDiagnostics, - // meaning it reports inside generated code. It should use None to skip generated code. string test = @" using System; using System.CodeDom.Compiler; diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/FavorDirectoryEnumerationCalls.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/FavorDirectoryEnumerationCalls.cs index 441cb536..c8a6004a 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/FavorDirectoryEnumerationCalls.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/FavorDirectoryEnumerationCalls.cs @@ -87,10 +87,7 @@ private static bool IsDirectoryExpression(ExpressionSyntax expression, SyntaxNod // For fully-qualified expressions like 'System.IO.Directory' if (expression is MemberAccessExpressionSyntax) { - ISymbol resolvedSymbol = symbolInfo.Symbol - ?? (symbolInfo.CandidateSymbols.Length > 0 ? symbolInfo.CandidateSymbols[0] : null); - - if (resolvedSymbol is INamedTypeSymbol namedType) + if (symbolInfo.Symbol is INamedTypeSymbol namedType) { return namedType.OriginalDefinition .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::System.IO.Directory"; diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer/DiagnosticUrlBuilder.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer/DiagnosticUrlBuilder.cs index 175e652e..343b7eef 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer/DiagnosticUrlBuilder.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer/DiagnosticUrlBuilder.cs @@ -3,9 +3,10 @@ namespace IntelliTect.Analyzer { - public static partial class DiagnosticUrlBuilder + public static class DiagnosticUrlBuilder { private const string BaseUrl = "https://github.com/IntelliTect/CodingGuidelines"; + private static readonly Regex _HyphenateRegex = new(@"\s+", RegexOptions.Compiled); /// /// Get the full diagnostic help url @@ -21,12 +22,9 @@ public static string GetUrl(string title, string diagnosticId) if (string.IsNullOrWhiteSpace(diagnosticId)) throw new System.ArgumentException("diagnostic ID cannot be empty", nameof(diagnosticId)); - string hyphenatedTitle = HyphenateRegex().Replace(title, "-"); + string hyphenatedTitle = _HyphenateRegex.Replace(title, "-"); return BaseUrl + $"#{diagnosticId.ToUpperInvariant()}" + $"---{hyphenatedTitle.ToUpperInvariant()}"; } - - [GeneratedRegex(@"\s", RegexOptions.Compiled)] - private static partial Regex HyphenateRegex(); } } From 62542e83bf83be78dbd4978a7d26438290d6e980 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Tue, 24 Feb 2026 00:11:37 -0800 Subject: [PATCH 3/3] PR Feedback --- .../AsyncVoidTests.cs | 22 ------------ .../DateTimeConversionTests.cs | 25 ------------- .../DiagnosticUriBuilderTests.cs | 36 ------------------- .../FavorEnumeratorDirectoryCallsTests.cs | 30 ---------------- .../NamingFieldPascalUnderscoreTests.cs | 20 ----------- 5 files changed, 133 deletions(-) diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AsyncVoidTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AsyncVoidTests.cs index 7b5f7387..2ac43b5f 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AsyncVoidTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/AsyncVoidTests.cs @@ -101,28 +101,6 @@ public async Task Sample() { } await VerifyCSharpFix(test, fixTest, allowNewCompilerDiagnostics: true); } - [TestMethod] - [Description("Analyzer should not crash when encountering non-async non-void methods")] - public void NonAsyncNonVoidMethod_NoDiagnosticAndNoCrash() - { - // The analyzer uses 'as IMethodSymbol' then dereferences without null check. - // This test ensures it handles the method symbol safely. - string test = @" - using System; - - namespace ConsoleApplication1 - { - class TypeName - { - public void SyncMethod() { } - public int GetValue() => 42; - public static void StaticMethod() { } - } - }"; - - VerifyCSharpDiagnostic(test); - } - protected override CodeFixProvider GetCSharpCodeFixProvider() { return new CodeFixes.AsyncVoid(); diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs index b37be19a..7938ef5b 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs @@ -339,31 +339,6 @@ static void Main(string[] args) }); } - [TestMethod] - [Description("Analyzer should not throw when DateTimeOffset/DateTime types are unresolvable")] - public void AnyDateTimeOffsetConstructorWithUnresolvableTypes_DoesNotThrow() - { - // AnalyzeObjectCreation throws InvalidOperationException if GetTypeByMetadataName returns null. - // Analyzer callbacks must never throw. This test uses code that won't resolve System types. - string source = @" -namespace ConsoleApp -{ - class DateTimeOffset - { - public DateTimeOffset(object arg) { } - } - - class Program - { - static void Main() - { - var dto = new DateTimeOffset(null); - } - } -}"; - VerifyCSharpDiagnostic(source); - } - protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() { return new Analyzers.BanImplicitDateTimeToDateTimeOffsetConversion(); diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DiagnosticUriBuilderTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DiagnosticUriBuilderTests.cs index 15d83acd..6b930487 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DiagnosticUriBuilderTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DiagnosticUriBuilderTests.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace IntelliTect.Analyzer.Tests @@ -20,40 +19,5 @@ public void GetUrl_GivenBlock00XXCode_ProperlyBuildsUrl(string title, string dia Assert.IsTrue(string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase), $"'{expected}' does not equal '{actual}'"); } - - [TestMethod] - [Description("GetUrl allocates a new Regex on every call — performance regression test")] - public void GetUrl_CalledRepeatedly_DoesNotAllocateExcessively() - { - // Warm up - DiagnosticUrlBuilder.GetUrl("Test Title", "INTL0001"); - - long before = GC.GetAllocatedBytesForCurrentThread(); - - const int iterations = 1000; - for (int i = 0; i < iterations; i++) - { - DiagnosticUrlBuilder.GetUrl("Test Title", "INTL0001"); - } - - long after = GC.GetAllocatedBytesForCurrentThread(); - long bytesPerCall = (after - before) / iterations; - - // A cached Regex or simple string.Replace should allocate ~200-400 bytes per call (strings only). - // A new Regex() per call allocates ~2000+ bytes. Threshold at 500 to catch the Regex allocation. - Assert.IsTrue(bytesPerCall < 500, - $"GetUrl allocated ~{bytesPerCall} bytes/call, suggesting a new Regex is created each time. " + - $"Expected < 500 bytes/call with a cached approach."); - } - - [TestMethod] - [Description("Titles with multiple whitespace types should be hyphenated correctly")] - public void GetUrl_TitleWithTabsAndMultipleSpaces_HyphenatesCorrectly() - { - string actual = DiagnosticUrlBuilder.GetUrl("Fields Multiple\tSpaces", "INTL9999"); - - Assert.IsTrue(actual.Contains("FIELDS-MULTIPLE-SPACES", StringComparison.OrdinalIgnoreCase), - $"Expected consecutive whitespace collapsed to a single hyphen but got: '{actual}'"); - } } } diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/FavorEnumeratorDirectoryCallsTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/FavorEnumeratorDirectoryCallsTests.cs index 75c570f4..05389a15 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/FavorEnumeratorDirectoryCallsTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/FavorEnumeratorDirectoryCallsTests.cs @@ -212,36 +212,6 @@ static void Main(string[] args) VerifyCSharpDiagnostic(source); } - [TestMethod] - public void DirectoryIdentifier_CaseInsensitiveOrdinal_ProducesInfoMessage() - { - string source = @" -using System; -using System.IO; - -namespace ConsoleApp -{ - class Program - { - static void Main(string[] args) - { - string[] files = Directory.GetFiles("".""); - } - } -}"; - VerifyCSharpDiagnostic(source, - new DiagnosticResult - { - Id = "INTL0301", - Severity = DiagnosticSeverity.Info, - Message = "Favor using the method `EnumerateFiles` over the `GetFiles` method", - Locations = - [ - new DiagnosticResultLocation("Test0.cs", 11, 30) - ] - }); - } - [TestMethod] [Description("Detect fully-qualified System.IO.Directory.GetFiles()")] public void FullyQualifiedDirectoryGetFiles_ProducesInfoMessage() diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingFieldPascalUnderscoreTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingFieldPascalUnderscoreTests.cs index b60d46d2..44f24a4e 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingFieldPascalUnderscoreTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingFieldPascalUnderscoreTests.cs @@ -483,26 +483,6 @@ class TypeName VerifyCSharpDiagnostic(test, expected); } - [TestMethod] - [Description("Verify real GeneratedCodeAttribute still suppresses correctly")] - public void FieldWithNamingViolation_RealGeneratedCodeAttribute_NoDiagnostic() - { - string test = @" - using System; - using System.CodeDom.Compiler; - - namespace ConsoleApplication1 - { - [GeneratedCode(""tool"", ""1.0"")] - class TypeName - { - public string myfield; - } - }"; - - VerifyCSharpDiagnostic(test); - } - protected override CodeFixProvider GetCSharpCodeFixProvider() {