-
-
Notifications
You must be signed in to change notification settings - Fork 115
feat: Add bUnit.analyzers package with BUNIT0002 analyzer #1798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Copilot
wants to merge
8
commits into
main
Choose a base branch
from
copilot/fix-analyzer-common-issues
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b4d93f3
Initial plan
Copilot 1b5514f
feat: Add bUnit.Analyzers project with BUNIT0002 analyzer
Copilot bf9848e
docs: Update README with bUnit.analyzers package information
Copilot 088c562
fix: Add missing System.Linq using statement in PreferGenericFindAnal…
Copilot a9d7ed0
test: Add comprehensive unit tests for BUNIT0002 analyzer
Copilot 0873523
docs: Add comment explaining public DiagnosticDescriptors class
Copilot c24c14d
docs: Fix documentation style per review feedback
Copilot ac2d44a
fix: Replace obsolete AnalyzerVerifier with modern CSharpAnalyzerTest
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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,132 @@ | ||
| --- | ||
| uid: bunit-analyzers | ||
| title: bUnit Analyzers | ||
| --- | ||
|
|
||
| # bUnit Analyzers | ||
|
|
||
| The `bunit.analyzers` package contains a set of Roslyn analyzers that help identify common mistakes and anti-patterns when writing bUnit tests. The analyzers are designed to provide early feedback during development, catching issues before tests are run. To use the analyzers, install the `bunit.analyzers` NuGet package in your test project. | ||
|
|
||
| This page describes the available analyzers and their usage. | ||
|
|
||
| ## Installation | ||
|
|
||
| Install the package via NuGet: | ||
|
|
||
| ```bash | ||
| dotnet add package bunit.analyzers | ||
| ``` | ||
|
|
||
| The analyzers will automatically run during compilation and provide warnings or suggestions in your IDE and build output. | ||
|
|
||
| ## Available Analyzers | ||
|
|
||
| ### BUNIT0002: Prefer Find<T> over casting | ||
|
|
||
| **Severity**: Info | ||
| **Category**: Usage | ||
|
|
||
| This analyzer detects when a cast is applied to the result of `Find(selector)` and suggests using the generic `Find<T>(selector)` method instead. Using the generic method is more concise, type-safe, and expresses intent more clearly. | ||
|
|
||
| #### Examples | ||
|
|
||
| **Incorrect** - triggers BUNIT0002: | ||
| ```csharp | ||
| using AngleSharp.Dom; | ||
|
|
||
| var cut = Render<MyComponent>(); | ||
| IHtmlAnchorElement link = (IHtmlAnchorElement)cut.Find("a"); | ||
| ``` | ||
|
|
||
| **Correct**: | ||
| ```csharp | ||
| using AngleSharp.Dom; | ||
|
|
||
| var cut = Render<MyComponent>(); | ||
| var link = cut.Find<IHtmlAnchorElement>("a"); | ||
| ``` | ||
|
|
||
| #### When to Use | ||
|
|
||
| `Find<T>()` should be used whenever a specific element type is needed: | ||
| - When working with AngleSharp element interfaces (`IHtmlAnchorElement`, `IHtmlButtonElement`, etc.) | ||
| - When type-specific properties or methods need to be accessed | ||
| - When clearer, more maintainable test code is desired | ||
|
|
||
| ### BUNIT0001: Razor test files should inherit from BunitContext | ||
|
|
||
| **Status**: Planned for future release | ||
|
|
||
| This analyzer will detect when Razor test files (`.razor` files) use variables or event callbacks from the test code without inheriting from `BunitContext`. Without the proper inheritance, the error "The render handle is not yet assigned" may be encountered. | ||
|
|
||
| #### Planned Examples | ||
|
|
||
| **Incorrect** - will trigger BUNIT0001 in the future: | ||
| ```razor | ||
| @code | ||
| { | ||
| [Fact] | ||
| public void Test() | ||
| { | ||
| using var ctx = new BunitContext(); | ||
|
|
||
| Action<MouseEventArgs> onClickHandler = _ => { Assert.True(true); }; | ||
|
|
||
| var cut = ctx.Render(@<MyComponent OnClick="onClickHandler" />); | ||
| cut.Find("button").Click(); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| **Correct**: | ||
| ```razor | ||
| @inherits BunitContext | ||
| @code | ||
| { | ||
| [Fact] | ||
| public async Task Test() | ||
| { | ||
| var wasInvoked = false; | ||
|
|
||
| Action<MouseEventArgs> onClick = _ => { wasInvoked = true; }; | ||
|
|
||
| var cut = Render(@<MyComponent OnClick="onClick" />); | ||
| var button = cut.Find("button"); | ||
| await button.ClickAsync(new MouseEventArgs()); | ||
|
|
||
| cut.WaitForAssertion(() => Assert.True(wasInvoked)); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| The analyzers can be configured in a project's `.editorconfig` file or using ruleset files. For example, to change the severity of BUNIT0002: | ||
|
|
||
| ```ini | ||
| # .editorconfig | ||
| [*.cs] | ||
| # Change BUNIT0002 from Info to Warning | ||
| dotnet_diagnostic.BUNIT0002.severity = warning | ||
|
|
||
| # Or disable it entirely | ||
| dotnet_diagnostic.BUNIT0002.severity = none | ||
| ``` | ||
|
|
||
| ## Contributing | ||
|
|
||
| We welcome contributions! If you have ideas for additional analyzers that could help bUnit users, please: | ||
|
|
||
| 1. Check existing [issues](https://github.com/bunit-dev/bUnit/issues) for similar suggestions | ||
| 2. Open a new issue describing the problem the analyzer would solve | ||
| 3. Submit a pull request with your implementation | ||
|
|
||
| Common areas for improvement include: | ||
| - Detecting incorrect usage of `WaitFor` methods | ||
| - Identifying missing or incorrect component parameter bindings | ||
| - Catching improper service injection patterns | ||
| - Finding opportunities to use helper methods | ||
|
|
||
| ## Feedback | ||
|
|
||
| If you encounter issues with the analyzers or have suggestions for improvements, please [open an issue](https://github.com/bunit-dev/bUnit/issues/new) on GitHub. |
This file contains hidden or 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 hidden or 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,15 @@ | ||
| { | ||
| "$schema": "https://raw.githubusercontent.com/dotnet/roslyn/main/src/Compilers/Core/Portable/DiagnosticAnalyzers/AnalyzerReleases.Unshipped.schema.json", | ||
| "document": [ | ||
| { | ||
| "id": "BUNIT0001", | ||
| "isEnabledByDefault": true, | ||
| "severity": "warning" | ||
| }, | ||
| { | ||
| "id": "BUNIT0002", | ||
| "isEnabledByDefault": true, | ||
| "severity": "info" | ||
| } | ||
| ] | ||
| } |
103 changes: 103 additions & 0 deletions
103
src/bunit.analyzers/Analyzers/PreferGenericFindAnalyzer.cs
This file contains hidden or 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,103 @@ | ||
| using System.Collections.Immutable; | ||
| using System.Linq; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.CSharp; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
| using Microsoft.CodeAnalysis.Diagnostics; | ||
|
|
||
| namespace Bunit.Analyzers; | ||
|
|
||
| /// <summary> | ||
| /// Analyzer that detects cast expressions from Find() and suggests using Find{T}() instead. | ||
| /// </summary> | ||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
| public class PreferGenericFindAnalyzer : DiagnosticAnalyzer | ||
| { | ||
| /// <inheritdoc/> | ||
| public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.PreferGenericFind); | ||
|
|
||
| /// <inheritdoc/> | ||
| public override void Initialize(AnalysisContext context) | ||
| { | ||
| if (context is null) | ||
| { | ||
| throw new System.ArgumentNullException(nameof(context)); | ||
| } | ||
|
|
||
| context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
| context.EnableConcurrentExecution(); | ||
|
|
||
| context.RegisterSyntaxNodeAction(AnalyzeCastExpression, SyntaxKind.CastExpression); | ||
| } | ||
|
|
||
| private static void AnalyzeCastExpression(SyntaxNodeAnalysisContext context) | ||
| { | ||
| var castExpression = (CastExpressionSyntax)context.Node; | ||
|
|
||
| // Check if the cast is on a Find() invocation | ||
| if (castExpression.Expression is not InvocationExpressionSyntax invocation) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Check if it's a member access expression (e.g., cut.Find(...)) | ||
| if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Check if the method name is "Find" | ||
| if (memberAccess.Name.Identifier.ValueText != "Find") | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Get the method symbol to verify it's the bUnit Find method | ||
| var methodSymbol = context.SemanticModel.GetSymbolInfo(memberAccess, context.CancellationToken).Symbol as IMethodSymbol; | ||
| if (methodSymbol is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Check if the method is from IRenderedFragment or related types | ||
| var containingType = methodSymbol.ContainingType; | ||
| if (containingType is null || !IsRenderedFragmentType(containingType)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Get the selector argument if present | ||
| var selector = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression.ToString() ?? "selector"; | ||
|
|
||
| // Get the cast type | ||
| var castType = castExpression.Type.ToString(); | ||
|
|
||
| var diagnostic = Diagnostic.Create( | ||
| DiagnosticDescriptors.PreferGenericFind, | ||
| castExpression.GetLocation(), | ||
| castType, | ||
| selector); | ||
|
|
||
| context.ReportDiagnostic(diagnostic); | ||
| } | ||
|
|
||
| private static bool IsRenderedFragmentType(INamedTypeSymbol type) | ||
| { | ||
| // Check if the type or any of its interfaces is IRenderedFragment | ||
| var typeName = type.Name; | ||
| if (typeName is "IRenderedFragment" or "IRenderedComponent" or "RenderedFragment" or "RenderedComponent") | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| foreach (var @interface in type.AllInterfaces) | ||
| { | ||
| if (@interface.Name is "IRenderedFragment" or "IRenderedComponent") | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
| } |
This file contains hidden or 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,38 @@ | ||
| using Microsoft.CodeAnalysis; | ||
|
|
||
| namespace Bunit.Analyzers; | ||
|
|
||
| /// <summary> | ||
| /// Diagnostic descriptors for bUnit analyzers. | ||
| /// Public to allow testing frameworks to reference diagnostic IDs and descriptors. | ||
| /// </summary> | ||
| public static class DiagnosticDescriptors | ||
| { | ||
| private const string Category = "Usage"; | ||
|
|
||
| /// <summary> | ||
| /// BUNIT0001: Razor test files should inherit from BunitContext when using local variables in component parameters. | ||
| /// </summary> | ||
| public static readonly DiagnosticDescriptor MissingInheritsInRazorFile = new( | ||
| id: "BUNIT0001", | ||
| title: "Razor test files should inherit from BunitContext", | ||
| messageFormat: "Razor test file should inherit from BunitContext using @inherits BunitContext to avoid render handle errors", | ||
| category: Category, | ||
| defaultSeverity: DiagnosticSeverity.Warning, | ||
| isEnabledByDefault: true, | ||
| description: "When writing tests in Razor files that use variables or event callbacks from the test code, the file must inherit from BunitContext. Otherwise, you may encounter the error: The render handle is not yet assigned.", | ||
| helpLinkUri: "https://bunit.dev/docs/analyzers/bunit0001.html"); | ||
|
|
||
| /// <summary> | ||
| /// BUNIT0002: Prefer Find{T} over casting. | ||
| /// </summary> | ||
| public static readonly DiagnosticDescriptor PreferGenericFind = new( | ||
| id: "BUNIT0002", | ||
| title: "Prefer Find<T> over casting", | ||
| messageFormat: "Use Find<{0}>(\"{1}\") instead of casting", | ||
| category: Category, | ||
| defaultSeverity: DiagnosticSeverity.Info, | ||
| isEnabledByDefault: true, | ||
| description: "When finding elements with a specific type, use Find<T>(selector) instead of casting the result of Find(selector).", | ||
| helpLinkUri: "https://bunit.dev/docs/analyzers/bunit0002.html"); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is
bunit.analyzers.testsneeded here? can probably be removedThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bunit.analyzers.testsmust remain in the exclusion list. Removing it causes the root Directory.Build.props to add AspNetCore implicit usings that the analyzer test project doesn't have as dependencies, resulting in build errors.