From c354cb42d78790846badb97880202c0c50197627 Mon Sep 17 00:00:00 2001 From: Alex S Date: Thu, 26 Feb 2026 12:19:44 -0800 Subject: [PATCH 1/3] tech debt fix --- .../ReferenceProtectorAnalyzerTests.cs | 48 +++++++++++++++++++ .../ReferenceProtector.Analyzers.cs | 20 ++++++-- .../ReferenceProtector.IntegrationTests.cs | 47 ++++++++++++++++++ 3 files changed, 111 insertions(+), 4 deletions(-) diff --git a/src/Analyzers/ReferenceProtector.Analyzers.Tests/ReferenceProtectorAnalyzerTests.cs b/src/Analyzers/ReferenceProtector.Analyzers.Tests/ReferenceProtectorAnalyzerTests.cs index 8219930..8943ca0 100644 --- a/src/Analyzers/ReferenceProtector.Analyzers.Tests/ReferenceProtectorAnalyzerTests.cs +++ b/src/Analyzers/ReferenceProtector.Analyzers.Tests/ReferenceProtectorAnalyzerTests.cs @@ -414,6 +414,54 @@ TestProject.csproj PackageReferenceDirect SomeOtherPackage await test.RunAsync(TestContext.Current.CancellationToken); } + /// + /// Verifies that a tech debt exception whose From does not match the current project does NOT trigger RP0006. + /// This covers the case where a broad rule (From: *) has exceptions for other projects that aren't part + /// of the current compilation — declared references only contain references for the current project. + /// + [Fact] + public async Task TechDebtException_ForDifferentProject_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": "Tech debt for TestProject", + "IsTechDebt": true + }, + { + "From": "OtherProject.csproj", + "To": "SomeLib.csproj", + "Justification": "Tech debt for OtherProject", + "IsTechDebt": true + } + ] + } + ] + } + """)); + + test.TestState.AdditionalFiles.Add( + (ReferenceProtectorAnalyzer.DeclaredReferencesFile, """ + TestProject.csproj ProjectReferenceDirect ReferencedProject.csproj + """)); + + // TestProject→ReferencedProject is covered by the first exception (still needed), so no RP0004 or RP0006. + // The second exception (OtherProject→SomeLib) is for a different project — should NOT trigger RP0006. + await test.RunAsync(TestContext.Current.CancellationToken); + } + /// /// Verifies IsTechDebt defaults to false when not specified in JSON. /// diff --git a/src/Analyzers/ReferenceProtector.Analyzers/ReferenceProtector.Analyzers.cs b/src/Analyzers/ReferenceProtector.Analyzers/ReferenceProtector.Analyzers.cs index be63a2a..ba7aa34 100644 --- a/src/Analyzers/ReferenceProtector.Analyzers/ReferenceProtector.Analyzers.cs +++ b/src/Analyzers/ReferenceProtector.Analyzers/ReferenceProtector.Analyzers.cs @@ -152,7 +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); + ReportStaleTechDebtProjectExceptions(context, declaredReferences.ToImmutableArray(), thisProjectDependencyRules.ToImmutableArray(), dependencyRulesFile.Path, projectPath); } // Analyze package dependencies @@ -165,7 +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); + ReportStaleTechDebtPackageExceptions(context, packageReferences.ToImmutableArray(), thisPackageDependencyRules.ToImmutableArray(), dependencyRulesFile.Path, projectPath); } } @@ -259,7 +259,8 @@ private void ReportStaleTechDebtProjectExceptions( CompilationAnalysisContext context, ImmutableArray declaredReferences, ImmutableArray dependencyRules, - string dependencyRulesFile) + string dependencyRulesFile, + string projectPath) { foreach (var rule in dependencyRules) { @@ -271,6 +272,11 @@ private void ReportStaleTechDebtProjectExceptions( if (!exception.IsTechDebt) continue; + // Only evaluate exceptions whose From matches the current project, + // since declared references only contain references for this compilation. + if (!IsMatchByName(exception.From, projectPath)) + continue; + var exceptionStillNeeded = declaredReferences.Any(reference => IsMatchByName(exception.From, reference.Source) && IsMatchByName(exception.To, reference.Target) && @@ -295,7 +301,8 @@ private void ReportStaleTechDebtPackageExceptions( CompilationAnalysisContext context, ImmutableArray declaredReferences, ImmutableArray dependencyRules, - string dependencyRulesFile) + string dependencyRulesFile, + string projectPath) { foreach (var rule in dependencyRules) { @@ -307,6 +314,11 @@ private void ReportStaleTechDebtPackageExceptions( if (!exception.IsTechDebt) continue; + // Only evaluate exceptions whose From matches the current project, + // since declared references only contain references for this compilation. + if (!IsMatchByName(exception.From, projectPath)) + continue; + var exceptionStillNeeded = declaredReferences.Any(reference => IsMatchByName(exception.From, reference.Source) && IsMatchByName(exception.To, reference.Target)); diff --git a/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs b/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs index 36610d7..fb40076 100644 --- a/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs +++ b/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs @@ -261,6 +261,53 @@ public async Task TechDebtException_StillNeeded_NoWarning_Async() Assert.Empty(warnings); } + /// + /// Validates that a tech debt exception scoped to a different project does NOT produce RP0006 during the current project's compilation. + /// + [Fact] + public async Task TechDebtException_ForDifferentProject_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": "*", + "To": "*", + "LinkType": "Direct", + "Policy": "Forbidden", + "Description": "test rule", + "Exceptions": [ + { + "From": "{{projectA.Replace("\\", "\\\\")}}", + "To": "{{projectB.Replace("\\", "\\\\")}}", + "Justification": "Tech debt for A", + "IsTechDebt": true + }, + { + "From": "{{projectB.Replace("\\", "\\\\")}}", + "To": "*SomeOtherProject.csproj", + "Justification": "Tech debt for B, not relevant for A", + "IsTechDebt": true + } + ] + } + ] + } + """); + + var warnings = await Build(additionalArgs: + $"/p:DependencyRulesFile={testRulesPath}"); + + // A→B is covered by the first exception (still needed), so no RP0004 or RP0006 for A. + // The second exception (B→*SomeOtherProject.csproj) is for project B, so project A should NOT report RP0006 for it. + // Project B has no references, so no warnings from B either. + Assert.Empty(warnings); + } + /// /// Validates that a non-tech-debt exception that no longer matches does NOT produce an RP0006 warning. /// From ab46262b1c6a0893cfff74ee9a2e08bdf7f33b25 Mon Sep 17 00:00:00 2001 From: Alex S Date: Thu, 26 Feb 2026 12:26:06 -0800 Subject: [PATCH 2/3] Update tests --- .../ReferenceProtector.IntegrationTests.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs b/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs index fb40076..d97b06f 100644 --- a/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs +++ b/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs @@ -271,6 +271,8 @@ public async Task TechDebtException_ForDifferentProject_NoWarning_Async() var projectB = CreateProject("B"); await AddProjectReference("A", "B"); var testRulesPath = Path.Combine(TestDirectory, "testRules.json"); + // Use a From that references a project NOT in this solution (NonExistent.csproj), + // simulating a broad rule with tech debt exceptions for projects compiled separately. File.WriteAllText(testRulesPath, $$""" { "ProjectDependencies": [ @@ -288,9 +290,9 @@ public async Task TechDebtException_ForDifferentProject_NoWarning_Async() "IsTechDebt": true }, { - "From": "{{projectB.Replace("\\", "\\\\")}}", - "To": "*SomeOtherProject.csproj", - "Justification": "Tech debt for B, not relevant for A", + "From": "*NonExistent.csproj", + "To": "*SomeLib.csproj", + "Justification": "Tech debt for a project not in this compilation", "IsTechDebt": true } ] @@ -303,8 +305,8 @@ public async Task TechDebtException_ForDifferentProject_NoWarning_Async() $"/p:DependencyRulesFile={testRulesPath}"); // A→B is covered by the first exception (still needed), so no RP0004 or RP0006 for A. - // The second exception (B→*SomeOtherProject.csproj) is for project B, so project A should NOT report RP0006 for it. - // Project B has no references, so no warnings from B either. + // The second exception (NonExistent→SomeLib) doesn't match any project being compiled, + // so it should NOT trigger RP0006 from either A or B. Assert.Empty(warnings); } From 01052e4e58c934a91ffc1a1127e62ab647ad936b Mon Sep 17 00:00:00 2001 From: Alex S Date: Thu, 26 Feb 2026 12:30:44 -0800 Subject: [PATCH 3/3] Clean artifacts before running integration tests --- .../ReferenceProtector.IntegrationTests.csproj | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.csproj b/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.csproj index a52de78..6215a4c 100644 --- a/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.csproj +++ b/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.csproj @@ -7,6 +7,16 @@ + + + + <_StalePackages Include="$(RepoRoot)\artifacts\*.nupkg" /> + + + + Exe net10.0