Skip to content

Commit

Permalink
Merge pull request #1639 from dennisdoomen/MergeFromDevelop
Browse files Browse the repository at this point in the history
Merge missed improvements from develop
  • Loading branch information
dennisdoomen authored Aug 11, 2021
2 parents 13cf5dc + 79ad97d commit dbe3fda
Show file tree
Hide file tree
Showing 25 changed files with 903 additions and 62 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Text;

namespace FluentAssertions.CallerIdentification
{
internal class AddNonEmptySymbolParsingStrategy : IParsingStrategy
{
public ParsingState Parse(char symbol, StringBuilder statement)
{
if (!char.IsWhiteSpace(symbol))
{
statement.Append(symbol);
}

return ParsingState.GoToNextSymbol;
}

public bool IsWaitingForContextEnd()
{
return false;
}

public void NotifyEndOfLineReached()
{
}
}
}
46 changes: 46 additions & 0 deletions Src/FluentAssertions/CallerIdentification/AwaitParsingStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Text;

namespace FluentAssertions.CallerIdentification
{
internal class AwaitParsingStrategy : IParsingStrategy
{
private const string KeywordToSkip = "await";

public ParsingState Parse(char symbol, StringBuilder statement)
{
if (IsLongEnoughToContainOurKeyword(statement) && EndsWithOurKeyword(statement))
{
statement.Remove(statement.Length - KeywordToSkip.Length, KeywordToSkip.Length);
}

return ParsingState.InProgress;
}

private static bool EndsWithOurKeyword(StringBuilder statement)
{
var leftIndex = statement.Length - 1;
var rightIndex = KeywordToSkip.Length - 1;

for (var offset = 0; offset < KeywordToSkip.Length; offset++)
{
if (statement[leftIndex - offset] != KeywordToSkip[rightIndex - offset])
{
return false;
}
}

return true;
}

private static bool IsLongEnoughToContainOurKeyword(StringBuilder statement) => statement.Length >= KeywordToSkip.Length;

public bool IsWaitingForContextEnd()
{
return false;
}

public void NotifyEndOfLineReached()
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace FluentAssertions.CallerIdentification
{
internal class CallerStatementBuilder
{
private readonly StringBuilder statement;
private readonly List<IParsingStrategy> priorityOrderedParsingStrategies;
private ParsingState parsingState = ParsingState.InProgress;

internal CallerStatementBuilder()
{
statement = new StringBuilder();
priorityOrderedParsingStrategies = new List<IParsingStrategy>
{
new QuotesParsingStrategy(),
new MultiLineCommentParsingStrategy(),
new SingleLineCommentParsingStrategy(),
new SemicolonParsingStrategy(),
new ShouldCallParsingStrategy(),
new AwaitParsingStrategy(),
new AddNonEmptySymbolParsingStrategy()
};
}

internal void Append(string symbols)
{
using var symbolEnumerator = symbols.GetEnumerator();
while (symbolEnumerator.MoveNext() && parsingState != ParsingState.Done)
{
var hasParsingStrategyWaitingForEndContext = priorityOrderedParsingStrategies
.Any(s => s.IsWaitingForContextEnd());

parsingState = ParsingState.InProgress;
foreach (var parsingStrategy in
priorityOrderedParsingStrategies
.SkipWhile(parsingStrategy =>
hasParsingStrategyWaitingForEndContext
&& !parsingStrategy.IsWaitingForContextEnd()))
{
parsingState = parsingStrategy.Parse(symbolEnumerator.Current, statement);
if (parsingState != ParsingState.InProgress)
{
break;
}
}
}

if (IsDone())
{
return;
}

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

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

public override string ToString() => statement.ToString();
}
}
32 changes: 32 additions & 0 deletions Src/FluentAssertions/CallerIdentification/IParsingStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Text;

namespace FluentAssertions.CallerIdentification
{
/// <summary>
/// Represents a stateful parsing strategy that is used to help identify the "caller" to use in an assertion message.
///
/// The strategies will be instantiated at the beginning of a "caller identification" task, and will live until
/// the statement can be identified (and thus some are stateful).
/// </summary>
internal interface IParsingStrategy
{
/// <summary>
/// Given a symbol, the parsing strategy should add/remove from the statement if needed, and then return
/// - InProgress if the symbol isn't relevant to the strategies (so other strategies can be tried)
/// - Handled if an action has been taken (and no other strategies should be used for this symbol)
/// - Done if the statement is complete, and thus further symbols should be read.
/// </summary>
ParsingState Parse(char symbol, StringBuilder statement);

/// <summary>
/// Returns true if strategy is in the middle of a context (ex: strategy read "/*" and is waiting for "*/"
/// </summary>
bool IsWaitingForContextEnd();

/// <summary>
/// Used to notify the strategy that we have reached the end of the line (very useful to detect the end of
/// a single line comment).
/// </summary>
void NotifyEndOfLineReached();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Text;

namespace FluentAssertions.CallerIdentification
{
internal class MultiLineCommentParsingStrategy : IParsingStrategy
{
private bool isCommentContext;
private char? commentContextPreviousChar;

public ParsingState Parse(char symbol, StringBuilder statement)
{
if (isCommentContext)
{
var isEndOfMultilineComment = symbol == '/' && commentContextPreviousChar == '*';
if (isEndOfMultilineComment)
{
isCommentContext = false;
commentContextPreviousChar = null;
}
else
{
commentContextPreviousChar = symbol;
}

return ParsingState.GoToNextSymbol;
}

var isStartOfMultilineComment =
symbol == '*'
&& statement.Length > 0
&& statement[statement.Length - 1] == '/';
if (isStartOfMultilineComment)
{
statement.Remove(statement.Length - 1, 1);
isCommentContext = true;
return ParsingState.GoToNextSymbol;
}

return ParsingState.InProgress;
}

public bool IsWaitingForContextEnd()
{
return isCommentContext;
}

public void NotifyEndOfLineReached()
{
}
}
}
9 changes: 9 additions & 0 deletions Src/FluentAssertions/CallerIdentification/ParsingState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace FluentAssertions.CallerIdentification
{
internal enum ParsingState
{
InProgress,
GoToNextSymbol,
Done
}
}
71 changes: 71 additions & 0 deletions Src/FluentAssertions/CallerIdentification/QuotesParsingStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Linq;
using System.Text;

namespace FluentAssertions.CallerIdentification
{
internal class QuotesParsingStrategy : IParsingStrategy
{
private char isQuoteEscapeSymbol = '\\';
private bool isQuoteContext;
private char? previousChar;

public ParsingState Parse(char symbol, StringBuilder statement)
{
if (symbol == '"')
{
if (isQuoteContext)
{
if (previousChar != isQuoteEscapeSymbol)
{
isQuoteContext = false;
isQuoteEscapeSymbol = '\\';
previousChar = null;
statement.Append(symbol);
return ParsingState.GoToNextSymbol;
}
}
else
{
isQuoteContext = true;
if (IsVerbatim(statement))
{
isQuoteEscapeSymbol = '"';
}
}
}

if (isQuoteContext)
{
statement.Append(symbol);
}

previousChar = symbol;
return isQuoteContext ? ParsingState.GoToNextSymbol : ParsingState.InProgress;
}

public bool IsWaitingForContextEnd()
{
return isQuoteContext;
}

public void NotifyEndOfLineReached()
{
}

private bool IsVerbatim(StringBuilder statement)
{
return
statement.Length >= 1
&&
new[]
{
"$@",
"@$",
}
.Any(verbatimStringOpener =>
previousChar == verbatimStringOpener[1]
&& statement[statement.Length - 1] == verbatimStringOpener[1]
&& statement[statement.Length - 2] == verbatimStringOpener[0]);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Text;

namespace FluentAssertions.CallerIdentification
{
internal class SemicolonParsingStrategy : IParsingStrategy
{
public ParsingState Parse(char symbol, StringBuilder statement)
{
if (symbol == ';')
{
statement.Clear();
return ParsingState.Done;
}

return ParsingState.InProgress;
}

public bool IsWaitingForContextEnd()
{
return false;
}

public void NotifyEndOfLineReached()
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Text;

namespace FluentAssertions.CallerIdentification
{
internal class ShouldCallParsingStrategy : IParsingStrategy
{
private const string ShouldCall = ".Should(";

public ParsingState Parse(char symbol, StringBuilder statement)
{
if (statement.Length >= ShouldCall.Length)
{
var leftIndex = statement.Length - 1;
var rightIndex = ShouldCall.Length - 1;

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

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

return ParsingState.InProgress;
}

public bool IsWaitingForContextEnd()
{
return false;
}

public void NotifyEndOfLineReached()
{
}
}
}
Loading

0 comments on commit dbe3fda

Please sign in to comment.