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
7 changes: 7 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ jobs:
language: [ 'csharp' ]

steps:
- name: Setup .NET SDKs
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x

- name: Checkout repository

uses: actions/checkout@v4

# Initializes the CodeQL tools for scanning.
Expand Down
1 change: 1 addition & 0 deletions FluentAssertions.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FRESOURCE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>

<s:String x:Key="/Default/Environment/Editor/MatchingBraceHighlighting/Style/@EntryValue">OUTLINE</s:String>
<s:String x:Key="/Default/Environment/Hierarchy/Build/SolutionBuilderNext/FileVerbosityLevel/@EntryValue">Minimal</s:String>
<s:String x:Key="/Default/Environment/Hierarchy/PsiConfigurationSettingsKey/LocationType/@EntryValue">SOLUTION_FOLDER</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002EDaemon_002ESettings_002EMigration_002ESwaWarningsModeSettingsMigrate/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>
Expand Down
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Common/MemberPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal class MemberPath
private static readonly MemberPathSegmentEqualityComparer MemberPathSegmentEqualityComparer = new();

public MemberPath(IMember member, string parentPath)
: this(member.ReflectedType, member.DeclaringType, parentPath.Combine(member.Name))
: this(member.ReflectedType, member.DeclaringType, parentPath.Combine(member.Expectation.Name))
{
}

Expand Down
11 changes: 0 additions & 11 deletions Src/FluentAssertions/Common/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,6 @@ public static bool ContainsSpecificCollectionIndex(this string indexedPath)
public static string EscapePlaceholders(this string value) =>
value.Replace("{", "{{", StringComparison.Ordinal).Replace("}", "}}", StringComparison.Ordinal);

/// <summary>
/// Replaces all characters that might conflict with formatting placeholders with their escaped counterparts.
/// </summary>
internal static string UnescapePlaceholders(this string value) =>
value.Replace("{{", "{", StringComparison.Ordinal).Replace("}}", "}", StringComparison.Ordinal);

/// <summary>
/// Joins a string with one or more other strings using a specified separator.
/// </summary>
Expand All @@ -78,11 +72,6 @@ public static string Combine(this string @this, string other, string separator =
return other.Length != 0 ? other : string.Empty;
}

if (other.Length == 0)
{
return @this;
}

if (other.StartsWith('['))
{
separator = string.Empty;
Expand Down
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Common/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public static bool IsEquivalentTo(this IMember property, IMember otherProperty)
{
return (property.DeclaringType.IsSameOrInherits(otherProperty.DeclaringType) ||
otherProperty.DeclaringType.IsSameOrInherits(property.DeclaringType)) &&
property.Name == otherProperty.Name;
property.Expectation.Name == otherProperty.Expectation.Name;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ internal static class AssertionChainExtensions
/// </summary>
public static AssertionChain For(this AssertionChain chain, IEquivalencyValidationContext context)
{
chain.OverrideCallerIdentifier(() => context.CurrentNode.Description);
chain.OverrideCallerIdentifier(() => context.CurrentNode.Subject.Description);

return chain
.WithReportable("configuration", () => context.Options.ToString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ public bool IsCyclicReference(object expectation)
bool compareByMembers = expectation is not null && Options.GetEqualityStrategy(expectation.GetType())
is EqualityStrategy.Members or EqualityStrategy.ForceMembers;

var reference = new ObjectReference(expectation, CurrentNode.PathAndName, compareByMembers);
var reference = new ObjectReference(expectation, CurrentNode.Subject.PathAndName, compareByMembers);
return CyclicReferenceDetector.IsCyclicReference(reference);
}

public ITraceWriter TraceWriter { get; set; }

public override string ToString()
{
return Invariant($"{{Path=\"{CurrentNode.Description}\"}}");
return Invariant($"{{Path=\"{CurrentNode.Subject.PathAndName}\"}}");
}
}
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Equivalency/EquivalencyValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ private static void AssertEquivalencyForCyclicReference(Comparands comparands, A

private void TryToProveNodesAreEquivalent(Comparands comparands, IEquivalencyValidationContext context)
{
using var _ = context.Tracer.WriteBlock(node => node.Description);
using var _ = context.Tracer.WriteBlock(node => node.Expectation.Description);

foreach (IEquivalencyStep step in AssertionOptions.EquivalencyPlan)
{
Expand Down
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Equivalency/Execution/ObjectInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public ObjectInfo(Comparands comparands, INode currentNode)
{
Type = currentNode.Type;
ParentType = currentNode.ParentType;
Path = currentNode.PathAndName;
Path = currentNode.Expectation.PathAndName;
CompileTimeType = comparands.CompileTimeType;
RuntimeType = comparands.RuntimeType;
}
Expand Down
15 changes: 4 additions & 11 deletions Src/FluentAssertions/Equivalency/Field.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,21 @@
namespace FluentAssertions.Equivalency;

/// <summary>
/// A specialized type of <see cref="INode "/> that represents a field of an object in a structural equivalency assertion.
/// A specialized type of <see cref="INode"/> that represents a field of an object in a structural equivalency assertion.
/// </summary>
internal class Field : Node, IMember
{
private readonly FieldInfo fieldInfo;
private bool? isBrowsable;

public Field(FieldInfo fieldInfo, INode parent)
: this(fieldInfo.ReflectedType, fieldInfo, parent)
{
}

public Field(Type reflectedType, FieldInfo fieldInfo, INode parent)
{
this.fieldInfo = fieldInfo;
DeclaringType = fieldInfo.DeclaringType;
ReflectedType = reflectedType;
Path = parent.PathAndName;
ReflectedType = fieldInfo.ReflectedType;
Subject = new Pathway(parent.Subject.PathAndName, fieldInfo.Name, pathAndName => $"field {parent.GetSubjectId().Combine(pathAndName)}");
Expectation = new Pathway(parent.Expectation.PathAndName, fieldInfo.Name, pathAndName => $"field {pathAndName}");
GetSubjectId = parent.GetSubjectId;
Name = fieldInfo.Name;
Type = fieldInfo.FieldType;
ParentType = fieldInfo.DeclaringType;
RootIsCollection = parent.RootIsCollection;
Expand All @@ -40,8 +35,6 @@ public object GetValue(object obj)

public Type DeclaringType { get; set; }

public override string Description => $"field {GetSubjectId().Combine(PathAndName)}";

public CSharpAccessModifier GetterAccessibility => fieldInfo.GetCSharpAccessModifier();

public CSharpAccessModifier SetterAccessibility => fieldInfo.GetCSharpAccessModifier();
Expand Down
42 changes: 14 additions & 28 deletions Src/FluentAssertions/Equivalency/INode.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using JetBrains.Annotations;

namespace FluentAssertions.Equivalency;

Expand All @@ -15,14 +14,6 @@ public interface INode
/// </summary>
GetSubjectId GetSubjectId { get; }

/// <summary>
/// Gets the name of this node.
/// </summary>
/// <example>
/// "Property2"
/// </example>
string Name { get; set; }

/// <summary>
/// Gets the type of this node, e.g. the type of the field or property, or the type of the collection item.
/// </summary>
Expand All @@ -34,24 +25,17 @@ public interface INode
/// <value>
/// Is <see langword="null"/> for the root object.
/// </value>
[CanBeNull]
Type ParentType { get; }

/// <summary>
/// Gets the path from the root object UNTIL the current node, separated by dots or index/key brackets.
/// Gets the path from the root of the subject upto and including the current node.
/// </summary>
/// <example>
/// "Parent[0].Property2"
/// </example>
string Path { get; }
Pathway Subject { get; internal set; }

/// <summary>
/// Gets the full path from the root object up to and including the name of the node.
/// Gets the path from the root of the expectation upto and including the current node.
/// </summary>
/// <example>
/// "Parent[0]"
/// </example>
string PathAndName { get; }
Pathway Expectation { get; }

/// <summary>
/// Gets a zero-based number representing the depth within the object graph
Expand All @@ -62,14 +46,6 @@ public interface INode
/// </remarks>
int Depth { get; }

/// <summary>
/// Gets the path including the description of the subject.
/// </summary>
/// <example>
/// "property subject.Parent[0].Property2"
/// </example>
string Description { get; }

/// <summary>
/// Gets a value indicating whether the current node is the root.
/// </summary>
Expand All @@ -79,4 +55,14 @@ public interface INode
/// Gets a value indicating if the root of this graph is a collection.
/// </summary>
bool RootIsCollection { get; }

/// <summary>
/// Adjusts the current node to reflect a remapped subject member during a structural equivalency check.
/// Ensures that assertion failures report the correct subject member name when the matching process selects
/// a different member in the subject compared to the expectation.
/// </summary>
/// <param name="subjectMember">
/// The specific member in the subject that the current node should be remapped to.
/// </param>
void AdjustForRemappedSubject(IMember subjectMember);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public MappedMemberMatchingRule(string expectationMemberName, string subjectMemb
public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyOptions options, AssertionChain assertionChain)
{
if (parent.Type.IsSameOrInherits(typeof(TExpectation)) && subject is TSubject &&
expectedMember.Name == expectationMemberName)
expectedMember.Subject.Name == expectationMemberName)
{
var member = MemberFactory.Find(subject, subjectMemberName, parent);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui
path = path.WithCollectionAsRoot();
}

if (path.IsEquivalentTo(expectedMember.PathAndName))
if (path.IsEquivalentTo(expectedMember.Expectation.PathAndName))
{
var member = MemberFactory.Find(subject, subjectPath.MemberName, parent);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui
if (options.IncludedProperties != MemberVisibility.None)
{
PropertyInfo propertyInfo = subject.GetType().FindProperty(
expectedMember.Name,
expectedMember.Subject.Name,
options.IncludedProperties | MemberVisibility.ExplicitlyImplemented | MemberVisibility.DefaultInterfaceProperties);

subjectMember = propertyInfo is not null && !propertyInfo.IsIndexer() ? new Property(propertyInfo, parent) : null;
Expand All @@ -25,7 +25,7 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui
if (subjectMember is null && options.IncludedFields != MemberVisibility.None)
{
FieldInfo fieldInfo = subject.GetType().FindField(
expectedMember.Name,
expectedMember.Subject.Name,
options.IncludedFields);

subjectMember = fieldInfo is not null ? new Field(fieldInfo, parent) : null;
Expand All @@ -34,12 +34,12 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui
if (subjectMember is null)
{
assertionChain.FailWith(
$"Expectation has {expectedMember.Description} that the other object does not have.");
$"Expectation has {expectedMember.Expectation} that the other object does not have.");
}
else if (options.IgnoreNonBrowsableOnSubject && !subjectMember.IsBrowsable)
{
assertionChain.FailWith(
$"Expectation has {expectedMember.Description} that is non-browsable in the other object, and non-browsable " +
$"Expectation has {expectedMember.Expectation} that is non-browsable in the other object, and non-browsable " +
"members on the subject are ignored with the current configuration");
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui
{
if (options.IncludedProperties != MemberVisibility.None)
{
PropertyInfo property = subject.GetType().FindProperty(expectedMember.Name,
PropertyInfo property = subject.GetType().FindProperty(expectedMember.Expectation.Name,
options.IncludedProperties | MemberVisibility.ExplicitlyImplemented);

if (property is not null && !property.IsIndexer())
Expand All @@ -23,7 +23,7 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui
}

FieldInfo field = subject.GetType()
.FindField(expectedMember.Name, options.IncludedFields);
.FindField(expectedMember.Expectation.Name, options.IncludedFields);

return field is not null ? new Field(field, parent) : null;
}
Expand Down
Loading