Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
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
6 changes: 5 additions & 1 deletion 6 Src/FluentAssertions/Common/MemberPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ public bool IsSameAs(MemberPath candidate)
return false;
}

private bool IsParentOf(MemberPath candidate)
/// <summary>
/// Determines whether the current path is the prefix of <paramref name="candidate"/>
/// (i.e., the current path "owns" the candidate), treating numeric indices and wildcards as interchangeable.
/// </summary>
public bool IsParentOf(MemberPath candidate)
{
string[] candidateSegments = candidate.Segments;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,8 @@

namespace FluentAssertions.Equivalency.Selection;

internal class CollectionMemberSelectionRuleDecorator : IMemberSelectionRule
internal class CollectionMemberSelectionRuleDecorator(IMemberSelectionRule selectionRule) : IPathBasedSelectionRule
{
private readonly IMemberSelectionRule selectionRule;

public CollectionMemberSelectionRuleDecorator(IMemberSelectionRule selectionRule)
{
this.selectionRule = selectionRule;
}

public bool IncludesMembers => selectionRule.IncludesMembers;

public IEnumerable<IMember> SelectMembers(INode currentNode, IEnumerable<IMember> selectedMembers,
Expand All @@ -19,6 +12,12 @@ public IEnumerable<IMember> SelectMembers(INode currentNode, IEnumerable<IMember
return selectionRule.SelectMembers(currentNode, selectedMembers, context);
}

public bool SelectsMembersOf(INode currentNode)
{
return selectionRule is IPathBasedSelectionRule pathBasedRule &&
pathBasedRule.SelectsMembersOf(currentNode);
}

public override string ToString()
{
return selectionRule.ToString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,9 @@ namespace FluentAssertions.Equivalency.Selection;
/// <summary>
/// Selection rule that removes a particular property from the structural comparison.
/// </summary>
internal class ExcludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule
internal class ExcludeMemberByPathSelectionRule(MemberPath pathToExclude) : SelectMemberByPathSelectionRule
{
private MemberPath memberToExclude;

public ExcludeMemberByPathSelectionRule(MemberPath pathToExclude)
{
memberToExclude = pathToExclude;
}
private MemberPath memberToExclude = pathToExclude;

protected override void AddOrRemoveMembersFrom(List<IMember> selectedMembers, INode parent, string parentPath,
MemberSelectionContext context)
Expand All @@ -27,6 +22,8 @@ public void AppendPath(MemberPath nextPath)
memberToExclude = memberToExclude.AsParentCollectionOf(nextPath);
}

protected override MemberPath MemberPath => memberToExclude;

public MemberPath CurrentPath => memberToExclude;

public override string ToString()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace FluentAssertions.Equivalency.Selection;

/// <summary>
/// Represents a selection rule whose effect is determined by a configured member path.
/// This allows callers to ask whether the rule targets any members within the subtree rooted at
/// a given <see cref="INode"/>, even when the rule is wrapped by decorators such as the
/// collection-member options decorator.
/// </summary>
internal interface IPathBasedSelectionRule : IMemberSelectionRule
{
/// <summary>
/// Returns <c>true</c> when this rule targets at least one member of <paramref name="currentNode"/>
/// or one of its descendants.
/// </summary>
bool SelectsMembersOf(INode currentNode);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,20 @@ namespace FluentAssertions.Equivalency.Selection;
/// <summary>
/// Selection rule that includes a particular property in the structural comparison.
/// </summary>
internal class IncludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule
internal class IncludeMemberByPathSelectionRule(MemberPath pathToInclude) : SelectMemberByPathSelectionRule
{
private readonly MemberPath memberToInclude;

public IncludeMemberByPathSelectionRule(MemberPath pathToInclude)
{
memberToInclude = pathToInclude;
}

public override bool IncludesMembers => true;

protected override MemberPath MemberPath => pathToInclude;

protected override void AddOrRemoveMembersFrom(List<IMember> selectedMembers, INode parent, string parentPath,
MemberSelectionContext context)
{
foreach (MemberInfo memberInfo in context.Type.GetMembers(MemberKind.Public | MemberKind.Internal))
{
var memberPath = new MemberPath(context.Type, memberInfo.DeclaringType, parentPath.Combine(memberInfo.Name));

if (memberToInclude.IsSameAs(memberPath) || memberToInclude.IsParentOrChildOf(memberPath))
if (pathToInclude.IsSameAs(memberPath) || pathToInclude.IsParentOrChildOf(memberPath))
{
selectedMembers.Add(MemberFactory.Create(memberInfo, parent));
}
Expand All @@ -35,6 +30,6 @@ protected override void AddOrRemoveMembersFrom(List<IMember> selectedMembers, IN

public override string ToString()
{
return "Include member root." + memberToInclude;
return "Include member root." + pathToInclude;
}
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using FluentAssertions.Common;

namespace FluentAssertions.Equivalency.Selection;

internal abstract class SelectMemberByPathSelectionRule : IMemberSelectionRule
internal abstract class SelectMemberByPathSelectionRule : IPathBasedSelectionRule
{
private static readonly Regex LeadingCollectionIndexRegex = new(@"^\[[0-9]+]\.?");

public virtual bool IncludesMembers => false;

protected abstract MemberPath MemberPath { get; }

public IEnumerable<IMember> SelectMembers(INode currentNode, IEnumerable<IMember> selectedMembers,
MemberSelectionContext context)
{
var currentPath = RemoveRootIndexQualifier(currentNode.Expectation.PathAndName);
var members = selectedMembers.ToList();
AddOrRemoveMembersFrom(members, currentNode, currentPath, context);
AddOrRemoveMembersFrom(members, currentNode, GetPathRelativeToSelectionRoot(currentNode), context);

return members;
}

/// <summary>
/// Returns <c>true</c> if this rule would select members within the subtree rooted at
/// <paramref name="currentNode"/>, meaning the rule path targets any member at or below the current node.
/// </summary>
Comment thread
dennisdoomen marked this conversation as resolved.
public bool SelectsMembersOf(INode currentNode)
{
if (currentNode.IsRoot)
{
return !MemberPath.ToString().IsNullOrEmpty();
}

string currentPath = GetPathRelativeToSelectionRoot(currentNode);

if (string.IsNullOrEmpty(currentPath))
{
return !MemberPath.ToString().IsNullOrEmpty();
}

// Compare normalized path segments rather than raw strings so collection rules like "Items[].Name"
// still match the concrete node path "Items[0].Name", and so paths built through
// MemberPath.AsParentCollectionOf are interpreted consistently.
return new MemberPath(currentPath).IsParentOf(MemberPath);
}

protected abstract void AddOrRemoveMembersFrom(List<IMember> selectedMembers,
INode parent, string parentPath,
MemberSelectionContext context);

private static string RemoveRootIndexQualifier(string path)
/// <summary>
/// Returns the path used for member-selection matching at <paramref name="currentNode"/>.
/// For items inside a root collection, Fluent Assertions exposes paths such as <c>[0].Name</c>, but
/// selection rules are expressed relative to the collection item type (for example <c>Name</c>).
/// This method removes that root-item index so both sides use the same coordinate system.
/// </summary>
private static string GetPathRelativeToSelectionRoot(INode currentNode)
{
Match match = new Regex(@"^\[[0-9]+]").Match(path);

if (match.Success)
if (currentNode.IsRoot)
{
path = path.Substring(match.Length);
return string.Empty;
}

return path;
string expectationPath = currentNode.Expectation.PathAndName;
return RemoveLeadingCollectionIndex(expectationPath);
}

/// <summary>
/// Removes only the leading root-collection index from <paramref name="path"/>, such as turning
/// <c>[0]</c> into an empty path and <c>[0].Name</c> into <c>Name</c>.
/// Nested collection indices are intentionally left in place; they are handled later by
/// <see cref="MemberPath"/> comparison, which treats <c>[]</c> and concrete indices as equivalent.
/// </summary>
private static string RemoveLeadingCollectionIndex(string path)
{
Match match = LeadingCollectionIndexRegex.Match(path);
return match.Success ? path.Substring(match.Length) : path;
Comment thread
dennisdoomen marked this conversation as resolved.
}
}
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.