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
@@ -0,0 +1,77 @@
using ProjectReferencesRuler.ProjectParsing;
using ProjectReferencesRuler.ProjectRunners;
using ProjectReferencesRuler.Rules;
using ProjectReferencesRuler.Rules.References;
using Xunit;

namespace ProjectReferencesRuler
{
/// <summary>
/// Integration tests that exercise the full stack (extractor → ruler → runner)
/// using real XML test project files - mirroring a typical README use-case.
///
/// Regression coverage for: <PackageReference Remove="..." /> causing an
/// ArgumentNullException because the element has no Include attribute, leaving
/// Reference.To as null, which then crashes Regex.IsMatch.
/// </summary>
public class PackageReferenceRemoveIntegrationTests
{
/// <summary>
/// Reproduces the production crash: processing Dg.Component.xml which contains
/// &lt;PackageReference Remove="XUnit" /&gt;
/// must not throw an ArgumentNullException.
/// </summary>
[Fact]
public void GetComplaintsForPackageReferences_WhenProjectContainsPackageReferenceRemoveElement_DoesNotThrow()
{
// A catch-all forbidden rule is used to force every extracted reference
// through the full regex-matching path, which is where the crash occurred.
var runner = CreateRunner(
new ReferenceRule(
@"*",
@"*",
RuleKind.Forbidden,
description: "Catch-all rule to exercise the full evaluation path."
)
);

var exception = Record.Exception(() => runner.GetComplaintsForPackageReferences());

Assert.Null(exception);
}

/// <summary>
/// A &lt;PackageReference Remove="XUnit" /&gt; element should be silently skipped
/// and never surfaced as a reference or a rule violation.
/// </summary>
[Fact]
public void GetComplaintsForPackageReferences_WhenProjectContainsPackageReferenceRemoveElement_RemoveElementIsIgnoredAndNotReportedAsViolation()
{
var runner = CreateRunner(
new ReferenceRule(
@"*",
@"XUnit",
RuleKind.Forbidden,
description: "XUnit package references are forbidden."
)
);

var complaints = runner.GetComplaintsForPackageReferences();

Assert.DoesNotContain("XUnit", complaints ?? string.Empty);
}

private static ReferencesRulerRunner CreateRunner(params ReferenceRule[] rules) =>
new ReferencesRulerRunner(
extractor: new CsprojReferencesExtractor(),
referencesRuler: new ReferencesRuler(
patternParser: new WildcardPatternParser(),
rules: rules
),
filesRunner: new ProjectFilesRunner(
solutionPath: @"../../../TestProjectFiles/",
filesExtension: "*.xml"
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
<PackageReference Include="Dg" Version="5.1.5" />
<PackageReference Include="SimpleInjector" Version="4.0.12" />
<PackageReference Include="Deblazer" Version="4.1.8" />
<PackageReference Remove="XUnit" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,21 @@ public IEnumerable<Reference> GetProjectReferences(string csprojPath)
from: from,
to: Path.GetFileNameWithoutExtension(CleanPath(tuple.Item1)),
isPrivateAssetsAllSet: tuple.Item2 == "All",
versionOrNull: null));
versionOrNull: null
));
}

public IEnumerable<Reference> GetPackageReferences(string csprojPath)
{
var from = Path.GetFileNameWithoutExtension(csprojPath);
return GetReferences(csprojPath, PackageReference)
.Where(tuple => !string.IsNullOrEmpty(tuple.Item1))
.Select(tuple => new Reference(
from: from,
to: tuple.Item1,
isPrivateAssetsAllSet: tuple.Item2 == "All",
versionOrNull: tuple.Item3));
versionOrNull: tuple.Item3
));
}

public IEnumerable<string> GetProjectReferencePaths(string csprojPath)
Expand Down Expand Up @@ -71,7 +74,8 @@ public Project GetProjectProperties(string csprojPath)
return new Project(
name: CleanPath(Path.GetFileNameWithoutExtension(csprojPath)),
importedProps: GetImportedProps(doc).Select(CleanPath).ToList(),
targetFrameworks: GetTargetFrameworks(doc).ToList());
targetFrameworks: GetTargetFrameworks(doc).ToList()
);
}

/// <summary>
Expand All @@ -86,12 +90,18 @@ private IEnumerable<string> GetTargetFrameworks(XDocument doc)
{
foreach (var propertyGroup in doc.Root.ElementsIgnoreNamespace(PropertyGroup))
{
foreach (var targetFrameworkElement in propertyGroup.ElementsIgnoreNamespace(TargetFramework))
foreach (
var targetFrameworkElement in propertyGroup.ElementsIgnoreNamespace(
TargetFramework
)
)
{
yield return targetFrameworkElement.Value.Trim();
}

foreach (var targetFrameworks in propertyGroup.ElementsIgnoreNamespace(TargetFrameworks))
foreach (
var targetFrameworks in propertyGroup.ElementsIgnoreNamespace(TargetFrameworks)
)
{
foreach (var targetFramework in targetFrameworks.Value.Split(','))
{
Expand All @@ -109,20 +119,26 @@ private IEnumerable<string> GetImportedProps(XDocument doc)
}
}

private IEnumerable<(string, string, string)> GetReferences(string csprojPath, string referenceType)
private IEnumerable<(string, string, string)> GetReferences(
string csprojPath,
string referenceType
)
{
var doc = ParseXml(csprojPath);

var projectReferenceItemGroup = GetItemGroups(doc, elementName: referenceType);

return projectReferenceItemGroup
.ElementsIgnoreNamespace(referenceType)
.Select(xel => (GetIncludeContent(xel), GetPrivateAssetsContent(xel), GetVersionOrNull(xel)));
.Select(xel =>
(GetIncludeContent(xel), GetPrivateAssetsContent(xel), GetVersionOrNull(xel))
);
}

private static IEnumerable<XElement> GetItemGroups(XDocument doc, string elementName)
{
return doc.Root.ElementsIgnoreNamespace(ItemGroup)
return doc
.Root.ElementsIgnoreNamespace(ItemGroup)
.Where(ig => ig.ElementsIgnoreNamespace(elementName).Any());
}

Expand All @@ -144,16 +160,20 @@ private static string GetVersionOrNull(XElement pr)

internal static class Extensions
{
public static IEnumerable<XElement> ElementsIgnoreNamespace(this XElement element, string name)
public static IEnumerable<XElement> ElementsIgnoreNamespace(
this XElement element,
string name
)
{
return element.Elements()
.Where(e => e.Name.LocalName == name);
return element.Elements().Where(e => e.Name.LocalName == name);
}

public static IEnumerable<XElement> ElementsIgnoreNamespace(this IEnumerable<XElement> elements, string name)
public static IEnumerable<XElement> ElementsIgnoreNamespace(
this IEnumerable<XElement> elements,
string name
)
{
return elements.Elements()
.Where(e => e.Name.LocalName == name);
return elements.Elements().Where(e => e.Name.LocalName == name);
}
}
}
Loading