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
23 changes: 23 additions & 0 deletions Src/FluentAssertions/AndWhichConstraint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,29 @@ public AndWhichConstraint(TParent parent, TSubject subject, AssertionChain asser
/// <remarks>
/// If <paramref name="subjects"/> contains more than one object, a clear exception is thrown.
/// </remarks>
// REFACTOR: In a next major version, we need to remove this overload and make the AssertionChain required
public AndWhichConstraint(TParent parent, IEnumerable<TSubject> subjects)
: base(parent)
{
getSubject = new Lazy<TSubject>(() => Single(subjects));
}

/// <summary>
/// Creates an object that allows continuing an assertion executed through <paramref name="parent"/> and
/// which resulted in a potential collection of objects through <paramref name="subjects"/> on an
/// existing <paramref name="assertionChain"/>.
/// </summary>
/// <remarks>
/// If <paramref name="subjects"/> contains more than one object, a clear exception is thrown.
/// </remarks>
public AndWhichConstraint(TParent parent, IEnumerable<TSubject> subjects, AssertionChain assertionChain)
: base(parent)
{
getSubject = new Lazy<TSubject>(() => Single(subjects));

this.assertionChain = assertionChain;
}

/// <summary>
/// Creates an object that allows continuing an assertion executed through <paramref name="parent"/> and
/// which resulted in a potential collection of objects through <paramref name="subjects"/> on an
Expand Down Expand Up @@ -108,6 +125,12 @@ public TSubject Which
{
assertionChain.WithCallerPostfix(pathPostfix).ReuseOnce();
}
else
{
// Make sure the caller identification restarts with the code following the Which property.
assertionChain?.AdvanceToNextIdentifier();
assertionChain?.ReuseOnce();
}

return getSubject.Value;
}
Expand Down
51 changes: 31 additions & 20 deletions Src/FluentAssertions/CallerIdentification/CallerStatementBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,62 @@

namespace FluentAssertions.CallerIdentification;

// REFACTOR: This is not a builder, but a parser, so rename it accordingly.
internal class CallerStatementBuilder
{
private readonly StringBuilder statement;
private readonly List<IParsingStrategy> priorityOrderedParsingStrategies;
private ParsingState parsingState = ParsingState.InProgress;
private readonly List<IParsingStrategy> parsingStrategies;
private readonly List<string> candidates = new();
private ParsingState state = ParsingState.InProgress;

internal CallerStatementBuilder()
{
statement = new StringBuilder();

priorityOrderedParsingStrategies =
parsingStrategies =
[
new QuotesParsingStrategy(),
new MultiLineCommentParsingStrategy(),
new SingleLineCommentParsingStrategy(),
new SemicolonParsingStrategy(),
new ShouldCallParsingStrategy(),
new WhichParsingStrategy(),
new AwaitParsingStrategy(),
new AddNonEmptySymbolParsingStrategy()
];
}

internal void Append(string symbols)
/// <summary>
/// Gets the identifiers preceding a Should or Which clause as extracted from lines of code passed to <see cref="Append"/>
/// </summary>
public string[] Identifiers => candidates.ToArray();

public void Append(string symbols)
{
using var symbolEnumerator = symbols.GetEnumerator();

while (symbolEnumerator.MoveNext() && parsingState != ParsingState.Done)
while (symbolEnumerator.MoveNext() && state != ParsingState.Completed)
{
var hasParsingStrategyWaitingForEndContext = priorityOrderedParsingStrategies
.Exists(s => s.IsWaitingForContextEnd());
// The logic ensures that parsing does not continue with irrelevant strategies when a strategy is currently in the middle
// of a multi-symbol context (e.g., waiting for "_/" to match the beginning "/_"). In such cases, it skips over strategies
// that aren't waiting for the "end of context" while allowing the active (waiting) strategy to resume processing.
IEnumerable<IParsingStrategy> activeParsers = parsingStrategies;
if (parsingStrategies.Exists(s => s.IsWaitingForContextEnd()))
{
activeParsers = parsingStrategies.SkipWhile(parsingStrategy => !parsingStrategy.IsWaitingForContextEnd());
}

parsingState = ParsingState.InProgress;
state = ParsingState.InProgress;

foreach (var parsingStrategy in
priorityOrderedParsingStrategies
.SkipWhile(parsingStrategy =>
hasParsingStrategyWaitingForEndContext
&& !parsingStrategy.IsWaitingForContextEnd()))
foreach (IParsingStrategy parser in activeParsers)
{
parsingState = parsingStrategy.Parse(symbolEnumerator.Current, statement);
state = parser.Parse(symbolEnumerator.Current, statement);
if (state == ParsingState.CandidateFound)
{
candidates.Add(statement.ToString());
}

if (parsingState != ParsingState.InProgress)
if (state != ParsingState.InProgress)
{
break;
}
Expand All @@ -57,11 +71,8 @@ internal void Append(string symbols)
return;
}

priorityOrderedParsingStrategies
.ForEach(strategy => strategy.NotifyEndOfLineReached());
parsingStrategies.ForEach(strategy => strategy.NotifyEndOfLineReached());
}

internal bool IsDone() => parsingState == ParsingState.Done;

public override string ToString() => statement.ToString();
public bool IsDone() => state == ParsingState.Completed;
}
19 changes: 18 additions & 1 deletion Src/FluentAssertions/CallerIdentification/ParsingState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,24 @@

internal enum ParsingState
{
/// <summary>
/// Is returned by a parser when the next one can take a look at the current symbol
/// </summary>
InProgress,

/// <summary>
/// Is returned by a parser when it decides a symbol has been processed enough and no
/// other parsers need to look at the current symbol anymore.
/// </summary>
GoToNextSymbol,
Done

/// <summary>
/// Is returned by a parser if it has found a candidate identifier.
/// </summary>
CandidateFound,

/// <summary>
/// Is returned by a parser to indicate that the parsing has been fully completed.
/// </summary>
Completed
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public ParsingState Parse(char symbol, StringBuilder statement)
if (symbol is ';')
{
statement.Clear();
return ParsingState.Done;
return ParsingState.Completed;
}

return ParsingState.InProgress;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,49 @@

namespace FluentAssertions.CallerIdentification;

/// <summary>
/// Tries to determine if the statement ends with ".Should(" or ".Should.", and assumes
/// the preceding identifier is the actual identifier.
/// </summary>
internal class ShouldCallParsingStrategy : IParsingStrategy
{
private const string ShouldCall = ".Should";
private const string ExpectedPhrase = ".Should";

public ParsingState Parse(char symbol, StringBuilder statement)
{
if (statement.Length >= ShouldCall.Length + 1)
if (IsLongEnough(statement) && EndsWithExpectedPhrase(statement) && EndsWithInvocation(statement))
{
var leftIndex = statement.Length - 2;
var rightIndex = ShouldCall.Length - 1;
// Remove the ".Should." or ".Should(" part from the statement, so we keep the actual identifier
statement.Remove(statement.Length - (ExpectedPhrase.Length + 1), ExpectedPhrase.Length + 1);
return ParsingState.CandidateFound;
}

for (var i = 0; i < ShouldCall.Length; i++)
{
if (statement[leftIndex - i] != ShouldCall[rightIndex - i])
{
return ParsingState.InProgress;
}
}
return ParsingState.InProgress;
}

private static bool IsLongEnough(StringBuilder statement) => statement.Length >= ExpectedPhrase.Length + 1;

if (statement[^1] is not ('(' or '.'))
private static bool EndsWithExpectedPhrase(StringBuilder statement)
{
// Start from the index on the character just before the last ( or .
var rightIndexInStatement = statement.Length - 2;

var rightIndexInExpectedPhrase = ExpectedPhrase.Length - 1;

// Do a reverse comparison to see if the statement ends with ".Should"
for (var i = 0; i < ExpectedPhrase.Length; i++)
{
if (statement[rightIndexInStatement - i] != ExpectedPhrase[rightIndexInExpectedPhrase - i])
{
return ParsingState.InProgress;
return false;
}

statement.Remove(statement.Length - (ShouldCall.Length + 1), ShouldCall.Length + 1);
return ParsingState.Done;
}

return ParsingState.InProgress;
return true;
}

private static bool EndsWithInvocation(StringBuilder statement) => statement[^1] is '(' or '.';

public bool IsWaitingForContextEnd()
{
return false;
Expand Down
54 changes: 54 additions & 0 deletions Src/FluentAssertions/CallerIdentification/WhichParsingStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Text;

namespace FluentAssertions.CallerIdentification;

/// <summary>
/// Tries to find the <c>.Which.</c> construct and assumes everything preceding it has become irrelevant
/// for the chained assertion.
/// </summary>
internal class WhichParsingStrategy : IParsingStrategy
{
private const string ExpectedPhrase = ".Which.";

public ParsingState Parse(char symbol, StringBuilder statement)
{
if (IsLongEnough(statement) && EndsWithExpectedPhrase(statement))
{
// Remove everything we collected up to know and assume everything following the
// .Which. property is a new assertion
statement.Clear();
}

return ParsingState.InProgress;
}

private static bool IsLongEnough(StringBuilder statement) => statement.Length >= ExpectedPhrase.Length;

private static bool EndsWithExpectedPhrase(StringBuilder statement)
{
// Start from the index of the last character
var rightIndexInStatement = statement.Length - 1;

var rightIndexInExpectedPhrase = ExpectedPhrase.Length - 1;

// Do a reverse comparison to see if the statement ends with ".Which."
for (var i = 0; i < ExpectedPhrase.Length; i++)
{
if (statement[rightIndexInStatement - i] != ExpectedPhrase[rightIndexInExpectedPhrase - i])
{
return false;
}
}

return true;
}

public bool IsWaitingForContextEnd()
{
return false;
}

public void NotifyEndOfLineReached()
{
}
}
Loading