Skip to content

Commit

Permalink
## Added
Browse files Browse the repository at this point in the history
- 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
MooVC committed Feb 10, 2025
1 parent fcbe96e commit 32319f1
Show file tree
Hide file tree
Showing 14 changed files with 704 additions and 69 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to Valuify will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.3.0] - TBC

## Added

- 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`.

## [1.2.2] - 2025-01-08

## Fixed
Expand Down
1 change: 1 addition & 0 deletions Valuify.sln
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rules", "Rules", "{DF5E2435
docs\rules\VALFY01.md = docs\rules\VALFY01.md
docs\rules\VALFY02.md = docs\rules\VALFY02.md
docs\rules\VALFY03.md = docs\rules\VALFY03.md
docs\rules\VALFY04.md = docs\rules\VALFY04.md
EndProjectSection
EndProject
Global
Expand Down
107 changes: 107 additions & 0 deletions docs/rules/VALFY04.md
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 src/Valuify.Tests/IgnoreAttributeAnalyzerTests/WhenExecuted.cs
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);
}
}
2 changes: 1 addition & 1 deletion src/Valuify.Tests/Snippets/Declarations/Unannotated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
internal static class Unannotated
{
public static readonly Snippets Declaration = new(
Simple.Declaration.Body,
Ignored.Declaration.Body,
new(
"""
namespace Valuify.Classes.Testing
Expand Down
9 changes: 3 additions & 6 deletions src/Valuify.Tests/Snippets/SnippetsAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,10 @@ public sealed class SnippetsAttribute
private static readonly Type[] declarations = FindDeclarations();
private static readonly LanguageVersion[] languages = FindLanguages();

public SnippetsAttribute(
Type[]? exclusions = default,
Extensions extensions = DefaultExtensions,
Type[]? inclusions = default,
LanguageVersion[]? languages = default)
public SnippetsAttribute(Type[]? exclusions = default, Extensions extensions = DefaultExtensions, Type[]? inclusions = default)
{
Assemblies = assemblies;
Extensions = extensions;
Languages = languages ?? SnippetsAttribute.languages;

Declarations = inclusions is null
? declarations
Expand All @@ -44,6 +39,8 @@ public SnippetsAttribute(
{
Declarations = Declarations.Except(exclusions).ToArray();
}

Languages = languages;
}

private delegate IEnumerable<object[]> GetFrameworks(LanguageVersion minimum, Func<ReferenceAssemblies, LanguageVersion, object[]?>? prepare);
Expand Down
141 changes: 141 additions & 0 deletions src/Valuify/AttributeAnalyzer.cs
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);
}
}
Loading

0 comments on commit 32319f1

Please sign in to comment.