Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,54 @@ TestProject.csproj PackageReferenceDirect SomeOtherPackage
await test.RunAsync(TestContext.Current.CancellationToken);
}

/// <summary>
/// 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.
/// </summary>
[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);
}

/// <summary>
/// Verifies IsTechDebt defaults to false when not specified in JSON.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -259,7 +259,8 @@ private void ReportStaleTechDebtProjectExceptions(
CompilationAnalysisContext context,
ImmutableArray<ReferenceItem> declaredReferences,
ImmutableArray<ProjectDependency> dependencyRules,
string dependencyRulesFile)
string dependencyRulesFile,
string projectPath)
{
foreach (var rule in dependencyRules)
{
Expand All @@ -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) &&
Expand All @@ -295,7 +301,8 @@ private void ReportStaleTechDebtPackageExceptions(
CompilationAnalysisContext context,
ImmutableArray<ReferenceItem> declaredReferences,
ImmutableArray<PackageDependency> dependencyRules,
string dependencyRulesFile)
string dependencyRulesFile,
string projectPath)
{
foreach (var rule in dependencyRules)
{
Expand All @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,55 @@ public async Task TechDebtException_StillNeeded_NoWarning_Async()
Assert.Empty(warnings);
}

/// <summary>
/// Validates that a tech debt exception scoped to a different project does NOT produce RP0006 during the current project's compilation.
/// </summary>
[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);
}

/// <summary>
/// Validates that a non-tech-debt exception that no longer matches does NOT produce an RP0006 warning.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
</ProjectReference>
</ItemGroup>

<!-- Remove stale .nupkg files from artifacts before the Package project produces a new one.
Without this, NuGet may resolve to an older package version with a higher version number,
causing tests to run against stale analyzer code. -->
<Target Name="CleanStalePackages" BeforeTargets="ResolveProjectReferences">
<ItemGroup>
<_StalePackages Include="$(RepoRoot)\artifacts\*.nupkg" />
</ItemGroup>
<Delete Files="@(_StalePackages)" />
</Target>

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
Expand Down