From 3828bf70263d0ecc04033bb9d521f013f24c869f Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 23 Feb 2025 18:32:38 +0100 Subject: [PATCH 1/9] Restore `StringSyntax` annotations --- .../GenericCollectionAssertions.cs | 35 +++++++++++-------- .../Execution/AssertionChain.cs | 3 +- .../Specialized/ExceptionAssertions.cs | 23 +++++++----- .../Specialized/FunctionAssertions.cs | 2 +- .../Streams/BufferedStreamAssertions.cs | 7 ++-- .../Xml/Equivalency/XmlReaderValidator.cs | 4 ++- 6 files changed, 47 insertions(+), 27 deletions(-) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index 7984f73a72..908a8bfad4 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -1122,8 +1122,8 @@ public AndConstraint ContainItemsAssignableTo( /// /// Zero or more objects to format using the placeholders in . /// - public AndConstraint - NotContainItemsAssignableTo(string because = "", params object[] becauseArgs) => + public AndConstraint NotContainItemsAssignableTo( + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) => NotContainItemsAssignableTo(typeof(TExpectation), because, becauseArgs); /// @@ -1168,7 +1168,8 @@ public AndConstraint NotContainItemsAssignableTo(Type type, /// /// Zero or more objects to format using the placeholders in . /// - public AndWhichConstraint ContainSingle(string because = "", params object[] becauseArgs) + public AndWhichConstraint ContainSingle( + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { assertionChain .BecauseOf(because, becauseArgs) @@ -1217,7 +1218,7 @@ public AndWhichConstraint ContainSingle(string because = "", par /// /// is . public AndWhichConstraint ContainSingle(Expression> predicate, - string because = "", params object[] becauseArgs) + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { Guard.ThrowIfArgumentIsNull(predicate); @@ -1784,7 +1785,8 @@ public AndConstraint IntersectWith(IEnumerable otherCollection, /// /// Zero or more objects to format using the placeholders in . /// - public AndConstraint NotBeEmpty(string because = "", params object[] becauseArgs) + public AndConstraint NotBeEmpty( + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { assertionChain .BecauseOf(because, becauseArgs) @@ -1858,7 +1860,7 @@ public AndConstraint NotBeEquivalentTo(IEnumerable public AndConstraint NotBeEquivalentTo(IEnumerable unexpected, Func, EquivalencyOptions> config, - string because = "", params object[] becauseArgs) + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { Guard.ThrowIfArgumentIsNull(unexpected, nameof(unexpected), "Cannot verify inequivalence against a collection."); @@ -1984,7 +1986,8 @@ public AndConstraint NotBeInAscendingOrder( /// /// Empty and single element collections are considered to be ordered both in ascending and descending order at the same time. /// - public AndConstraint NotBeInAscendingOrder(string because = "", params object[] becauseArgs) + public AndConstraint NotBeInAscendingOrder( + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { return NotBeInAscendingOrder(GetComparer(), because, becauseArgs); } @@ -2109,7 +2112,8 @@ public AndConstraint NotBeInDescendingOrder( /// /// Empty and single element collections are considered to be ordered both in ascending and descending order at the same time. /// - public AndConstraint NotBeInDescendingOrder(string because = "", params object[] becauseArgs) + public AndConstraint NotBeInDescendingOrder( + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { return NotBeInDescendingOrder(GetComparer(), because, becauseArgs); } @@ -2147,7 +2151,8 @@ public AndConstraint NotBeInDescendingOrder(Func compari /// /// Zero or more objects to format using the placeholders in . /// - public AndConstraint NotBeNullOrEmpty(string because = "", params object[] becauseArgs) + public AndConstraint NotBeNullOrEmpty( + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { return NotBeNull(because, becauseArgs) .And.NotBeEmpty(because, becauseArgs); @@ -2671,7 +2676,8 @@ public AndConstraint NotContainNulls(Expression /// /// Zero or more objects to format using the placeholders in . /// - public AndConstraint NotContainNulls(string because = "", params object[] becauseArgs) + public AndConstraint NotContainNulls( + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { assertionChain .BecauseOf(because, becauseArgs) @@ -2786,7 +2792,7 @@ public AndConstraint NotHaveCount(int unexpected, [StringSyntax("Co /// /// is . public AndConstraint NotHaveSameCount(IEnumerable otherCollection, - string because = "", + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { Guard.ThrowIfArgumentIsNull(otherCollection, nameof(otherCollection), "Cannot verify count against a collection."); @@ -2956,7 +2962,8 @@ public AndConstraint OnlyHaveUniqueItems(Expression /// Zero or more objects to format using the placeholders in . /// - public AndConstraint OnlyHaveUniqueItems(string because = "", params object[] becauseArgs) + public AndConstraint OnlyHaveUniqueItems( + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { assertionChain .BecauseOf(because, becauseArgs) @@ -3306,7 +3313,7 @@ internal AndConstraint> BeOrderedBy( Expression> propertyExpression, IComparer comparer, SortOrder direction, - string because, + [StringSyntax("CompositeFormat")] string because, object[] becauseArgs) { if (IsValidProperty(propertyExpression, because, becauseArgs)) @@ -3535,7 +3542,7 @@ private AndConstraint NotBeOrderedBy( Expression> propertyExpression, IComparer comparer, SortOrder direction, - string because, + [StringSyntax("CompositeFormat")] string because, object[] becauseArgs) { if (IsValidProperty(propertyExpression, because, becauseArgs)) diff --git a/Src/FluentAssertions/Execution/AssertionChain.cs b/Src/FluentAssertions/Execution/AssertionChain.cs index 8be7c48ee9..5422c05c1e 100644 --- a/Src/FluentAssertions/Execution/AssertionChain.cs +++ b/Src/FluentAssertions/Execution/AssertionChain.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Threading; @@ -109,7 +110,7 @@ public AssertionChain BecauseOf(Reason reason) /// /// Zero or more objects to format using the placeholders in . /// - public AssertionChain BecauseOf(string because, params object[] becauseArgs) + public AssertionChain BecauseOf([StringSyntax("CompositeFormat")] string because, params object[] becauseArgs) { reason = () => { diff --git a/Src/FluentAssertions/Specialized/ExceptionAssertions.cs b/Src/FluentAssertions/Specialized/ExceptionAssertions.cs index fb3244279f..9ada515f2d 100644 --- a/Src/FluentAssertions/Specialized/ExceptionAssertions.cs +++ b/Src/FluentAssertions/Specialized/ExceptionAssertions.cs @@ -101,7 +101,8 @@ public virtual ExceptionAssertions WithMessage(string expectedWildca /// /// Zero or more objects to format using the placeholders in . /// - public virtual ExceptionAssertions WithInnerException(string because = "", + public virtual ExceptionAssertions WithInnerException( + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) where TInnerException : Exception { @@ -120,7 +121,8 @@ public virtual ExceptionAssertions WithInnerException /// Zero or more objects to format using the placeholders in . /// - public ExceptionAssertions WithInnerException(Type innerException, string because = "", + public ExceptionAssertions WithInnerException(Type innerException, + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { Guard.ThrowIfArgumentIsNull(innerException); @@ -139,7 +141,8 @@ public ExceptionAssertions WithInnerException(Type innerException, st /// /// Zero or more objects to format using the placeholders in . /// - public virtual ExceptionAssertions WithInnerExceptionExactly(string because = "", + public virtual ExceptionAssertions WithInnerExceptionExactly( + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) where TInnerException : Exception { @@ -158,7 +161,8 @@ public virtual ExceptionAssertions WithInnerExceptionExactly /// Zero or more objects to format using the placeholders in . /// - public ExceptionAssertions WithInnerExceptionExactly(Type innerException, string because = "", + public ExceptionAssertions WithInnerExceptionExactly(Type innerException, + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { Guard.ThrowIfArgumentIsNull(innerException); @@ -181,7 +185,7 @@ public ExceptionAssertions WithInnerExceptionExactly(Type innerExcept /// /// is . public ExceptionAssertions Where(Expression> exceptionExpression, - string because = "", params object[] becauseArgs) + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { Guard.ThrowIfArgumentIsNull(exceptionExpression); @@ -197,7 +201,8 @@ public ExceptionAssertions Where(Expression> return this; } - private IEnumerable AssertInnerExceptionExactly(Type innerException, string because = "", + private IEnumerable AssertInnerExceptionExactly(Type innerException, + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { assertionChain @@ -217,7 +222,8 @@ private IEnumerable AssertInnerExceptionExactly(Type innerException, return expectedExceptions; } - private IEnumerable AssertInnerExceptions(Type innerException, string because = "", + private IEnumerable AssertInnerExceptions(Type innerException, + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { assertionChain @@ -262,7 +268,8 @@ private static string BuildExceptionsString(IEnumerable exceptions) "\t" + Formatter.ToString(exception))); } - private void AssertExceptionMessage(IEnumerable messages, string expectation, string because, params object[] becauseArgs) + private void AssertExceptionMessage(IEnumerable messages, string expectation, + [StringSyntax("CompositeFormat")] string because, params object[] becauseArgs) { var results = new AssertionResultSet(); diff --git a/Src/FluentAssertions/Specialized/FunctionAssertions.cs b/Src/FluentAssertions/Specialized/FunctionAssertions.cs index 0219cab38f..99d5561c30 100644 --- a/Src/FluentAssertions/Specialized/FunctionAssertions.cs +++ b/Src/FluentAssertions/Specialized/FunctionAssertions.cs @@ -113,7 +113,7 @@ public AndWhichConstraint, T> NotThrowAfter(TimeSpan waitT } internal TResult NotThrowAfter(Func subject, IClock clock, TimeSpan waitTime, TimeSpan pollInterval, - string because, object[] becauseArgs) + [StringSyntax("CompositeFormat")] string because, object[] becauseArgs) { Guard.ThrowIfArgumentIsNegative(waitTime); Guard.ThrowIfArgumentIsNegative(pollInterval); diff --git a/Src/FluentAssertions/Streams/BufferedStreamAssertions.cs b/Src/FluentAssertions/Streams/BufferedStreamAssertions.cs index 0233d67c38..58b5d20955 100644 --- a/Src/FluentAssertions/Streams/BufferedStreamAssertions.cs +++ b/Src/FluentAssertions/Streams/BufferedStreamAssertions.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using FluentAssertions.Execution; @@ -41,7 +42,8 @@ public BufferedStreamAssertions(BufferedStream stream, AssertionChain assertionC /// /// Zero or more objects to format using the placeholders in . /// - public AndConstraint HaveBufferSize(int expected, string because = "", params object[] becauseArgs) + public AndConstraint HaveBufferSize(int expected, + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { assertionChain .BecauseOf(because, becauseArgs) @@ -72,7 +74,8 @@ public AndConstraint HaveBufferSize(int expected, string because = /// /// Zero or more objects to format using the placeholders in . /// - public AndConstraint NotHaveBufferSize(int unexpected, string because = "", params object[] becauseArgs) + public AndConstraint NotHaveBufferSize(int unexpected, + [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { assertionChain .BecauseOf(because, becauseArgs) diff --git a/Src/FluentAssertions/Xml/Equivalency/XmlReaderValidator.cs b/Src/FluentAssertions/Xml/Equivalency/XmlReaderValidator.cs index b82b4bba6f..ad676f998f 100644 --- a/Src/FluentAssertions/Xml/Equivalency/XmlReaderValidator.cs +++ b/Src/FluentAssertions/Xml/Equivalency/XmlReaderValidator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Xml; using FluentAssertions.Execution; @@ -15,7 +16,8 @@ internal class XmlReaderValidator private XmlIterator expectationIterator; private Node currentNode = Node.CreateRoot(); - public XmlReaderValidator(AssertionChain assertionChain, XmlReader subjectReader, XmlReader expectationReader, string because, object[] becauseArgs) + public XmlReaderValidator(AssertionChain assertionChain, XmlReader subjectReader, XmlReader expectationReader, + [StringSyntax("CompositeFormat")] string because, object[] becauseArgs) { this.assertionChain = assertionChain; assertionChain.BecauseOf(because, becauseArgs); From 7d18305efdf6074527bb62d9e6567af1bfe88fd5 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 23 Feb 2025 18:58:00 +0100 Subject: [PATCH 2/9] Adjust incorrect explaining comment about regex --- Src/FluentAssertions/Common/StringExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/FluentAssertions/Common/StringExtensions.cs b/Src/FluentAssertions/Common/StringExtensions.cs index 9730aaf3b9..cce0ca4833 100644 --- a/Src/FluentAssertions/Common/StringExtensions.cs +++ b/Src/FluentAssertions/Common/StringExtensions.cs @@ -120,7 +120,7 @@ public static string RemoveNewlineStyle(this string @this) public static string RemoveTrailingWhitespaceFromLines(this string input) { - // This regex matches whitespace characters (\s) that are followed by a line ending (\r?\n) + // This regex matches space (' ') and tab ('\t') characters followed by a line ending ('\r\n' or '\n') return Regex.Replace(input, @"[ \t]+(?=\r?\n)", string.Empty); } From 6751587db92a83805fbc35600bd9314022179e6a Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 23 Feb 2025 19:18:23 +0100 Subject: [PATCH 3/9] Remove `|` from regex character classes `|` means the literal `|` and not alternation. I.e. `[a|b]` means "`a`, `|` or `b`". On the other hand in groups `(a|b)` means "either `a` or `b`" --- Src/FluentAssertions/Execution/FailureMessageFormatter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Src/FluentAssertions/Execution/FailureMessageFormatter.cs b/Src/FluentAssertions/Execution/FailureMessageFormatter.cs index 3559ea97b6..9fc73c1682 100644 --- a/Src/FluentAssertions/Execution/FailureMessageFormatter.cs +++ b/Src/FluentAssertions/Execution/FailureMessageFormatter.cs @@ -94,7 +94,7 @@ public string Format(string message, object[] messageArgs) private static string SubstituteIdentifier(string message, string identifier, string fallbackIdentifier) { - const string pattern = @"(?:\s|^)\{context(?:\:(?[a-z|A-Z|\s]+))?\}"; + const string pattern = @"(?:\s|^)\{context(?:\:(?[a-zA-Z\s]+))?\}"; message = Regex.Replace(message, pattern, match => { @@ -125,7 +125,7 @@ private static string SubstituteIdentifier(string message, string identifier, st private static string SubstituteContextualTags(string message, ContextDataDictionary contextData) { - const string pattern = @"(?[a-z|A-Z]+)(?:\:(?[a-z|A-Z|\s]+))?\}(?!\})"; + const string pattern = @"(?[a-zA-Z]+)(?:\:(?[a-zA-Z\s]+))?\}(?!\})"; return Regex.Replace(message, pattern, match => { From c53c9eb2064fc9d55744670b4e9bf218e34fbf76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:55:52 +0000 Subject: [PATCH 4/9] Bump cspell from 8.17.3 to 8.17.5 Bumps [cspell](https://github.com/streetsidesoftware/cspell/tree/HEAD/packages/cspell) from 8.17.3 to 8.17.5. - [Release notes](https://github.com/streetsidesoftware/cspell/releases) - [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/packages/cspell/CHANGELOG.md) - [Commits](https://github.com/streetsidesoftware/cspell/commits/v8.17.5/packages/cspell) --- updated-dependencies: - dependency-name: cspell dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 293 ++++++++++++++++++++++++---------------------- package.json | 2 +- 2 files changed, 151 insertions(+), 144 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2d9c68502b..9b66594f76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,21 +7,21 @@ "": { "version": "1.0.1", "dependencies": { - "cspell": "^8.17.3" + "cspell": "^8.17.5" } }, "node_modules/@cspell/cspell-bundled-dicts": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.17.3.tgz", - "integrity": "sha512-6uOF726o3JnExAUKM20OJJXZo+Qf9Jt64nkVwnVXx7Upqr5I9Pb1npYPEAIpUA03SnWYmKwUIqhAmkwrN+bLPA==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.17.5.tgz", + "integrity": "sha512-b/Ntabar+g4gsRNwOct909cvatO/auHhNvBzJZfyFQzryI1nqHMaSFuDsrrtzbhQkGJ4GiMAKCXZC2EOdHMgmw==", "license": "MIT", "dependencies": { "@cspell/dict-ada": "^4.1.0", "@cspell/dict-al": "^1.1.0", "@cspell/dict-aws": "^4.0.9", "@cspell/dict-bash": "^4.2.0", - "@cspell/dict-companies": "^3.1.13", - "@cspell/dict-cpp": "^6.0.3", + "@cspell/dict-companies": "^3.1.14", + "@cspell/dict-cpp": "^6.0.4", "@cspell/dict-cryptocurrencies": "^5.0.4", "@cspell/dict-csharp": "^4.0.6", "@cspell/dict-css": "^4.0.17", @@ -31,14 +31,14 @@ "@cspell/dict-docker": "^1.1.12", "@cspell/dict-dotnet": "^5.0.9", "@cspell/dict-elixir": "^4.0.7", - "@cspell/dict-en_us": "^4.3.30", + "@cspell/dict-en_us": "^4.3.33", "@cspell/dict-en-common-misspellings": "^2.0.9", "@cspell/dict-en-gb": "1.1.33", - "@cspell/dict-filetypes": "^3.0.10", + "@cspell/dict-filetypes": "^3.0.11", "@cspell/dict-flutter": "^1.1.0", "@cspell/dict-fonts": "^4.0.4", "@cspell/dict-fsharp": "^1.1.0", - "@cspell/dict-fullstack": "^3.2.3", + "@cspell/dict-fullstack": "^3.2.5", "@cspell/dict-gaming-terms": "^1.1.0", "@cspell/dict-git": "^3.0.4", "@cspell/dict-golang": "^6.0.18", @@ -57,7 +57,7 @@ "@cspell/dict-markdown": "^2.0.9", "@cspell/dict-monkeyc": "^1.0.10", "@cspell/dict-node": "^5.0.6", - "@cspell/dict-npm": "^5.1.24", + "@cspell/dict-npm": "^5.1.27", "@cspell/dict-php": "^4.0.14", "@cspell/dict-powershell": "^5.0.14", "@cspell/dict-public-licenses": "^2.0.13", @@ -67,7 +67,7 @@ "@cspell/dict-rust": "^4.0.11", "@cspell/dict-scala": "^5.0.7", "@cspell/dict-shell": "^1.1.0", - "@cspell/dict-software-terms": "^4.2.4", + "@cspell/dict-software-terms": "^4.2.5", "@cspell/dict-sql": "^2.2.0", "@cspell/dict-svelte": "^1.0.6", "@cspell/dict-swift": "^2.0.5", @@ -80,30 +80,30 @@ } }, "node_modules/@cspell/cspell-json-reporter": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.17.3.tgz", - "integrity": "sha512-RWSfyHOin/d9CqLjz00JMvPkag3yUSsQZr6G9BnCT5cMEO/ws8wQZzA54CNj/LAOccbknTX65SSroPPAtxs56w==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.17.5.tgz", + "integrity": "sha512-+eVFCdnda74Frv8hguHYwDtxvqDuJJ/luFRl4dC5oknPMRab0JCHM1DDYjp3NzsehTex0HmcxplxqVW6QoDosg==", "license": "MIT", "dependencies": { - "@cspell/cspell-types": "8.17.3" + "@cspell/cspell-types": "8.17.5" }, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-pipe": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-8.17.3.tgz", - "integrity": "sha512-DqqSWKt9NLWPGloYxZTpzUhgdW8ObMkZmOOF6TyqpJ4IbckEct8ULgskNorTNRlmmjLniaNgvg6JSHuYO3Urxw==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-8.17.5.tgz", + "integrity": "sha512-VOIfFdIo3FYQFcSpIyGkqHupOx0LgfBrWs79IKnTT1II27VUHPF+0oGq0WWf4c2Zpd8tzdHvS3IUhGarWZq69g==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-resolver": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-8.17.3.tgz", - "integrity": "sha512-yQlVaIsWiax6RRuuacZs++kl6Y9rwH9ZkVlsG9fhdeCJ5Xf3WCW+vmX1chzhhKDzRr8CF9fsvb1uagd/5/bBYA==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-8.17.5.tgz", + "integrity": "sha512-5MhYInligPbGctWxoklAKxtg+sxvtJCuRKGSQHHA0JlCOLSsducypl780P6zvpjLK59XmdfC+wtFONxSmRbsuA==", "license": "MIT", "dependencies": { "global-directory": "^4.0.1" @@ -113,18 +113,18 @@ } }, "node_modules/@cspell/cspell-service-bus": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-8.17.3.tgz", - "integrity": "sha512-CC3nob/Kbuesz5WTW+LjAHnDFXJrA49pW5ckmbufJxNnoAk7EJez/qr7/ELMTf6Fl3A5xZ776Lhq7738Hy/fmQ==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-8.17.5.tgz", + "integrity": "sha512-Ur3IK0R92G/2J6roopG9cU/EhoYAMOx2um7KYlq93cdrly8RBAK2NCcGCL7DbjQB6C9RYEAV60ueMUnQ45RrCQ==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-types": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-8.17.3.tgz", - "integrity": "sha512-ozgeuSioX9z2wtlargfgdw3LKwDFAfm8gxu+xwNREvXiLsevb+lb7ZlY5/ay+MahqR5Hfs7XzYzBLTKL/ldn9g==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-8.17.5.tgz", + "integrity": "sha512-91y2+0teunRSRZj940ORDA3kdjyenrUiM+4j6nQQH24sAIAJdRmQl2LG3eUTmeaSReJGkZIpnToQ6DyU5cC88Q==", "license": "MIT", "engines": { "node": ">=18" @@ -158,15 +158,15 @@ } }, "node_modules/@cspell/dict-companies": { - "version": "3.1.13", - "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.1.13.tgz", - "integrity": "sha512-EAaFMxnSG4eQKup9D81EnWAYIzorLWG7b7Zzf+Suu0bVeFBpCYESss/EWtnmb5ZZNfKAGxtoMqfL3vRfyJERIQ==", + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.1.14.tgz", + "integrity": "sha512-iqo1Ce4L7h0l0GFSicm2wCLtfuymwkvgFGhmu9UHyuIcTbdFkDErH+m6lH3Ed+QuskJlpQ9dM7puMIGqUlVERw==", "license": "MIT" }, "node_modules/@cspell/dict-cpp": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-6.0.3.tgz", - "integrity": "sha512-OFrVXdxCeGKnon36Pe3yFjBuY4kzzEwWFf3vDz+cJTodZDkjFkBifQeTtt5YfimgF8cfAJZXkBCsxjipAgmAiw==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-6.0.4.tgz", + "integrity": "sha512-IvXx3TlM+OL0CFriapk7ZHmeY89dSSdo/BZ3DGf+WUS+BWd64H+z/xr3xkkqY0Eu6MV/vdzNfkLm5zl45FDMGg==", "license": "MIT" }, "node_modules/@cspell/dict-cryptocurrencies": { @@ -224,9 +224,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-en_us": { - "version": "4.3.30", - "resolved": "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.3.30.tgz", - "integrity": "sha512-p0G5fByj5fUnMyFUlkN3kaqE3nuQkqpYV47Gn9n8k2TszsdLY55xj9UoFE4YIcjOiyU1bR/YDJ5daiPMYXTJ/A==", + "version": "4.3.33", + "resolved": "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.3.33.tgz", + "integrity": "sha512-HniqQjzPVn24NEkHooBIw1cH+iO3AKMA9oDTwazUYQP1/ldqXsz6ce4+fdHia2nqypmic/lHVkQgIVhP48q/sA==", "license": "MIT" }, "node_modules/@cspell/dict-en-common-misspellings": { @@ -242,9 +242,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-filetypes": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-3.0.10.tgz", - "integrity": "sha512-JEN3627joBVtpa1yfkdN9vz1Z129PoKGHBKjXCEziJvf2Zt1LeULWYYYg/O6pzRR4yzRa5YbXDTuyrN7vX7DFg==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-3.0.11.tgz", + "integrity": "sha512-bBtCHZLo7MiSRUqx5KEiPdGOmXIlDGY+L7SJEtRWZENpAKE+96rT7hj+TUUYWBbCzheqHr0OXZJFEKDgsG/uZg==", "license": "MIT" }, "node_modules/@cspell/dict-flutter": { @@ -266,9 +266,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-fullstack": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@cspell/dict-fullstack/-/dict-fullstack-3.2.3.tgz", - "integrity": "sha512-62PbndIyQPH11mAv0PyiyT0vbwD0AXEocPpHlCHzfb5v9SspzCCbzQ/LIBiFmyRa+q5LMW35CnSVu6OXdT+LKg==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@cspell/dict-fullstack/-/dict-fullstack-3.2.5.tgz", + "integrity": "sha512-XNmNdovPUS9Vc2JvfBscy8zZfwyxR11sB4fxU2lXh7LzUvOn2/OkKAzj41JTdiWfVnJ/yvsRkspe+b7kr+DIQw==", "license": "MIT" }, "node_modules/@cspell/dict-gaming-terms": { @@ -386,9 +386,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-npm": { - "version": "5.1.24", - "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.1.24.tgz", - "integrity": "sha512-yAyyHetElLR236sqWQkBtiLbzCGexV5zzLMHyQPptKQQK88BTQR5f9wXW2EtSgJw/4gUchpSWQWxMlkIfK/iQQ==", + "version": "5.1.27", + "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.1.27.tgz", + "integrity": "sha512-LGss1yrjhxSmxL4VfMC+UBDMVHfqHudgC7b39M74EVys+nNC4/lqDHacb6Aw7i6aUn9mzdNIkdTTD+LdDcHvPA==", "license": "MIT" }, "node_modules/@cspell/dict-php": { @@ -449,9 +449,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-software-terms": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-4.2.4.tgz", - "integrity": "sha512-GRkuaFfjFHPYynyRMuisKyE3gRiVK0REClRWfnH9+5iCs5TKDURsMpWJGNsgQ6N5jAKKrtWXVKjepkDHjMldjQ==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-4.2.5.tgz", + "integrity": "sha512-CaRzkWti3AgcXoxuRcMijaNG7YUk/MH1rHjB8VX34v3UdCxXXeqvRyElRKnxhFeVLB/robb2UdShqh/CpskxRg==", "license": "MIT" }, "node_modules/@cspell/dict-sql": { @@ -491,12 +491,12 @@ "license": "MIT" }, "node_modules/@cspell/dynamic-import": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-8.17.3.tgz", - "integrity": "sha512-Kg6IJhGHPv+9OxpxaXUpcqgnHEOhMLRWHLyx7FADZ+CJyO4AVeWQfhpTRM6KXhzIl7dPlLG1g8JAQxaoy88KTw==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-8.17.5.tgz", + "integrity": "sha512-tY+cVkRou+0VKvH+K1NXv8/R7mOlW3BDGSs9fcgvhatj0m00Yf8blFC7tE4VVI9Qh2bkC/KDFqM24IqZbuwXUQ==", "license": "MIT", "dependencies": { - "@cspell/url": "8.17.3", + "@cspell/url": "8.17.5", "import-meta-resolve": "^4.1.0" }, "engines": { @@ -504,27 +504,27 @@ } }, "node_modules/@cspell/filetypes": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@cspell/filetypes/-/filetypes-8.17.3.tgz", - "integrity": "sha512-UFqRmJPccOSo+RYP/jZ4cr0s7ni37GrvnNAg1H/qIIxfmBYsexTAmsNzMqxp1M31NeI1Cx3LL7PspPMT0ms+7w==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/@cspell/filetypes/-/filetypes-8.17.5.tgz", + "integrity": "sha512-Fj6py2Rl+FEnMiXhRQUM1A5QmyeCLxi6dY/vQ0qfH6tp6KSaBiaC8wuPUKhr8hKyTd3+8lkUbobDhUf6xtMEXg==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@cspell/strong-weak-map": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-8.17.3.tgz", - "integrity": "sha512-l/CaFc3CITI/dC+whEBZ05Om0KXR3V2whhVOWOBPIqA5lCjWAyvWWvmFD+CxWd0Hs6Qcb/YDnMyJW14aioXN4g==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-8.17.5.tgz", + "integrity": "sha512-Z4eo+rZJr1086wZWycBiIG/n7gGvVoqn28I7ZicS8xedRYu/4yp2loHgLn4NpxG3e46+dNWs4La6vinod+UydQ==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@cspell/url": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@cspell/url/-/url-8.17.3.tgz", - "integrity": "sha512-gcsCz8g0qY94C8RXiAlUH/89n84Q9RSptP91XrvnLOT+Xva9Aibd7ywd5k9ameuf8Nagyl0ezB1MInZ30S9SRw==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/@cspell/url/-/url-8.17.5.tgz", + "integrity": "sha512-GNQqST7zI85dAFVyao6oiTeg5rNhO9FH1ZAd397qQhvwfxrrniNfuoewu8gPXyP0R4XBiiaCwhBL7w9S/F5guw==", "license": "MIT", "engines": { "node": ">=18.0" @@ -631,29 +631,29 @@ "license": "MIT" }, "node_modules/cspell": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/cspell/-/cspell-8.17.3.tgz", - "integrity": "sha512-fBZg674Dir9y/FWMwm2JyixM/1eB2vnqHJjRxOgGS/ZiZ3QdQ3LkK02Aqvlni8ffWYDZnYnYY9rfWmql9bb42w==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/cspell/-/cspell-8.17.5.tgz", + "integrity": "sha512-l3Cfp87d7Yrodem675irdxV6+7+OsdR+jNwYHe33Dgnd6ePEfooYrvmfGdXF9rlQrNLUQp/HqYgHJzSq19UEsg==", "license": "MIT", "dependencies": { - "@cspell/cspell-json-reporter": "8.17.3", - "@cspell/cspell-pipe": "8.17.3", - "@cspell/cspell-types": "8.17.3", - "@cspell/dynamic-import": "8.17.3", - "@cspell/url": "8.17.3", + "@cspell/cspell-json-reporter": "8.17.5", + "@cspell/cspell-pipe": "8.17.5", + "@cspell/cspell-types": "8.17.5", + "@cspell/dynamic-import": "8.17.5", + "@cspell/url": "8.17.5", "chalk": "^5.4.1", "chalk-template": "^1.1.0", "commander": "^13.1.0", - "cspell-dictionary": "8.17.3", - "cspell-gitignore": "8.17.3", - "cspell-glob": "8.17.3", - "cspell-io": "8.17.3", - "cspell-lib": "8.17.3", + "cspell-dictionary": "8.17.5", + "cspell-gitignore": "8.17.5", + "cspell-glob": "8.17.5", + "cspell-io": "8.17.5", + "cspell-lib": "8.17.5", "fast-json-stable-stringify": "^2.1.0", "file-entry-cache": "^9.1.0", "get-stdin": "^9.0.0", - "semver": "^7.6.3", - "tinyglobby": "^0.2.10" + "semver": "^7.7.1", + "tinyglobby": "^0.2.12" }, "bin": { "cspell": "bin.mjs", @@ -667,12 +667,12 @@ } }, "node_modules/cspell-config-lib": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-8.17.3.tgz", - "integrity": "sha512-+N32Q6xck3D2RqZIFwq8s0TnzHYMpyh4bgNtYqW5DIP3TLDiA4/MJGjwmLKAg/s9dkre6n8/++vVli3MZAOhIg==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-8.17.5.tgz", + "integrity": "sha512-XDc+UJO5RZ9S9e2Ajz332XjT7dv6Og2UqCiSnAlvHt7t/MacLHSPARZFIivheObNkWZ7E1iWI681RxKoH4o40w==", "license": "MIT", "dependencies": { - "@cspell/cspell-types": "8.17.3", + "@cspell/cspell-types": "8.17.5", "comment-json": "^4.2.5", "yaml": "^2.7.0" }, @@ -681,14 +681,14 @@ } }, "node_modules/cspell-dictionary": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-8.17.3.tgz", - "integrity": "sha512-89I/lpQKdkX17RCFrUIJnc70Rjfpup/o+ynHZen0hUxGTfLsEJPrK6H2oGvic3Yrv5q8IOtwM1p8vqPqBkBheA==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-8.17.5.tgz", + "integrity": "sha512-O/Uuhv1RuDu+5WYQml0surudweaTvr+2YJSmPSdlihByUSiogCbpGqwrRow7wQv/C5p1W1FlFjotvUfoR0fxHA==", "license": "MIT", "dependencies": { - "@cspell/cspell-pipe": "8.17.3", - "@cspell/cspell-types": "8.17.3", - "cspell-trie-lib": "8.17.3", + "@cspell/cspell-pipe": "8.17.5", + "@cspell/cspell-types": "8.17.5", + "cspell-trie-lib": "8.17.5", "fast-equals": "^5.2.2" }, "engines": { @@ -696,14 +696,14 @@ } }, "node_modules/cspell-gitignore": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-8.17.3.tgz", - "integrity": "sha512-rQamjb8R+Nwib/Bpcgf+xv5IdsOHgbP+fe4hCgv0jjgUPkeOR2c4dGwc0WS+2UkJbc+wQohpzBGDLRYGSB/hQw==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-8.17.5.tgz", + "integrity": "sha512-I27fgOUZzH14jeIYo65LooB60fZ42f6OJL1lOR9Mk6IrIlDyUtzherGR+xx5KshK2katYkX42Qu4zsVYM6VFPA==", "license": "MIT", "dependencies": { - "@cspell/url": "8.17.3", - "cspell-glob": "8.17.3", - "cspell-io": "8.17.3", + "@cspell/url": "8.17.5", + "cspell-glob": "8.17.5", + "cspell-io": "8.17.5", "find-up-simple": "^1.0.0" }, "bin": { @@ -714,12 +714,12 @@ } }, "node_modules/cspell-glob": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-8.17.3.tgz", - "integrity": "sha512-0ov9A0E6OuOO7KOxlGCxJ09LR/ubZ6xcGwWc5bu+jp/8onUowQfe+9vZdznj/o8/vcf5JkDzyhRSBsdhWKqoAg==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-8.17.5.tgz", + "integrity": "sha512-OXquou7UykInlGV5et5lNKYYrW0dwa28aEF995x1ocANND7o0bbHmFlbgyci/Lp4uFQai8sifmfFJbuIg2IC/A==", "license": "MIT", "dependencies": { - "@cspell/url": "8.17.3", + "@cspell/url": "8.17.5", "micromatch": "^4.0.8" }, "engines": { @@ -727,13 +727,13 @@ } }, "node_modules/cspell-grammar": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-8.17.3.tgz", - "integrity": "sha512-wfjkkvHthnKJtEaTgx3cPUPquGRXfgXSCwvMJaDyUi36KBlopXX38PejBTdmuqrvp7bINLSuHErml9wAfL5Fxw==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-8.17.5.tgz", + "integrity": "sha512-st2n+FVw25MvMbsGb3TeJNRr6Oih4g14rjOd/UJN0qn+ceH360SAShUFqSd4kHHu2ADazI/TESFU6FRtMTPNOg==", "license": "MIT", "dependencies": { - "@cspell/cspell-pipe": "8.17.3", - "@cspell/cspell-types": "8.17.3" + "@cspell/cspell-pipe": "8.17.5", + "@cspell/cspell-types": "8.17.5" }, "bin": { "cspell-grammar": "bin.mjs" @@ -743,47 +743,47 @@ } }, "node_modules/cspell-io": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-8.17.3.tgz", - "integrity": "sha512-NwEVb3Kr8loV1C8Stz9QSMgUrBkxqf2s7A9H2/RBnfvQBt9CWZS6NgoNxTPwHj3h1sUNl9reDkMQQzkKtgWGBQ==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-8.17.5.tgz", + "integrity": "sha512-oevM/8l0s6nc1NCYPqNFumrW50QSHoa6wqUT8cWs09gtZdE2AWG0U6bIE8ZEVz6e6FxS+6IenGKTdUUwP0+3fg==", "license": "MIT", "dependencies": { - "@cspell/cspell-service-bus": "8.17.3", - "@cspell/url": "8.17.3" + "@cspell/cspell-service-bus": "8.17.5", + "@cspell/url": "8.17.5" }, "engines": { "node": ">=18" } }, "node_modules/cspell-lib": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-8.17.3.tgz", - "integrity": "sha512-KpwYIj8HwFyTzCCQcyezlmomvyNfPwZQmqTh4V126sFvf9HLoMdfyq8KYDZmZ//4HzwrF/ufJOF3CpuVUiJHfA==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-8.17.5.tgz", + "integrity": "sha512-S3KuOrcST1d2BYmTXA+hnbRdho5n3w5GUvEaCx3QZQBwAPfLpAwJbe2yig1TxBpyEJ5LqP02i/mDg1pUCOP0hQ==", "license": "MIT", "dependencies": { - "@cspell/cspell-bundled-dicts": "8.17.3", - "@cspell/cspell-pipe": "8.17.3", - "@cspell/cspell-resolver": "8.17.3", - "@cspell/cspell-types": "8.17.3", - "@cspell/dynamic-import": "8.17.3", - "@cspell/filetypes": "8.17.3", - "@cspell/strong-weak-map": "8.17.3", - "@cspell/url": "8.17.3", + "@cspell/cspell-bundled-dicts": "8.17.5", + "@cspell/cspell-pipe": "8.17.5", + "@cspell/cspell-resolver": "8.17.5", + "@cspell/cspell-types": "8.17.5", + "@cspell/dynamic-import": "8.17.5", + "@cspell/filetypes": "8.17.5", + "@cspell/strong-weak-map": "8.17.5", + "@cspell/url": "8.17.5", "clear-module": "^4.1.2", "comment-json": "^4.2.5", - "cspell-config-lib": "8.17.3", - "cspell-dictionary": "8.17.3", - "cspell-glob": "8.17.3", - "cspell-grammar": "8.17.3", - "cspell-io": "8.17.3", - "cspell-trie-lib": "8.17.3", + "cspell-config-lib": "8.17.5", + "cspell-dictionary": "8.17.5", + "cspell-glob": "8.17.5", + "cspell-grammar": "8.17.5", + "cspell-io": "8.17.5", + "cspell-trie-lib": "8.17.5", "env-paths": "^3.0.0", "fast-equals": "^5.2.2", "gensequence": "^7.0.0", - "import-fresh": "^3.3.0", + "import-fresh": "^3.3.1", "resolve-from": "^5.0.0", "vscode-languageserver-textdocument": "^1.0.12", - "vscode-uri": "^3.0.8", + "vscode-uri": "^3.1.0", "xdg-basedir": "^5.1.0" }, "engines": { @@ -791,13 +791,13 @@ } }, "node_modules/cspell-trie-lib": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-8.17.3.tgz", - "integrity": "sha512-6LE5BeT2Rwv0bkQckpxX0K1fnFCWfeJ8zVPFtYOaix0trtqj0VNuwWzYDnxyW+OwMioCH29yRAMODa+JDFfUrA==", + "version": "8.17.5", + "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-8.17.5.tgz", + "integrity": "sha512-9hjI3nRQxtGEua6CgnLbK3sGHLx9dXR/BHwI/csRL4dN5GGRkE5X3CCoy1RJVL7iGFLIzi43+L10xeFRmWniKw==", "license": "MIT", "dependencies": { - "@cspell/cspell-pipe": "8.17.3", - "@cspell/cspell-types": "8.17.3", + "@cspell/cspell-pipe": "8.17.5", + "@cspell/cspell-types": "8.17.5", "gensequence": "^7.0.0" }, "engines": { @@ -1075,9 +1075,10 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1086,21 +1087,26 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", - "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", + "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "license": "MIT", "dependencies": { - "fdir": "^6.4.2", + "fdir": "^6.4.3", "picomatch": "^4.0.2" }, "engines": { "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -1114,6 +1120,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -1140,9 +1147,9 @@ "license": "MIT" }, "node_modules/vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", "license": "MIT" }, "node_modules/xdg-basedir": { diff --git a/package.json b/package.json index 9cda852c7c..c2002c9c6b 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,6 @@ "cspell": "cspell --config ./cSpell.json ./docs/**/*.md --no-progress --no-summary" }, "dependencies": { - "cspell": "^8.17.3" + "cspell": "^8.17.5" } } From 669f817c4420d2824e2238a2f530fae57f6b740d Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Thu, 27 Feb 2025 18:07:36 +0100 Subject: [PATCH 5/9] Handle missing caller identifier Fixes #3031 Regression from #3000 where previously `getCallerIdentifier() + callerPostfix` would return an empty string. --- .../Execution/SubjectIdentificationBuilder.cs | 2 +- .../Execution/AssertionChainSpecs.Chaining.cs | 14 ++++++++++++++ docs/_pages/releases.md | 6 ++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Src/FluentAssertions/Execution/SubjectIdentificationBuilder.cs b/Src/FluentAssertions/Execution/SubjectIdentificationBuilder.cs index b30f391c9f..8d8371000d 100644 --- a/Src/FluentAssertions/Execution/SubjectIdentificationBuilder.cs +++ b/Src/FluentAssertions/Execution/SubjectIdentificationBuilder.cs @@ -83,7 +83,7 @@ public string Build() if (scopeName is null) { - return callerIdentifier; + return callerIdentifier ?? ""; } else if (callerIdentifier is null) { diff --git a/Tests/FluentAssertions.Specs/Execution/AssertionChainSpecs.Chaining.cs b/Tests/FluentAssertions.Specs/Execution/AssertionChainSpecs.Chaining.cs index 11e959587d..45c53e0570 100644 --- a/Tests/FluentAssertions.Specs/Execution/AssertionChainSpecs.Chaining.cs +++ b/Tests/FluentAssertions.Specs/Execution/AssertionChainSpecs.Chaining.cs @@ -578,6 +578,20 @@ public void Discard_a_scope_after_continuing_chained_assertion() Assert.Contains("First \"assertion\"", failures); } + [Fact] + public void Returns_an_empty_identification_when_neither_scope_name_nor_caller_identifier_are_available() + { + // Arrange + var assertionChain = AssertionChain.GetOrCreate(); + assertionChain.OverrideCallerIdentifier(() => null); + + // Act + string identification = assertionChain.CallerIdentifier; + + // Assert + identification.Should().BeEmpty(); + } + // [Fact] // public void Get_info_about_line_breaks_from_parent_scope_after_continuing_chained_assertion() // { diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index 643d9d1972..e37c2fcf8f 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -7,6 +7,12 @@ sidebar: nav: "sidebar" --- +## 8.1.2 + +## Fixes + +* Fixed a regression from 8.1.0 where a `NullReferenceException` was thrown during subject identification - [#3036](https://github.com/fluentassertions/fluentassertions/pull/3036 + ## 8.1.1 ## Fixes From fa6eae15d0e318eb9a5f31706d80249623dd8d8c Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 2 Mar 2025 15:30:27 +0100 Subject: [PATCH 6/9] Avoid regex for checking if a path is nested --- .../Equivalency/Matching/MappedMemberMatchingRule.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Src/FluentAssertions/Equivalency/Matching/MappedMemberMatchingRule.cs b/Src/FluentAssertions/Equivalency/Matching/MappedMemberMatchingRule.cs index 0eace99527..6730620579 100644 --- a/Src/FluentAssertions/Equivalency/Matching/MappedMemberMatchingRule.cs +++ b/Src/FluentAssertions/Equivalency/Matching/MappedMemberMatchingRule.cs @@ -1,5 +1,4 @@ using System; -using System.Text.RegularExpressions; using FluentAssertions.Common; using FluentAssertions.Execution; @@ -16,12 +15,12 @@ internal class MappedMemberMatchingRule : IMemberMatchin public MappedMemberMatchingRule(string expectationMemberName, string subjectMemberName) { - if (Regex.IsMatch(expectationMemberName, @"[\.\[\]]")) + if (IsNestedPath(expectationMemberName)) { throw new ArgumentException("The expectation's member name cannot be a nested path", nameof(expectationMemberName)); } - if (Regex.IsMatch(subjectMemberName, @"[\.\[\]]")) + if (IsNestedPath(subjectMemberName)) { throw new ArgumentException("The subject's member name cannot be a nested path", nameof(subjectMemberName)); } @@ -30,6 +29,9 @@ public MappedMemberMatchingRule(string expectationMemberName, string subjectMemb this.subjectMemberName = subjectMemberName; } + private static bool IsNestedPath(string path) => + path.Contains('.', StringComparison.Ordinal) || path.Contains('[', StringComparison.Ordinal) || path.Contains(']', StringComparison.Ordinal); + public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyOptions options, AssertionChain assertionChain) { if (parent.Type.IsSameOrInherits(typeof(TExpectation)) && subject is TSubject && From 66df7d067960592f8ac4b595afe43b8c6b991c61 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 2 Mar 2025 15:51:43 +0100 Subject: [PATCH 7/9] Use char overload of `string.Replace` --- Src/FluentAssertions/Common/StringExtensions.cs | 2 +- .../Formatting/MultidimensionalArrayFormatterSpecs.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Src/FluentAssertions/Common/StringExtensions.cs b/Src/FluentAssertions/Common/StringExtensions.cs index cce0ca4833..833e4bcad3 100644 --- a/Src/FluentAssertions/Common/StringExtensions.cs +++ b/Src/FluentAssertions/Common/StringExtensions.cs @@ -115,7 +115,7 @@ public static string RemoveNewLines(this string @this) public static string RemoveNewlineStyle(this string @this) { return @this.Replace("\r\n", "\n", StringComparison.Ordinal) - .Replace("\r", "\n", StringComparison.Ordinal); + .Replace('\r', '\n'); } public static string RemoveTrailingWhitespaceFromLines(this string input) diff --git a/Tests/FluentAssertions.Specs/Formatting/MultidimensionalArrayFormatterSpecs.cs b/Tests/FluentAssertions.Specs/Formatting/MultidimensionalArrayFormatterSpecs.cs index c824a0accd..a2dde8f348 100644 --- a/Tests/FluentAssertions.Specs/Formatting/MultidimensionalArrayFormatterSpecs.cs +++ b/Tests/FluentAssertions.Specs/Formatting/MultidimensionalArrayFormatterSpecs.cs @@ -75,6 +75,6 @@ public void When_formatting_a_multi_dimensional_array_with_bounds_it_should_show // Assert result.Should().Match( "{{{'1-5-7', '1-5-8', '1-5-9', '1-5-10'}, {'1-6-7', '1-6-8', '1-6-9', '1-6-10'}, {'1-7-7', '1-7-8', '1-7-9', '1-7-10'}}, {{'2-5-7', '2-5-8', '2-5-9', '2-5-10'}, {'2-6-7', '2-6-8', '2-6-9', '2-6-10'}, {'2-7-7', '2-7-8', '2-7-9', '2-7-10'}}}" - .Replace("'", "\"")); + .Replace('\'', '"')); } } From 0a649cb006744405050f0a72ad48442831b3c8cd Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 2 Mar 2025 16:02:48 +0100 Subject: [PATCH 8/9] Add polyfill for splitting on a single `char` or `string` --- Src/FluentAssertions/Common/MemberPath.cs | 2 +- Src/FluentAssertions/Common/StringExtensions.cs | 2 +- .../Equivalency/Execution/ObjectReference.cs | 2 +- .../Equivalency/Tracing/StringBuilderTraceWriter.cs | 2 +- Src/FluentAssertions/Formatting/XElementValueFormatter.cs | 2 +- Src/FluentAssertions/Polyfill/SystemExtensions.cs | 6 ++++++ 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Src/FluentAssertions/Common/MemberPath.cs b/Src/FluentAssertions/Common/MemberPath.cs index 80a69302a6..5679ad3594 100644 --- a/Src/FluentAssertions/Common/MemberPath.cs +++ b/Src/FluentAssertions/Common/MemberPath.cs @@ -116,7 +116,7 @@ public bool HasSameParentAs(MemberPath path) private string[] Segments => segments ??= dottedPath .Replace("[]", "[*]", StringComparison.Ordinal) - .Split(new[] { '.', '[', ']' }, StringSplitOptions.RemoveEmptyEntries); + .Split(['.', '[', ']'], StringSplitOptions.RemoveEmptyEntries); /// /// Returns a copy of the current object as if it represented an un-indexed item in a collection. diff --git a/Src/FluentAssertions/Common/StringExtensions.cs b/Src/FluentAssertions/Common/StringExtensions.cs index 833e4bcad3..88e164df1a 100644 --- a/Src/FluentAssertions/Common/StringExtensions.cs +++ b/Src/FluentAssertions/Common/StringExtensions.cs @@ -103,7 +103,7 @@ public static string Capitalize(this string @this) public static string IndentLines(this string @this) { return string.Join(Environment.NewLine, - @this.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Select(x => $"\t{x}")); + @this.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).Select(x => $"\t{x}")); } public static string RemoveNewLines(this string @this) diff --git a/Src/FluentAssertions/Equivalency/Execution/ObjectReference.cs b/Src/FluentAssertions/Equivalency/Execution/ObjectReference.cs index e888baa9b2..c0b3fb9fdb 100644 --- a/Src/FluentAssertions/Equivalency/Execution/ObjectReference.cs +++ b/Src/FluentAssertions/Equivalency/Execution/ObjectReference.cs @@ -39,7 +39,7 @@ public override bool Equals(object obj) private string[] GetPathElements() => pathElements ??= path.ToUpperInvariant().Replace("][", "].[", StringComparison.Ordinal) - .Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); + .Split('.', StringSplitOptions.RemoveEmptyEntries); private bool IsParentOrChildOf(ObjectReference other) { diff --git a/Src/FluentAssertions/Equivalency/Tracing/StringBuilderTraceWriter.cs b/Src/FluentAssertions/Equivalency/Tracing/StringBuilderTraceWriter.cs index 9e6fcd72f7..a0752de0e3 100644 --- a/Src/FluentAssertions/Equivalency/Tracing/StringBuilderTraceWriter.cs +++ b/Src/FluentAssertions/Equivalency/Tracing/StringBuilderTraceWriter.cs @@ -28,7 +28,7 @@ public IDisposable AddBlock(string trace) private void WriteLine(string trace) { - foreach (string traceLine in trace.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries)) + foreach (string traceLine in trace.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)) { builder.Append(new string(' ', depth * 2)).AppendLine(traceLine); } diff --git a/Src/FluentAssertions/Formatting/XElementValueFormatter.cs b/Src/FluentAssertions/Formatting/XElementValueFormatter.cs index 0845dc96ed..953fea7476 100644 --- a/Src/FluentAssertions/Formatting/XElementValueFormatter.cs +++ b/Src/FluentAssertions/Formatting/XElementValueFormatter.cs @@ -48,6 +48,6 @@ private static string FormatElementWithChildren(XElement element) private static string[] SplitIntoSeparateLines(XElement element) { string formattedXml = element.ToString(); - return formattedXml.Split([Environment.NewLine], StringSplitOptions.None); + return formattedXml.Split(Environment.NewLine, StringSplitOptions.None); } } diff --git a/Src/FluentAssertions/Polyfill/SystemExtensions.cs b/Src/FluentAssertions/Polyfill/SystemExtensions.cs index 88a5af634d..a725bd4f3c 100644 --- a/Src/FluentAssertions/Polyfill/SystemExtensions.cs +++ b/Src/FluentAssertions/Polyfill/SystemExtensions.cs @@ -23,6 +23,12 @@ public static bool Contains(this string str, char value, StringComparison compar // https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs,1014 public static bool StartsWith(this string str, char value) => str.Length != 0 && str[0] == value; + + public static string[] Split(this string str, char separator, StringSplitOptions options = StringSplitOptions.None) => + str.Split([separator], options); + + public static string[] Split(this string str, string separator, StringSplitOptions options = StringSplitOptions.None) => + str.Split([separator], options); } #endif From 04defbe85c05581c443d819b5d01bf84f0d62222 Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Sun, 9 Feb 2025 20:55:10 +0100 Subject: [PATCH 9/9] Reworked formatting and support multi-dimensional arrays. --- .editorconfig | 8 + Build/_build.v3.ncrunchproject | 5 + FluentAssertions.sln.DotSettings | 3 + FluentAssertions.v3.ncrunchsolution | 8 + Src/FluentAssertions/Formatting/Anchor.cs | 93 ++++ .../Formatting/BuildingLineState.cs | 44 ++ .../Formatting/DefaultValueFormatter.cs | 7 +- .../Formatting/EnumerableValueFormatter.cs | 21 +- .../Formatting/FlushedLineState.cs | 51 ++ .../Formatting/FormattedObjectGraph.cs | 254 ++++----- Src/FluentAssertions/Formatting/ILineState.cs | 26 + Src/FluentAssertions/Formatting/Line.cs | 111 ++++ .../Formatting/LineCollection.cs | 95 ++++ .../Formatting/StringValueFormatter.cs | 9 +- Tests/.editorconfig | 14 + .../Approval.Tests.v3.ncrunchproject | 5 + .../FluentAssertions/net47.verified.txt | 3 +- .../FluentAssertions/net6.0.verified.txt | 3 +- .../netstandard2.0.verified.txt | 3 +- .../netstandard2.1.verified.txt | 3 +- .../Benchmarks.net472.v3.ncrunchproject | 3 + .../Formatting/FormatterSpecs.cs | 516 ++++++++++++------ .../ReferenceTypeAssertionsSpecs.cs | 9 +- .../Xml/XmlNodeFormatterSpecs.cs | 4 +- docs/_pages/releases.md | 6 +- 25 files changed, 961 insertions(+), 343 deletions(-) create mode 100644 Build/_build.v3.ncrunchproject create mode 100644 FluentAssertions.v3.ncrunchsolution create mode 100644 Src/FluentAssertions/Formatting/Anchor.cs create mode 100644 Src/FluentAssertions/Formatting/BuildingLineState.cs create mode 100644 Src/FluentAssertions/Formatting/FlushedLineState.cs create mode 100644 Src/FluentAssertions/Formatting/ILineState.cs create mode 100644 Src/FluentAssertions/Formatting/Line.cs create mode 100644 Src/FluentAssertions/Formatting/LineCollection.cs create mode 100644 Tests/Approval.Tests/Approval.Tests.v3.ncrunchproject create mode 100644 Tests/Benchmarks/Benchmarks.net472.v3.ncrunchproject diff --git a/.editorconfig b/.editorconfig index 341cae8083..1902d22742 100644 --- a/.editorconfig +++ b/.editorconfig @@ -194,6 +194,11 @@ dotnet_diagnostic.IDE0055.severity = error dotnet_diagnostic.CS1574.severity = error # StyleCop + +# Purpose: An opening square bracket within a C# statement is not spaced correctly. +# Reason: Doesn't understand the new collection initializers +dotnet_diagnostic.SA1010.severity = none + # SA1028: Code should not contain trailing whitespace dotnet_diagnostic.SA1028.severity = suggestion # SA1101: Prefix local calls with this @@ -303,6 +308,9 @@ dotnet_diagnostic.AV2305.severity = none # AV2407: Region should be removed dotnet_diagnostic.AV2407.severity = none +# Keep at least a single blank line between members +resharper_csharp_blank_lines_around_single_line_invocable = 1 + # Convert lambda expression to method group resharper_convert_closure_to_method_group_highlighting = none diff --git a/Build/_build.v3.ncrunchproject b/Build/_build.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/Build/_build.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/FluentAssertions.sln.DotSettings b/FluentAssertions.sln.DotSettings index bdc20de29a..14861b12e8 100644 --- a/FluentAssertions.sln.DotSettings +++ b/FluentAssertions.sln.DotSettings @@ -1,4 +1,7 @@  + Default + Inherit + ReturnDefaultValue True True False diff --git a/FluentAssertions.v3.ncrunchsolution b/FluentAssertions.v3.ncrunchsolution new file mode 100644 index 0000000000..fe9b0700d2 --- /dev/null +++ b/FluentAssertions.v3.ncrunchsolution @@ -0,0 +1,8 @@ + + + True + False + True + True + + \ No newline at end of file diff --git a/Src/FluentAssertions/Formatting/Anchor.cs b/Src/FluentAssertions/Formatting/Anchor.cs new file mode 100644 index 0000000000..798b09e035 --- /dev/null +++ b/Src/FluentAssertions/Formatting/Anchor.cs @@ -0,0 +1,93 @@ +namespace FluentAssertions.Formatting; + +/// +/// Represents a point in the formatted object graph where a new fragment or line can be inserted. +/// +internal class Anchor +{ + private readonly FormattedObjectGraph parent; + private readonly int indentation; + private readonly int characterIndex; + private readonly Line line; + private readonly bool lineWasEmptyAtCreation; + + public Anchor(FormattedObjectGraph parent, Line line) + { + indentation = parent.Indentation; + this.parent = parent; + this.line = line; + lineWasEmptyAtCreation = line is null || line.Length == 0; + + // Track the point in the graph where this instance was created. + characterIndex = line?.LengthWithoutOffset ?? 0; + } + + public bool UseLineBreaks { get; set; } + + public void InsertFragment(string fragment) + { + // Insert the fragment to the line and character position the anchor points at. + if (line is null) + { + parent.InsertAtLineStartOrTop(fragment); + } + else + { + line.Insert(characterIndex, fragment); + } + + // If the current line already contained text beyond the anchor point, move that part to the next line. + if (line is not null && !RenderOnSingleLine) + { + parent.SplitLine(line, characterIndex + fragment.Length); + } + } + + public void InsertLineOrFragment(string fragment) + { + if (RenderOnSingleLine) + { + if (line is null) + { + parent.InsertAtLineStartOrTop(fragment); + } + else + { + line.Insert(characterIndex, fragment); + } + } + else + { + string fragmentWithWhitespace = FormattedObjectGraph.MakeWhitespace(indentation) + fragment; + + // If the line was empty when the anchor was created, we can insert the fragment right here. + // But if it wasn't empty, we need to continue the fragment on the next line. + if (lineWasEmptyAtCreation) + { + parent.InsertAtTop(fragmentWithWhitespace); + } + else + { + parent.AddLineAfter(line, fragmentWithWhitespace); + } + } + } + + internal void AddLineOrFragment(string fragment) + { + if (line is null) + { + parent.AddLineOrFragment(fragment); + } + else if (RenderOnSingleLine) + { + line.Append(fragment); + } + else + { + parent.AddLine(fragment); + } + } + + private bool RenderOnSingleLine => !UseLineBreaks && !parent.HasLinesBeyond(line); +} diff --git a/Src/FluentAssertions/Formatting/BuildingLineState.cs b/Src/FluentAssertions/Formatting/BuildingLineState.cs new file mode 100644 index 0000000000..6cfe6c3b0c --- /dev/null +++ b/Src/FluentAssertions/Formatting/BuildingLineState.cs @@ -0,0 +1,44 @@ +using System.Text; + +namespace FluentAssertions.Formatting; + +/// +/// Represents the behavior of when it's still in the building phase and tries +/// to be as efficient as possible by using a . +/// +internal class BuildingLineState : ILineState +{ + private StringBuilder builder = new(); + + public ILineState Flush() + { + var newState = new FlushedLineState(builder.ToString()); + builder = null; + + return newState; + } + + public int Length => builder.Length; + + public void Append(string fragment) + { + builder.Append(fragment); + } + + public void InsertAtStart(string fragment) + { + builder.Insert(0, fragment); + } + + public void InsertAt(int startIndex, string fragment) + { + builder.Insert(startIndex, fragment); + } + + public Line Truncate(int characterIndex, int indentation, int whitespaceOffset) + { + return null; + } + + public string Render() => builder.ToString(); +} diff --git a/Src/FluentAssertions/Formatting/DefaultValueFormatter.cs b/Src/FluentAssertions/Formatting/DefaultValueFormatter.cs index d35e44443c..01ff94aab3 100644 --- a/Src/FluentAssertions/Formatting/DefaultValueFormatter.cs +++ b/Src/FluentAssertions/Formatting/DefaultValueFormatter.cs @@ -76,8 +76,10 @@ private void WriteTypeAndMemberValues(object obj, FormattedObjectGraph formatted private void WriteTypeName(FormattedObjectGraph formattedGraph, Type type) { - var typeName = type.HasFriendlyName() ? TypeDisplayName(type) : string.Empty; - formattedGraph.AddFragment(typeName); + if (type.HasFriendlyName()) + { + formattedGraph.AddFragment(TypeDisplayName(type)); + } } private void WriteTypeValue(object obj, FormattedObjectGraph formattedGraph, FormatChild formatChild, Type type) @@ -89,7 +91,6 @@ private void WriteTypeValue(object obj, FormattedObjectGraph formattedGraph, For } else { - formattedGraph.EnsureInitialNewLine(); formattedGraph.AddLine("{"); WriteMemberValues(obj, members, formattedGraph, formatChild); formattedGraph.AddFragmentOnNewLine("}"); diff --git a/Src/FluentAssertions/Formatting/EnumerableValueFormatter.cs b/Src/FluentAssertions/Formatting/EnumerableValueFormatter.cs index 09d6a25a28..fe15e11746 100644 --- a/Src/FluentAssertions/Formatting/EnumerableValueFormatter.cs +++ b/Src/FluentAssertions/Formatting/EnumerableValueFormatter.cs @@ -33,8 +33,9 @@ public void Format(object value, FormattedObjectGraph formattedGraph, Formatting using var iterator = new Iterator(collection, MaxItems); - var iteratorGraph = formattedGraph.KeepOnSingleLineAsLongAsPossible(); - FormattedObjectGraph.PossibleMultilineFragment separatingCommaGraph = null; + var startingAnchor = formattedGraph.GetAnchor(); + startingAnchor.UseLineBreaks = context.UseLineBreaks; + Anchor commaSeparatorAnchor = null; while (iterator.MoveNext()) { @@ -46,26 +47,26 @@ public void Format(object value, FormattedObjectGraph formattedGraph, Formatting { using IDisposable _ = formattedGraph.WithIndentation(); string moreItemsMessage = value is ICollection c ? $"…{c.Count - MaxItems} more…" : "…more…"; - iteratorGraph.AddLineOrFragment(moreItemsMessage); + formattedGraph.AddLineOrFragment(moreItemsMessage); } - separatingCommaGraph?.InsertLineOrFragment(", "); - separatingCommaGraph = formattedGraph.KeepOnSingleLineAsLongAsPossible(); + commaSeparatorAnchor?.InsertFragment(", "); + commaSeparatorAnchor = formattedGraph.GetAnchor(); - // We cannot know whether or not the enumerable will take up more than one line of - // output until we have formatted the first item. So we format the first item, then + // We cannot know whether the enumerable will take up more than one line of + // output until we have formatted all items. So we format items, then // go back and insert the enumerable's opening brace in the correct place depending // on whether that first item was all on one line or not. if (iterator.IsLast) { - iteratorGraph.AddStartingLineOrFragment("{"); - iteratorGraph.AddLineOrFragment("}"); + startingAnchor.InsertLineOrFragment("{"); + startingAnchor.AddLineOrFragment("}"); } } if (iterator.IsEmpty) { - iteratorGraph.AddFragment("{empty}"); + formattedGraph.AddFragment("{empty}"); } } } diff --git a/Src/FluentAssertions/Formatting/FlushedLineState.cs b/Src/FluentAssertions/Formatting/FlushedLineState.cs new file mode 100644 index 0000000000..4697c7d686 --- /dev/null +++ b/Src/FluentAssertions/Formatting/FlushedLineState.cs @@ -0,0 +1,51 @@ +using System.Text; + +namespace FluentAssertions.Formatting; + +/// +/// Represents the behavior of when most of the appending and inserting +/// has completed, and it no longer needs an internal . +/// +/// +internal class FlushedLineState(string content) : ILineState +{ + private string content = content; + + public ILineState Flush() + { + return this; + } + + public int Length => content.Length; + + public void Append(string fragment) + { + content += fragment; + } + + public void InsertAtStart(string fragment) + { + content = fragment + content; + } + + public void InsertAt(int startIndex, string fragment) + { + content = content.Insert(startIndex, fragment); + } + + public Line Truncate(int characterIndex, int indentation, int whitespaceOffset) + { + string truncatedContent = content.Substring(characterIndex + whitespaceOffset); + + if (truncatedContent.Trim().Length > 0) + { + content = content.Substring(0, characterIndex + whitespaceOffset); + + return new Line(new string(' ', whitespaceOffset) + truncatedContent, indentation, whitespaceOffset); + } + + return null; + } + + public string Render() => content; +} diff --git a/Src/FluentAssertions/Formatting/FormattedObjectGraph.cs b/Src/FluentAssertions/Formatting/FormattedObjectGraph.cs index 91e8d421bf..641fadac5a 100644 --- a/Src/FluentAssertions/Formatting/FormattedObjectGraph.cs +++ b/Src/FluentAssertions/Formatting/FormattedObjectGraph.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using FluentAssertions.Execution; namespace FluentAssertions.Formatting; @@ -17,17 +14,38 @@ namespace FluentAssertions.Formatting; /// public class FormattedObjectGraph { - private readonly int maxLines; - private readonly List lines = []; - private readonly StringBuilder lineBuilder = new(); - private int indentation; - private string lineBuilderWhitespace = string.Empty; + private readonly LineCollection lines; + /// + /// The current line that is being written to, or if there is no active line. + /// + private Line currentLine; + + /// + /// This class is used by the class to collect all the output of the (nested calls of an) into + /// a the final representation. + /// + /// + /// The will ensure that the number of lines will be limited + /// to the maximum number of lines provided through its constructor. It will throw + /// a if the number of lines exceeds the maximum. + /// public FormattedObjectGraph(int maxLines) { - this.maxLines = maxLines; + lines = new LineCollection(maxLines); } + /// + /// Represents the current level of indentation applied to newly added lines or fragments. + /// + /// + /// The indentation level determines the amount of leading whitespace to be added to each line, + /// calculated based on . It is incremented or decremented with methods such as + /// , and affects all subsequent lines or fragments added to + /// the . + /// + internal int Indentation { get; private set; } + /// /// The number of spaces that should be used by every indentation level. /// @@ -36,7 +54,7 @@ public FormattedObjectGraph(int maxLines) /// /// Returns the number of lines of text currently in the graph. /// - public int LineCount => lines.Count + (lineBuilder.Length > 0 ? 1 : 0); + public int LineCount => lines.Count; /// /// Starts a new line with the provided text fragment. Additional text can be added to @@ -45,84 +63,72 @@ public FormattedObjectGraph(int maxLines) public void AddFragmentOnNewLine(string fragment) { FlushCurrentLine(); - - AddFragment(fragment); + GetCurrentLine().Append(fragment); } /// - /// Starts a new line with the provided line of text that does not allow - /// adding more fragments of text. + /// If there's only one line, adds a fragment to that line. If there are more lines, adds the fragment as + /// a new line that does not allow any further fragments. /// - public void AddLine(string line) + public void AddLineOrFragment(string fragment) { - FlushCurrentLine(); - - AppendWithoutExceedingMaximumLines(Whitespace + line); - } - - /// - /// Adds a new fragment of text to the current line. - /// - public void AddFragment(string fragment) - { - if (lineBuilderWhitespace.Length > 0) + if (lines.Count == 1) { - lineBuilder.Append(lineBuilderWhitespace); - lineBuilderWhitespace = string.Empty; + AddFragment(fragment); + } + else + { + AddLine(fragment); } - - lineBuilder.Append(fragment); } /// - /// Adds a new line if there are no lines and no fragment that would cause a new line. + /// Starts a new line with the provided text that does not allow adding more + /// fragments of text. /// - internal void EnsureInitialNewLine() + public void AddLine(string content) { - if (LineCount == 0) - { - InsertInitialNewLine(); - } + FlushCurrentLine(); + + GetCurrentLine().Append(content); + FlushCurrentLine(); } /// - /// Inserts an empty line as the first line unless it is already. + /// Adds a new fragment of text to the current line. /// - private void InsertInitialNewLine() + public void AddFragment(string fragment) { - if (lines.Count == 0 || !string.IsNullOrEmpty(lines[0])) - { - lines.Insert(0, string.Empty); - lineBuilderWhitespace = Whitespace; - } + GetCurrentLine().Append(fragment); } private void FlushCurrentLine() { - string line = lineBuilder.ToString().TrimEnd(); - if (line.Length > 0) + // We only need to flush the line if there's something to flush. + if (currentLine is not null) { - AppendWithoutExceedingMaximumLines(lineBuilderWhitespace + line); + currentLine.Flush(); + currentLine = null; } - - lineBuilder.Clear(); - lineBuilderWhitespace = Whitespace; } - private void AppendWithoutExceedingMaximumLines(string line) + private Line GetCurrentLine() { - if (lines.Count == maxLines) + // We prefer to lazily initialize the current line so we don't waste memory. + if (currentLine is null) { - lines.Add(string.Empty); - - lines.Add( - $"(Output has exceeded the maximum of {maxLines} lines. " + - $"Increase {nameof(FormattingOptions)}.{nameof(FormattingOptions.MaxLines)} on {nameof(AssertionScope)} or {nameof(AssertionConfiguration)} to include more lines.)"); + currentLine = new Line(Indentation); + lines.Add(currentLine); + } - throw new MaxLinesExceededException(); + // A single-line rendering doesn't need any indentation, so we postpone that decision + // until we know whether there will be more lines. + if (lines.Count > 1) + { + currentLine.EnsureWhitespace(); } - lines.Add(line); + return currentLine; } /// @@ -133,124 +139,60 @@ private void AppendWithoutExceedingMaximumLines(string line) /// public IDisposable WithIndentation() { - indentation++; + Indentation++; return new Disposable(() => { - if (indentation > 0) + if (Indentation > 0) { - indentation--; + Indentation--; } }); } /// - /// Returns the final textual multi-line representation of the object graph. - /// - public override string ToString() - { - return string.Join(Environment.NewLine, lines.Concat([lineBuilder.ToString()])); - } - - internal PossibleMultilineFragment KeepOnSingleLineAsLongAsPossible() - { - return new PossibleMultilineFragment(this); - } - - private string Whitespace => MakeWhitespace(indentation); - - private static string MakeWhitespace(int indent) => new(' ', indent * SpacesPerIndentation); - - /// - /// Write fragments that may be on a single line or span multiple lines, - /// and this is not known until later parts of the fragment are written. + /// Get a reference to the current line (or the last line if there is no active line), so that we can + /// insert fragments and lines at that specific point. /// - internal record PossibleMultilineFragment + internal Anchor GetAnchor() { - private readonly FormattedObjectGraph parentGraph; - private readonly int startingLineBuilderIndex; - private readonly int startingLineCount; - - public PossibleMultilineFragment(FormattedObjectGraph parentGraph) - { - this.parentGraph = parentGraph; - startingLineBuilderIndex = parentGraph.lineBuilder.Length; - startingLineCount = parentGraph.lines.Count; - } - - /// - /// Write the fragment at the position the graph was in when this instance was created. - /// - /// - /// If more lines have been added since this instance was created then write the - /// fragment on a new line, otherwise write it on the same line. - /// - /// - internal void AddStartingLineOrFragment(string fragment) + if (lines.Count == 0) { - if (FormatOnSingleLine) - { - parentGraph.lineBuilder.Insert(startingLineBuilderIndex, fragment); - } - else - { - parentGraph.InsertInitialNewLine(); - parentGraph.lines.Insert(startingLineCount + 1, parentGraph.Whitespace + fragment); - InsertAtStartOfLine(startingLineCount + 2, MakeWhitespace(1)); - } + return new Anchor(this, null); } - private bool FormatOnSingleLine => parentGraph.lines.Count == startingLineCount; + return new Anchor(this, currentLine ?? lines.Last()); + } - private void InsertAtStartOfLine(int lineIndex, string insertion) - { - if (!parentGraph.lines[lineIndex].StartsWith(insertion, StringComparison.Ordinal)) - { - parentGraph.lines[lineIndex] = parentGraph.lines[lineIndex].Insert(0, insertion); - } - } + internal static string MakeWhitespace(int indent) => new(' ', indent * SpacesPerIndentation); - public void InsertLineOrFragment(string fragment) - { - if (FormatOnSingleLine) - { - parentGraph.lineBuilder.Insert(startingLineBuilderIndex, fragment); - } - else - { - parentGraph.lines[startingLineCount] = parentGraph.lines[startingLineCount] - .Insert(startingLineBuilderIndex, InsertNewLineIntoFragment(fragment)); - } - } + internal bool HasLinesBeyond(Line line) => lines.HasLinesBeyond(line); - private string InsertNewLineIntoFragment(string fragment) - { - if (StartingLineHasBeenAddedTo()) - { - return fragment + Environment.NewLine + MakeWhitespace(parentGraph.indentation + 1); - } + internal void AddLineAfter(Line line, string content) + { + lines.AddLineAfter(line, new Line(content)); + } - return fragment; - } + internal void InsertAtTop(string content) + { + lines.InsertAtTop(new Line(content)); + } - private bool StartingLineHasBeenAddedTo() => parentGraph.lines[startingLineCount].Length > startingLineBuilderIndex; + internal void InsertAtLineStartOrTop(string fragment) + { + lines.InsertAtLineStartOrTop(fragment); + } - /// - /// If more lines have been added since this instance was created then write the - /// fragment on a new line, otherwise write it on the same line. - /// - internal void AddLineOrFragment(string fragment) - { - if (FormatOnSingleLine) - { - parentGraph.AddFragment(fragment); - } - else - { - parentGraph.AddFragmentOnNewLine(fragment); - } - } + internal void SplitLine(Line line, int characterIndex) + { + lines.SplitLine(line, characterIndex); + } - internal void AddFragment(string fragment) => parentGraph.AddFragment(fragment); + /// + /// Returns the final textual multi-line representation of the object graph. + /// + public override string ToString() + { + return string.Join(Environment.NewLine, lines.Select(line => line.ToString())); } } diff --git a/Src/FluentAssertions/Formatting/ILineState.cs b/Src/FluentAssertions/Formatting/ILineState.cs new file mode 100644 index 0000000000..1b0e0fe307 --- /dev/null +++ b/Src/FluentAssertions/Formatting/ILineState.cs @@ -0,0 +1,26 @@ +namespace FluentAssertions.Formatting; + +/// +/// Represents the state management of a line for structured content building or rendering. +/// +/// +/// This interface defines the operations that can be performed on a line, +/// including appending content, inserting content at specific positions, +/// truncating the line, and rendering its content. +/// +internal interface ILineState +{ + ILineState Flush(); + + int Length { get; } + + void Append(string fragment); + + void InsertAtStart(string fragment); + + void InsertAt(int startIndex, string fragment); + + Line Truncate(int characterIndex, int indentation, int whitespaceOffset); + + string Render(); +} diff --git a/Src/FluentAssertions/Formatting/Line.cs b/Src/FluentAssertions/Formatting/Line.cs new file mode 100644 index 0000000000..bab01f9edc --- /dev/null +++ b/Src/FluentAssertions/Formatting/Line.cs @@ -0,0 +1,111 @@ +using System; + +namespace FluentAssertions.Formatting; + +/// +/// Represents a single line of output rendered through the . +/// +internal class Line +{ + /// + /// The level of indentation at the time this line was created. + /// + private int indentation; + + private ILineState state; + + /// + /// If any whitespace was inserted at the beginning of the line without an knowing about it, this + /// will be the length of that whitespace so that any calls to will be offset by this amount. + /// + private int whitespaceOffset; + + /// + /// Creates an empty line with the specified indentation that will be applied when + /// actual fragments are added. + /// + public Line(int indentation) + { + state = new BuildingLineState(); + this.indentation = indentation; + } + + public Line(string content) + { + state = new FlushedLineState(content); + } + + public Line(string truncatedContent, int indentation, int whitespaceOffset) + { + state = new FlushedLineState(truncatedContent); + this.indentation = indentation; + this.whitespaceOffset = whitespaceOffset; + } + + /// + /// Is used to close off the internal string builder. + /// + public void Flush() + { + state = state.Flush(); + } + + /// + /// Gets the length of the content, including any whitespace that was inserted at the beginning of the line. + /// + public int Length => state.Length; + + /// + /// Gets the length of the content without the offset of any whitespace that was inserted at the beginning of the line. + /// + public int LengthWithoutOffset => Length - whitespaceOffset; + + public void Append(string fragment) + { + state.Append(fragment); + } + + public void InsertAtStart(string fragment) + { + state.InsertAtStart(fragment); + } + + public void Insert(int characterIndex, string fragment) + { + int startIndex = Math.Min(characterIndex + whitespaceOffset, Length); + state.InsertAt(startIndex, fragment); + } + + /// + /// Ensures that the line is prefixed with the correct amount of whitespace. + /// + /// + /// Since we don't add the whitespace for the first line until we know that there is a second line, we need to be able + /// to fixup the whitespace for the second line at a later time. + /// + public void EnsureWhitespace() + { + if (indentation > 0) + { + string whitespace = FormattedObjectGraph.MakeWhitespace(indentation); + whitespaceOffset = whitespace.Length; + + state.InsertAt(0, whitespace); + + indentation = 0; + } + } + + /// + /// Truncates the current line at the specified character index and returns the remainder as a new line. + /// Returns if the remainder is empty. + /// + public Line Truncate(int characterIndex) + { + Flush(); + + return state.Truncate(characterIndex, indentation, whitespaceOffset); + } + + public override string ToString() => state.Render().TrimEnd(); +} diff --git a/Src/FluentAssertions/Formatting/LineCollection.cs b/Src/FluentAssertions/Formatting/LineCollection.cs new file mode 100644 index 0000000000..ec07978a17 --- /dev/null +++ b/Src/FluentAssertions/Formatting/LineCollection.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using FluentAssertions.Execution; + +namespace FluentAssertions.Formatting; + +/// +/// A collection of lines that will throw a when the number of lines +/// exceeds the maximum. +/// +internal class LineCollection(int maxLines) : IEnumerable +{ + private readonly List lines = []; + + public int Count => lines.Count; + + public bool HasLinesBeyond(Line line) + { + // Null means that we're referring to the top of the list + return (line is null && lines.Count > 1) || (line is not null && lines.IndexOf(line) < (lines.Count - 1)); + } + + public void Add(Line line) + { + lines.Add(line); + OnCollectionIsModified(); + } + + public void AddLineAfter(Line line, Line newLine) + { + int index = lines.IndexOf(line); + + Insert(index + 1, newLine); + } + + public void InsertAtTop(Line newLine) + { + Insert(0, newLine); + } + + public void InsertAtLineStartOrTop(string fragment) + { + // If there's a single line, insert at the beginning of that line + // If there are more than one line, insert as a new line at the top + if (lines.Count == 1) + { + lines[0].InsertAtStart(fragment); + } + else + { + Insert(0, new Line(fragment)); + } + } + + public void SplitLine(Line line, int characterIndex) + { + int lineIndex = lines.IndexOf(line); + + Line remainder = line.Truncate(characterIndex); + if (remainder is not null) + { + Insert(lineIndex + 1, remainder); + } + } + + private void Insert(int index, Line item) + { + lines.Insert(index, item); + OnCollectionIsModified(); + + if (index == 0 && lines.Count > 1) + { + lines[1].EnsureWhitespace(); + } + } + + private void OnCollectionIsModified() + { + if (lines.Count > maxLines) + { + lines.Add(new Line(0)); + + lines.Add(new Line( + $"(Output has exceeded the maximum of {maxLines} lines. " + + $"Increase {nameof(FormattingOptions)}.{nameof(FormattingOptions.MaxLines)} on {nameof(AssertionScope)} or {nameof(AssertionConfiguration)} to include more lines.)")); + + throw new MaxLinesExceededException(); + } + } + + public IEnumerator GetEnumerator() => lines.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/Src/FluentAssertions/Formatting/StringValueFormatter.cs b/Src/FluentAssertions/Formatting/StringValueFormatter.cs index 752619dbca..fdf30f7b52 100644 --- a/Src/FluentAssertions/Formatting/StringValueFormatter.cs +++ b/Src/FluentAssertions/Formatting/StringValueFormatter.cs @@ -20,6 +20,13 @@ public void Format(object value, FormattedObjectGraph formattedGraph, Formatting "{value}" """; - formattedGraph.AddFragment(result); + if (context.UseLineBreaks) + { + formattedGraph.AddFragmentOnNewLine(result); + } + else + { + formattedGraph.AddFragment(result); + } } } diff --git a/Tests/.editorconfig b/Tests/.editorconfig index d7c1e5c6d3..2936a92f5c 100644 --- a/Tests/.editorconfig +++ b/Tests/.editorconfig @@ -1,5 +1,9 @@ [*.cs] + + + + # IDE0051: Private member is unused dotnet_diagnostic.IDE0051.severity = none # IDE0070: GetHashCode implementation can be simplified @@ -35,6 +39,11 @@ dotnet_diagnostic.CA1052.severity = none # CA1062: Validate arguments of public methods dotnet_diagnostic.CA1062.severity = none + +# Purpose: Implement IDisposable correctly +# Reason: Not important in test classes +dotnet_diagnostic.CA1063.severity = none + # CA1064: Exceptions should be public dotnet_diagnostic.CA1064.severity = none # CA1307: Specify StringComparison @@ -57,6 +66,11 @@ dotnet_diagnostic.CA1812.severity = none dotnet_diagnostic.CA1813.severity = none # CA1814: Prefer jagged arrays over multidimensional dotnet_diagnostic.CA1814.severity = none + +# Purpose: Call GC.SuppressFinalize correctly +# Reason: Not important in test classes +dotnet_diagnostic.CA1816.severity = none + # CA1822: Member does not access instance data and can be marked as static dotnet_diagnostic.CA1822.severity = none # CA1825: Avoid unnecessary zero-length array allocations. Use Array.Empty() instead diff --git a/Tests/Approval.Tests/Approval.Tests.v3.ncrunchproject b/Tests/Approval.Tests/Approval.Tests.v3.ncrunchproject new file mode 100644 index 0000000000..7b5b2139ff --- /dev/null +++ b/Tests/Approval.Tests/Approval.Tests.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index 05b4c07fd9..a1ac90a110 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -1390,7 +1390,8 @@ namespace FluentAssertions.Formatting public static int SpacesPerIndentation { get; } public void AddFragment(string fragment) { } public void AddFragmentOnNewLine(string fragment) { } - public void AddLine(string line) { } + public void AddLine(string content) { } + public void AddLineOrFragment(string fragment) { } public override string ToString() { } public System.IDisposable WithIndentation() { } } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt index 784751b7cd..a8f3c5051d 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt @@ -1409,7 +1409,8 @@ namespace FluentAssertions.Formatting public static int SpacesPerIndentation { get; } public void AddFragment(string fragment) { } public void AddFragmentOnNewLine(string fragment) { } - public void AddLine(string line) { } + public void AddLine(string content) { } + public void AddLineOrFragment(string fragment) { } public override string ToString() { } public System.IDisposable WithIndentation() { } } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt index 0d8eb031a6..a5068f244d 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt @@ -1334,7 +1334,8 @@ namespace FluentAssertions.Formatting public static int SpacesPerIndentation { get; } public void AddFragment(string fragment) { } public void AddFragmentOnNewLine(string fragment) { } - public void AddLine(string line) { } + public void AddLine(string content) { } + public void AddLineOrFragment(string fragment) { } public override string ToString() { } public System.IDisposable WithIndentation() { } } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index fb4ece81e4..3b545b4271 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -1390,7 +1390,8 @@ namespace FluentAssertions.Formatting public static int SpacesPerIndentation { get; } public void AddFragment(string fragment) { } public void AddFragmentOnNewLine(string fragment) { } - public void AddLine(string line) { } + public void AddLine(string content) { } + public void AddLineOrFragment(string fragment) { } public override string ToString() { } public System.IDisposable WithIndentation() { } } diff --git a/Tests/Benchmarks/Benchmarks.net472.v3.ncrunchproject b/Tests/Benchmarks/Benchmarks.net472.v3.ncrunchproject new file mode 100644 index 0000000000..95a483b433 --- /dev/null +++ b/Tests/Benchmarks/Benchmarks.net472.v3.ncrunchproject @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Tests/FluentAssertions.Specs/Formatting/FormatterSpecs.cs b/Tests/FluentAssertions.Specs/Formatting/FormatterSpecs.cs index 788fd49e77..b21bf168f6 100644 --- a/Tests/FluentAssertions.Specs/Formatting/FormatterSpecs.cs +++ b/Tests/FluentAssertions.Specs/Formatting/FormatterSpecs.cs @@ -12,7 +12,7 @@ namespace FluentAssertions.Specs.Formatting; [Collection("FormatterSpecs")] -public sealed class FormatterSpecs : IDisposable +public class FormatterSpecs : IDisposable { [Fact] public void When_value_contains_cyclic_reference_it_should_create_descriptive_error_message() @@ -185,60 +185,27 @@ public void When_the_object_is_a_generic_type_without_custom_string_representati } }; - var expectedStuff = new List> - { - new() - { - StuffId = 1, - Description = "Stuff_1", - Children = [1, 2, 3, 4] - }, - new() - { - StuffId = 2, - Description = "WRONG_DESCRIPTION", - Children = [1, 2, 3, 4] - } - }; - // Act - Action act = () => stuff.Should().NotBeNull() - .And.Equal(expectedStuff, (t1, t2) => t1.StuffId == t2.StuffId && t1.Description == t2.Description); + var actual = Formatter.ToString(stuff); // Assert - act.Should().Throw() - .WithMessage( - """ - Expected stuff to be equal to + actual.Should().Match( + """ + { + FluentAssertions.Specs.Formatting.FormatterSpecs+Stuff`1[[System.Int32*]] { - FluentAssertions.Specs.Formatting.FormatterSpecs+Stuff`1[[System.Int32*]] - { - Children = {1, 2, 3, 4}, - Description = "Stuff_1", - StuffId = 1 - }, - FluentAssertions.Specs.Formatting.FormatterSpecs+Stuff`1[[System.Int32*]] - { - Children = {1, 2, 3, 4}, - Description = "WRONG_DESCRIPTION", - StuffId = 2 - } - }, but + Children = {1, 2, 3, 4}, + Description = "Stuff_1", + StuffId = 1 + }, + FluentAssertions.Specs.Formatting.FormatterSpecs+Stuff`1[[System.Int32*]] { - FluentAssertions.Specs.Formatting.FormatterSpecs+Stuff`1[[System.Int32*]] - { - Children = {1, 2, 3, 4}, - Description = "Stuff_1", - StuffId = 1 - }, - FluentAssertions.Specs.Formatting.FormatterSpecs+Stuff`1[[System.Int32*]] - { - Children = {1, 2, 3, 4}, - Description = "Stuff_2", - StuffId = 2 - } - } differs at index 1. - """); + Children = {1, 2, 3, 4}, + Description = "Stuff_2", + StuffId = 2 + } + } + """); } [Fact] @@ -278,36 +245,21 @@ public void When_the_object_is_an_anonymous_type_it_should_show_the_properties_r Children = new[] { 1, 2, 3, 4 }, }; - var expectedStuff = new - { - SingleChild = new { ChildId = 4 }, - Children = new[] { 10, 20, 30, 40 }, - }; - // Act - Action act = () => stuff.Should().Be(expectedStuff); + string actual = Formatter.ToString(stuff); // Assert - act.Should().Throw() - .Which.Message.Should().Be( - """ - Expected stuff to be - { - Children = {10, 20, 30, 40}, - SingleChild = - { - ChildId = 4 - } - }, but found + actual.Should().Match( + """ + { + Children = {1, 2, 3, 4}, + Description = "absent", + SingleChild = { - Children = {1, 2, 3, 4}, - Description = "absent", - SingleChild = - { - ChildId = 8 - } - }. - """); + ChildId = 8 + } + } + """); } [Fact] @@ -315,20 +267,7 @@ public void When_the_object_is_a_list_of_anonymous_type_it_should_show_the_properties_recursively_with_newlines_and_indentation() { // Arrange - var stuff = new[] - { - new - { - Description = "absent", - }, - new - { - Description = "absent", - }, - }; - - var expectedStuff = new[] - { + var expectedStuff = new { ComplexChildren = new[] @@ -336,41 +275,26 @@ public void new { Property = "hello" }, new { Property = "goodbye" }, }, - }, - }; + }; // Act - Action act = () => stuff.Should().BeEquivalentTo(expectedStuff); + var actual = Formatter.ToString(expectedStuff); // Assert - act.Should().Throw() - .Which.Message.Should().Match( - """ - Expected stuff to be a collection with 1 item(s), but* + actual.Should().Be( + """ + { + ComplexChildren = { { - Description = "absent" - },* + Property = "hello" + }, { - Description = "absent" + Property = "goodbye" } } - contains 1 item(s) more than - - { - { - ComplexChildren =* - { - { - Property = "hello" - },* - { - Property = "goodbye" - } - } - } - }.* - """); + } + """); } [Fact] @@ -386,7 +310,7 @@ public void When_the_object_is_an_empty_anonymous_type_it_should_show_braces_on_ // Assert act.Should().Throw() - .Which.Message.Should().Match("*but found { }*"); + .Which.Message.Should().Match("*but found *{ }*"); } [Fact] @@ -395,27 +319,18 @@ public void When_the_object_is_a_tuple_it_should_show_the_properties_recursively // Arrange (int TupleId, string Description, List Children) stuff = (1, "description", [1, 2, 3, 4]); - (int, string, List) expectedStuff = (2, "WRONG_DESCRIPTION", new List { 4, 5, 6, 7 }); - // Act - Action act = () => stuff.Should().Be(expectedStuff); + string actual = Formatter.ToString(stuff); // Assert - act.Should().Throw() - .Which.Message.Should().Match( - """ - Expected stuff to be equal to* - { - Item1 = 2,* - Item2 = "WRONG_DESCRIPTION",* - Item3 = {4, 5, 6, 7} - }, but found* - { - Item1 = 1,* - Item2 = "description",* - Item3 = {1, 2, 3, 4} - }.* - """); + actual.Should().Match( + """ + { + Item1 = 1,* + Item2 = "description",* + Item3 = {1, 2, 3, 4} + } + """); } [Fact] @@ -428,32 +343,22 @@ public void When_the_object_is_a_record_it_should_show_the_properties_recursivel SingleChild: new ChildRecord(ChildRecordId: 80), RecordChildren: [4, 5, 6, 7]); - var expectedStuff = new - { - RecordDescription = "WRONG_DESCRIPTION", - }; - - // Act - Action act = () => stuff.Should().Be(expectedStuff); + var actual = Formatter.ToString(stuff); // Assert - act.Should().Throw() - .Which.Message.Should().Match( - """ - Expected stuff to be* - { - RecordDescription = "WRONG_DESCRIPTION" - }, but found FluentAssertions.Specs.Formatting.FormatterSpecs+StuffRecord + actual.Should().Match( + """ + FluentAssertions.Specs.Formatting.FormatterSpecs+StuffRecord + { + RecordChildren = {4, 5, 6, 7},* + RecordDescription = "descriptive",* + RecordId = 9,* + SingleChild = FluentAssertions.Specs.Formatting.FormatterSpecs+ChildRecord { - RecordChildren = {4, 5, 6, 7},* - RecordDescription = "descriptive",* - RecordId = 9,* - SingleChild = FluentAssertions.Specs.Formatting.FormatterSpecs+ChildRecord - { - ChildRecordId = 80 - } - }. - """); + ChildRecordId = 80 + } + } + """); } [Fact] @@ -913,7 +818,7 @@ public void string result = Formatter.ToString(subject, new FormattingOptions { UseLineBreaks = true }); // Assert - result.Should().Contain($"FluentAssertions.Specs.Formatting.FormatterSpecs+A, {Environment.NewLine}"); + result.Should().Contain($"FluentAssertions.Specs.Formatting.FormatterSpecs+A,{Environment.NewLine}"); result.Should().Contain($"FluentAssertions.Specs.Formatting.FormatterSpecs+B{Environment.NewLine}"); } @@ -1002,7 +907,7 @@ public void When_defining_a_custom_enumerable_value_formatter_it_should_respect_ // Act string str = Formatter.ToString(values); - str.Should().Match(Environment.NewLine + + str.Should().Match( "{*FluentAssertions*FormatterSpecs+CustomClass" + Environment.NewLine + " {" + Environment.NewLine + " IntProperty = 1," + Environment.NewLine + @@ -1030,6 +935,297 @@ public FormatterScope(IValueFormatter formatter) public void Dispose() => Formatter.RemoveFormatter(formatter); } + [Fact] + public void Can_render_an_array_containing_anonymous_types() + { + // Act + var actual = Formatter.ToString(new[] { new { Value = 1 }, new { Value = 2 } }); + + // Assert + actual.Should().Be( + """ + { + { + Value = 1 + }, + { + Value = 2 + } + } + """); + } + + [Fact] + public void Can_render_an_array_on_a_single_line() + { + // Act + var actual = Formatter.ToString(new[] { "abc", "def", "efg" }); + + // Assert + actual.Should().Be(@"{""abc"", ""def"", ""efg""}"); + } + + [Fact] + public void Can_render_an_array_using_line_breaks() + { + // Act + var actual = Formatter.ToString(new[] { "abc", "def", "efg" }, new FormattingOptions + { + UseLineBreaks = true + }); + + // Assert + actual.Should().Be(""" + { + "abc", + "def", + "efg" + } + """); + } + + [Fact] + public void Can_render_a_single_item_array_using_line_breaks() + { + // Act + var actual = Formatter.ToString(new[] { "abc" }, new FormattingOptions + { + UseLineBreaks = true + }); + + // Assert + actual.Should().Be(""" + { + "abc" + } + """); + } + + [Fact] + public void Can_render_a_single_item_array_on_a_single_line() + { + // Act + var actual = Formatter.ToString(new[] { "abc" }); + + // Assert + actual.Should().Be("""{"abc"}"""); + } + + [Fact] + public void Can_render_a_collection_with_anonymous_types_using_line_breaks() + { + // Act + var actual = Formatter.ToString(new[] + { + new { Value = "abc" }, new { Value = "def" }, new { Value = "efg" } + }, new FormattingOptions { UseLineBreaks = true }); + + // Assert + actual.Should().Be( + """ + { + { + Value = + "abc" + }, + { + Value = + "def" + }, + { + Value = + "efg" + } + } + """); + } + + [Fact] + public void Can_render_a_simple_anonymous_object() + { + // Act + var actual = Formatter.ToString(new + { + SingleChild = new { ChildId = 4 }, + Children = new[] { 10, 20, 30, 40 }, + }); + + // Assert + actual.Should().Be( + """ + { + Children = {10, 20, 30, 40}, + SingleChild = + { + ChildId = 4 + } + } + """); + } + + [Fact] + public void Can_format_a_multi_dimensional_array_with_linebreaks() + { + // Arrange + var points = new Point[][] + { + [new Point("0,0")], + [new Point("1,0")], + }; + + // Act + var result = Formatter.ToString(points, new FormattingOptions { UseLineBreaks = true }); + + // Arrange + result.Should().Be( + """ + { + { + P0,0 + }, + { + P1,0 + } + } + """); + } + + [Fact] + public void Can_format_an_enumerable_using_line_breaks() + { + // Arrange + Point[] points = [new("0,0"), new("1,0")]; + + var result = Formatter.ToString(points, new FormattingOptions { UseLineBreaks = true }); + + result.Should().Be( + """ + { + P0,0, + P1,0 + } + """); + } + + [Fact] + public void Can_format_an_enumerable_without_line_breaks() + { + // Arrange + Point[] points = [new("0,0"), new("1,0")]; + + // Act + var result = Formatter.ToString(points, new FormattingOptions { UseLineBreaks = false }); + + // Assert + result.Should().Be("{P0,0, P1,0}"); + } + + private class Point(string name) + { + public override string ToString() => "P" + name; + } + + [Fact] + public void A_formatter_can_force_new_line() + { + // Arrange + var formatter = new FormatterUsingAddLine(); + using var _ = new FormatterScope(formatter); + + // Act + string result = Formatter.ToString(null); + + // Assert + result.Should().Be( + """ + first fragment + separate line + last fragment + """); + } + + private class FormatterUsingAddLine : IValueFormatter + { + public bool CanHandle(object value) => true; + + public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild) + { + formattedGraph.AddFragment("first fragment"); + formattedGraph.AddLine("separate line"); + formattedGraph.AddFragment("last fragment"); + } + } + + [Fact] + public void A_formatter_can_insert_a_line_or_fragment() + { + // Arrange + var formatter = new FormatterUsingInsertLineOrFragment(); + using var _ = new FormatterScope(formatter); + + // Act + string result = Formatter.ToString(null); + + // Assert + result.Should().Be("fragment"); + } + + private class FormatterUsingInsertLineOrFragment : IValueFormatter + { + public bool CanHandle(object value) => true; + + public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild) + { + formattedGraph.GetAnchor().InsertLineOrFragment("fragment"); + } + } + + [Fact] + public void A_formatter_can_use_an_anchor_on_an_empty_graph() + { + using var _ = new FormatterScope(new FormatterUsingInsertFragment()); + + // Act + string result = Formatter.ToString(null); + + // Assert + result.Should().Be("fragment"); + } + + private class FormatterUsingInsertFragment : IValueFormatter + { + public bool CanHandle(object value) => true; + + public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild) + { + formattedGraph.GetAnchor().InsertFragment("fragment"); + } + } + + [Fact] + public void Can_insert_a_fragment_when_using_linebreaks() + { + using var _ = new FormatterScope(new InsertUsingLinebreaksFormatter()); + + // Act + string result = Formatter.ToString(null); + + // Assert + result.Should().Be("fragment"); + } + + private class InsertUsingLinebreaksFormatter : IValueFormatter + { + public bool CanHandle(object value) => true; + + public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild) + { + Anchor anchor = formattedGraph.GetAnchor(); + anchor.UseLineBreaks = true; + anchor.InsertFragment("fragment"); + } + } + public void Dispose() => AssertionEngine.ResetToDefaults(); } diff --git a/Tests/FluentAssertions.Specs/Primitives/ReferenceTypeAssertionsSpecs.cs b/Tests/FluentAssertions.Specs/Primitives/ReferenceTypeAssertionsSpecs.cs index 54f2c74587..4bf90fff87 100644 --- a/Tests/FluentAssertions.Specs/Primitives/ReferenceTypeAssertionsSpecs.cs +++ b/Tests/FluentAssertions.Specs/Primitives/ReferenceTypeAssertionsSpecs.cs @@ -43,11 +43,9 @@ public void When_two_different_objects_are_expected_to_be_the_same_it_should_fai .Should().Throw() .WithMessage( """ - Expected subject to refer to - { + Expected subject to refer to { UserName = "JohnDoe" - } because they are the same, but found - { + } because they are the same, but found { Name = "John Doe" }. """); @@ -61,8 +59,7 @@ public void When_a_derived_class_has_longer_formatting_than_the_base_class() act.Should().Throw() .WithMessage( """ - Expected subject to be empty, but found at least one item - { + Expected subject to be empty, but found at least one item { FluentAssertions.Specs.Primitives.Complex { Statement = "goodbye" diff --git a/Tests/FluentAssertions.Specs/Xml/XmlNodeFormatterSpecs.cs b/Tests/FluentAssertions.Specs/Xml/XmlNodeFormatterSpecs.cs index ef83197743..9f2ea3fc86 100644 --- a/Tests/FluentAssertions.Specs/Xml/XmlNodeFormatterSpecs.cs +++ b/Tests/FluentAssertions.Specs/Xml/XmlNodeFormatterSpecs.cs @@ -18,7 +18,7 @@ public void When_a_node_is_20_chars_long_it_should_not_be_trimmed() string result = Formatter.ToString(xmlDoc); // Assert - result.Should().Be(@"" + Environment.NewLine); + result.Should().Be(@""); } [Fact] @@ -32,6 +32,6 @@ public void When_a_node_is_longer_then_20_chars_it_should_be_trimmed() string result = Formatter.ToString(xmlDoc); // Assert - result.Should().Be(@"