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..d97b06f 100644
--- a/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs
+++ b/tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs
@@ -261,6 +261,55 @@ 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");
+ // 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": [
+ {
+ "From": "*",
+ "To": "*",
+ "LinkType": "Direct",
+ "Policy": "Forbidden",
+ "Description": "test rule",
+ "Exceptions": [
+ {
+ "From": "{{projectA.Replace("\\", "\\\\")}}",
+ "To": "{{projectB.Replace("\\", "\\\\")}}",
+ "Justification": "Tech debt for A",
+ "IsTechDebt": true
+ },
+ {
+ "From": "*NonExistent.csproj",
+ "To": "*SomeLib.csproj",
+ "Justification": "Tech debt for a project not in this compilation",
+ "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 (NonExistent→SomeLib) doesn't match any project being compiled,
+ // so it should NOT trigger RP0006 from either A or B.
+ Assert.Empty(warnings);
+ }
+
///
/// Validates that a non-tech-debt exception that no longer matches does NOT produce an RP0006 warning.
///
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