From 632b4e7e46b6d76cf5b069a908e6dfbe514f4a11 Mon Sep 17 00:00:00 2001 From: Alex S Date: Wed, 25 Feb 2026 15:50:38 -0800 Subject: [PATCH 1/6] First pass --- README.md | 3 + .../ReferenceProtectorAnalyzerTests.cs | 225 ++++++++++++++++++ .../AnalyzerReleases.Shipped.md | 7 + .../DiagnosticDescriptors.cs | 8 + .../Models/DependencyRules.cs | 3 +- .../ReferenceProtector.Analyzers.cs | 75 +++++- src/Build/DependencyRules.schema.json | 5 + 7 files changed, 324 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6ebb3d9..9189267 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ The following warnings are generated by this package: | RP0003 | No dependency rules matched the current project | | RP0004 | Project reference 'x' ==> 'y' violates dependency rule or one of its exceptions | | RP0005 | Package reference 'x' ==> 'y' violates dependency rule or one of its exceptions | +| RP0006 | Tech debt exception 'x' ==> 'y' no longer matches any declared reference and can be removed | ## How to use Add a package reference to the [ReferenceProtector](https://www.nuget.org/packages/ReferenceProtector) package in your projects, or as a common package reference in the repo's [`Directory.Packages.props`](./Directory.Build.props). @@ -82,6 +83,8 @@ Top `ProjectDependencies` object will contain a list of rules to validate agains Top `PackageDependences` object will have the same format as `ProjectDependencies` with `LinkType` omitted, since only direct package references will be considered. Also, `Description` section will be part of `RP0005` warning (as opposed to `RP0004`) +Adding `IsTechDebt: true` to an exception (if the `Policy: "Forbidden"`) will mark the exception to the rule as undesireable and analyzer will produce a warning when such an exception no longer holds and can be removed from the rules. + ## Matching logic Each reference between the projects / packages during the build is evaluated against provided list of policies. First each pair of dependencies is evaluated against `From` and `To` patterns, based on their full path. For project dependencies - if the match is successful - their link type is evaluated: if current pair has a direct dependency on each other and `LinkType` value is `Direct` or `DirectOrTransient` - the match is successful, otherwise (the dependency is transient) - `LinkType` should be `Transient` or `DirectOrTransient` for the match to be successful. Package dependencies are only viewed as direct references. Then the exceptions are evaluated using the same pattern matching logic with `From` and `To` fields. The decision logic is as follows diff --git a/src/Analyzers/ReferenceProtector.Analyzers.Tests/ReferenceProtectorAnalyzerTests.cs b/src/Analyzers/ReferenceProtector.Analyzers.Tests/ReferenceProtectorAnalyzerTests.cs index 0b0f207..8219930 100644 --- a/src/Analyzers/ReferenceProtector.Analyzers.Tests/ReferenceProtectorAnalyzerTests.cs +++ b/src/Analyzers/ReferenceProtector.Analyzers.Tests/ReferenceProtectorAnalyzerTests.cs @@ -231,4 +231,229 @@ private AnalyzerTest GetAnalyzer() => }, ReferenceAssemblies = ReferenceAssemblies.Net.Net90 }; + + /// + /// Verifies that the analyzer reports RP0006 when a tech debt exception no longer matches any declared project reference. + /// + [Fact] + public async Task TechDebtException_NoLongerNeeded_ShouldReportDiagnostic_Async() + { + var test = GetAnalyzer(); + test.TestState.AdditionalFiles.Add( + ("DependencyRules.json", """ + { + "ProjectDependencies": [ + { + "From": "*", + "To": "*", + "Description": "No direct project references allowed", + "Policy": "Forbidden", + "LinkType": "Direct", + "Exceptions": [ + { + "From": "TestProject.csproj", + "To": "OldProject.csproj", + "Justification": "Legacy dependency to be removed", + "IsTechDebt": true + } + ] + } + ] + } + """)); + + // OldProject.csproj is NOT in the declared references + test.TestState.AdditionalFiles.Add( + (ReferenceProtectorAnalyzer.DeclaredReferencesFile, """ + TestProject.csproj ProjectReferenceDirect SomeOtherProject.csproj + """)); + + test.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning("RP0004") + .WithNoLocation() + .WithMessage("Project reference 'TestProject.csproj' ==> 'SomeOtherProject.csproj' violates dependency rule 'No direct project references allowed' or one of its exceptions. Please remove the dependency or update 'DependencyRules.json' file to allow it.")); + + test.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning("RP0006") + .WithNoLocation() + .WithMessage("Tech debt exception 'TestProject.csproj' ==> 'OldProject.csproj' in rule 'No direct project references allowed' no longer matches any declared reference and can be removed from 'DependencyRules.json'")); + + await test.RunAsync(TestContext.Current.CancellationToken); + } + + /// + /// Verifies that the analyzer does NOT report RP0006 when a tech debt exception still matches a declared project reference. + /// + [Fact] + public async Task TechDebtException_StillNeeded_ShouldNotReportDiagnostic_Async() + { + var test = GetAnalyzer(); + test.TestState.AdditionalFiles.Add( + ("DependencyRules.json", """ + { + "ProjectDependencies": [ + { + "From": "*", + "To": "*", + "Description": "No direct project references allowed", + "Policy": "Forbidden", + "LinkType": "Direct", + "Exceptions": [ + { + "From": "TestProject.csproj", + "To": "ReferencedProject.csproj", + "Justification": "Legacy dependency to be removed", + "IsTechDebt": true + } + ] + } + ] + } + """)); + + test.TestState.AdditionalFiles.Add( + (ReferenceProtectorAnalyzer.DeclaredReferencesFile, """ + TestProject.csproj ProjectReferenceDirect ReferencedProject.csproj + """)); + + // No diagnostics expected - the tech debt exception is still needed + await test.RunAsync(TestContext.Current.CancellationToken); + } + + /// + /// Verifies that a non-tech-debt exception that no longer matches does NOT trigger RP0006. + /// + [Fact] + public async Task NonTechDebtException_NoLongerNeeded_ShouldNotReportDiagnostic_Async() + { + var test = GetAnalyzer(); + test.TestState.AdditionalFiles.Add( + ("DependencyRules.json", """ + { + "ProjectDependencies": [ + { + "From": "*", + "To": "*", + "Description": "No direct project references allowed", + "Policy": "Forbidden", + "LinkType": "Direct", + "Exceptions": [ + { + "From": "TestProject.csproj", + "To": "OldProject.csproj", + "Justification": "Legitimate exception" + } + ] + } + ] + } + """)); + + test.TestState.AdditionalFiles.Add( + (ReferenceProtectorAnalyzer.DeclaredReferencesFile, """ + TestProject.csproj ProjectReferenceDirect SomeOtherProject.csproj + """)); + + // RP0004 for the violating reference, but NO RP0006 since the exception is not tech debt + test.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning("RP0004") + .WithNoLocation() + .WithMessage("Project reference 'TestProject.csproj' ==> 'SomeOtherProject.csproj' violates dependency rule 'No direct project references allowed' or one of its exceptions. Please remove the dependency or update 'DependencyRules.json' file to allow it.")); + + await test.RunAsync(TestContext.Current.CancellationToken); + } + + /// + /// Verifies that the analyzer reports RP0006 when a tech debt exception for a package reference no longer matches. + /// + [Fact] + public async Task TechDebtPackageException_NoLongerNeeded_ShouldReportDiagnostic_Async() + { + var test = GetAnalyzer(); + test.TestState.AdditionalFiles.Add( + ("DependencyRules.json", """ + { + "PackageDependencies": [ + { + "From": "TestProject.csproj", + "To": "*", + "Description": "No packages allowed", + "Policy": "Forbidden", + "Exceptions": [ + { + "From": "TestProject.csproj", + "To": "OldPackage", + "Justification": "Legacy package to be removed", + "IsTechDebt": true + } + ] + } + ], + "ProjectDependencies": [ + { + "From": "TestProject.csproj", + "To": "*", + "Description": "Allowed", + "Policy": "Allowed", + "LinkType": "DirectOrTransitive" + } + ] + } + """)); + + test.TestState.AdditionalFiles.Add( + (ReferenceProtectorAnalyzer.DeclaredReferencesFile, """ + TestProject.csproj PackageReferenceDirect SomeOtherPackage + """)); + + test.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning("RP0005") + .WithNoLocation() + .WithMessage("Package reference 'TestProject.csproj' ==> 'SomeOtherPackage' violates dependency rule 'No packages allowed' or one of its exceptions. Please remove the dependency or update 'DependencyRules.json' file to allow it.")); + + test.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning("RP0006") + .WithNoLocation() + .WithMessage("Tech debt exception 'TestProject.csproj' ==> 'OldPackage' in rule 'No packages allowed' no longer matches any declared reference and can be removed from 'DependencyRules.json'")); + + await test.RunAsync(TestContext.Current.CancellationToken); + } + + /// + /// Verifies IsTechDebt defaults to false when not specified in JSON. + /// + [Fact] + public async Task TechDebtException_DefaultFalse_NoFlagSpecified_ShouldNotReportDiagnostic_Async() + { + var test = GetAnalyzer(); + test.TestState.AdditionalFiles.Add( + ("DependencyRules.json", """ + { + "ProjectDependencies": [ + { + "From": "*", + "To": "*", + "Description": "No direct project references allowed", + "Policy": "Forbidden", + "LinkType": "Direct", + "Exceptions": [ + { + "From": "TestProject.csproj", + "To": "OldProject.csproj", + "Justification": "Legitimate exception", + "IsTechDebt": false + } + ] + } + ] + } + """)); + + test.TestState.AdditionalFiles.Add( + (ReferenceProtectorAnalyzer.DeclaredReferencesFile, """ + TestProject.csproj ProjectReferenceDirect SomeOtherProject.csproj + """)); + + // Only RP0004 for SomeOtherProject, no RP0006 since IsTechDebt is explicitly false + test.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning("RP0004") + .WithNoLocation() + .WithMessage("Project reference 'TestProject.csproj' ==> 'SomeOtherProject.csproj' violates dependency rule 'No direct project references allowed' or one of its exceptions. Please remove the dependency or update 'DependencyRules.json' file to allow it.")); + + await test.RunAsync(TestContext.Current.CancellationToken); + } } diff --git a/src/Analyzers/ReferenceProtector.Analyzers/AnalyzerReleases.Shipped.md b/src/Analyzers/ReferenceProtector.Analyzers/AnalyzerReleases.Shipped.md index 6c9b9c9..bee4377 100644 --- a/src/Analyzers/ReferenceProtector.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Analyzers/ReferenceProtector.Analyzers/AnalyzerReleases.Shipped.md @@ -12,3 +12,10 @@ RP0002 | Usage | Warning | Make sure the dependency rules file '{0}' is in the c RP0003 | Usage | Info | No dependency rules matched the current project '{0}' RP0004 | Usage | Warning | Project reference '{0}' ==> '{1}' violates dependency rule '{2}' or one of its exceptions RP0005 | Usage | Warning | Package reference '{0}' ==> '{1}' violates dependency rule '{2}' or one of its exceptions + +## Release 1.3.7 + +### New Rules +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +RP0006 | Usage | Warning | Technical debt dependency can be removed from the rules exception \ No newline at end of file diff --git a/src/Analyzers/ReferenceProtector.Analyzers/DiagnosticDescriptors.cs b/src/Analyzers/ReferenceProtector.Analyzers/DiagnosticDescriptors.cs index 619d960..0d71eab 100644 --- a/src/Analyzers/ReferenceProtector.Analyzers/DiagnosticDescriptors.cs +++ b/src/Analyzers/ReferenceProtector.Analyzers/DiagnosticDescriptors.cs @@ -45,4 +45,12 @@ internal static class Descriptors category: "Usage", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor TechDebtExceptionNoLongerNeeded = new( + id: "RP0006", + title: "Tech debt exception is no longer needed", + messageFormat: "Tech debt exception '{0}' ==> '{1}' in rule '{2}' no longer matches any declared reference and can be removed from '{3}'", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); } \ No newline at end of file diff --git a/src/Analyzers/ReferenceProtector.Analyzers/Models/DependencyRules.cs b/src/Analyzers/ReferenceProtector.Analyzers/Models/DependencyRules.cs index 2b3e38d..3533d3c 100644 --- a/src/Analyzers/ReferenceProtector.Analyzers/Models/DependencyRules.cs +++ b/src/Analyzers/ReferenceProtector.Analyzers/Models/DependencyRules.cs @@ -25,7 +25,8 @@ internal record PackageDependency( internal record Exceptions( string From, string To, - string Justification); + string Justification, + bool IsTechDebt = false); internal enum Policy { diff --git a/src/Analyzers/ReferenceProtector.Analyzers/ReferenceProtector.Analyzers.cs b/src/Analyzers/ReferenceProtector.Analyzers/ReferenceProtector.Analyzers.cs index a052ba8..a06e60d 100644 --- a/src/Analyzers/ReferenceProtector.Analyzers/ReferenceProtector.Analyzers.cs +++ b/src/Analyzers/ReferenceProtector.Analyzers/ReferenceProtector.Analyzers.cs @@ -42,7 +42,8 @@ public class ReferenceProtectorAnalyzer : DiagnosticAnalyzer Descriptors.InvalidDependencyRulesFormat, Descriptors.NoDependencyRulesMatchedCurrentProject, Descriptors.ProjectReferenceViolation, - Descriptors.PackageReferenceViolation + Descriptors.PackageReferenceViolation, + Descriptors.TechDebtExceptionNoLongerNeeded ]; /// @@ -151,6 +152,7 @@ private void AnalyzeDependencyRules(CompilationAnalysisContext context) } AnalyzeDeclaredProjectReferences(context, declaredReferences.ToImmutableArray(), thisProjectDependencyRules.ToImmutableArray(), Descriptors.ProjectReferenceViolation, dependencyRulesFile.Path); + ReportStaleTechDebtProjectExceptions(context, declaredReferences.ToImmutableArray(), thisProjectDependencyRules.ToImmutableArray(), dependencyRulesFile.Path); } // Analyze package dependencies @@ -163,6 +165,7 @@ private void AnalyzeDependencyRules(CompilationAnalysisContext context) .Where(r => r.LinkType == ReferenceKind.PackageReferenceDirect); AnalyzeDeclaredPackageReferences(context, packageReferences.ToImmutableArray(), thisPackageDependencyRules.ToImmutableArray(), Descriptors.PackageReferenceViolation, dependencyRulesFile.Path); + ReportStaleTechDebtPackageExceptions(context, packageReferences.ToImmutableArray(), thisPackageDependencyRules.ToImmutableArray(), dependencyRulesFile.Path); } } @@ -252,6 +255,76 @@ private void AnalyzeDeclaredPackageReferences( } } + private void ReportStaleTechDebtProjectExceptions( + CompilationAnalysisContext context, + ImmutableArray declaredReferences, + ImmutableArray dependencyRules, + string dependencyRulesFile) + { + foreach (var rule in dependencyRules) + { + if (rule.Exceptions == null) + continue; + + foreach (var exception in rule.Exceptions) + { + if (!exception.IsTechDebt) + continue; + + var exceptionStillNeeded = declaredReferences.Any(reference => + IsMatchByName(exception.From, reference.Source) && + IsMatchByName(exception.To, reference.Target) && + (rule.LinkType != LinkType.Transitive && reference.LinkType == ReferenceKind.ProjectReferenceDirect || + rule.LinkType != LinkType.Direct && reference.LinkType == ReferenceKind.ProjectReferenceTransitive)); + + if (!exceptionStillNeeded) + { + context.ReportDiagnostic(Diagnostic.Create( + Descriptors.TechDebtExceptionNoLongerNeeded, + Location.None, + exception.From, + exception.To, + rule.Description, + dependencyRulesFile)); + } + } + } + } + + private void ReportStaleTechDebtPackageExceptions( + CompilationAnalysisContext context, + ImmutableArray declaredReferences, + ImmutableArray dependencyRules, + string dependencyRulesFile) + { + foreach (var rule in dependencyRules) + { + if (rule.Exceptions == null) + continue; + + foreach (var exception in rule.Exceptions) + { + if (!exception.IsTechDebt) + continue; + + var exceptionStillNeeded = declaredReferences.Any(reference => + IsMatchByName(exception.From, reference.Source) && + IsMatchByName(exception.To, reference.Target)); + + if (!exceptionStillNeeded) + { + context.ReportDiagnostic(Diagnostic.Create( + Descriptors.TechDebtExceptionNoLongerNeeded, + Location.None, + exception.From, + exception.To, + rule.Description, + dependencyRulesFile)); + } + } + } + } + private static bool IsMatchByName(string pattern, string project) { var regex = Regex.Escape(pattern).Replace("\\*", ".*") + "$"; diff --git a/src/Build/DependencyRules.schema.json b/src/Build/DependencyRules.schema.json index 5907ce5..ccdac95 100644 --- a/src/Build/DependencyRules.schema.json +++ b/src/Build/DependencyRules.schema.json @@ -138,6 +138,11 @@ "Justification": { "type": "string", "description": "A human-readable justification for why this exception exists." + }, + "IsTechDebt": { + "type": "boolean", + "default": false, + "description": "When true, marks this exception as temporary technical debt. The analyzer will warn (RP0006) when the exception no longer matches any declared reference and can be removed." } } } From 7e5e018b355dc764baecf7afb6fdb1b5e146fa45 Mon Sep 17 00:00:00 2001 From: Alex S Date: Wed, 25 Feb 2026 15:59:44 -0800 Subject: [PATCH 2/6] Add integration test --- .../ReferenceProtector.IntegrationTests.cs | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs b/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs index c4dfeda..36610d7 100644 --- a/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs +++ b/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs @@ -169,4 +169,139 @@ public async Task PackageReference_DependencyRuleViolated_FeatureDisabled_NoWarn Assert.Empty(warnings); } + + /// + /// Validates that a stale tech debt exception (one that no longer matches any declared reference) produces an RP0006 warning. + /// + [Fact] + public async Task TechDebtException_NoLongerNeeded_ProducesWarning_Async() + { + var projectA = CreateProject("A"); + var projectB = CreateProject("B"); + await AddProjectReference("A", "B"); + var testRulesPath = Path.Combine(TestDirectory, "testRules.json"); + File.WriteAllText(testRulesPath, $$""" + { + "ProjectDependencies": [ + { + "From": "{{projectA.Replace("\\", "\\\\")}}", + "To": "*", + "LinkType": "Direct", + "Policy": "Forbidden", + "Description": "test rule", + "Exceptions": [ + { + "From": "{{projectA.Replace("\\", "\\\\")}}", + "To": "{{projectB.Replace("\\", "\\\\")}}", + "Justification": "Current tech debt", + "IsTechDebt": true + }, + { + "From": "{{projectA.Replace("\\", "\\\\")}}", + "To": "*OldProject.csproj", + "Justification": "Stale tech debt", + "IsTechDebt": true + } + ] + } + ] + } + """); + + var warnings = await Build(additionalArgs: + $"/p:DependencyRulesFile={testRulesPath}"); + + // The exception for A→B still matches, so no RP0006 for it. + // The exception for A→*OldProject.csproj is stale, so RP0006 is expected. + var warning = Assert.Single(warnings); + Assert.Equal(new Warning() + { + Message = $"RP0006: Tech debt exception '{projectA}' ==> '*OldProject.csproj' in rule 'test rule' no longer matches any declared reference and can be removed from '{testRulesPath}'", + Project = "A/A.csproj", + }, warning); + } + + /// + /// Validates that a tech debt exception that still matches a declared reference does NOT produce an RP0006 warning. + /// + [Fact] + public async Task TechDebtException_StillNeeded_NoWarning_Async() + { + var projectA = CreateProject("A"); + var projectB = CreateProject("B"); + await AddProjectReference("A", "B"); + var testRulesPath = Path.Combine(TestDirectory, "testRules.json"); + File.WriteAllText(testRulesPath, $$""" + { + "ProjectDependencies": [ + { + "From": "{{projectA.Replace("\\", "\\\\")}}", + "To": "*", + "LinkType": "Direct", + "Policy": "Forbidden", + "Description": "test rule", + "Exceptions": [ + { + "From": "{{projectA.Replace("\\", "\\\\")}}", + "To": "{{projectB.Replace("\\", "\\\\")}}", + "Justification": "Current tech debt - still needed", + "IsTechDebt": true + } + ] + } + ] + } + """); + + var warnings = await Build(additionalArgs: + $"/p:DependencyRulesFile={testRulesPath}"); + + // The tech debt exception still matches A→B, so no RP0006. + // And the exception suppresses the RP0004 violation. + Assert.Empty(warnings); + } + + /// + /// Validates that a non-tech-debt exception that no longer matches does NOT produce an RP0006 warning. + /// + [Fact] + public async Task NonTechDebtException_NoLongerNeeded_NoWarning_Async() + { + var projectA = CreateProject("A"); + var projectB = CreateProject("B"); + await AddProjectReference("A", "B"); + var testRulesPath = Path.Combine(TestDirectory, "testRules.json"); + File.WriteAllText(testRulesPath, $$""" + { + "ProjectDependencies": [ + { + "From": "{{projectA.Replace("\\", "\\\\")}}", + "To": "*", + "LinkType": "Direct", + "Policy": "Forbidden", + "Description": "test rule", + "Exceptions": [ + { + "From": "{{projectA.Replace("\\", "\\\\")}}", + "To": "{{projectB.Replace("\\", "\\\\")}}", + "Justification": "Legitimate exception" + }, + { + "From": "{{projectA.Replace("\\", "\\\\")}}", + "To": "*OldProject.csproj", + "Justification": "Legitimate exception, not tech debt" + } + ] + } + ] + } + """); + + var warnings = await Build(additionalArgs: + $"/p:DependencyRulesFile={testRulesPath}"); + + // No RP0006 because neither exception has IsTechDebt=true. + // A→B is covered by the first exception, so no RP0004 either. + Assert.Empty(warnings); + } } \ No newline at end of file From 83459658545521de379fbf2175fb5a1f337cb4da Mon Sep 17 00:00:00 2001 From: Alex S Date: Wed, 25 Feb 2026 16:08:07 -0800 Subject: [PATCH 3/6] Fix samples --- Directory.Packages.props | 2 +- samples/Directory.Build.props | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7c87955..ed2ca33 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ true true - true + true diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index 47be018..24cb87f 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -8,6 +8,7 @@ true $(MSBuildThisFileDirectory)DependencyRules.json buildResult.sarif,version=2.1 + false \ No newline at end of file From 62762d2ec1300175939f46dc6a294d29dd0ecb30 Mon Sep 17 00:00:00 2001 From: Alex S Date: Wed, 25 Feb 2026 16:10:10 -0800 Subject: [PATCH 4/6] Updated samples --- samples/DependencyRules.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/samples/DependencyRules.json b/samples/DependencyRules.json index 75b9e4c..9f1c4f2 100644 --- a/samples/DependencyRules.json +++ b/samples/DependencyRules.json @@ -12,13 +12,19 @@ "From": "*\\ClassA.csproj", "To": "*\\ClassB.csproj", "Justification": "This is an exception for testing purposes" + }, + { + "From": "*\\ClassA.csproj", + "To": "*\\OldClass.csproj", + "IsTechDebt": true, + "Justification": "This is a tech debt exception for testing purposes. [Waning is expected]" } ] }, { "From": "*\\ClassA.csproj", "To": "*\\ClassC.csproj", - "Description": "Can't reference this project transitively", + "Description": "Can't reference this project transitively [Warning is expected]", "Policy": "Forbidden", "LinkType": "Transitive", "Exceptions": [ From 951eaf2ef74bdc8e18d73370779726d002ec9a2e Mon Sep 17 00:00:00 2001 From: Alex S Date: Wed, 25 Feb 2026 16:16:49 -0800 Subject: [PATCH 5/6] Only react to forbidden rules --- .../ReferenceProtector.Analyzers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Analyzers/ReferenceProtector.Analyzers/ReferenceProtector.Analyzers.cs b/src/Analyzers/ReferenceProtector.Analyzers/ReferenceProtector.Analyzers.cs index a06e60d..be63a2a 100644 --- a/src/Analyzers/ReferenceProtector.Analyzers/ReferenceProtector.Analyzers.cs +++ b/src/Analyzers/ReferenceProtector.Analyzers/ReferenceProtector.Analyzers.cs @@ -263,7 +263,7 @@ private void ReportStaleTechDebtProjectExceptions( { foreach (var rule in dependencyRules) { - if (rule.Exceptions == null) + if (rule.Policy != Policy.Forbidden || rule.Exceptions == null) continue; foreach (var exception in rule.Exceptions) @@ -299,7 +299,7 @@ private void ReportStaleTechDebtPackageExceptions( { foreach (var rule in dependencyRules) { - if (rule.Exceptions == null) + if (rule.Policy != Policy.Forbidden || rule.Exceptions == null) continue; foreach (var exception in rule.Exceptions) From 4b33b1cc6437e372240ebc960cfe559a95502814 Mon Sep 17 00:00:00 2001 From: Alex S Date: Wed, 25 Feb 2026 16:28:08 -0800 Subject: [PATCH 6/6] Update readme with schema version --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9189267..f925e46 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,8 @@ Schema of the rules file is as follows: } ``` +Note: for stability - use the version in the schema url, instead of `main`. For example for version `v1.2.3` - use `"$schema": https://raw.githubusercontent.com/olstakh/ReferenceProtector/v1.2.3/src/Build/DependencyRules.schema.json` + Top `ProjectDependencies` object will contain a list of rules to validate against. Each rule has the following schema: - `From` / `To` - Full path regex for source and target projects to be matched.