Skip to content

Commit 6a94122

Browse files
authored
Scope RT0006 error to current compilation only (#19)
1 parent 0e00ae5 commit 6a94122

4 files changed

Lines changed: 123 additions & 4 deletions

File tree

src/Analyzers/ReferenceProtector.Analyzers.Tests/ReferenceProtectorAnalyzerTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,54 @@ TestProject.csproj PackageReferenceDirect SomeOtherPackage
414414
await test.RunAsync(TestContext.Current.CancellationToken);
415415
}
416416

417+
/// <summary>
418+
/// Verifies that a tech debt exception whose From does not match the current project does NOT trigger RP0006.
419+
/// This covers the case where a broad rule (From: *) has exceptions for other projects that aren't part
420+
/// of the current compilation — declared references only contain references for the current project.
421+
/// </summary>
422+
[Fact]
423+
public async Task TechDebtException_ForDifferentProject_ShouldNotReportDiagnostic_Async()
424+
{
425+
var test = GetAnalyzer();
426+
test.TestState.AdditionalFiles.Add(
427+
("DependencyRules.json", """
428+
{
429+
"ProjectDependencies": [
430+
{
431+
"From": "*",
432+
"To": "*",
433+
"Description": "No direct project references allowed",
434+
"Policy": "Forbidden",
435+
"LinkType": "Direct",
436+
"Exceptions": [
437+
{
438+
"From": "TestProject.csproj",
439+
"To": "ReferencedProject.csproj",
440+
"Justification": "Tech debt for TestProject",
441+
"IsTechDebt": true
442+
},
443+
{
444+
"From": "OtherProject.csproj",
445+
"To": "SomeLib.csproj",
446+
"Justification": "Tech debt for OtherProject",
447+
"IsTechDebt": true
448+
}
449+
]
450+
}
451+
]
452+
}
453+
"""));
454+
455+
test.TestState.AdditionalFiles.Add(
456+
(ReferenceProtectorAnalyzer.DeclaredReferencesFile, """
457+
TestProject.csproj ProjectReferenceDirect ReferencedProject.csproj
458+
"""));
459+
460+
// TestProject→ReferencedProject is covered by the first exception (still needed), so no RP0004 or RP0006.
461+
// The second exception (OtherProject→SomeLib) is for a different project — should NOT trigger RP0006.
462+
await test.RunAsync(TestContext.Current.CancellationToken);
463+
}
464+
417465
/// <summary>
418466
/// Verifies IsTechDebt defaults to false when not specified in JSON.
419467
/// </summary>

src/Analyzers/ReferenceProtector.Analyzers/ReferenceProtector.Analyzers.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ private void AnalyzeDependencyRules(CompilationAnalysisContext context)
152152
}
153153

154154
AnalyzeDeclaredProjectReferences(context, declaredReferences.ToImmutableArray(), thisProjectDependencyRules.ToImmutableArray(), Descriptors.ProjectReferenceViolation, dependencyRulesFile.Path);
155-
ReportStaleTechDebtProjectExceptions(context, declaredReferences.ToImmutableArray(), thisProjectDependencyRules.ToImmutableArray(), dependencyRulesFile.Path);
155+
ReportStaleTechDebtProjectExceptions(context, declaredReferences.ToImmutableArray(), thisProjectDependencyRules.ToImmutableArray(), dependencyRulesFile.Path, projectPath);
156156
}
157157

158158
// Analyze package dependencies
@@ -165,7 +165,7 @@ private void AnalyzeDependencyRules(CompilationAnalysisContext context)
165165
.Where(r => r.LinkType == ReferenceKind.PackageReferenceDirect);
166166

167167
AnalyzeDeclaredPackageReferences(context, packageReferences.ToImmutableArray(), thisPackageDependencyRules.ToImmutableArray(), Descriptors.PackageReferenceViolation, dependencyRulesFile.Path);
168-
ReportStaleTechDebtPackageExceptions(context, packageReferences.ToImmutableArray(), thisPackageDependencyRules.ToImmutableArray(), dependencyRulesFile.Path);
168+
ReportStaleTechDebtPackageExceptions(context, packageReferences.ToImmutableArray(), thisPackageDependencyRules.ToImmutableArray(), dependencyRulesFile.Path, projectPath);
169169
}
170170
}
171171

@@ -259,7 +259,8 @@ private void ReportStaleTechDebtProjectExceptions(
259259
CompilationAnalysisContext context,
260260
ImmutableArray<ReferenceItem> declaredReferences,
261261
ImmutableArray<ProjectDependency> dependencyRules,
262-
string dependencyRulesFile)
262+
string dependencyRulesFile,
263+
string projectPath)
263264
{
264265
foreach (var rule in dependencyRules)
265266
{
@@ -271,6 +272,11 @@ private void ReportStaleTechDebtProjectExceptions(
271272
if (!exception.IsTechDebt)
272273
continue;
273274

275+
// Only evaluate exceptions whose From matches the current project,
276+
// since declared references only contain references for this compilation.
277+
if (!IsMatchByName(exception.From, projectPath))
278+
continue;
279+
274280
var exceptionStillNeeded = declaredReferences.Any(reference =>
275281
IsMatchByName(exception.From, reference.Source) &&
276282
IsMatchByName(exception.To, reference.Target) &&
@@ -295,7 +301,8 @@ private void ReportStaleTechDebtPackageExceptions(
295301
CompilationAnalysisContext context,
296302
ImmutableArray<ReferenceItem> declaredReferences,
297303
ImmutableArray<PackageDependency> dependencyRules,
298-
string dependencyRulesFile)
304+
string dependencyRulesFile,
305+
string projectPath)
299306
{
300307
foreach (var rule in dependencyRules)
301308
{
@@ -307,6 +314,11 @@ private void ReportStaleTechDebtPackageExceptions(
307314
if (!exception.IsTechDebt)
308315
continue;
309316

317+
// Only evaluate exceptions whose From matches the current project,
318+
// since declared references only contain references for this compilation.
319+
if (!IsMatchByName(exception.From, projectPath))
320+
continue;
321+
310322
var exceptionStillNeeded = declaredReferences.Any(reference =>
311323
IsMatchByName(exception.From, reference.Source) &&
312324
IsMatchByName(exception.To, reference.Target));

tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,55 @@ public async Task TechDebtException_StillNeeded_NoWarning_Async()
261261
Assert.Empty(warnings);
262262
}
263263

264+
/// <summary>
265+
/// Validates that a tech debt exception scoped to a different project does NOT produce RP0006 during the current project's compilation.
266+
/// </summary>
267+
[Fact]
268+
public async Task TechDebtException_ForDifferentProject_NoWarning_Async()
269+
{
270+
var projectA = CreateProject("A");
271+
var projectB = CreateProject("B");
272+
await AddProjectReference("A", "B");
273+
var testRulesPath = Path.Combine(TestDirectory, "testRules.json");
274+
// Use a From that references a project NOT in this solution (NonExistent.csproj),
275+
// simulating a broad rule with tech debt exceptions for projects compiled separately.
276+
File.WriteAllText(testRulesPath, $$"""
277+
{
278+
"ProjectDependencies": [
279+
{
280+
"From": "*",
281+
"To": "*",
282+
"LinkType": "Direct",
283+
"Policy": "Forbidden",
284+
"Description": "test rule",
285+
"Exceptions": [
286+
{
287+
"From": "{{projectA.Replace("\\", "\\\\")}}",
288+
"To": "{{projectB.Replace("\\", "\\\\")}}",
289+
"Justification": "Tech debt for A",
290+
"IsTechDebt": true
291+
},
292+
{
293+
"From": "*NonExistent.csproj",
294+
"To": "*SomeLib.csproj",
295+
"Justification": "Tech debt for a project not in this compilation",
296+
"IsTechDebt": true
297+
}
298+
]
299+
}
300+
]
301+
}
302+
""");
303+
304+
var warnings = await Build(additionalArgs:
305+
$"/p:DependencyRulesFile={testRulesPath}");
306+
307+
// A→B is covered by the first exception (still needed), so no RP0004 or RP0006 for A.
308+
// The second exception (NonExistent→SomeLib) doesn't match any project being compiled,
309+
// so it should NOT trigger RP0006 from either A or B.
310+
Assert.Empty(warnings);
311+
}
312+
264313
/// <summary>
265314
/// Validates that a non-tech-debt exception that no longer matches does NOT produce an RP0006 warning.
266315
/// </summary>

tests/ReferenceProtector.IntegrationTests/ReferenceProtector.IntegrationTests.csproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77
</ProjectReference>
88
</ItemGroup>
99

10+
<!-- Remove stale .nupkg files from artifacts before the Package project produces a new one.
11+
Without this, NuGet may resolve to an older package version with a higher version number,
12+
causing tests to run against stale analyzer code. -->
13+
<Target Name="CleanStalePackages" BeforeTargets="ResolveProjectReferences">
14+
<ItemGroup>
15+
<_StalePackages Include="$(RepoRoot)\artifacts\*.nupkg" />
16+
</ItemGroup>
17+
<Delete Files="@(_StalePackages)" />
18+
</Target>
19+
1020
<PropertyGroup>
1121
<OutputType>Exe</OutputType>
1222
<TargetFramework>net10.0</TargetFramework>

0 commit comments

Comments
 (0)