Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,31 @@ static void Main()
await VerifyCSharpFix(test, fixTest);
}

[TestMethod]
[Description("Analyzer should not report on generated code")]
public void AttributesOnSameLine_InGeneratedCode_NoDiagnostic()
{
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,5 +161,115 @@ static void Main(string[] args)
";
VerifyCSharpDiagnostic(source);
}

[TestMethod]
[Description("Cast<IdentifierNameSyntax>() throws InvalidCastException on generic method calls")]
public void GenericMethodCallOnDirectory_DoesNotThrow()
{
// memberAccess.ChildNodes().Cast<IdentifierNameSyntax>() 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<T> GetItems<T>() => new List<T>();
}

class Program
{
static void Main(string[] args)
{
var items = Directory.GetItems<string>();
}
}
}";
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("Detect fully-qualified System.IO.Directory.GetFiles()")]
public void FullyQualifiedDirectoryGetFiles_ProducesInfoMessage()
{
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("Detect 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)
]
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,38 @@ class TypeName
VerifyCSharpDiagnostic(test, expected);
}

[TestMethod]
[Description("Custom GeneratedCodeAttribute in different namespace should not suppress diagnostics")]
public void FieldWithNamingViolation_CustomGeneratedCodeAttribute_ShouldStillWarn()
{
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);
}


protected override CodeFixProvider GetCSharpCodeFixProvider()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,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]
Expand Down Expand Up @@ -249,6 +249,29 @@ bool Bar(Func<bool, bool> func)
VerifyCSharpDiagnostic(test, result);
}

[TestMethod]
[Description("Analyzer should skip generated code")]
public void UnusedLocalVariable_InGeneratedCode_NoDiagnostic()
{
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -49,37 +49,52 @@ 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))
{
if (memberAccess.ChildNodes().Cast<IdentifierNameSyntax>().Any(x =>
string.Equals(x.Identifier.Text, "GetFiles", StringComparison.CurrentCultureIgnoreCase)))
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 (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<IdentifierNameSyntax>().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)
{
if (symbolInfo.Symbol 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AttributeData> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,11 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context)
return;
}

INamedTypeSymbol generatedCodeAttribute = context.Compilation
.GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute");
ImmutableArray<AttributeData> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AttributeData> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(_Rule);

Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace IntelliTect.Analyzer
public static class DiagnosticUrlBuilder
{
private const string BaseUrl = "https://github.com/IntelliTect/CodingGuidelines";
private static readonly Regex _HyphenateRegex = new(@"\s+", RegexOptions.Compiled);

/// <summary>
/// Get the full diagnostic help url
Expand All @@ -21,8 +22,7 @@ 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()}";
}
Expand Down
Loading