-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Added a new `Ignore` attribute that allows engineers to mark properties that should be disregarded from equality checks (#14). - Added a new `VALFY04` analyzer which will warn engineers whenever a property is annotated with the `Ignore` attribute but the `Valuify` attribute is missing from the `class`.
- Loading branch information
Showing
14 changed files
with
704 additions
and
69 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
# VALFY04: Type does not utilize Valuify | ||
|
||
<table> | ||
<tr> | ||
<td>Type Name</td> | ||
<td>VALFY04_IgnoreAttributeAnalyzer</td> | ||
</tr> | ||
<tr> | ||
<td>Diagnostic Id</td> | ||
<td>VALFY04</td> | ||
</tr> | ||
<tr> | ||
<td>Category</td> | ||
<td>Usage</td> | ||
</tr> | ||
<tr> | ||
<td>Severity</td> | ||
<td>Info</td> | ||
</tr> | ||
<tr> | ||
<td>Is Enabled By Default</td> | ||
<td>Yes</td> | ||
</tr> | ||
</table> | ||
|
||
## Cause | ||
|
||
The property is not considered by Valuify because the type has not been annotated with the `Valuify` attribute. | ||
|
||
## Rule Description | ||
|
||
A violation of this rule occurs when a property is marked with the `Ignore` attribute, but the containing `class` is not annotated with the `Valuify` attribute. Therefore, no extension methods will be generated, making use of the `Ignore` attribute redundant. | ||
|
||
For example: | ||
|
||
```csharp | ||
public class Example | ||
{ | ||
[Ignore] | ||
public string Property { get; set; } | ||
} | ||
``` | ||
|
||
In this example, the `Ignore` attribute on `Property`, and the `class` itself, will be ignored by `Valuify`, suggesting a misunderstanding by the engineer as to its intended usage. | ||
|
||
## How to Fix Violations | ||
|
||
Reevaluate the decision to apply the `Ignore` attribute. If the `Ignore` attribute usage is deemed correct, annotate the type with the `Valuify` attribute, otherwise remove the `Ignore` attribute. | ||
|
||
For example: | ||
|
||
```csharp | ||
[Valuify] | ||
public class Example | ||
{ | ||
[Ignore] | ||
public string Property { get; set; } | ||
} | ||
``` | ||
or alternatively: | ||
|
||
```csharp | ||
public class Example | ||
{ | ||
public string Property { get; set; } | ||
} | ||
``` | ||
|
||
## When to Suppress Warnings | ||
|
||
Warnings from this rule should be suppressed only if there is a strong justification for not using the `Valuify` attribute on the containing type when the `Ignore` attribute is applied. | ||
|
||
If suppression is desired, one of the following approaches can be used: | ||
|
||
```csharp | ||
[Valuify] | ||
public class Example | ||
{ | ||
#pragma warning disable VALFY04 // Type does not utilize Valuify | ||
[Ignore] | ||
public string Property { get; set; } | ||
|
||
#pragma warning restore VALFY04 // Type does not utilize Valuify | ||
} | ||
``` | ||
|
||
or alternatively: | ||
|
||
```csharp | ||
public class Example | ||
{ | ||
[Ignore] | ||
[SuppressMessage("Usage", "VALFY04:Type does not utilize Valuify", Justification = "Explanation for suppression")] | ||
public string Property { get; set; } | ||
} | ||
``` | ||
|
||
## How to Disable VALFY04 | ||
|
||
It is not recommended to disable the rule, as this may result in some confusion if expected extension methods are not present. | ||
|
||
```ini | ||
# Disable VALFY04: Type does not utilize Valuify | ||
[*.cs] | ||
dotnet_diagnostic.VALFY04.severity = none | ||
``` |
61 changes: 61 additions & 0 deletions
61
src/Valuify.Tests/IgnoreAttributeAnalyzerTests/WhenExecuted.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
namespace Valuify.IgnoreAttributeAnalyzerTests; | ||
|
||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.Testing; | ||
using Microsoft.CodeAnalysis.Text; | ||
using Valuify.Snippets; | ||
using Valuify.Snippets.Declarations; | ||
using AnalyzerTest = Valuify.AnalyzerTest<Valuify.IgnoreAttributeAnalyzer>; | ||
|
||
public sealed class WhenExecuted | ||
{ | ||
[Theory] | ||
[Snippets(exclusions: [typeof(Unannotated)], extensions: Extensions.None)] | ||
public async Task GivenAClassWhenCompliantThenNoDiagnosticsAreRaised(ReferenceAssemblies assembly, Expectations expectations, LanguageVersion language) | ||
{ | ||
// Arrange | ||
var test = new AnalyzerTest(assembly, language); | ||
|
||
expectations.IsDeclaredIn(test.TestState); | ||
|
||
// Act | ||
Func<Task> act = () => test.RunAsync(); | ||
|
||
// Assert | ||
await act.ShouldNotThrowAsync(); | ||
} | ||
|
||
[Fact] | ||
public async Task GivenATypeWhenUnannotatedThenCompatibleTargetTypeRuleIsRaised() | ||
{ | ||
Dictionary<LanguageVersion, LinePosition> positions = new() | ||
{ | ||
{ LanguageVersion.CSharp2, new LinePosition(18, 9) }, | ||
{ LanguageVersion.CSharp3, new LinePosition(11, 9) }, | ||
{ LanguageVersion.CSharp6, new LinePosition(8, 9) }, | ||
{ LanguageVersion.CSharp9, new LinePosition(8, 9) }, | ||
}; | ||
|
||
foreach (Expectations expectation in Unannotated.Declaration.Render(Extensions.None)) | ||
{ | ||
// Arrange | ||
var test = new AnalyzerTest(ReferenceAssemblies.Net.Net90, expectation.Minimum); | ||
|
||
expectation.IsDeclaredIn(test.TestState); | ||
|
||
test.ExpectedDiagnostics.Add(GetExpectedMissingValuifyRule(positions[expectation.Minimum])); | ||
|
||
// Act | ||
Func<Task> act = () => test.RunAsync(); | ||
|
||
// Assert | ||
await act.ShouldNotThrowAsync(); | ||
} | ||
} | ||
|
||
private static DiagnosticResult GetExpectedMissingValuifyRule(LinePosition position) | ||
{ | ||
return new DiagnosticResult(IgnoreAttributeAnalyzer.MissingValuifyRule) | ||
.WithLocation(position); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
namespace Valuify; | ||
|
||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
using static Valuify.ValuifyAttributeAnalyzer_Resources; | ||
|
||
/// <summary> | ||
/// Serves as a base for analyzers. | ||
/// </summary> | ||
/// <typeparam name="TResource"> | ||
/// The type associated with the resources for the analyzer. | ||
/// </typeparam> | ||
public abstract class AttributeAnalyzer<TResource> | ||
: DiagnosticAnalyzer | ||
{ | ||
private const string Branch = "master"; | ||
|
||
/// <inheritdoc/> | ||
public sealed override void Initialize(AnalysisContext context) | ||
{ | ||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
context.EnableConcurrentExecution(); | ||
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.Attribute); | ||
} | ||
|
||
/// <summary> | ||
/// Reports a diagnostic message associated with the <paramref name="descriptor"/> at the specified <paramref name="location"/>. | ||
/// </summary> | ||
/// <param name="context"> | ||
/// The context to which the report will be issued. | ||
/// </param> | ||
/// <param name="descriptor"> | ||
/// The diagnostic descriptor to report. | ||
/// </param> | ||
/// <param name="location"> | ||
/// The location at which the issue was identified. | ||
/// </param> | ||
/// <param name="messageArgs"> | ||
/// Any parameters associated with the descriptor. | ||
/// </param> | ||
protected static void Raise(SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor, Location location, params object?[] messageArgs) | ||
{ | ||
var diagnostic = Diagnostic.Create(descriptor, location, messageArgs); | ||
|
||
context.ReportDiagnostic(diagnostic); | ||
} | ||
|
||
/// <summary> | ||
/// Gets the fully qualified URL for the documentation associated with the rule denoted by <paramref name="ruleId"/>. | ||
/// </summary> | ||
/// <param name="ruleId"> | ||
/// The unique id of the rule. | ||
/// </param> | ||
/// <returns> | ||
/// The fully qualified URL for the documentation associated with the rule denoted by <paramref name="ruleId"/>. | ||
/// </returns> | ||
protected static string GetHelpLinkUri(string ruleId) | ||
{ | ||
return $"https://github.com/MooVC/Valuify/blob/{Branch}/docs/rules/{ruleId}.md"; | ||
} | ||
|
||
/// <summary> | ||
/// Gets the localzed resource string associated with the specified <paramref name="name"/>. | ||
/// </summary> | ||
/// <param name="name"> | ||
/// The name of the resource string to retrieve. | ||
/// </param> | ||
/// <returns> | ||
/// The localzed resource string associated with the specified <paramref name="name"/>. | ||
/// </returns> | ||
protected static LocalizableResourceString GetResourceString(string name) | ||
{ | ||
return new(name, ResourceManager, typeof(TResource)); | ||
} | ||
|
||
/// <summary> | ||
/// Gets the <see cref="IMethodSymbol"/> associated with the <paramref name="syntax"/>. | ||
/// </summary> | ||
/// <param name="context"> | ||
/// The analysis context from which the semantic model is obtained. | ||
/// </param> | ||
/// <param name="syntax"> | ||
/// The attribute for which the symbol is to be retrieved. | ||
/// </param> | ||
/// <returns> | ||
/// The <see cref="IMethodSymbol"/> associated with the <paramref name="syntax"/>. | ||
/// </returns> | ||
protected static IMethodSymbol? GetSymbol(SyntaxNodeAnalysisContext context, AttributeSyntax syntax) | ||
{ | ||
return context | ||
.SemanticModel | ||
.GetSymbolInfo(syntax, cancellationToken: context.CancellationToken) | ||
.Symbol as IMethodSymbol; | ||
} | ||
|
||
/// <summary> | ||
/// Analyses the location at which the matching attribute has been detected. | ||
/// </summary> | ||
/// <param name="attribute"> | ||
/// The syntax for the detected attribute. | ||
/// </param> | ||
/// <param name="context"> | ||
/// The analysis context, providing access to the semantic model and facilitating reporting. | ||
/// </param> | ||
/// <param name="location"> | ||
/// The location at which the attribute was identified. | ||
/// </param> | ||
protected abstract void Analyze(AttributeSyntax attribute, SyntaxNodeAnalysisContext context, Location location); | ||
|
||
/// <summary> | ||
/// Determines whether or not the <paramref name="symbol"/> is a match for the required attribute. | ||
/// </summary> | ||
/// <param name="symbol"> | ||
/// The symbol to check. | ||
/// </param> | ||
/// <returns> | ||
/// <see langword="true"/> if the <paramref name="symbol"/> is a match, otherwise <see langword="false"/>. | ||
/// </returns> | ||
protected abstract bool IsMatch(IMethodSymbol symbol); | ||
|
||
private void AnalyzeNode(SyntaxNodeAnalysisContext context) | ||
{ | ||
if (context.Node is not AttributeSyntax attribute) | ||
{ | ||
return; | ||
} | ||
|
||
IMethodSymbol? symbol = GetSymbol(context, attribute); | ||
|
||
if (symbol is null || !IsMatch(symbol)) | ||
{ | ||
return; | ||
} | ||
|
||
Location location = attribute.GetLocation(); | ||
|
||
Analyze(attribute, context, location); | ||
} | ||
} |
Oops, something went wrong.