From 5f23466f186a2a9d993a466580db317b4f66ded8 Mon Sep 17 00:00:00 2001 From: Marco Fogliatto <2962955+mfogliatto@users.noreply.github.com> Date: Sat, 9 Aug 2025 12:39:12 +0200 Subject: [PATCH 1/4] Fix unit test --- .../Providers/MSBuildProjectMetadataProviderTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ReferenceCop.MSBuild.Tests/Providers/MSBuildProjectMetadataProviderTests.cs b/src/ReferenceCop.MSBuild.Tests/Providers/MSBuildProjectMetadataProviderTests.cs index 3ae6438..b384e09 100644 --- a/src/ReferenceCop.MSBuild.Tests/Providers/MSBuildProjectMetadataProviderTests.cs +++ b/src/ReferenceCop.MSBuild.Tests/Providers/MSBuildProjectMetadataProviderTests.cs @@ -40,7 +40,7 @@ public void GetProjectReferences_WhenReferenceHasNoWarn_ReturnsNoWarnValue() references.Should().ContainSingle(); var reference = references.Single(); reference.Path.Should().Be("TestReferenceWithNoWarn.csproj"); - reference.NoWarn.Should().ContainSingle().Which.Should().Be("RC0001;RC0002"); + reference.NoWarn.Should().Contain("RC0001").And.Contain("RC0002"); } finally { @@ -151,7 +151,7 @@ private string CreateTempProjectFileWithReferenceNoWarn() net6.0 - + "; File.WriteAllText(tempFile, projectContent); From 83360b3a1f0178c49bc782739b13346c9acb7f7c Mon Sep 17 00:00:00 2001 From: Marco Fogliatto <2962955+mfogliatto@users.noreply.github.com> Date: Sat, 23 Aug 2025 07:16:44 +0200 Subject: [PATCH 2/4] Fix Copilot instructions file location --- .../copilot-instructions.md | 438 +++++++++--------- src/ReferenceCop.sln | 1 + 2 files changed, 220 insertions(+), 219 deletions(-) rename .github/{instructions => }/copilot-instructions.md (97%) diff --git a/.github/instructions/copilot-instructions.md b/.github/copilot-instructions.md similarity index 97% rename from .github/instructions/copilot-instructions.md rename to .github/copilot-instructions.md index ec59923..8736253 100644 --- a/.github/instructions/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,220 +1,220 @@ -# claude.md - -## Project Overview - -**Project Name:** ReferenceCop -**Description:** -ReferenceCop is a suite of tools (including a Roslyn analyzer and an MSBuild task) designed to help .NET teams enforce dependency management rules at build time. It prevents discouraged or prohibited references from being added to projects, ensuring adherence to clean architecture principles. - -**Tech Stack:** -- .NET (Roslyn analyzers & MSBuild) -- C# -- MSBuild tasks - -## Main Objectives - -1. Enforce architectural constraints by restricting certain project/package references. -2. Make configuration simple and maintainable for large .NET codebases. -3. Provide clear, actionable diagnostics during the build process. - -## Architecture Patterns - -### Component Structure -- **Core Library**: Contains rule configuration models, violation detectors, and providers -- **MSBuild Integration**: Provides build-time validation using the Task pattern -- **Roslyn Integration**: Provides IDE-time validation using the DiagnosticAnalyzer pattern -- **Package**: Aggregates components into a single distributable NuGet package - -### Key Abstractions -- `IViolationDetector`: Core interface for all rule validators -- `ReferenceCopConfig`: Configuration model for rule definitions -- `Violation`: Represents a rule violation with severity and messaging - -### Rule Types -- `AssemblyName`: Validates referenced assembly names against patterns -- `ProjectTag`: Validates project references based on project tag metadata -- `ProjectPath`: Validates project references based on folder structure - -## Development Workflow - -### Build and Test -```bash -dotnet build src/ReferenceCop.sln -dotnet test src/ReferenceCop.sln -``` - -### Package Creation -```bash -# For local development builds -dotnet pack src/ReferenceCop.Package/ReferenceCop.Package.csproj -c Debug - -# For release builds -dotnet pack src/ReferenceCop.Package/ReferenceCop.Package.csproj -c Release -``` - -### Debugging -- Set `LaunchDebugger` property in MSBuild to trigger debugging: - - For MSBuild task: `LaunchDebugger=MSBuild` - - For Roslyn analyzer: `LaunchDebugger=Roslyn` - -## Integration Testing -- The project uses itself for validation (dogfooding) -- Special handling in `.Package.csproj` to avoid circular references during development - -## Contribution Guidelines - -### Code Style Guidelines - -Please ensure all code follows StyleCop-based conventions as defined in the project's configuration file located at `src\stylecop.json`. This file defines the following important style guidelines: - -1. **Documentation Rules**: Internal elements, private fields, and interfaces don't require documentation comments. -2. **Ordering Rules**: - - Using directives should be placed inside namespaces - - System using directives should be listed first -3. **Layout Rules**: - - All files must end with a newline - - Use 4 spaces for indentation, not tabs - - Maintain consistent indentation when adding or modifying code blocks -4. **File Organization**: - - Each type (class, interface, struct, enum) should have its own file - - The file name should match the type name exactly - - Nested types can be in the same file as their parent type - -5. **Whitespace Rules**: - - No trailing whitespace at the end of lines (SA1028) - - No empty lines with whitespace characters - - Use a single blank line to separate logical code blocks - - Use a single space after keywords like `if`, `for`, `while`, etc. - - Use a single space before opening braces - -6. **Indentation Guidelines**: - - Always preserve the existing indentation style when modifying code - - Match the surrounding code's indentation level precisely when adding new code - - Ensure that all opening and closing braces maintain proper alignment - - For method parameters and arguments that span multiple lines, align parameters with the first parameter - - Do not mix tabs and spaces for indentation - -When adding or modifying code, adhere to these style conventions for consistency across the project. - -### Testing Guidelines - -**Unit Test Conventions:** - -1. **Test Naming**: - Use `MethodName_WhenScenario_DoesSomething` for test method names. - *Example*: - `CalculateTotal_WhenDiscountIsApplied_ReducesFinalPrice` - -2. **Mocking**: - - Use **NSubstitute** for simple substitutions. - - **Inline mocks**: Define NSubstitute substitutions inline (where used) if no setup is needed. - - **Use builders if available**: Use existing builders if present in the project; otherwise, NSubstitute. - -3. **Assertions**: - Use **FluentAssertions** with expressive syntax: - *Example*: - `result.Should().Be(expectedValue).Because("the discount should apply");` - -4. **Test Structure**: - Explicitly separate test phases with comments and maintain proper indentation: -```csharp -// Arrange. -var service = Substitute.For(); - -// Act. -var result = sut.Calculate(); - -// Assert. -result.Should().NotBeNull(); -``` - - When adding test methods, ensure that indentation is consistent with existing code: - - Use 4 spaces for indentation levels - - Align closing braces with their corresponding opening statements - - Maintain proper indentation for nested code blocks within test methods - -5. **Framework**: -Use **MSTest** attributes: -- Annotate test classes with `[TestClass]` -- Test methods with `[TestMethod]` -- Setup/teardown with `[TestInitialize]`/`[TestCleanup]` -- Data-driven tests with `[DataTestMethod]` + `[DataRow]` - -6. **Readability**: -- Keep tests focused on a single scenario. Mock only dependencies critical to the tested behavior. -- When arranging and asserting in tests, **avoid duplicating strings or values**. - Instead, assign these values to variables (or constants, when appropriate) in the Arrange section, - and reuse those variables in the Assert section. This ensures maintainability and clarity. - -### Pull Request Guidelines - -When creating pull requests, always use the repository's pull request template located at `.github/PULL_REQUEST_TEMPLATE.md`. This template includes the following sections: - -- Description of changes -- Type of change (Enhancement, Bug fix, Chore) -- Related issue link -- Testing information -- Screenshots (if applicable) -- Additional context - -Ensure all required fields in the template are filled out appropriately when creating pull requests. - -### Common Tasks - -#### Adding a New Rule Type -1. Create rule class inheriting from `ReferenceCopConfig.Rule` -2. Add to `XmlArrayItem` attributes in `ReferenceCopConfig` -3. Create detector implementing `IViolationDetector` -4. Update task/analyzer to use the new detector - -#### Modifying Rule Processing -- Core detection logic in `Detectors/` namespace -- MSBuild integration in `ReferenceCopTask.cs` -- Roslyn integration in `ReferenceCopAnalyzer.cs` - -### Code Generation and Modification Guidelines - -When adding or modifying code in the ReferenceCop project, follow these guidelines: - -1. **Code Addition**: - - Match existing indentation patterns precisely - - Ensure all nested blocks follow the 4-space indentation rule - - Maintain the same brace style as surrounding code - -2. **Code Modification**: - - Preserve the original indentation when replacing or modifying existing code - - When refactoring, ensure that the modified code follows the same indentation patterns - - Do not change indentation of surrounding, unchanged code - -3. **Line Wrapping**: - - For method signatures that span multiple lines, indent continuation lines with 4 spaces - - For parameter lists, align parameters with the first parameter - - For chained method calls, use consistent indentation for each line - -4. **XML Documentation**: - - Ensure XML doc comments maintain consistent indentation with the code they document - - Indent XML doc elements consistently within the comment block - -## Instructions for the AI Agent - -- Write clean, idiomatic C# code. -- Follow the coding standards and style described in the sections above. -- Add XML documentation for all public methods. -- Write MSTest tests for all new code. -- Pay careful attention to code indentation: - - Maintain 4-space indentation throughout the codebase - - Align braces correctly according to the existing code style - - Ensure that code additions match the surrounding indentation exactly - - When generating code, verify that indentation remains consistent with existing patterns -- If you need clarification, request it in the `[ASSISTANT NOTE]` section below. - -## Reference - -Use as reference the code that is located in #src\ReferenceCop. - -## Output Format - -- Return code changes as code blocks, specifying the filename and relative path. -- If making multiple file changes, group by file. -- Summarize changes below each code block. +# claude.md + +## Project Overview + +**Project Name:** ReferenceCop +**Description:** +ReferenceCop is a suite of tools (including a Roslyn analyzer and an MSBuild task) designed to help .NET teams enforce dependency management rules at build time. It prevents discouraged or prohibited references from being added to projects, ensuring adherence to clean architecture principles. + +**Tech Stack:** +- .NET (Roslyn analyzers & MSBuild) +- C# +- MSBuild tasks + +## Main Objectives + +1. Enforce architectural constraints by restricting certain project/package references. +2. Make configuration simple and maintainable for large .NET codebases. +3. Provide clear, actionable diagnostics during the build process. + +## Architecture Patterns + +### Component Structure +- **Core Library**: Contains rule configuration models, violation detectors, and providers +- **MSBuild Integration**: Provides build-time validation using the Task pattern +- **Roslyn Integration**: Provides IDE-time validation using the DiagnosticAnalyzer pattern +- **Package**: Aggregates components into a single distributable NuGet package + +### Key Abstractions +- `IViolationDetector`: Core interface for all rule validators +- `ReferenceCopConfig`: Configuration model for rule definitions +- `Violation`: Represents a rule violation with severity and messaging + +### Rule Types +- `AssemblyName`: Validates referenced assembly names against patterns +- `ProjectTag`: Validates project references based on project tag metadata +- `ProjectPath`: Validates project references based on folder structure + +## Development Workflow + +### Build and Test +```bash +dotnet build src/ReferenceCop.sln +dotnet test src/ReferenceCop.sln +``` + +### Package Creation +```bash +# For local development builds +dotnet pack src/ReferenceCop.Package/ReferenceCop.Package.csproj -c Debug + +# For release builds +dotnet pack src/ReferenceCop.Package/ReferenceCop.Package.csproj -c Release +``` + +### Debugging +- Set `LaunchDebugger` property in MSBuild to trigger debugging: + - For MSBuild task: `LaunchDebugger=MSBuild` + - For Roslyn analyzer: `LaunchDebugger=Roslyn` + +## Integration Testing +- The project uses itself for validation (dogfooding) +- Special handling in `.Package.csproj` to avoid circular references during development + +## Contribution Guidelines + +### Code Style Guidelines + +Please ensure all code follows StyleCop-based conventions as defined in the project's configuration file located at `src\stylecop.json`. This file defines the following important style guidelines: + +1. **Documentation Rules**: Internal elements, private fields, and interfaces don't require documentation comments. +2. **Ordering Rules**: + - Using directives should be placed inside namespaces + - System using directives should be listed first +3. **Layout Rules**: + - All files must end with a newline + - Use 4 spaces for indentation, not tabs + - Maintain consistent indentation when adding or modifying code blocks +4. **File Organization**: + - Each type (class, interface, struct, enum) should have its own file + - The file name should match the type name exactly + - Nested types can be in the same file as their parent type + +5. **Whitespace Rules**: + - No trailing whitespace at the end of lines (SA1028) + - No empty lines with whitespace characters + - Use a single blank line to separate logical code blocks + - Use a single space after keywords like `if`, `for`, `while`, etc. + - Use a single space before opening braces + +6. **Indentation Guidelines**: + - Always preserve the existing indentation style when modifying code + - Match the surrounding code's indentation level precisely when adding new code + - Ensure that all opening and closing braces maintain proper alignment + - For method parameters and arguments that span multiple lines, align parameters with the first parameter + - Do not mix tabs and spaces for indentation + +When adding or modifying code, adhere to these style conventions for consistency across the project. + +### Testing Guidelines + +**Unit Test Conventions:** + +1. **Test Naming**: + Use `MethodName_WhenScenario_DoesSomething` for test method names. + *Example*: + `CalculateTotal_WhenDiscountIsApplied_ReducesFinalPrice` + +2. **Mocking**: + - Use **NSubstitute** for simple substitutions. + - **Inline mocks**: Define NSubstitute substitutions inline (where used) if no setup is needed. + - **Use builders if available**: Use existing builders if present in the project; otherwise, NSubstitute. + +3. **Assertions**: + Use **FluentAssertions** with expressive syntax: + *Example*: + `result.Should().Be(expectedValue).Because("the discount should apply");` + +4. **Test Structure**: + Explicitly separate test phases with comments and maintain proper indentation: +```csharp +// Arrange. +var service = Substitute.For(); + +// Act. +var result = sut.Calculate(); + +// Assert. +result.Should().NotBeNull(); +``` + + When adding test methods, ensure that indentation is consistent with existing code: + - Use 4 spaces for indentation levels + - Align closing braces with their corresponding opening statements + - Maintain proper indentation for nested code blocks within test methods + +5. **Framework**: +Use **MSTest** attributes: +- Annotate test classes with `[TestClass]` +- Test methods with `[TestMethod]` +- Setup/teardown with `[TestInitialize]`/`[TestCleanup]` +- Data-driven tests with `[DataTestMethod]` + `[DataRow]` + +6. **Readability**: +- Keep tests focused on a single scenario. Mock only dependencies critical to the tested behavior. +- When arranging and asserting in tests, **avoid duplicating strings or values**. + Instead, assign these values to variables (or constants, when appropriate) in the Arrange section, + and reuse those variables in the Assert section. This ensures maintainability and clarity. + +### Pull Request Guidelines + +When creating pull requests, always use the repository's pull request template located at `.github/PULL_REQUEST_TEMPLATE.md`. This template includes the following sections: + +- Description of changes +- Type of change (Enhancement, Bug fix, Chore) +- Related issue link +- Testing information +- Screenshots (if applicable) +- Additional context + +Ensure all required fields in the template are filled out appropriately when creating pull requests. + +### Common Tasks + +#### Adding a New Rule Type +1. Create rule class inheriting from `ReferenceCopConfig.Rule` +2. Add to `XmlArrayItem` attributes in `ReferenceCopConfig` +3. Create detector implementing `IViolationDetector` +4. Update task/analyzer to use the new detector + +#### Modifying Rule Processing +- Core detection logic in `Detectors/` namespace +- MSBuild integration in `ReferenceCopTask.cs` +- Roslyn integration in `ReferenceCopAnalyzer.cs` + +### Code Generation and Modification Guidelines + +When adding or modifying code in the ReferenceCop project, follow these guidelines: + +1. **Code Addition**: + - Match existing indentation patterns precisely + - Ensure all nested blocks follow the 4-space indentation rule + - Maintain the same brace style as surrounding code + +2. **Code Modification**: + - Preserve the original indentation when replacing or modifying existing code + - When refactoring, ensure that the modified code follows the same indentation patterns + - Do not change indentation of surrounding, unchanged code + +3. **Line Wrapping**: + - For method signatures that span multiple lines, indent continuation lines with 4 spaces + - For parameter lists, align parameters with the first parameter + - For chained method calls, use consistent indentation for each line + +4. **XML Documentation**: + - Ensure XML doc comments maintain consistent indentation with the code they document + - Indent XML doc elements consistently within the comment block + +## Instructions for the AI Agent + +- Write clean, idiomatic C# code. +- Follow the coding standards and style described in the sections above. +- Add XML documentation for all public methods. +- Write MSTest tests for all new code. +- Pay careful attention to code indentation: + - Maintain 4-space indentation throughout the codebase + - Align braces correctly according to the existing code style + - Ensure that code additions match the surrounding indentation exactly + - When generating code, verify that indentation remains consistent with existing patterns +- If you need clarification, request it in the `[ASSISTANT NOTE]` section below. + +## Reference + +Use as reference the code that is located in #src\ReferenceCop. + +## Output Format + +- Return code changes as code blocks, specifying the filename and relative path. +- If making multiple file changes, group by file. +- Summarize changes below each code block. - Ask for clarification in `[ASSISTANT NOTE]` if anything is unclear. \ No newline at end of file diff --git a/src/ReferenceCop.sln b/src/ReferenceCop.sln index c808d09..9603185 100644 --- a/src/ReferenceCop.sln +++ b/src/ReferenceCop.sln @@ -32,6 +32,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferenceCop.MSBuild", "Ref EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{0F98B7C6-ADDF-456C-BC57-2712D8B102DD}" ProjectSection(SolutionItems) = preProject + ..\.github\copilot-instructions.md = ..\.github\copilot-instructions.md ..\.github\PULL_REQUEST_TEMPLATE.md = ..\.github\PULL_REQUEST_TEMPLATE.md EndProjectSection EndProject From 4d906a498059da5368142a9f2a3721e42f2906fd Mon Sep 17 00:00:00 2001 From: Marco Fogliatto <2962955+mfogliatto@users.noreply.github.com> Date: Sat, 23 Aug 2025 07:59:33 +0200 Subject: [PATCH 3/4] Add ToString for ReferenceEvaluationContext --- ...emblyNameViolationDetectorTests.cs.updated | 0 ...ojectPathViolationDetectorTests.cs.updated | 0 ...rojectTagViolationDetectorTests.cs.updated | 0 .../ReferenceEvaluationContextTests.cs | 79 +++++++++++++++++++ .../Detectors/ReferenceEvaluationContext.cs | 11 +++ 5 files changed, 90 insertions(+) delete mode 100644 src/ReferenceCop.Tests/Detectors/AssemblyNameViolationDetectorTests.cs.updated delete mode 100644 src/ReferenceCop.Tests/Detectors/ProjectPathViolationDetectorTests.cs.updated delete mode 100644 src/ReferenceCop.Tests/Detectors/ProjectTagViolationDetectorTests.cs.updated create mode 100644 src/ReferenceCop.Tests/Detectors/ReferenceEvaluationContextTests.cs diff --git a/src/ReferenceCop.Tests/Detectors/AssemblyNameViolationDetectorTests.cs.updated b/src/ReferenceCop.Tests/Detectors/AssemblyNameViolationDetectorTests.cs.updated deleted file mode 100644 index e69de29..0000000 diff --git a/src/ReferenceCop.Tests/Detectors/ProjectPathViolationDetectorTests.cs.updated b/src/ReferenceCop.Tests/Detectors/ProjectPathViolationDetectorTests.cs.updated deleted file mode 100644 index e69de29..0000000 diff --git a/src/ReferenceCop.Tests/Detectors/ProjectTagViolationDetectorTests.cs.updated b/src/ReferenceCop.Tests/Detectors/ProjectTagViolationDetectorTests.cs.updated deleted file mode 100644 index e69de29..0000000 diff --git a/src/ReferenceCop.Tests/Detectors/ReferenceEvaluationContextTests.cs b/src/ReferenceCop.Tests/Detectors/ReferenceEvaluationContextTests.cs new file mode 100644 index 0000000..fdb0442 --- /dev/null +++ b/src/ReferenceCop.Tests/Detectors/ReferenceEvaluationContextTests.cs @@ -0,0 +1,79 @@ +namespace ReferenceCop.Tests +{ + using FluentAssertions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class ReferenceEvaluationContextTests + { + [TestMethod] + public void ToString_WhenReferenceIsNull_ReturnsFormattedStringWithNull() + { + // Arrange. + var context = new ReferenceEvaluationContext(null); + + // Act. + string result = context.ToString(); + + // Assert. + result.Should().Be("Reference: null (Warnings Enabled)"); + } + + [TestMethod] + public void ToString_WhenReferenceIsNotNull_ReturnsFormattedStringWithReference() + { + // Arrange. + const string assemblyName = "TestAssembly"; + var context = new ReferenceEvaluationContext(assemblyName); + + // Act. + string result = context.ToString(); + + // Assert. + result.Should().Be("Reference: TestAssembly (Warnings Enabled)"); + } + + [TestMethod] + public void ToString_WhenWarningsAreSuppressed_ReturnsFormattedStringWithSuppressedStatus() + { + // Arrange. + const string assemblyName = "TestAssembly"; + var context = new ReferenceEvaluationContext(assemblyName, isWarningSuppressed: true); + + // Act. + string result = context.ToString(); + + // Assert. + result.Should().Be("Reference: TestAssembly (Warnings Suppressed)"); + } + + [TestMethod] + public void ToString_WithCustomReferenceType_UsesReferenceToStringMethod() + { + // Arrange. + var testReference = new TestReference("CustomAssembly"); + var context = new ReferenceEvaluationContext(testReference); + + // Act. + string result = context.ToString(); + + // Assert. + result.Should().Be("Reference: CustomAssembly (Warnings Enabled)"); + } + + private class TestReference + { + public TestReference(string name) + { + this.Name = name; + } + + public string Name { get; } + + public override string ToString() + { + return this.Name; + } + } + } +} diff --git a/src/ReferenceCop/Detectors/ReferenceEvaluationContext.cs b/src/ReferenceCop/Detectors/ReferenceEvaluationContext.cs index b0dc708..19d7ed1 100644 --- a/src/ReferenceCop/Detectors/ReferenceEvaluationContext.cs +++ b/src/ReferenceCop/Detectors/ReferenceEvaluationContext.cs @@ -26,5 +26,16 @@ public ReferenceEvaluationContext(TAssemblyIdentity reference, bool isWarningSup /// Gets a value indicating whether warnings are suppressed for this reference. /// public bool IsWarningSuppressed { get; } + + /// + /// Returns a string representation of this reference evaluation context. + /// + /// A string containing the reference identity and warning suppression status. + public override string ToString() + { + string referenceString = this.Reference?.ToString() ?? "null"; + string suppressionStatus = this.IsWarningSuppressed ? "Warnings Suppressed" : "Warnings Enabled"; + return $"Reference: {referenceString} ({suppressionStatus})"; + } } } From a7a8049c60dd7bf51658954c475a1bb354f81926 Mon Sep 17 00:00:00 2001 From: Marco Fogliatto <2962955+mfogliatto@users.noreply.github.com> Date: Sat, 23 Aug 2025 08:06:04 +0200 Subject: [PATCH 4/4] Add NoWarn collection for Roslyn Analyzer NoWarn support --- src/ReferenceCop.MSBuild/ReferenceCop.targets | 32 ++++ .../DiagnosticFactoryTests.cs | 8 +- .../NoWarnAssembliesProviderTests.cs | 163 ++++++++++++++++++ .../DiagnosticDescriptors.cs | 2 +- src/ReferenceCop.Roslyn/DiagnosticFactory.cs | 2 +- .../Providers/INoWarnAssembliesProvider.cs | 24 +++ .../Providers/NoWarnAssembliesProvider.cs | 71 ++++++++ .../ReferenceCopAnalyzer.cs | 18 +- 8 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 src/ReferenceCop.Roslyn.Tests/Providers/NoWarnAssembliesProviderTests.cs create mode 100644 src/ReferenceCop.Roslyn/Providers/INoWarnAssembliesProvider.cs create mode 100644 src/ReferenceCop.Roslyn/Providers/NoWarnAssembliesProvider.cs diff --git a/src/ReferenceCop.MSBuild/ReferenceCop.targets b/src/ReferenceCop.MSBuild/ReferenceCop.targets index 6dcecd5..0a3bc43 100644 --- a/src/ReferenceCop.MSBuild/ReferenceCop.targets +++ b/src/ReferenceCop.MSBuild/ReferenceCop.targets @@ -1,6 +1,38 @@ + + + + + + + + + + + + + + + + <_ReferenceCopNoWarnItem Include="@(ProjectReference->'%(Identity)')" Condition="'%(ProjectReference.NoWarn)' != ''"> + %(ProjectReference.NoWarn) + + <_ReferenceCopNoWarnItem Include="@(Reference->'%(Identity)')" Condition="'%(Reference.NoWarn)' != ''"> + %(Reference.NoWarn) + + <_ReferenceCopNoWarnItem Include="@(PackageReference->'%(Identity)')" Condition="'%(PackageReference.NoWarn)' != ''"> + %(PackageReference.NoWarn) + + + + + + @(_ReferenceCopNoWarnItem->'%(Identity)|%(NoWarn)', ';') + + + diff --git a/src/ReferenceCop.Roslyn.Tests/DiagnosticFactoryTests.cs b/src/ReferenceCop.Roslyn.Tests/DiagnosticFactoryTests.cs index dd807c6..e1a7eae 100644 --- a/src/ReferenceCop.Roslyn.Tests/DiagnosticFactoryTests.cs +++ b/src/ReferenceCop.Roslyn.Tests/DiagnosticFactoryTests.cs @@ -34,7 +34,7 @@ public void CreateFor_WhenViolationHasErrorSeverity_CreatesErrorDiagnostic() var violation = new Violation(rule, referenceName); // Act - var diagnostic = global::ReferenceCop.DiagnosticFactory.CreateFor(violation); + var diagnostic = DiagnosticFactory.CreateFor(violation); // Assert using (new AssertionScope()) @@ -72,7 +72,7 @@ public void CreateFor_WhenViolationHasWarningSeverity_CreatesWarningDiagnostic() var violation = new Violation(rule, referenceName); // Act - var diagnostic = global::ReferenceCop.DiagnosticFactory.CreateFor(violation); + var diagnostic = DiagnosticFactory.CreateFor(violation); // Assert using (new AssertionScope()) @@ -111,7 +111,7 @@ public void CreateFor_WhenViolationHasUnknownSeverity_ReturnsNull() var violation = new Violation(rule, referenceName); // Act - var diagnostic = global::ReferenceCop.DiagnosticFactory.CreateFor(violation); + var diagnostic = DiagnosticFactory.CreateFor(violation); // Assert diagnostic.Should().BeNull(); @@ -129,7 +129,7 @@ public void CreateFor_WhenExceptionIsProvided_CreatesErrorDiagnostic() var exception = new InvalidOperationException(exceptionMessage); // Act - var diagnostic = global::ReferenceCop.DiagnosticFactory.CreateFor(exception); + var diagnostic = DiagnosticFactory.CreateFor(exception); // Assert using (new AssertionScope()) diff --git a/src/ReferenceCop.Roslyn.Tests/Providers/NoWarnAssembliesProviderTests.cs b/src/ReferenceCop.Roslyn.Tests/Providers/NoWarnAssembliesProviderTests.cs new file mode 100644 index 0000000..aee5731 --- /dev/null +++ b/src/ReferenceCop.Roslyn.Tests/Providers/NoWarnAssembliesProviderTests.cs @@ -0,0 +1,163 @@ +namespace ReferenceCop.Roslyn.Tests +{ + using System.Linq; + using FluentAssertions; + using FluentAssertions.Execution; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using ReferenceCop; + + [TestClass] + public class NoWarnAssembliesProviderTests + { + [TestMethod] + public void GetNoWarnByAssembly_WhenNullInput_ReturnsEmptyDictionary() + { + // Arrange. + var provider = new NoWarnAssembliesProvider(); + + // Act. + var result = provider.GetNoWarnByAssembly(null); + + // Assert. + using (new AssertionScope()) + { + result.Should().NotBeNull(); + result.Should().BeEmpty(); + } + } + + [TestMethod] + public void GetNoWarnByAssembly_WhenEmptyInput_ReturnsEmptyDictionary() + { + // Arrange. + var provider = new NoWarnAssembliesProvider(); + + // Act. + var result = provider.GetNoWarnByAssembly(string.Empty); + + // Assert. + using (new AssertionScope()) + { + result.Should().NotBeNull(); + result.Should().BeEmpty(); + } + } + + [TestMethod] + public void GetNoWarnByAssembly_WhenValidInput_ReturnsDictionaryWithEntries() + { + // Arrange. + var provider = new NoWarnAssembliesProvider(); + string noWarnAssemblies = "Assembly1|RC0001,RC0002;Assembly2|RC0002"; + + // Act. + var result = provider.GetNoWarnByAssembly(noWarnAssemblies); + + // Assert. + using (new AssertionScope()) + { + result.Should().NotBeNull(); + result.Should().HaveCount(2); + result.Should().ContainKey("Assembly1"); + result.Should().ContainKey("Assembly2"); + + var assembly1Codes = result["Assembly1"].ToList(); + assembly1Codes.Should().HaveCount(2); + assembly1Codes.Should().Contain("RC0001"); + assembly1Codes.Should().Contain("RC0002"); + + var assembly2Codes = result["Assembly2"].ToList(); + assembly2Codes.Should().HaveCount(1); + assembly2Codes.Should().Contain("RC0002"); + } + } + + [TestMethod] + public void GetNoWarnByAssembly_WhenInvalidEntries_IgnoresInvalidEntries() + { + // Arrange. + var provider = new NoWarnAssembliesProvider(); + string noWarnAssemblies = "Assembly1|RC0001;InvalidEntry;Assembly2|RC0002"; + + // Act. + var result = provider.GetNoWarnByAssembly(noWarnAssemblies); + + // Assert. + using (new AssertionScope()) + { + result.Should().NotBeNull(); + result.Should().HaveCount(2); + result.Should().ContainKey("Assembly1"); + result.Should().ContainKey("Assembly2"); + } + } + + [TestMethod] + public void GetNoWarnByAssembly_WhenInputContainsWhitespace_PreservesAssemblyNamesTrimsCodeValues() + { + // Arrange. + var provider = new NoWarnAssembliesProvider(); + string noWarnAssemblies = " Assembly1 | RC0001 , RC0002 ; Assembly2 | RC0002 "; + + // Act. + var result = provider.GetNoWarnByAssembly(noWarnAssemblies); + + // Assert. + using (new AssertionScope()) + { + result.Should().NotBeNull(); + result.Should().HaveCount(2); + result.Should().ContainKey(" Assembly1 "); + result.Should().ContainKey(" Assembly2 "); + + var assembly1Codes = result[" Assembly1 "].ToList(); + assembly1Codes.Should().HaveCount(2); + assembly1Codes.Should().Contain("RC0001"); + assembly1Codes.Should().Contain("RC0002"); + } + } + + [TestMethod] + public void GetNoWarnByAssembly_WhenMultipleCodesForSameAssembly_StoresAllCodes() + { + // Arrange. + var provider = new NoWarnAssembliesProvider(); + string noWarnAssemblies = "Assembly1|RC0001,RC0002,RC0003"; + + // Act. + var result = provider.GetNoWarnByAssembly(noWarnAssemblies); + + // Assert. + using (new AssertionScope()) + { + result.Should().NotBeNull(); + result.Should().HaveCount(1); + + var codes = result["Assembly1"].ToList(); + codes.Should().HaveCount(3); + codes.Should().Contain("RC0001"); + codes.Should().Contain("RC0002"); + codes.Should().Contain("RC0003"); + } + } + + [TestMethod] + public void GetNoWarnByAssembly_WhenEntryHasNoCodes_StoresEmptyCollection() + { + // Arrange. + var provider = new NoWarnAssembliesProvider(); + string noWarnAssemblies = "Assembly1|"; + + // Act. + var result = provider.GetNoWarnByAssembly(noWarnAssemblies); + + // Assert. + using (new AssertionScope()) + { + result.Should().NotBeNull(); + result.Should().HaveCount(1); + result["Assembly1"].Should().BeEmpty(); + } + } + } +} diff --git a/src/ReferenceCop.Roslyn/DiagnosticDescriptors.cs b/src/ReferenceCop.Roslyn/DiagnosticDescriptors.cs index 8f8f1a6..8413df7 100644 --- a/src/ReferenceCop.Roslyn/DiagnosticDescriptors.cs +++ b/src/ReferenceCop.Roslyn/DiagnosticDescriptors.cs @@ -1,4 +1,4 @@ -namespace ReferenceCop +namespace ReferenceCop.Roslyn { using Microsoft.CodeAnalysis; diff --git a/src/ReferenceCop.Roslyn/DiagnosticFactory.cs b/src/ReferenceCop.Roslyn/DiagnosticFactory.cs index b9b315c..535e8a2 100644 --- a/src/ReferenceCop.Roslyn/DiagnosticFactory.cs +++ b/src/ReferenceCop.Roslyn/DiagnosticFactory.cs @@ -1,4 +1,4 @@ -namespace ReferenceCop +namespace ReferenceCop.Roslyn { using System; using Microsoft.CodeAnalysis; diff --git a/src/ReferenceCop.Roslyn/Providers/INoWarnAssembliesProvider.cs b/src/ReferenceCop.Roslyn/Providers/INoWarnAssembliesProvider.cs new file mode 100644 index 0000000..97b5f46 --- /dev/null +++ b/src/ReferenceCop.Roslyn/Providers/INoWarnAssembliesProvider.cs @@ -0,0 +1,24 @@ +namespace ReferenceCop +{ + using System.Collections.Generic; + + /// + /// Provider interface for accessing NoWarn assembly configurations. + /// + public interface INoWarnAssembliesProvider + { + /// + /// Gets a dictionary of assembly names to their associated NoWarn codes from a raw string. + /// + /// + /// The raw NoWarn assemblies configuration string. + /// The string follows the format: "AssemblyName1|Code1,Code2;AssemblyName2|Code3,Code4" + /// where: + /// - Each assembly entry is separated by semicolons (;) + /// - Each assembly entry consists of an assembly name and NoWarn codes separated by a pipe character (|) + /// - NoWarn codes are comma-separated + /// + /// A dictionary where the key is the assembly name and the value is a collection of NoWarn codes. + Dictionary> GetNoWarnByAssembly(string noWarnAssembliesString); + } +} \ No newline at end of file diff --git a/src/ReferenceCop.Roslyn/Providers/NoWarnAssembliesProvider.cs b/src/ReferenceCop.Roslyn/Providers/NoWarnAssembliesProvider.cs new file mode 100644 index 0000000..2fde15c --- /dev/null +++ b/src/ReferenceCop.Roslyn/Providers/NoWarnAssembliesProvider.cs @@ -0,0 +1,71 @@ +namespace ReferenceCop +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// + /// Provider for NoWarn assembly configurations in ReferenceCop. + /// + public class NoWarnAssembliesProvider : INoWarnAssembliesProvider + { + private static readonly Dictionary> EmptyDictionary = new Dictionary>(); + + private static readonly char[] AssemblyEntriesSeparator = new[] { ';' }; + private static readonly char[] EntryPartsSeparator = new[] { '|' }; + private static readonly char[] NoWarnCodesSeparator = new[] { ',' }; + + /// + /// Gets a dictionary of assembly names to their associated NoWarn codes from a raw string. + /// + /// + /// The raw NoWarn assemblies configuration string. + /// The string follows the format: "AssemblyName1|Code1,Code2;AssemblyName2|Code3,Code4" + /// where: + /// - Each assembly entry is separated by semicolons (;). + /// - Each assembly entry consists of an assembly name and NoWarn codes separated by a pipe character (|). + /// - NoWarn codes are comma-separated. + /// + /// A dictionary where the key is the assembly name and the value is a collection of NoWarn codes. + public Dictionary> GetNoWarnByAssembly(string noWarnAssembliesString) + { + if (string.IsNullOrEmpty(noWarnAssembliesString)) + { + return EmptyDictionary; + } + + var entries = noWarnAssembliesString.Split(AssemblyEntriesSeparator, StringSplitOptions.RemoveEmptyEntries); + + if (entries.Length == 0) + { + return EmptyDictionary; + } + + var result = new Dictionary>(entries.Length); + foreach (var entry in entries) + { + var parts = entry.Split(EntryPartsSeparator, 2); + if (parts.Length == 2) + { + var assemblyName = parts[0]; + var noWarnCodesString = parts[1]; + + if (string.IsNullOrEmpty(noWarnCodesString)) + { + result[assemblyName] = Array.Empty(); + } + else + { + var noWarnCodes = noWarnCodesString.Split(NoWarnCodesSeparator, StringSplitOptions.RemoveEmptyEntries) + .Select(code => code.Trim()) + .ToArray(); + + result[assemblyName] = noWarnCodes; + } + } + } + + return result; + } + } +} diff --git a/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs b/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs index ba093d5..c03cfac 100644 --- a/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs +++ b/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs @@ -1,4 +1,4 @@ -namespace ReferenceCop +namespace ReferenceCop.Roslyn { using System; using System.Collections.Immutable; @@ -12,9 +12,16 @@ public class ReferenceCopAnalyzer : DiagnosticAnalyzer { private const string LaunchDebuggerKey = "build_property.LaunchDebugger"; private const string RoslynDebuggerTriggerValue = "Roslyn"; + private const string NoWarnAssembliesKey = "build_property.ReferenceCop_NoWarnAssemblies"; + private readonly INoWarnAssembliesProvider noWarnAssembliesProvider; private IViolationDetector assemblyNameViolationDetector; + public ReferenceCopAnalyzer() + { + this.noWarnAssembliesProvider = new NoWarnAssembliesProvider(); + } + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( DiagnosticDescriptors.GeneralError, DiagnosticDescriptors.IllegalReferenceRule, @@ -53,9 +60,16 @@ private static void LaunchDebuggerIfRequested(CompilationAnalysisContext compila private void AnalyzeCompilation(CompilationAnalysisContext compilationAnalysisContext) { var compilation = compilationAnalysisContext.Compilation; + compilationAnalysisContext.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(NoWarnAssembliesKey, out string noWarnAssemblies); + var noWarnByAssembly = this.noWarnAssembliesProvider.GetNoWarnByAssembly(noWarnAssemblies); var evaluationContexts = compilation.ReferencedAssemblyNames - .Select(assemblyRef => ReferenceEvaluationContextFactory.Create(assemblyRef)) + .Select(assemblyRef => + { + var assemblyName = assemblyRef.Name; + var noWarnCodes = noWarnByAssembly.TryGetValue(assemblyName, out var codes) ? codes : null; + return ReferenceEvaluationContextFactory.Create(assemblyRef, noWarnCodes); + }) .ToList(); foreach (var violation in this.assemblyNameViolationDetector.GetViolationsFrom(evaluationContexts))