From 527289b3c6dff639a22a6dc186871e70dec3c90a Mon Sep 17 00:00:00 2001 From: Victor Pogor Date: Mon, 1 Jul 2024 17:14:35 +1000 Subject: [PATCH 01/14] added factory --- Directory.Packages.props | 1 + src/OffDotNet.CodeAnalysis/Dependencies.cs | 18 ++++++ .../Lexer/CursorFactory.cs | 42 +++++++++++++ .../Lexer/ICursorFactory.cs | 33 ++++++++++ src/OffDotNet.CodeAnalysis/Lexer/ILexer.cs | 10 ++++ .../Lexer/ITextCursor.cs | 60 +++++++++++++++++++ .../Lexer/TextCursor.cs | 2 +- .../OffDotNet.CodeAnalysis.csproj | 6 +- src/OffDotNet.CodeAnalysis/packages.lock.json | 6 ++ .../Lexer/TextCursorTests.cs | 13 ++++ .../packages.lock.json | 11 +++- 11 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 src/OffDotNet.CodeAnalysis/Dependencies.cs create mode 100644 src/OffDotNet.CodeAnalysis/Lexer/CursorFactory.cs create mode 100644 src/OffDotNet.CodeAnalysis/Lexer/ICursorFactory.cs create mode 100644 src/OffDotNet.CodeAnalysis/Lexer/ILexer.cs create mode 100644 src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 0ce7905..e06db6a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,5 +1,6 @@ + diff --git a/src/OffDotNet.CodeAnalysis/Dependencies.cs b/src/OffDotNet.CodeAnalysis/Dependencies.cs new file mode 100644 index 0000000..3d399e1 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Dependencies.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis; + +using Lexer; +using Microsoft.Extensions.DependencyInjection; + +public static class Dependencies +{ + public static IServiceCollection AddCodeAnalysis(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } +} diff --git a/src/OffDotNet.CodeAnalysis/Lexer/CursorFactory.cs b/src/OffDotNet.CodeAnalysis/Lexer/CursorFactory.cs new file mode 100644 index 0000000..c2da183 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Lexer/CursorFactory.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Lexer; + +/// +/// Factory class for creating instances of . +/// +internal sealed class CursorFactory : ICursorFactory +{ + /// + /// Creates an instance of from a read-only byte span. + /// + /// The text represented as a read-only byte span. + /// An instance of initialized with the provided text. + public ITextCursor Create(ReadOnlySpan text) + { + return new TextCursor(text); + } + + /// + /// Creates an instance of from a read-only character span. + /// + /// The text represented as a read-only character span. + /// An instance of initialized with the provided text. + public ITextCursor Create(ReadOnlySpan text) + { + return new TextCursor(text); + } + + /// + /// Creates an instance of from a string. + /// + /// The text represented as a string. + /// An instance of initialized with the provided text. + public ITextCursor Create(string text) + { + return new TextCursor(text); + } +} diff --git a/src/OffDotNet.CodeAnalysis/Lexer/ICursorFactory.cs b/src/OffDotNet.CodeAnalysis/Lexer/ICursorFactory.cs new file mode 100644 index 0000000..384336e --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Lexer/ICursorFactory.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Lexer; + +/// +/// Factory interface for creating instances of . +/// +public interface ICursorFactory +{ + /// + /// Creates an instance of from a read-only byte span. + /// + /// The text represented as a read-only byte span. + /// An instance of initialized with the provided text. + ITextCursor Create(ReadOnlySpan text); + + /// + /// Creates an instance of from a read-only character span. + /// + /// The text represented as a read-only character span. + /// An instance of initialized with the provided text. + ITextCursor Create(ReadOnlySpan text); + + /// + /// Creates an instance of from a string. + /// + /// The text represented as a string. + /// An instance of initialized with the provided text. + ITextCursor Create(string text); +} diff --git a/src/OffDotNet.CodeAnalysis/Lexer/ILexer.cs b/src/OffDotNet.CodeAnalysis/Lexer/ILexer.cs new file mode 100644 index 0000000..262fff1 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Lexer/ILexer.cs @@ -0,0 +1,10 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Lexer; + +public interface ILexer +{ +} diff --git a/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs b/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs new file mode 100644 index 0000000..4119925 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Lexer; + +using Utils; + +/// Represents a text cursor for navigating and processing text data. +public interface ITextCursor : IDisposable +{ + /// + /// Gets the current byte at the cursor position. + /// + Option Current { get; } + + /// + /// Gets the length of the text. + /// + int Length { get; } + + /// + /// Gets a value indicating whether the cursor is at the end of the text. + /// + bool IsAtEnd { get; } + + /// + /// Peeks at the byte at the specified delta from the current position. + /// + /// The delta from the current position. + /// The byte at the specified delta if available; otherwise, . + Option Peek(int delta = 0); + + /// + /// Advances the cursor by the specified delta. + /// + /// The delta by which to advance the cursor. + void Advance(int delta = 1); + + /// + /// Advances the cursor while the specified predicate is true. + /// + /// The predicate to test each byte against. + void Advance(Predicate predicate); + + /// + /// Tries to advance the cursor if the current byte matches the specified byte. + /// + /// The byte to match against. + /// True if the cursor was advanced; otherwise, false. + bool TryAdvance(byte b); + + /// + /// Tries to advance the cursor if the subsequent bytes match the specified subtext. + /// + /// The subtext to match against. + /// True if the cursor was advanced; otherwise, false. + bool TryAdvance(ReadOnlySpan subtext); +} diff --git a/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs b/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs index e3c8459..0a3277f 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs @@ -10,7 +10,7 @@ namespace OffDotNet.CodeAnalysis.Lexer; /// /// Represents a text cursor for navigating and processing text data. /// -internal sealed class TextCursor : IDisposable +internal sealed class TextCursor : ITextCursor { private readonly IMemoryOwner _textOwner; private int _position; diff --git a/src/OffDotNet.CodeAnalysis/OffDotNet.CodeAnalysis.csproj b/src/OffDotNet.CodeAnalysis/OffDotNet.CodeAnalysis.csproj index 2ef1a36..bea9298 100644 --- a/src/OffDotNet.CodeAnalysis/OffDotNet.CodeAnalysis.csproj +++ b/src/OffDotNet.CodeAnalysis/OffDotNet.CodeAnalysis.csproj @@ -1 +1,5 @@ - + + + + + diff --git a/src/OffDotNet.CodeAnalysis/packages.lock.json b/src/OffDotNet.CodeAnalysis/packages.lock.json index 94dcfcd..bcafec9 100644 --- a/src/OffDotNet.CodeAnalysis/packages.lock.json +++ b/src/OffDotNet.CodeAnalysis/packages.lock.json @@ -2,6 +2,12 @@ "version": 2, "dependencies": { "net8.0": { + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Direct", + "requested": "[8.0.1, )", + "resolved": "8.0.1", + "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" + }, "Roslynator.Analyzers": { "type": "Direct", "requested": "[4.12.4, )", diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs index 1766bbe..6a00feb 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs @@ -24,6 +24,19 @@ public void Class_ShouldImplementIDisposableInterface() Assert.IsAssignableFrom(cursor); } + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"Class should implement {nameof(ITextCursor)} interface")] + public void Class_ShouldImplementITextCursorInterface() + { + // Arrange + + // Act + var cursor = new TextCursor(string.Empty); + + // Assert + Assert.IsAssignableFrom(cursor); + } + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] [Fact(DisplayName = "Dispose() method should not throw any exception")] public void Dispose_ShouldNotThrowAnyException() diff --git a/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json b/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json index b68722f..1596b3d 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json +++ b/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json @@ -162,7 +162,16 @@ } }, "offdotnet.codeanalysis": { - "type": "Project" + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "CentralTransitive", + "requested": "[8.0.1, )", + "resolved": "8.0.1", + "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" } } } From c7d37166ca341453d04c28b1df4b7c55c53adb69 Mon Sep 17 00:00:00 2001 From: Victor Pogor Date: Thu, 4 Jul 2024 01:17:09 +1000 Subject: [PATCH 02/14] added abstract node --- .editorconfig | 3 + Directory.Packages.props | 2 +- OffDotNet.sln | 14 + .../Dependencies.cs | 27 + src/OffDotNet.CodeAnalysis.Pdf/Lexer/Lexer.cs | 12 + .../OffDotNet.CodeAnalysis.Pdf.csproj | 6 + .../Syntax/RawSyntaxNode.cs | 29 + .../Syntax/RawSyntaxTrivia.cs | 28 + .../Syntax/SyntaxKind.cs | 19 + .../Syntax/SyntaxKindFacts.cs | 22 + .../packages.lock.json | 45 ++ src/OffDotNet.CodeAnalysis/Dependencies.cs | 10 +- .../OffDotNet.CodeAnalysis.csproj | 6 + .../Syntax/AbstractNode.cs | 182 ++++++ .../Utils/ExceptionUtilities.cs | 14 + src/OffDotNet.CodeAnalysis/packages.lock.json | 6 +- .../OffDotNet.CodeAnalysis.Pdf.Tests.csproj | 6 + .../Syntax/RawSyntaxNodeTests.cs | 133 +++++ .../Syntax/RawSyntaxTriviaTests.cs | 165 ++++++ .../packages.lock.json | 194 +++++++ .../Syntax/AbstractNodeTests.cs | 549 ++++++++++++++++++ .../packages.lock.json | 13 +- 22 files changed, 1477 insertions(+), 8 deletions(-) create mode 100644 src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs create mode 100644 src/OffDotNet.CodeAnalysis.Pdf/Lexer/Lexer.cs create mode 100644 src/OffDotNet.CodeAnalysis.Pdf/OffDotNet.CodeAnalysis.Pdf.csproj create mode 100644 src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxNode.cs create mode 100644 src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxTrivia.cs create mode 100644 src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKind.cs create mode 100644 src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKindFacts.cs create mode 100644 src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json create mode 100644 src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs create mode 100644 src/OffDotNet.CodeAnalysis/Utils/ExceptionUtilities.cs create mode 100644 tests/OffDotNet.CodeAnalysis.Pdf.Tests/OffDotNet.CodeAnalysis.Pdf.Tests.csproj create mode 100644 tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxNodeTests.cs create mode 100644 tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTriviaTests.cs create mode 100644 tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json create mode 100644 tests/OffDotNet.CodeAnalysis.Tests/Syntax/AbstractNodeTests.cs diff --git a/.editorconfig b/.editorconfig index 534c039..1bbf35a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -255,6 +255,9 @@ dotnet_diagnostic.IDE0060.severity = warning # Nullable are treated as errors dotnet_diagnostic.CS8625.severity = error +# SA1500: Braces for multi-line statements should not share line +dotnet_diagnostic.SA1500.severity = none + [tests/**/*.{cs,vb}] # SA1600: Elements should be documented diff --git a/Directory.Packages.props b/Directory.Packages.props index e06db6a..eecdd7a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,7 +9,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OffDotNet.sln b/OffDotNet.sln index 3690572..f71d375 100644 --- a/OffDotNet.sln +++ b/OffDotNet.sln @@ -62,6 +62,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OffDotNet.CodeAnalysis", "s EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OffDotNet.CodeAnalysis.Tests", "tests\OffDotNet.CodeAnalysis.Tests\OffDotNet.CodeAnalysis.Tests.csproj", "{0D625237-2FE0-48E1-B71E-85020A8F0153}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OffDotNet.CodeAnalysis.Pdf", "src\OffDotNet.CodeAnalysis.Pdf\OffDotNet.CodeAnalysis.Pdf.csproj", "{07CBC834-454F-4272-B55C-4E0529971EDC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OffDotNet.CodeAnalysis.Pdf.Tests", "tests\OffDotNet.CodeAnalysis.Pdf.Tests\OffDotNet.CodeAnalysis.Pdf.Tests.csproj", "{8B36AFA6-CA16-49F7-8CAC-C37989EBD13F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -76,6 +80,14 @@ Global {0D625237-2FE0-48E1-B71E-85020A8F0153}.Debug|Any CPU.Build.0 = Debug|Any CPU {0D625237-2FE0-48E1-B71E-85020A8F0153}.Release|Any CPU.ActiveCfg = Release|Any CPU {0D625237-2FE0-48E1-B71E-85020A8F0153}.Release|Any CPU.Build.0 = Release|Any CPU + {07CBC834-454F-4272-B55C-4E0529971EDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07CBC834-454F-4272-B55C-4E0529971EDC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07CBC834-454F-4272-B55C-4E0529971EDC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07CBC834-454F-4272-B55C-4E0529971EDC}.Release|Any CPU.Build.0 = Release|Any CPU + {8B36AFA6-CA16-49F7-8CAC-C37989EBD13F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B36AFA6-CA16-49F7-8CAC-C37989EBD13F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B36AFA6-CA16-49F7-8CAC-C37989EBD13F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B36AFA6-CA16-49F7-8CAC-C37989EBD13F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -87,6 +99,8 @@ Global {293A6DEE-237E-45F8-B770-717F6846A913} = {D587B5E1-06B4-4CA7-8FE4-62B0DF71BAD6} {7712F911-485F-4DEE-8B16-2E8DF1C4FA0F} = {6BFF3E0A-DE9E-4854-99CF-C8CB861B3FBE} {0D625237-2FE0-48E1-B71E-85020A8F0153} = {9AD1AB50-CB0F-43D9-B721-DE1572FC6875} + {07CBC834-454F-4272-B55C-4E0529971EDC} = {6BFF3E0A-DE9E-4854-99CF-C8CB861B3FBE} + {8B36AFA6-CA16-49F7-8CAC-C37989EBD13F} = {9AD1AB50-CB0F-43D9-B721-DE1572FC6875} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1C5CCEAE-E0B4-4074-9291-2AAD38F9117F} diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs b/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs new file mode 100644 index 0000000..4e14e3a --- /dev/null +++ b/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf; + +using Microsoft.Extensions.DependencyInjection; +using OffDotNet.CodeAnalysis.Lexer; + +/// +/// Provides extension methods for registering code analysis services. +/// +public static class Dependencies +{ + /// + /// Adds the code analysis services to the specified . + /// + /// The service collection to which the services will be added. + /// The same service collection so that multiple calls can be chained. + public static IServiceCollection AddPdfCodeAnalysis(this IServiceCollection services) + { + services.AddCoreCodeAnalysis(); + services.AddSingleton(); + return services; + } +} diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Lexer/Lexer.cs b/src/OffDotNet.CodeAnalysis.Pdf/Lexer/Lexer.cs new file mode 100644 index 0000000..d65de7d --- /dev/null +++ b/src/OffDotNet.CodeAnalysis.Pdf/Lexer/Lexer.cs @@ -0,0 +1,12 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Lexer; + +using OffDotNet.CodeAnalysis.Lexer; + +public class Lexer : ILexer +{ +} diff --git a/src/OffDotNet.CodeAnalysis.Pdf/OffDotNet.CodeAnalysis.Pdf.csproj b/src/OffDotNet.CodeAnalysis.Pdf/OffDotNet.CodeAnalysis.Pdf.csproj new file mode 100644 index 0000000..cb7f008 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis.Pdf/OffDotNet.CodeAnalysis.Pdf.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxNode.cs b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxNode.cs new file mode 100644 index 0000000..037d76e --- /dev/null +++ b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxNode.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Syntax; + +using OffDotNet.CodeAnalysis.Syntax; + +internal abstract class RawSyntaxNode : AbstractNode +{ + protected RawSyntaxNode(SyntaxKind kind) + : base((ushort)kind) + { + Kind = kind; + } + + protected RawSyntaxNode(SyntaxKind kind, int fullWidth) + : base((ushort)kind, fullWidth) + { + Kind = kind; + } + + public SyntaxKind Kind { get; } + + public override string KindText => this.Kind.ToString(); + + public string Language => "PDF"; +} diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxTrivia.cs b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxTrivia.cs new file mode 100644 index 0000000..33fbfb0 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxTrivia.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Syntax; + +using OffDotNet.CodeAnalysis.Syntax; +using Utils; + +internal sealed class RawSyntaxTrivia : RawSyntaxNode +{ + internal RawSyntaxTrivia(SyntaxKind kind, ReadOnlySpan text) + : base(kind, text.Length) + { + Debug.Assert(kind.IsTrivia(), "Invalid trivia kind"); + Text = Encoding.ASCII.GetString(text); + } + + public string Text { get; } + + public override bool IsTrivia => true; + + internal override Option GetSlot(int index) + { + throw new NotImplementedException(); + } +} diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKind.cs b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKind.cs new file mode 100644 index 0000000..1383968 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKind.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Syntax; + +using OffDotNet.CodeAnalysis.Syntax; + +public enum SyntaxKind : ushort +{ + None = AbstractNode.NoneKind, + List = AbstractNode.ListKind, + + // Trivia + EndOfLineTrivia, + WhitespaceTrivia, + SingleLineCommentTrivia, +} diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKindFacts.cs b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKindFacts.cs new file mode 100644 index 0000000..dc040a0 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKindFacts.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Syntax; + +public static class SyntaxKindFacts +{ + public static bool IsTrivia(this SyntaxKind kind) + { + switch (kind) + { + case SyntaxKind.EndOfLineTrivia: + case SyntaxKind.WhitespaceTrivia: + case SyntaxKind.SingleLineCommentTrivia: + return true; + default: + return false; + } + } +} diff --git a/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json b/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json new file mode 100644 index 0000000..fe494a6 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json @@ -0,0 +1,45 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Direct", + "requested": "[8.0.1, )", + "resolved": "8.0.1", + "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" + }, + "Roslynator.Analyzers": { + "type": "Direct", + "requested": "[4.12.4, )", + "resolved": "4.12.4", + "contentHash": "isl8hAh7yFNjyBEC4YlTSi+xGBblqBUC/2MCMmnBPwuXPewb7XYnMRzT3vXbP/gOGwT8hZUOy1g/aRH3lAF/NQ==" + }, + "SonarAnalyzer.CSharp": { + "type": "Direct", + "requested": "[9.28.0.94264, )", + "resolved": "9.28.0.94264", + "contentHash": "P5ri/HgukP7KPoN/Lo0njq0d5IdUMpiqHoDVT3QRauwwCWXk0ACmC3FNtMR0l4X4TzfqmIZV+SeSLIDijNp6sQ==" + }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.556, )", + "resolved": "1.2.0-beta.556", + "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.556" + } + }, + "StyleCop.Analyzers.Unstable": { + "type": "Transitive", + "resolved": "1.2.0.556", + "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" + }, + "offdotnet.codeanalysis": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )" + } + } + } + } +} \ No newline at end of file diff --git a/src/OffDotNet.CodeAnalysis/Dependencies.cs b/src/OffDotNet.CodeAnalysis/Dependencies.cs index 3d399e1..5c68c6e 100644 --- a/src/OffDotNet.CodeAnalysis/Dependencies.cs +++ b/src/OffDotNet.CodeAnalysis/Dependencies.cs @@ -8,9 +8,17 @@ namespace OffDotNet.CodeAnalysis; using Lexer; using Microsoft.Extensions.DependencyInjection; +/// +/// Provides extension methods for registering code analysis services. +/// public static class Dependencies { - public static IServiceCollection AddCodeAnalysis(this IServiceCollection services) + /// + /// Adds the code analysis services to the specified . + /// + /// The service collection to which the services will be added. + /// The same service collection so that multiple calls can be chained. + public static IServiceCollection AddCoreCodeAnalysis(this IServiceCollection services) { services.AddSingleton(); return services; diff --git a/src/OffDotNet.CodeAnalysis/OffDotNet.CodeAnalysis.csproj b/src/OffDotNet.CodeAnalysis/OffDotNet.CodeAnalysis.csproj index bea9298..6962d5d 100644 --- a/src/OffDotNet.CodeAnalysis/OffDotNet.CodeAnalysis.csproj +++ b/src/OffDotNet.CodeAnalysis/OffDotNet.CodeAnalysis.csproj @@ -2,4 +2,10 @@ + + + + + + diff --git a/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs b/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs new file mode 100644 index 0000000..bff5f1e --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs @@ -0,0 +1,182 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Syntax; + +using Utils; + +[DebuggerDisplay("{GetDebuggerDisplay(), nq}")] +internal abstract class AbstractNode +{ + internal const int NoneKind = 0; + internal const int ListKind = 1; + + protected const int SlotCountTooLarge = 0b0000000000001111; + + private NodeFlagsAndSlotCount _nodeFlagsAndSlotCount; + + protected AbstractNode(ushort rawKind) + { + RawKind = rawKind; + } + + protected AbstractNode(ushort rawKind, int fullWidth) + { + RawKind = rawKind; + FullWidth = fullWidth; + } + + public ushort RawKind { get; } + + public abstract string KindText { get; } + + public virtual bool IsToken { get; } + + public virtual bool IsTrivia => false; + + public bool IsList => RawKind == ListKind; + + public bool HasLeadingTrivia => LeadingTriviaWidth != 0; + + public bool HasTrailingTrivia => LeadingTriviaWidth != 0; + + public int FullWidth { get; } + + public virtual int Width => FullWidth - LeadingTriviaWidth - TrailingTriviaWidth; + + public virtual Option Value => default; + + public virtual string ValueText => string.Empty; + + public virtual Option LeadingTrivia => default; + + public virtual Option TrailingTrivia => default; + + public virtual int LeadingTriviaWidth => this.FullWidth != 0 && this.GetFirstTerminal().IsSome(out var firstTerminal) + ? firstTerminal.LeadingTriviaWidth + : 0; + + public virtual int TrailingTriviaWidth => this.FullWidth != 0 && this.GetLastTerminal().IsSome(out var lastTerminal) + ? lastTerminal.TrailingTriviaWidth + : 0; + + public int SlotCount + { + get + { + var count = _nodeFlagsAndSlotCount.SmallSlotCount; + return count == SlotCountTooLarge ? GetSlotCount() : count; + } + + protected init + { + Debug.Assert(value <= byte.MaxValue, "Slot count is too large."); + _nodeFlagsAndSlotCount.SmallSlotCount = (byte)value; + } + } + + internal abstract Option GetSlot(int index); + + protected virtual int GetSlotCount() => 0; + + public virtual int GetSlotOffset(int index) + { + var offset = 0; + for (var i = 0; i < index; i++) + { + if (this.GetSlot(i).IsSome(out var slot)) + { + offset += slot.FullWidth; + } + } + + return offset; + } + + private Option GetFirstTerminal() + { + Option node = this; + + do + { + Option firstChild = default; + _ = node.IsSome(out var nodeValue); + Debug.Assert(nodeValue != null, "Node is null."); + + for (int i = 0, n = nodeValue.SlotCount; i < n; i++) + { + if (nodeValue.GetSlot(i).IsSome(out var child)) + { + firstChild = child; + break; + } + } + + node = firstChild; + } while (node.IsSome(out var nd) && nd._nodeFlagsAndSlotCount.SmallSlotCount > 0); + + return node; + } + + private Option GetLastTerminal() + { + Option node = this; + + do + { + Option lastChild = default; + _ = node.IsSome(out var nodeValue); + Debug.Assert(nodeValue != null, "Node is null."); + + for (var i = nodeValue.SlotCount - 1; i >= 0; i--) + { + if (nodeValue.GetSlot(i).IsSome(out var child)) + { + lastChild = child; + break; + } + } + + node = lastChild; + } while (node.IsSome(out var nd) && nd._nodeFlagsAndSlotCount.SmallSlotCount > 0); + + return node; + } + + private string GetDebuggerDisplay() + { + return this.GetType().Name + " " + this.KindText + " " + this; + } + + [StructLayout(LayoutKind.Auto)] + private struct NodeFlagsAndSlotCount + { + private const ushort SlotCountMask = 0b1111000000000000; + private const ushort NodeFlagsMask = 0b0000111111111111; + private const int SlotCountShift = 12; + + private ushort _data; + + public byte SmallSlotCount + { + readonly get + { + var shifted = _data >> SlotCountShift; + Debug.Assert(shifted <= SlotCountTooLarge, "Slot count is too large."); + return (byte)shifted; + } + + set + { + if (value > SlotCountTooLarge) + { + value = SlotCountTooLarge; + } + + _data = (ushort)((_data & NodeFlagsMask) | (value << SlotCountShift)); + } + } + } +} diff --git a/src/OffDotNet.CodeAnalysis/Utils/ExceptionUtilities.cs b/src/OffDotNet.CodeAnalysis/Utils/ExceptionUtilities.cs new file mode 100644 index 0000000..7bbe160 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Utils/ExceptionUtilities.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Utils; + +using System.Runtime.CompilerServices; + +internal static class ExceptionUtilities +{ + internal static Exception Unreachable([CallerFilePath] string? path = null, [CallerLineNumber] int line = 0) + => new InvalidOperationException($"This program location is thought to be unreachable. File='{path}' Line={line}"); +} diff --git a/src/OffDotNet.CodeAnalysis/packages.lock.json b/src/OffDotNet.CodeAnalysis/packages.lock.json index bcafec9..c916d16 100644 --- a/src/OffDotNet.CodeAnalysis/packages.lock.json +++ b/src/OffDotNet.CodeAnalysis/packages.lock.json @@ -16,9 +16,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.26.0.92422, )", - "resolved": "9.26.0.92422", - "contentHash": "xfyzQhAZ5Qyxkl/KkLUfvLQR6eMCXJG1bngvJNFypZYkuGU0Inb4lWi6I6HCgUnVCJSoafLRJzyWC0HYinhR3A==" + "requested": "[9.28.0.94264, )", + "resolved": "9.28.0.94264", + "contentHash": "P5ri/HgukP7KPoN/Lo0njq0d5IdUMpiqHoDVT3QRauwwCWXk0ACmC3FNtMR0l4X4TzfqmIZV+SeSLIDijNp6sQ==" }, "StyleCop.Analyzers": { "type": "Direct", diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/OffDotNet.CodeAnalysis.Pdf.Tests.csproj b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/OffDotNet.CodeAnalysis.Pdf.Tests.csproj new file mode 100644 index 0000000..8475a9c --- /dev/null +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/OffDotNet.CodeAnalysis.Pdf.Tests.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxNodeTests.cs b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxNodeTests.cs new file mode 100644 index 0000000..c12875c --- /dev/null +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxNodeTests.cs @@ -0,0 +1,133 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Tests.Syntax; + +using OffDotNet.CodeAnalysis.Pdf.Syntax; +using OffDotNet.CodeAnalysis.Syntax; + +[WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] +public class RawSyntaxNodeTests +{ + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"Class should inherit from {nameof(RawSyntaxNode)}")] + public void Class_ShouldInheritFromAbstractNode() + { + // Arrange + + // Act + var actual = Substitute.For(SyntaxKind.None); + + // Assert + Assert.IsAssignableFrom(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(RawSyntaxNode.RawKind)} property should the value passed to the constructor")] + [InlineData(SyntaxKind.None)] + [InlineData(SyntaxKind.List)] + public void RawKindProperty_ShouldReturnTheValuePassedToTheConstructor(SyntaxKind rawKind) + { + // Arrange + var rawNode = Substitute.For(rawKind); + + // Act + var kind = rawNode.RawKind; + + // Assert + Assert.Equal((int)rawKind, kind); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.IsToken)} property should return false by default")] + public void IsTokenProperty_ShouldReturnFalseByDefault() + { + // Arrange + var rawNode = Substitute.For(SyntaxKind.None); + + // Act + var isToken = rawNode.IsToken; + + // Assert + Assert.False(isToken); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.IsTrivia)} property should return false by default")] + public void IsTriviaProperty_ShouldReturnFalseByDefault() + { + // Arrange + var rawNode = Substitute.For(SyntaxKind.None); + + // Act + var isTrivia = rawNode.IsTrivia; + + // Assert + Assert.False(isTrivia); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(RawSyntaxNode.FullWidth)} property should return the value passed to the constructor")] + [InlineData(0)] + [InlineData(1)] + [InlineData(int.MaxValue)] + public void FullWidthProperty_ShouldReturnTheValuePassedToTheConstructor(int fullWidth) + { + // Arrange + var rawNode = Substitute.For(SyntaxKind.None, fullWidth); + + // Act + var width = rawNode.FullWidth; + + // Assert + Assert.Equal(fullWidth, width); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(RawSyntaxNode.Kind)} property should return the value passed to the constructor")] + [InlineData(SyntaxKind.None)] + [InlineData(SyntaxKind.List)] + public void KindProperty_ShouldReturnTheValuePassedToTheConstructor(SyntaxKind kind) + { + // Arrange + var rawNode = Substitute.For(kind); + + // Act + var actual = rawNode.Kind; + + // Assert + Assert.Equal(kind, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(RawSyntaxNode.KindText)} property should return the name of the syntax kind", Skip = "Not implemented")] + [InlineData(SyntaxKind.None, "None")] + [InlineData(SyntaxKind.List, "List")] + public void KindTextProperty_ShouldReturnTheNameOfTheSyntaxKind(SyntaxKind kind, string expected) + { + // Arrange + var rawNode = Substitute.For(kind); + + // Act + var kindText = rawNode.KindText; + + // Assert + Assert.Equal(expected, kindText); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.Language)} property should return PDF")] + public void LanguageProperty_ShouldReturnPDF() + { + // Arrange + var rawNode = Substitute.For(SyntaxKind.None); + + // Act + var language = rawNode.Language; + + // Assert + Assert.Equal("PDF", language); + } +} diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTriviaTests.cs b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTriviaTests.cs new file mode 100644 index 0000000..cfee016 --- /dev/null +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTriviaTests.cs @@ -0,0 +1,165 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Tests.Syntax; + +using OffDotNet.CodeAnalysis.Pdf.Syntax; +using OffDotNet.CodeAnalysis.Syntax; + +[WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] +public class RawSyntaxTriviaTests +{ + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"Class should inherit from {nameof(RawSyntaxTrivia)}")] + public void Class_ShouldInheritFromAbstractNode1() + { + // Arrange + + // Act + var actual = new RawSyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "u8); + + // Assert + Assert.IsAssignableFrom(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"Class should inherit from {nameof(RawSyntaxNode)}")] + public void Class_ShouldInheritFromAbstractNode2() + { + // Arrange + + // Act + var actual = new RawSyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "u8); + + // Assert + Assert.IsAssignableFrom(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(RawSyntaxTrivia.RawKind)} property should the value passed to the constructor")] + [InlineData(SyntaxKind.EndOfLineTrivia)] + [InlineData(SyntaxKind.WhitespaceTrivia)] + [InlineData(SyntaxKind.SingleLineCommentTrivia)] + public void RawKindProperty_ShouldReturnTheValuePassedToTheConstructor(SyntaxKind rawKind) + { + // Arrange + var rawNode = new RawSyntaxTrivia(rawKind, " "u8); + + // Act + var kind = rawNode.RawKind; + + // Assert + Assert.Equal((int)rawKind, kind); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxTrivia.IsToken)} property should return false by default")] + public void IsTokenProperty_ShouldReturnFalseByDefault() + { + // Arrange + var rawNode = new RawSyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "u8); + + // Act + var isToken = rawNode.IsToken; + + // Assert + Assert.False(isToken); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxTrivia.IsTrivia)} property should return true by default")] + public void IsTriviaProperty_ShouldReturnFalseByDefault() + { + // Arrange + var rawNode = new RawSyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "u8); + + // Act + var isTrivia = rawNode.IsTrivia; + + // Assert + Assert.True(isTrivia); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(RawSyntaxTrivia.FullWidth)} property should return the value passed to the constructor")] + [InlineData("\r")] + [InlineData("\n")] + [InlineData("\r\n")] + public void FullWidthProperty_ShouldReturnTheValuePassedToTheConstructor(string text) + { + // Arrange + ReadOnlySpan textSpan = Encoding.ASCII.GetBytes(text); + var rawNode = new RawSyntaxTrivia(SyntaxKind.EndOfLineTrivia, textSpan); + + // Act + var width = rawNode.FullWidth; + + // Assert + Assert.Equal(text.Length, width); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(RawSyntaxTrivia.Kind)} property should return the value passed to the constructor")] + [InlineData(SyntaxKind.EndOfLineTrivia)] + [InlineData(SyntaxKind.WhitespaceTrivia)] + [InlineData(SyntaxKind.SingleLineCommentTrivia)] + public void KindProperty_ShouldReturnTheValuePassedToTheConstructor(SyntaxKind kind) + { + // Arrange + var rawNode = new RawSyntaxTrivia(kind, " "u8); + + // Act + var actual = rawNode.Kind; + + // Assert + Assert.Equal(kind, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(RawSyntaxTrivia.KindText)} property should return the name of the syntax kind")] + [InlineData(SyntaxKind.EndOfLineTrivia, "EndOfLineTrivia")] + [InlineData(SyntaxKind.WhitespaceTrivia, "WhitespaceTrivia")] + [InlineData(SyntaxKind.SingleLineCommentTrivia, "SingleLineCommentTrivia")] + public void KindTextProperty_ShouldReturnTheNameOfTheSyntaxKind(SyntaxKind kind, string expected) + { + // Arrange + var rawNode = new RawSyntaxTrivia(kind, " "u8); + + // Act + var kindText = rawNode.KindText; + + // Assert + Assert.Equal(expected, kindText); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxTrivia.Language)} property should return PDF")] + public void LanguageProperty_ShouldReturnPDF() + { + // Arrange + var rawNode = new RawSyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "u8); + + // Act + var language = rawNode.Language; + + // Assert + Assert.Equal("PDF", language); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxTrivia.Text)} property should return the value passed to the constructor")] + public void TextProperty_ShouldReturnTheValuePassedToTheConstructor() + { + // Arrange + var text = " "u8; + var rawNode = new RawSyntaxTrivia(SyntaxKind.WhitespaceTrivia, text); + + // Act + var actual = rawNode.Text; + + // Assert + Assert.Equal(" ", actual); + } +} diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json new file mode 100644 index 0000000..56b5ff9 --- /dev/null +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json @@ -0,0 +1,194 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[6.0.2, )", + "resolved": "6.0.2", + "contentHash": "bJShQ6uWRTQ100ZeyiMqcFlhP7WJ+bCuabUs885dJiBEzMsJMSFr7BOyeCw4rgvQokteGi5rKQTlkhfQPUXg2A==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.10.0, )", + "resolved": "17.10.0", + "contentHash": "0/2HeACkaHEYU3wc83YlcD2Fi4LMtECJjqrtvw0lPi9DCEa35zSPt1j4fuvM8NagjDqJuh1Ja35WcRtn1Um6/A==", + "dependencies": { + "Microsoft.CodeCoverage": "17.10.0", + "Microsoft.TestPlatform.TestHost": "17.10.0" + } + }, + "NSubstitute": { + "type": "Direct", + "requested": "[5.1.0, )", + "resolved": "5.1.0", + "contentHash": "ZCqOP3Kpp2ea7QcLyjMU4wzE+0wmrMN35PQMsdPOHYc2IrvjmusG9hICOiqiOTPKN0gJon6wyCn6ZuGHdNs9hQ==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "NSubstitute.Analyzers.CSharp": { + "type": "Direct", + "requested": "[1.0.17, )", + "resolved": "1.0.17", + "contentHash": "Pwz0MD7CAM/G/fvJjM3ceOfI+S0IgjanHcK7evwyrW9qAWUG8fgiEXYfSX1/s3h2JUNDOw6ik0G8zp+RT61Y1g==" + }, + "Roslynator.Analyzers": { + "type": "Direct", + "requested": "[4.12.4, )", + "resolved": "4.12.4", + "contentHash": "isl8hAh7yFNjyBEC4YlTSi+xGBblqBUC/2MCMmnBPwuXPewb7XYnMRzT3vXbP/gOGwT8hZUOy1g/aRH3lAF/NQ==" + }, + "SonarAnalyzer.CSharp": { + "type": "Direct", + "requested": "[9.28.0.94264, )", + "resolved": "9.28.0.94264", + "contentHash": "P5ri/HgukP7KPoN/Lo0njq0d5IdUMpiqHoDVT3QRauwwCWXk0ACmC3FNtMR0l4X4TzfqmIZV+SeSLIDijNp6sQ==" + }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.556, )", + "resolved": "1.2.0-beta.556", + "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.556" + } + }, + "xunit": { + "type": "Direct", + "requested": "[2.8.1, )", + "resolved": "2.8.1", + "contentHash": "MLBz2NQp3rtSIoJdjj3DBEr/EeOFlQYF3oCCljat3DY9GQ7yYmtjIAv8Zyfm5BcwYso5sjvIe5scuHaJPVCGIQ==", + "dependencies": { + "xunit.analyzers": "1.14.0", + "xunit.assert": "2.8.1", + "xunit.core": "[2.8.1]" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[2.8.1, )", + "resolved": "2.8.1", + "contentHash": "qBTK0WAcnw65mymIjVDqWUTdqjMyzjwu9e9SF0oGYfYELgbcteDZ4fQLJaXw8mzkvpAD7YdoexBbg8VYQFkWWA==" + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.10.0", + "contentHash": "yC7oSlnR54XO5kOuHlVOKtxomNNN1BWXX8lK1G2jaPXT9sUok7kCOoA4Pgs0qyFaCtMrNsprztYMeoEGqCm4uA==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.10.0", + "contentHash": "KkwhjQevuDj0aBRoPLY6OLAhGqbPUEBuKLbaCs0kUVw29qiOYncdORd4mLVJbn9vGZ7/iFGQ/+AoJl0Tu5Umdg==", + "dependencies": { + "System.Reflection.Metadata": "1.6.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.10.0", + "contentHash": "LWpMdfqhHvcUkeMCvNYJO8QlPLlYz9XPPb+ZbaXIKhdmjAV0wqTSrTiW5FLaf7RRZT50AQADDOYMOe0HxDxNgA==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.10.0", + "Newtonsoft.Json": "13.0.1" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "StyleCop.Analyzers.Unstable": { + "type": "Transitive", + "resolved": "1.2.0.556", + "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "1.6.0", + "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" + }, + "xunit.abstractions": { + "type": "Transitive", + "resolved": "2.0.3", + "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.14.0", + "contentHash": "KcFBmV2150xZHPUebV3YLR5gGl8R4wLuPOoxMiwCf1L4bL8ls0dcwtGFzr6NvQRgg6dWgSqbE52I6SYyeB0VnQ==" + }, + "xunit.assert": { + "type": "Transitive", + "resolved": "2.8.1", + "contentHash": "DDM18ur+PeNFhQ4w/vO+uvCUy8hA3OS5+AMf/CFov9Wco7Le49zzj0hovRWwa8f/3vaUfjL5r+IkPvqEHu2IIg==" + }, + "xunit.core": { + "type": "Transitive", + "resolved": "2.8.1", + "contentHash": "Ng4Q/DOwotESPl5CufcdqgP6O2KDpdEcIvNfA3upzfCiBrkj5WsmLhf/XUsCVolzvHA7b1WUlyeTo7j1ulG4gQ==", + "dependencies": { + "xunit.extensibility.core": "[2.8.1]", + "xunit.extensibility.execution": "[2.8.1]" + } + }, + "xunit.extensibility.core": { + "type": "Transitive", + "resolved": "2.8.1", + "contentHash": "ilfAsxEhpne9AXXf3W+O65mRgGum94m2xHYm1yeJ1m7eiINM6OOwpaHhoNC/KWEQ2u/WF6/XiEs+Q0TOq7hiGA==", + "dependencies": { + "xunit.abstractions": "2.0.3" + } + }, + "xunit.extensibility.execution": { + "type": "Transitive", + "resolved": "2.8.1", + "contentHash": "38UnJW+64Wn8QIabujcNEw0HKvWw2AlYCgU8GNwCCDqyrSuRYb7zwetn7SHoHfbL9e9FAvEiAMXmc2wSUY8sVQ==", + "dependencies": { + "xunit.extensibility.core": "[2.8.1]" + } + }, + "offdotnet.codeanalysis": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )" + } + }, + "offdotnet.codeanalysis.pdf": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", + "OffDotNet.CodeAnalysis": "[1.0.0, )" + } + }, + "offdotnet.codeanalysis.tests": { + "type": "Project", + "dependencies": { + "Microsoft.NET.Test.Sdk": "[17.10.0, )", + "NSubstitute": "[5.1.0, )", + "OffDotNet.CodeAnalysis": "[1.0.0, )", + "xunit": "[2.8.1, )" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "CentralTransitive", + "requested": "[8.0.1, )", + "resolved": "8.0.1", + "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" + } + } + } +} \ No newline at end of file diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Syntax/AbstractNodeTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Syntax/AbstractNodeTests.cs new file mode 100644 index 0000000..f3f859d --- /dev/null +++ b/tests/OffDotNet.CodeAnalysis.Tests/Syntax/AbstractNodeTests.cs @@ -0,0 +1,549 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Tests.Syntax; + +using System.Diagnostics.CodeAnalysis; +using OffDotNet.CodeAnalysis.Syntax; +using OffDotNet.CodeAnalysis.Utils; + +[WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] +public class AbstractNodeTests +{ + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(AbstractNode.RawKind)} property should the value passed to the constructor")] + [InlineData(0)] + [InlineData(1)] + [InlineData(ushort.MaxValue)] + public void RawKindProperty_ShouldReturnTheValuePassedToTheConstructor(ushort rawKind) + { + // Arrange + var rawNode = new MockAbstractNode(rawKind); + + // Act + var kind = rawNode.RawKind; + + // Assert + Assert.Equal(rawKind, kind); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(AbstractNode.KindText)} property should return the name of the syntax kind")] + [InlineData(0, "RawKind0")] + [InlineData(1, "RawKind1")] + public void KindTextProperty_ShouldReturnTheNameOfTheSyntaxKind(ushort rawKind, string expected) + { + // Arrange + var rawNode = new MockAbstractNode(rawKind); + + // Act + var kindText = rawNode.KindText; + + // Assert + Assert.Equal(expected, kindText); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.IsToken)} property should return false by default")] + public void IsTokenProperty_ShouldReturnFalseByDefault() + { + // Arrange + const ushort RawKind = 0; + var rawNode = new MockAbstractNode(RawKind); + + // Act + var isToken = rawNode.IsToken; + + // Assert + Assert.False(isToken); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.IsTrivia)} property should return false by default")] + public void IsTriviaProperty_ShouldReturnFalseByDefault() + { + // Arrange + const ushort RawKind = 0; + var rawNode = new MockAbstractNode(RawKind); + + // Act + var isTrivia = rawNode.IsTrivia; + + // Assert + Assert.False(isTrivia); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = + $"{nameof(AbstractNode.IsList)} property should return true only if the {nameof(AbstractNode.RawKind)} is {nameof(AbstractNode.ListKind)}")] + [InlineData(0, false)] + [InlineData(1, true)] + public void IsListProperty_ShouldReturnTrueOnlyIfTheRawKindIsListKind(ushort rawKind, bool expected) + { + // Arrange + var rawNode = new MockAbstractNode(rawKind); + + // Act + var isList = rawNode.IsList; + + // Assert + Assert.Equal(expected, isList); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(AbstractNode.FullWidth)} property should return the value passed to the constructor")] + [InlineData(0)] + [InlineData(1)] + [InlineData(int.MaxValue)] + public void FullWidthProperty_ShouldReturnTheValuePassedToTheConstructor(int fullWidth) + { + // Arrange + var rawNode = new MockAbstractNode(0, fullWidth); + + // Act + var width = rawNode.FullWidth; + + // Assert + Assert.Equal(fullWidth, width); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = + $"{nameof(AbstractNode.Width)} property should exclude the {nameof(AbstractNode.LeadingTriviaWidth)} and {nameof(AbstractNode.TrailingTriviaWidth)}")] + public void WidthProperty_ShouldExcludeTheLeadingTriviaWidthAndTrailingTriviaWidth() + { + // Arrange + var rawNode = new MockAbstractNodeWithTwoSlots(0); + + // Act + var width = rawNode.Width; + + // Assert + Assert.Equal(12, width); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.SlotCount)} property should return 0 by default")] + public void SlotCountProperty_ShouldReturnZeroByDefault() + { + // Arrange + var rawNode = new MockAbstractNode(0); + + // Act + var slotCount = rawNode.SlotCount; + + // Assert + Assert.Equal(0, slotCount); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.SlotCount)} property should return the value passed to the constructor")] + public void SlotCountProperty_ShouldReturnTheValuePassedToTheConstructor() + { + // Arrange + var rawNode = new MockAbstractNodeWithTwoSlots(0); + + // Act + var slotCount = rawNode.SlotCount; + + // Assert + Assert.Equal(2, slotCount); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.SlotCount)} property should return the large slot count if the slot count is too large")] + public void SlotCountProperty_ShouldReturnTheLargeSlotCountIfTheSlotCountIsTooLarge() + { + // Arrange + var rawNode = new MockAbstractNodeWithManySlots(0); + + // Act + var slotCount = rawNode.SlotCount; + + // Assert + Assert.Equal(255, slotCount); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.GetSlotOffset)} method with index=0 should return 0")] + public void GetSlotOffsetMethod_WithIndex0_ShouldReturn0() + { + // Arrange + var rawNode = new MockAbstractNode(0); + + // Act + var actual = rawNode.GetSlotOffset(0); + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.GetSlotOffset)} method with index=1 and the slot does not exist should return 0")] + public void GetSlotOffsetMethod_WithIndex1AndTheSlotDoesNotExist_ShouldReturn0() + { + // Arrange + var rawNode = new MockAbstractNode(0); + + // Act + var actual = rawNode.GetSlotOffset(1); + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = + $"{nameof(AbstractNode.GetSlotOffset)} method with index=1 and the slot exists should return the {nameof(AbstractNode.FullWidth)} of the slot")] + public void GetSlotOffsetMethod_WithIndex1AndTheSlotExists_ShouldReturnTheFullWidthOfTheSlot() + { + // Arrange + var rawNode = new MockAbstractNodeWithTwoSlots(0); + + // Act + var actual = rawNode.GetSlotOffset(1); + + // Assert + Assert.Equal(14, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.LeadingTriviaWidth)} property should return 0 by default")] + public void LeadingTriviaWidthProperty_ShouldReturn0ByDefault() + { + // Arrange + var rawNode = new MockAbstractNode(0); + + // Act + var actual = rawNode.LeadingTriviaWidth; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.LeadingTriviaWidth)} property should return 0 if the node itself is a terminal node")] + public void LeadingTriviaWidthProperty_WithTerminalNode_ShouldReturn0() + { + // Arrange + const int FullWidth = 10; + var rawNode = Substitute.ForPartsOf((ushort)0, FullWidth); + + // Act + var actual = rawNode.LeadingTriviaWidth; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = + $"{nameof(AbstractNode.LeadingTriviaWidth)} property should return the {nameof(AbstractNode.LeadingTriviaWidth)} of the first terminal node")] + public void LeadingTriviaWidthProperty_WithNonTerminalNode_ShouldReturnTheLeadingTriviaWidthOfTheFirstTerminalNode() + { + // Arrange + var rawNode = new MockAbstractNodeWithTwoSlots(0); + + // Act + var actual = rawNode.LeadingTriviaWidth; + + // Assert + Assert.Equal(14, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.TrailingTriviaWidth)} property should return 0 by default")] + public void TrailingTriviaWidthProperty_ShouldReturn0ByDefault() + { + // Arrange + var rawNode = new MockAbstractNode(0); + + // Act + var actual = rawNode.TrailingTriviaWidth; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.TrailingTriviaWidth)} property should return 0 if the node itself is a terminal node")] + public void TrailingTriviaWidthProperty_WithTerminalNode_ShouldReturn0() + { + // Arrange + const int FullWidth = 10; + var rawNode = Substitute.ForPartsOf((ushort)0, FullWidth); + + // Act + var actual = rawNode.TrailingTriviaWidth; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = + $"{nameof(AbstractNode.TrailingTriviaWidth)} property should return the {nameof(AbstractNode.TrailingTriviaWidth)} of the last terminal node")] + public void TrailingTriviaWidthProperty_WithNonTerminalNode_ShouldReturnTheTrailingTriviaWidthOfTheLastTerminalNode() + { + // Arrange + var rawNode = new MockAbstractNodeWithTwoSlots(0); + + // Act + var actual = rawNode.TrailingTriviaWidth; + + // Assert + Assert.Equal(10, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.HasLeadingTrivia)} property should return false by default")] + public void HasLeadingTriviaProperty_ShouldReturnFalseByDefault() + { + // Arrange + var rawNode = new MockAbstractNode(0); + + // Act + var actual = rawNode.HasLeadingTrivia; + + // Assert + Assert.False(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.HasLeadingTrivia)} property should return true if the {nameof(AbstractNode.LeadingTriviaWidth)} is not 0")] + public void HasLeadingTriviaProperty_WithLeadingTrivia_ShouldReturnTrue() + { + // Arrange + var rawNode = new MockAbstractNodeWithTwoSlots(0); + + // Act + var actual = rawNode.HasLeadingTrivia; + + // Assert + Assert.True(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.HasTrailingTrivia)} property should return false by default")] + public void HasTrailingTriviaProperty_ShouldReturnFalseByDefault() + { + // Arrange + var rawNode = new MockAbstractNode(0); + + // Act + var actual = rawNode.HasTrailingTrivia; + + // Assert + Assert.False(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.HasTrailingTrivia)} property should return true if the {nameof(AbstractNode.TrailingTriviaWidth)} is not 0")] + public void HasTrailingTriviaProperty_WithTrailingTrivia_ShouldReturnTrue() + { + // Arrange + var rawNode = new MockAbstractNodeWithTwoSlots(0); + + // Act + var actual = rawNode.HasTrailingTrivia; + + // Assert + Assert.True(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.LeadingTrivia)} property should return {nameof(Option.None)} by default")] + public void LeadingTriviaProperty_ShouldReturnNoneByDefault() + { + // Arrange + var rawNode = new MockAbstractNode(0); + + // Act + var actual = rawNode.LeadingTrivia.IsSome(out _); + + // Assert + Assert.False(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.TrailingTrivia)} property should return {nameof(Option.None)} by default")] + public void TrailingTriviaProperty_ShouldReturnNoneByDefault() + { + // Arrange + var rawNode = new MockAbstractNode(0); + + // Act + var actual = rawNode.TrailingTrivia.IsSome(out _); + + // Assert + Assert.False(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.Value)} property should return {nameof(Option.None)} by default")] + public void ValueProperty_ShouldReturnNoneByDefault() + { + // Arrange + var rawNode = new MockAbstractNode(0); + + // Act + var actual = rawNode.Value.IsSome(out _); + + // Assert + Assert.False(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.Value)} property should return the populated value")] + public void ValueProperty_ShouldReturnThePopulatedValue() + { + // Arrange + var rawNode = new MockTerminalNode(0); + object expected = "Node0"; + + // Act + var actual = rawNode.Value; + + // Assert + Assert.True(actual.IsSome(out var actualValue)); + Assert.Equal(expected, actualValue); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.ValueText)} should return an empty string by default")] + public void ValueTextProperty_ShouldReturnAnEmptyStringByDefault() + { + // Arrange + var rawNode = new MockAbstractNode(0); + + // Act + var actual = rawNode.ValueText; + + // Assert + Assert.Equal(string.Empty, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.ValueText)} property should return the populated value")] + public void ValueTextProperty_ShouldReturnThePopulatedValue() + { + // Arrange + var rawNode = new MockTerminalNode(0); + const string Expected = "Node0"; + + // Act + var actual = rawNode.ValueText; + + // Assert + Assert.Equal(Expected, actual); + } +} + +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Test classes are small.")] +internal class MockAbstractNode : AbstractNode +{ + public MockAbstractNode(ushort rawKind) + : base(rawKind) + { + KindText = $"RawKind{rawKind}"; + } + + public MockAbstractNode(ushort rawKind, int fullWidth) + : base(rawKind, fullWidth) + { + KindText = $"RawKind{rawKind}"; + } + + public override string KindText { get; } + + internal override Option GetSlot(int index) + { + return Option.None; + } +} + +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Test classes are small.")] +internal class MockAbstractNodeWithTwoSlots : AbstractNode +{ + public MockAbstractNodeWithTwoSlots(ushort rawKind) + : this(rawKind, 36) + { + } + + private MockAbstractNodeWithTwoSlots(ushort rawKind, int fullWidth) + : base(rawKind, fullWidth) + { + KindText = $"{RawKind}{rawKind}"; + SlotCount = 2; + } + + public override string KindText { get; } + + internal override Option GetSlot(int index) + { + return index switch + { + 0 => Option.Some(new MockTerminalNode(1, 14)), + 1 => Option.Some(new MockTerminalNode(2, 10)), + _ => Option.None, + }; + } +} + +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Test classes are small.")] +internal class MockAbstractNodeWithManySlots : AbstractNode +{ + public MockAbstractNodeWithManySlots(ushort rawKind) + : this(rawKind, 14) + { + } + + private MockAbstractNodeWithManySlots(ushort rawKind, int fullWidth) + : base(rawKind, fullWidth) + { + KindText = $"{RawKind}{rawKind}"; + SlotCount = 16; + } + + public override string KindText { get; } + + internal override Option GetSlot(int index) + { + return index switch + { + _ => Option.None, + }; + } + + protected override int GetSlotCount() => 255; +} + +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Test classes are small.")] +internal class MockTerminalNode : AbstractNode +{ + public MockTerminalNode(ushort rawKind) + : base(rawKind) + { + KindText = $"RawKind{rawKind}"; + } + + public MockTerminalNode(ushort rawKind, int fullWidth) + : base(rawKind, fullWidth) + { + KindText = $"RawKind{rawKind}"; + } + + public override string KindText { get; } + + public override Option Value => ValueText; + + public override string ValueText => $"Node{RawKind}"; + + public override int LeadingTriviaWidth => FullWidth; + + public override int TrailingTriviaWidth => FullWidth; + + internal override Option GetSlot(int index) => Option.None; +} diff --git a/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json b/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json index 1596b3d..aeedcc2 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json +++ b/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json @@ -41,9 +41,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.26.0.92422, )", - "resolved": "9.26.0.92422", - "contentHash": "xfyzQhAZ5Qyxkl/KkLUfvLQR6eMCXJG1bngvJNFypZYkuGU0Inb4lWi6I6HCgUnVCJSoafLRJzyWC0HYinhR3A==" + "requested": "[9.28.0.94264, )", + "resolved": "9.28.0.94264", + "contentHash": "P5ri/HgukP7KPoN/Lo0njq0d5IdUMpiqHoDVT3QRauwwCWXk0ACmC3FNtMR0l4X4TzfqmIZV+SeSLIDijNp6sQ==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -167,6 +167,13 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )" } }, + "offdotnet.codeanalysis.pdf": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", + "OffDotNet.CodeAnalysis": "[1.0.0, )" + } + }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "CentralTransitive", "requested": "[8.0.1, )", From e978aaaaf909dee22356584d03b162e4b5ed5e7a Mon Sep 17 00:00:00 2001 From: Victor Pogor Date: Sat, 6 Jul 2024 13:25:41 +1000 Subject: [PATCH 03/14] added more logic in abstract node --- src/Directory.Build.props | 4 + src/GlobalUsings.cs | 3 + .../packages.lock.json | 9 +- .../PooledObjects/SharedObjectPools.cs | 38 +++++ .../PooledObjects/StackPooledObjectPolicy.cs | 53 +++++++ .../Syntax/AbstractNode.cs | 139 +++++++++++++++++- src/OffDotNet.CodeAnalysis/packages.lock.json | 6 + .../packages.lock.json | 10 +- .../Syntax/AbstractNodeTests.cs | 103 ++++++++++++- .../packages.lock.json | 14 +- 10 files changed, 356 insertions(+), 23 deletions(-) create mode 100644 src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs create mode 100644 src/OffDotNet.CodeAnalysis/PooledObjects/StackPooledObjectPolicy.cs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 758bc4c..e138c98 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -8,5 +8,9 @@ + + + + diff --git a/src/GlobalUsings.cs b/src/GlobalUsings.cs index 9bb8d54..f7df012 100644 --- a/src/GlobalUsings.cs +++ b/src/GlobalUsings.cs @@ -6,6 +6,9 @@ #pragma warning disable SA1200 global using System.Buffers; global using System.Diagnostics; +global using System.Diagnostics.CodeAnalysis; +global using System.Globalization; global using System.Runtime.InteropServices; global using System.Text; + #pragma warning restore SA1200 diff --git a/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json b/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json index fe494a6..a360c25 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json +++ b/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json @@ -8,6 +8,12 @@ "resolved": "8.0.1", "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" }, + "Microsoft.Extensions.ObjectPool": { + "type": "Direct", + "requested": "[8.0.6, )", + "resolved": "8.0.6", + "contentHash": "p/wKtBfxVIP0RVQ9Pmtb3In8rnxWmxetZTiUzREtomnhi+y12I+JJMjAY3eiCd2Mhx+0U060ahP+9Ve/cjEmoA==" + }, "Roslynator.Analyzers": { "type": "Direct", "requested": "[4.12.4, )", @@ -37,7 +43,8 @@ "offdotnet.codeanalysis": { "type": "Project", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )" + "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", + "Microsoft.Extensions.ObjectPool": "[8.0.6, )" } } } diff --git a/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs b/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs new file mode 100644 index 0000000..d880c1c --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +#pragma warning disable CS0618 // Type or member is obsolete +namespace OffDotNet.CodeAnalysis.PooledObjects; + +using Microsoft.Extensions.ObjectPool; +using AbstractNodesCacheTuple = (Syntax.AbstractNode Node, bool Leading, bool Trailing); + +internal static class SharedObjectPools +{ + /// The abstract nodes cache. + internal static readonly ObjectPool> AbstractNodesCache; + + /// The string builder pool. + internal static readonly ObjectPool StringBuilderPool; + + static SharedObjectPools() + { + var defaultObjectPoolProvider = new DefaultObjectPoolProvider(); + var defaultStringBuilderPoolProvider = new DefaultObjectPoolProvider { MaximumRetained = 32, }; + +#if DEBUG + var objectPoolProvider = new LeakTrackingObjectPoolProvider(defaultObjectPoolProvider); + var stringBuilderPoolProvider = new LeakTrackingObjectPoolProvider(defaultStringBuilderPoolProvider); +#else + var objectPoolProvider = defaultObjectPoolProvider; + var stringBuilderPoolProvider = defaultStringBuilderPoolProvider; +#endif + + var stackPooledObjectPolicy = new StackPooledObjectPolicy(initialCapacity: 2, maximumRetainedCapacity: 16); + + AbstractNodesCache = objectPoolProvider.Create(stackPooledObjectPolicy); + StringBuilderPool = stringBuilderPoolProvider.CreateStringBuilderPool(); + } +} diff --git a/src/OffDotNet.CodeAnalysis/PooledObjects/StackPooledObjectPolicy.cs b/src/OffDotNet.CodeAnalysis/PooledObjects/StackPooledObjectPolicy.cs new file mode 100644 index 0000000..8c9c572 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/PooledObjects/StackPooledObjectPolicy.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.PooledObjects; + +using Microsoft.Extensions.ObjectPool; + +internal sealed class StackPooledObjectPolicy : PooledObjectPolicy> +{ + public StackPooledObjectPolicy() + { + } + + public StackPooledObjectPolicy(int initialCapacity, int maximumRetainedCapacity) + { + InitialCapacity = initialCapacity; + MaximumRetainedCapacity = maximumRetainedCapacity; + } + + /// + /// Gets the initial capacity of pooled byte array instances. + /// + /// Defaults to 8. + public int InitialCapacity { get; init; } = 8; + + /// + /// Gets the maximum capacity of a single byte array instance that is allowed to be + /// retained, when is invoked. + /// + /// Defaults to 128. + public int MaximumRetainedCapacity { get; init; } = 128; + + /// + public override Stack Create() + { + return new Stack(capacity: InitialCapacity); + } + + /// + public override bool Return(Stack obj) + { + if (obj.Count > this.MaximumRetainedCapacity) + { + // Too big. Discard this one. + return false; + } + + obj.Clear(); + return true; + } +} diff --git a/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs b/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs index bff5f1e..5cfb2f5 100644 --- a/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs +++ b/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs @@ -5,63 +5,91 @@ namespace OffDotNet.CodeAnalysis.Syntax; +using PooledObjects; using Utils; +using AbstractNodesCacheTuple = (AbstractNode Node, bool Leading, bool Trailing); +/// Represents the abstract base class for all terminal and non-terminal nodes in the syntax tree. [DebuggerDisplay("{GetDebuggerDisplay(), nq}")] internal abstract class AbstractNode { + /// Represents a kind of node that does not exist. internal const int NoneKind = 0; + + /// Represents a kind of node that is a list. internal const int ListKind = 1; + /// Indicates that the slot count is too large. protected const int SlotCountTooLarge = 0b0000000000001111; - private NodeFlagsAndSlotCount _nodeFlagsAndSlotCount; + private readonly NodeFlagsAndSlotCount _nodeFlagsAndSlotCount; + /// Initializes a new instance of the class with the specified raw kind. + /// The raw kind of the node. protected AbstractNode(ushort rawKind) { RawKind = rawKind; } + /// Initializes a new instance of the class with the specified raw kind and full width. + /// The raw kind of the node. + /// The full width of the node. protected AbstractNode(ushort rawKind, int fullWidth) { RawKind = rawKind; FullWidth = fullWidth; } + /// Gets the raw kind of the node. public ushort RawKind { get; } + /// Gets the kind text of the node. public abstract string KindText { get; } - public virtual bool IsToken { get; } + /// Gets a value indicating whether the node is a token. + public virtual bool IsToken => false; + /// Gets a value indicating whether the node is trivia. public virtual bool IsTrivia => false; + /// Gets a value indicating whether the node is a list. public bool IsList => RawKind == ListKind; + /// Gets a value indicating whether the node has leading trivia. public bool HasLeadingTrivia => LeadingTriviaWidth != 0; - public bool HasTrailingTrivia => LeadingTriviaWidth != 0; + /// Gets a value indicating whether the node has trailing trivia. + public bool HasTrailingTrivia => TrailingTriviaWidth != 0; + /// Gets the full width of the node. public int FullWidth { get; } + /// Gets the width of the node, excluding leading and trailing trivia. public virtual int Width => FullWidth - LeadingTriviaWidth - TrailingTriviaWidth; + /// Gets the value of the node. public virtual Option Value => default; + /// Gets the text value of the node. public virtual string ValueText => string.Empty; + /// Gets the leading trivia of the node. public virtual Option LeadingTrivia => default; + /// Gets the trailing trivia of the node. public virtual Option TrailingTrivia => default; + /// Gets the width of the leading trivia. public virtual int LeadingTriviaWidth => this.FullWidth != 0 && this.GetFirstTerminal().IsSome(out var firstTerminal) ? firstTerminal.LeadingTriviaWidth : 0; + /// Gets the width of the trailing trivia. public virtual int TrailingTriviaWidth => this.FullWidth != 0 && this.GetLastTerminal().IsSome(out var lastTerminal) ? lastTerminal.TrailingTriviaWidth : 0; + /// Gets the number of slots in the node. public int SlotCount { get @@ -77,10 +105,9 @@ protected init } } - internal abstract Option GetSlot(int index); - - protected virtual int GetSlotCount() => 0; - + /// Gets the offset of the specified slot. + /// The index of the slot. + /// The offset of the slot. public virtual int GetSlotOffset(int index) { var offset = 0; @@ -95,6 +122,97 @@ public virtual int GetSlotOffset(int index) return offset; } + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + { + var stringBuilder = SharedObjectPools.StringBuilderPool.Get(); + using var writer = new StringWriter(stringBuilder, CultureInfo.InvariantCulture); + + WriteTo(writer, leading: false, trailing: false); + + var result = stringBuilder.ToString(); + SharedObjectPools.StringBuilderPool.Return(stringBuilder); + return result; + } + + /// Returns a full string representation of the node, including leading and trailing trivia. + /// A full string representation of the node. + public virtual string ToFullString() + { + var stringBuilder = SharedObjectPools.StringBuilderPool.Get(); + using var writer = new StringWriter(stringBuilder, CultureInfo.InvariantCulture); + + WriteTo(writer, leading: true, trailing: true); + + var result = stringBuilder.ToString(); + SharedObjectPools.StringBuilderPool.Return(stringBuilder); + return result; + } + + /// Gets the slot at the specified index. + /// The index of the slot. + /// The slot at the specified index. + internal abstract Option GetSlot(int index); + + /// Gets the number of slots in the node. + /// The number of slots. + protected virtual int GetSlotCount() => 0; + + /// Writes the trivia of the node to the specified writer. + /// The writer to which the trivia will be written. + protected virtual void WriteTriviaTo(TextWriter writer) + { + Debug.Fail("Should not get here."); + } + + /// Writes the token of the node to the specified writer. + /// The writer to which the token will be written. + /// Indicates whether to include leading trivia. + /// Indicates whether to include trailing trivia. + protected virtual void WriteTokenTo(TextWriter writer, bool leading, bool trailing) + { + Debug.Fail("Should not get here."); + } + + /// Writes the node to the specified writer. + /// The writer to which the node will be written. + /// Indicates whether to include leading trivia. + /// Indicates whether to include trailing trivia. + [SuppressMessage("Minor Code Smell", "S3626:Jump statements should not be redundant", Justification = "False positive.")] + protected void WriteTo(TextWriter writer, bool leading, bool trailing) + { + var stack = SharedObjectPools.AbstractNodesCache.Get(); + stack.Push((this, leading, trailing)); + + ProcessStack(writer, stack); + SharedObjectPools.AbstractNodesCache.Return(stack); + return; + + static void ProcessStack(TextWriter writer, Stack stack) + { + while (stack.Count > 0) + { + var currentTuple = stack.Pop(); + + if (currentTuple.Node.IsTrivia) + { + currentTuple.Node.WriteTriviaTo(writer); + continue; + } + + if (currentTuple.Node.IsToken) + { + currentTuple.Node.WriteTokenTo(writer, currentTuple.Leading, currentTuple.Trailing); + } + + // TODO: handle nested nodes + } + } + } + + /// Gets the first terminal node in the tree. + /// The first terminal node. private Option GetFirstTerminal() { Option node = this; @@ -120,6 +238,8 @@ private Option GetFirstTerminal() return node; } + /// Gets the last terminal node in the tree. + /// The last terminal node. private Option GetLastTerminal() { Option node = this; @@ -145,6 +265,8 @@ private Option GetLastTerminal() return node; } + /// Gets the debugger display string for the node. + /// The debugger display string. private string GetDebuggerDisplay() { return this.GetType().Name + " " + this.KindText + " " + this; @@ -159,6 +281,9 @@ private struct NodeFlagsAndSlotCount private ushort _data; + /// + /// Gets or sets the small slot count. + /// public byte SmallSlotCount { readonly get diff --git a/src/OffDotNet.CodeAnalysis/packages.lock.json b/src/OffDotNet.CodeAnalysis/packages.lock.json index c916d16..77178e3 100644 --- a/src/OffDotNet.CodeAnalysis/packages.lock.json +++ b/src/OffDotNet.CodeAnalysis/packages.lock.json @@ -8,6 +8,12 @@ "resolved": "8.0.1", "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" }, + "Microsoft.Extensions.ObjectPool": { + "type": "Direct", + "requested": "[8.0.6, )", + "resolved": "8.0.6", + "contentHash": "p/wKtBfxVIP0RVQ9Pmtb3In8rnxWmxetZTiUzREtomnhi+y12I+JJMjAY3eiCd2Mhx+0U060ahP+9Ve/cjEmoA==" + }, "Roslynator.Analyzers": { "type": "Direct", "requested": "[4.12.4, )", diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json index 56b5ff9..4feaafb 100644 --- a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json @@ -164,13 +164,15 @@ "offdotnet.codeanalysis": { "type": "Project", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )" + "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", + "Microsoft.Extensions.ObjectPool": "[8.0.6, )" } }, "offdotnet.codeanalysis.pdf": { "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", + "Microsoft.Extensions.ObjectPool": "[8.0.6, )", "OffDotNet.CodeAnalysis": "[1.0.0, )" } }, @@ -188,6 +190,12 @@ "requested": "[8.0.1, )", "resolved": "8.0.1", "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" + }, + "Microsoft.Extensions.ObjectPool": { + "type": "CentralTransitive", + "requested": "[8.0.6, )", + "resolved": "8.0.6", + "contentHash": "p/wKtBfxVIP0RVQ9Pmtb3In8rnxWmxetZTiUzREtomnhi+y12I+JJMjAY3eiCd2Mhx+0U060ahP+9Ve/cjEmoA==" } } } diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Syntax/AbstractNodeTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Syntax/AbstractNodeTests.cs index f3f859d..5354833 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/Syntax/AbstractNodeTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Tests/Syntax/AbstractNodeTests.cs @@ -400,7 +400,7 @@ public void ValueProperty_ShouldReturnNoneByDefault() public void ValueProperty_ShouldReturnThePopulatedValue() { // Arrange - var rawNode = new MockTerminalNode(0); + var rawNode = new MockSyntaxNode(0); object expected = "Node0"; // Act @@ -430,7 +430,7 @@ public void ValueTextProperty_ShouldReturnAnEmptyStringByDefault() public void ValueTextProperty_ShouldReturnThePopulatedValue() { // Arrange - var rawNode = new MockTerminalNode(0); + var rawNode = new MockSyntaxNode(0); const string Expected = "Node0"; // Act @@ -439,6 +439,51 @@ public void ValueTextProperty_ShouldReturnThePopulatedValue() // Assert Assert.Equal(Expected, actual); } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(ToString)} method should return the text when the node is a trivia node")] + public void ToStringMethod_WithTriviaNode_ShouldReturnTheText() + { + // Arrange + const string Text = "RawKind0"; + var rawNode = new MockTriviaNode(0, 2); + + // Act + var actual = rawNode.ToString(); + + // Assert + Assert.Equal(Text, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(ToString)} method should return the text when the node is a token node")] + public void ToStringMethod_WithTokenNode_ShouldReturnTheText() + { + // Arrange + const string Text = "RawKind0"; + var rawNode = new MockSyntaxNode(0, 2); + + // Act + var actual = rawNode.ToString(); + + // Assert + Assert.Equal(Text, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractNode.ToFullString)} method should return the text when the node is a token node and has leading & trailing trivia")] + public void ToFullStringMethod_WithTokenNodeAndLeadingTrivia_ShouldReturnTheText() + { + // Arrange + const string Text = "RawKind0"; + var rawNode = new MockSyntaxNode(0, 2); + + // Act + var actual = rawNode.ToFullString(); + + // Assert + Assert.Equal(Text, actual); + } } [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Test classes are small.")] @@ -485,8 +530,8 @@ internal override Option GetSlot(int index) { return index switch { - 0 => Option.Some(new MockTerminalNode(1, 14)), - 1 => Option.Some(new MockTerminalNode(2, 10)), + 0 => Option.Some(new MockSyntaxNode(1, 14)), + 1 => Option.Some(new MockSyntaxNode(2, 10)), _ => Option.None, }; } @@ -521,15 +566,15 @@ internal override Option GetSlot(int index) } [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Test classes are small.")] -internal class MockTerminalNode : AbstractNode +internal class MockSyntaxNode : AbstractNode { - public MockTerminalNode(ushort rawKind) + public MockSyntaxNode(ushort rawKind) : base(rawKind) { KindText = $"RawKind{rawKind}"; } - public MockTerminalNode(ushort rawKind, int fullWidth) + public MockSyntaxNode(ushort rawKind, int fullWidth) : base(rawKind, fullWidth) { KindText = $"RawKind{rawKind}"; @@ -541,9 +586,53 @@ public MockTerminalNode(ushort rawKind, int fullWidth) public override string ValueText => $"Node{RawKind}"; + public override bool IsToken => true; + public override int LeadingTriviaWidth => FullWidth; public override int TrailingTriviaWidth => FullWidth; internal override Option GetSlot(int index) => Option.None; + + protected override void WriteTokenTo(TextWriter writer, bool leading, bool trailing) + { + if (leading) + { + writer.Write(""); + } + + writer.Write(KindText); + + if (trailing) + { + writer.Write(""); + } + } +} + +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Test classes are small.")] +internal class MockTriviaNode : AbstractNode +{ + public MockTriviaNode(ushort rawKind) + : base(rawKind) + { + KindText = $"RawKind{rawKind}"; + } + + public MockTriviaNode(ushort rawKind, int fullWidth) + : base(rawKind, fullWidth) + { + KindText = $"RawKind{rawKind}"; + } + + public override string KindText { get; } + + public override bool IsTrivia => true; + + internal override Option GetSlot(int index) => Option.None; + + protected override void WriteTriviaTo(TextWriter writer) + { + writer.Write(KindText); + } } diff --git a/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json b/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json index aeedcc2..1595069 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json +++ b/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json @@ -162,16 +162,10 @@ } }, "offdotnet.codeanalysis": { - "type": "Project", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )" - } - }, - "offdotnet.codeanalysis.pdf": { "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", - "OffDotNet.CodeAnalysis": "[1.0.0, )" + "Microsoft.Extensions.ObjectPool": "[8.0.6, )" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { @@ -179,6 +173,12 @@ "requested": "[8.0.1, )", "resolved": "8.0.1", "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" + }, + "Microsoft.Extensions.ObjectPool": { + "type": "CentralTransitive", + "requested": "[8.0.6, )", + "resolved": "8.0.6", + "contentHash": "p/wKtBfxVIP0RVQ9Pmtb3In8rnxWmxetZTiUzREtomnhi+y12I+JJMjAY3eiCd2Mhx+0U060ahP+9Ve/cjEmoA==" } } } From e409ed3d9e6860985239b9ccdd253a377d38c3c4 Mon Sep 17 00:00:00 2001 From: Victor Pogor Date: Sat, 20 Jul 2024 12:20:28 +1000 Subject: [PATCH 04/14] updated the packages --- Directory.Packages.props | 8 +-- .../packages.lock.json | 14 ++--- src/OffDotNet.CodeAnalysis/packages.lock.json | 12 ++-- .../packages.lock.json | 62 +++++++++---------- .../packages.lock.json | 58 ++++++++--------- 5 files changed, 77 insertions(+), 77 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index eecdd7a..25ed3a9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,7 @@ - + @@ -9,7 +9,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -25,8 +25,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json b/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json index a360c25..23d3b7e 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json +++ b/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.Extensions.ObjectPool": { "type": "Direct", - "requested": "[8.0.6, )", - "resolved": "8.0.6", - "contentHash": "p/wKtBfxVIP0RVQ9Pmtb3In8rnxWmxetZTiUzREtomnhi+y12I+JJMjAY3eiCd2Mhx+0U060ahP+9Ve/cjEmoA==" + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "2yLweyqmpuuFSRo+I3sLHMxmnAgNcI537kBJiyv49U2ZEqo00jZcG8lrnD8uCiOJp9IklYyTZULtbsXoFVzsjQ==" }, "Roslynator.Analyzers": { "type": "Direct", @@ -22,9 +22,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.28.0.94264, )", - "resolved": "9.28.0.94264", - "contentHash": "P5ri/HgukP7KPoN/Lo0njq0d5IdUMpiqHoDVT3QRauwwCWXk0ACmC3FNtMR0l4X4TzfqmIZV+SeSLIDijNp6sQ==" + "requested": "[9.29.0.95321, )", + "resolved": "9.29.0.95321", + "contentHash": "dAvGL7gavcUYk83p1StVzvMFcbwN4hr08hjthsRr/wR/uPFgjd7LUn7QGN/1iH92aA7P72x06RRNtdUAqmVHdw==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -44,7 +44,7 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", - "Microsoft.Extensions.ObjectPool": "[8.0.6, )" + "Microsoft.Extensions.ObjectPool": "[8.0.7, )" } } } diff --git a/src/OffDotNet.CodeAnalysis/packages.lock.json b/src/OffDotNet.CodeAnalysis/packages.lock.json index 77178e3..fe3a08b 100644 --- a/src/OffDotNet.CodeAnalysis/packages.lock.json +++ b/src/OffDotNet.CodeAnalysis/packages.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.Extensions.ObjectPool": { "type": "Direct", - "requested": "[8.0.6, )", - "resolved": "8.0.6", - "contentHash": "p/wKtBfxVIP0RVQ9Pmtb3In8rnxWmxetZTiUzREtomnhi+y12I+JJMjAY3eiCd2Mhx+0U060ahP+9Ve/cjEmoA==" + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "2yLweyqmpuuFSRo+I3sLHMxmnAgNcI537kBJiyv49U2ZEqo00jZcG8lrnD8uCiOJp9IklYyTZULtbsXoFVzsjQ==" }, "Roslynator.Analyzers": { "type": "Direct", @@ -22,9 +22,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.28.0.94264, )", - "resolved": "9.28.0.94264", - "contentHash": "P5ri/HgukP7KPoN/Lo0njq0d5IdUMpiqHoDVT3QRauwwCWXk0ACmC3FNtMR0l4X4TzfqmIZV+SeSLIDijNp6sQ==" + "requested": "[9.29.0.95321, )", + "resolved": "9.29.0.95321", + "contentHash": "dAvGL7gavcUYk83p1StVzvMFcbwN4hr08hjthsRr/wR/uPFgjd7LUn7QGN/1iH92aA7P72x06RRNtdUAqmVHdw==" }, "StyleCop.Analyzers": { "type": "Direct", diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json index 4feaafb..da788e6 100644 --- a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json @@ -41,9 +41,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.28.0.94264, )", - "resolved": "9.28.0.94264", - "contentHash": "P5ri/HgukP7KPoN/Lo0njq0d5IdUMpiqHoDVT3QRauwwCWXk0ACmC3FNtMR0l4X4TzfqmIZV+SeSLIDijNp6sQ==" + "requested": "[9.29.0.95321, )", + "resolved": "9.29.0.95321", + "contentHash": "dAvGL7gavcUYk83p1StVzvMFcbwN4hr08hjthsRr/wR/uPFgjd7LUn7QGN/1iH92aA7P72x06RRNtdUAqmVHdw==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -56,20 +56,20 @@ }, "xunit": { "type": "Direct", - "requested": "[2.8.1, )", - "resolved": "2.8.1", - "contentHash": "MLBz2NQp3rtSIoJdjj3DBEr/EeOFlQYF3oCCljat3DY9GQ7yYmtjIAv8Zyfm5BcwYso5sjvIe5scuHaJPVCGIQ==", + "requested": "[2.9.0, )", + "resolved": "2.9.0", + "contentHash": "PtU3rZ0ThdmdJqTbK7GkgFf6iBaCR6Q0uvJHznID+XEYk2v6O/b7sRxqnbi3B2gRDXxjTqMkVNayzwsqsFUxRw==", "dependencies": { - "xunit.analyzers": "1.14.0", - "xunit.assert": "2.8.1", - "xunit.core": "[2.8.1]" + "xunit.analyzers": "1.15.0", + "xunit.assert": "2.9.0", + "xunit.core": "[2.9.0]" } }, "xunit.runner.visualstudio": { "type": "Direct", - "requested": "[2.8.1, )", - "resolved": "2.8.1", - "contentHash": "qBTK0WAcnw65mymIjVDqWUTdqjMyzjwu9e9SF0oGYfYELgbcteDZ4fQLJaXw8mzkvpAD7YdoexBbg8VYQFkWWA==" + "requested": "[2.8.2, )", + "resolved": "2.8.2", + "contentHash": "vm1tbfXhFmjFMUmS4M0J0ASXz3/U5XvXBa6DOQUL3fEz4Vt6YPhv+ESCarx6M6D+9kJkJYZKCNvJMas1+nVfmQ==" }, "Castle.Core": { "type": "Transitive", @@ -128,51 +128,51 @@ }, "xunit.analyzers": { "type": "Transitive", - "resolved": "1.14.0", - "contentHash": "KcFBmV2150xZHPUebV3YLR5gGl8R4wLuPOoxMiwCf1L4bL8ls0dcwtGFzr6NvQRgg6dWgSqbE52I6SYyeB0VnQ==" + "resolved": "1.15.0", + "contentHash": "s+M8K/Rtlgr6CmD7AYQKrNTvT5sh0l0ZKDoZ3Z/ExhlIwfV9mGAMR4f7KqIB7SSK7ZOhqDTgTUMYPmKfmvWUWQ==" }, "xunit.assert": { "type": "Transitive", - "resolved": "2.8.1", - "contentHash": "DDM18ur+PeNFhQ4w/vO+uvCUy8hA3OS5+AMf/CFov9Wco7Le49zzj0hovRWwa8f/3vaUfjL5r+IkPvqEHu2IIg==" + "resolved": "2.9.0", + "contentHash": "Z/1pyia//860wEYTKn6Q5dmgikJdRjgE4t5AoxJkK8oTmidzPLEPG574kmm7LFkMLbH6Frwmgb750kcyR+hwoA==" }, "xunit.core": { "type": "Transitive", - "resolved": "2.8.1", - "contentHash": "Ng4Q/DOwotESPl5CufcdqgP6O2KDpdEcIvNfA3upzfCiBrkj5WsmLhf/XUsCVolzvHA7b1WUlyeTo7j1ulG4gQ==", + "resolved": "2.9.0", + "contentHash": "uRaop9tZsZMCaUS4AfbSPGYHtvywWnm8XXFNUqII7ShWyDBgdchY6gyDNgO4AK1Lv/1NNW61Zq63CsDV6oH6Jg==", "dependencies": { - "xunit.extensibility.core": "[2.8.1]", - "xunit.extensibility.execution": "[2.8.1]" + "xunit.extensibility.core": "[2.9.0]", + "xunit.extensibility.execution": "[2.9.0]" } }, "xunit.extensibility.core": { "type": "Transitive", - "resolved": "2.8.1", - "contentHash": "ilfAsxEhpne9AXXf3W+O65mRgGum94m2xHYm1yeJ1m7eiINM6OOwpaHhoNC/KWEQ2u/WF6/XiEs+Q0TOq7hiGA==", + "resolved": "2.9.0", + "contentHash": "zjDEUSxsr6UNij4gIwCgMqQox+oLDPRZ+mubwWLci+SssPBFQD1xeRR4SvgBuXqbE0QXCJ/STVTp+lxiB5NLVA==", "dependencies": { "xunit.abstractions": "2.0.3" } }, "xunit.extensibility.execution": { "type": "Transitive", - "resolved": "2.8.1", - "contentHash": "38UnJW+64Wn8QIabujcNEw0HKvWw2AlYCgU8GNwCCDqyrSuRYb7zwetn7SHoHfbL9e9FAvEiAMXmc2wSUY8sVQ==", + "resolved": "2.9.0", + "contentHash": "5ZTQZvmPLlBw6QzCOwM0KnMsZw6eGjbmC176QHZlcbQoMhGIeGcYzYwn5w9yXxf+4phtplMuVqTpTbFDQh2bqQ==", "dependencies": { - "xunit.extensibility.core": "[2.8.1]" + "xunit.extensibility.core": "[2.9.0]" } }, "offdotnet.codeanalysis": { "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", - "Microsoft.Extensions.ObjectPool": "[8.0.6, )" + "Microsoft.Extensions.ObjectPool": "[8.0.7, )" } }, "offdotnet.codeanalysis.pdf": { "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", - "Microsoft.Extensions.ObjectPool": "[8.0.6, )", + "Microsoft.Extensions.ObjectPool": "[8.0.7, )", "OffDotNet.CodeAnalysis": "[1.0.0, )" } }, @@ -182,7 +182,7 @@ "Microsoft.NET.Test.Sdk": "[17.10.0, )", "NSubstitute": "[5.1.0, )", "OffDotNet.CodeAnalysis": "[1.0.0, )", - "xunit": "[2.8.1, )" + "xunit": "[2.9.0, )" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { @@ -193,9 +193,9 @@ }, "Microsoft.Extensions.ObjectPool": { "type": "CentralTransitive", - "requested": "[8.0.6, )", - "resolved": "8.0.6", - "contentHash": "p/wKtBfxVIP0RVQ9Pmtb3In8rnxWmxetZTiUzREtomnhi+y12I+JJMjAY3eiCd2Mhx+0U060ahP+9Ve/cjEmoA==" + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "2yLweyqmpuuFSRo+I3sLHMxmnAgNcI537kBJiyv49U2ZEqo00jZcG8lrnD8uCiOJp9IklYyTZULtbsXoFVzsjQ==" } } } diff --git a/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json b/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json index 1595069..8dc8f96 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json +++ b/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json @@ -41,9 +41,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.28.0.94264, )", - "resolved": "9.28.0.94264", - "contentHash": "P5ri/HgukP7KPoN/Lo0njq0d5IdUMpiqHoDVT3QRauwwCWXk0ACmC3FNtMR0l4X4TzfqmIZV+SeSLIDijNp6sQ==" + "requested": "[9.29.0.95321, )", + "resolved": "9.29.0.95321", + "contentHash": "dAvGL7gavcUYk83p1StVzvMFcbwN4hr08hjthsRr/wR/uPFgjd7LUn7QGN/1iH92aA7P72x06RRNtdUAqmVHdw==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -56,20 +56,20 @@ }, "xunit": { "type": "Direct", - "requested": "[2.8.1, )", - "resolved": "2.8.1", - "contentHash": "MLBz2NQp3rtSIoJdjj3DBEr/EeOFlQYF3oCCljat3DY9GQ7yYmtjIAv8Zyfm5BcwYso5sjvIe5scuHaJPVCGIQ==", + "requested": "[2.9.0, )", + "resolved": "2.9.0", + "contentHash": "PtU3rZ0ThdmdJqTbK7GkgFf6iBaCR6Q0uvJHznID+XEYk2v6O/b7sRxqnbi3B2gRDXxjTqMkVNayzwsqsFUxRw==", "dependencies": { - "xunit.analyzers": "1.14.0", - "xunit.assert": "2.8.1", - "xunit.core": "[2.8.1]" + "xunit.analyzers": "1.15.0", + "xunit.assert": "2.9.0", + "xunit.core": "[2.9.0]" } }, "xunit.runner.visualstudio": { "type": "Direct", - "requested": "[2.8.1, )", - "resolved": "2.8.1", - "contentHash": "qBTK0WAcnw65mymIjVDqWUTdqjMyzjwu9e9SF0oGYfYELgbcteDZ4fQLJaXw8mzkvpAD7YdoexBbg8VYQFkWWA==" + "requested": "[2.8.2, )", + "resolved": "2.8.2", + "contentHash": "vm1tbfXhFmjFMUmS4M0J0ASXz3/U5XvXBa6DOQUL3fEz4Vt6YPhv+ESCarx6M6D+9kJkJYZKCNvJMas1+nVfmQ==" }, "Castle.Core": { "type": "Transitive", @@ -128,44 +128,44 @@ }, "xunit.analyzers": { "type": "Transitive", - "resolved": "1.14.0", - "contentHash": "KcFBmV2150xZHPUebV3YLR5gGl8R4wLuPOoxMiwCf1L4bL8ls0dcwtGFzr6NvQRgg6dWgSqbE52I6SYyeB0VnQ==" + "resolved": "1.15.0", + "contentHash": "s+M8K/Rtlgr6CmD7AYQKrNTvT5sh0l0ZKDoZ3Z/ExhlIwfV9mGAMR4f7KqIB7SSK7ZOhqDTgTUMYPmKfmvWUWQ==" }, "xunit.assert": { "type": "Transitive", - "resolved": "2.8.1", - "contentHash": "DDM18ur+PeNFhQ4w/vO+uvCUy8hA3OS5+AMf/CFov9Wco7Le49zzj0hovRWwa8f/3vaUfjL5r+IkPvqEHu2IIg==" + "resolved": "2.9.0", + "contentHash": "Z/1pyia//860wEYTKn6Q5dmgikJdRjgE4t5AoxJkK8oTmidzPLEPG574kmm7LFkMLbH6Frwmgb750kcyR+hwoA==" }, "xunit.core": { "type": "Transitive", - "resolved": "2.8.1", - "contentHash": "Ng4Q/DOwotESPl5CufcdqgP6O2KDpdEcIvNfA3upzfCiBrkj5WsmLhf/XUsCVolzvHA7b1WUlyeTo7j1ulG4gQ==", + "resolved": "2.9.0", + "contentHash": "uRaop9tZsZMCaUS4AfbSPGYHtvywWnm8XXFNUqII7ShWyDBgdchY6gyDNgO4AK1Lv/1NNW61Zq63CsDV6oH6Jg==", "dependencies": { - "xunit.extensibility.core": "[2.8.1]", - "xunit.extensibility.execution": "[2.8.1]" + "xunit.extensibility.core": "[2.9.0]", + "xunit.extensibility.execution": "[2.9.0]" } }, "xunit.extensibility.core": { "type": "Transitive", - "resolved": "2.8.1", - "contentHash": "ilfAsxEhpne9AXXf3W+O65mRgGum94m2xHYm1yeJ1m7eiINM6OOwpaHhoNC/KWEQ2u/WF6/XiEs+Q0TOq7hiGA==", + "resolved": "2.9.0", + "contentHash": "zjDEUSxsr6UNij4gIwCgMqQox+oLDPRZ+mubwWLci+SssPBFQD1xeRR4SvgBuXqbE0QXCJ/STVTp+lxiB5NLVA==", "dependencies": { "xunit.abstractions": "2.0.3" } }, "xunit.extensibility.execution": { "type": "Transitive", - "resolved": "2.8.1", - "contentHash": "38UnJW+64Wn8QIabujcNEw0HKvWw2AlYCgU8GNwCCDqyrSuRYb7zwetn7SHoHfbL9e9FAvEiAMXmc2wSUY8sVQ==", + "resolved": "2.9.0", + "contentHash": "5ZTQZvmPLlBw6QzCOwM0KnMsZw6eGjbmC176QHZlcbQoMhGIeGcYzYwn5w9yXxf+4phtplMuVqTpTbFDQh2bqQ==", "dependencies": { - "xunit.extensibility.core": "[2.8.1]" + "xunit.extensibility.core": "[2.9.0]" } }, "offdotnet.codeanalysis": { "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", - "Microsoft.Extensions.ObjectPool": "[8.0.6, )" + "Microsoft.Extensions.ObjectPool": "[8.0.7, )" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { @@ -176,9 +176,9 @@ }, "Microsoft.Extensions.ObjectPool": { "type": "CentralTransitive", - "requested": "[8.0.6, )", - "resolved": "8.0.6", - "contentHash": "p/wKtBfxVIP0RVQ9Pmtb3In8rnxWmxetZTiUzREtomnhi+y12I+JJMjAY3eiCd2Mhx+0U060ahP+9Ve/cjEmoA==" + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "2yLweyqmpuuFSRo+I3sLHMxmnAgNcI537kBJiyv49U2ZEqo00jZcG8lrnD8uCiOJp9IklYyTZULtbsXoFVzsjQ==" } } } From e809b2188df0e16dce28457d3a6a5fa52af72429 Mon Sep 17 00:00:00 2001 From: Victor Pogor Date: Sat, 20 Jul 2024 14:25:37 +1000 Subject: [PATCH 05/14] added syntax token --- .../Syntax/RawSyntaxNode.cs | 21 + .../Syntax/RawSyntaxToken.cs | 164 ++++++ .../Syntax/RawSyntaxTrivia.cs | 62 +- .../Syntax/SyntaxFacts.cs | 32 + .../Syntax/SyntaxKind.cs | 29 + .../Syntax/SyntaxKindFacts.cs | 6 +- .../Syntax/AbstractNode.cs | 46 +- src/OffDotNet.CodeAnalysis/Utils/Boxing.cs | 22 + .../Syntax/RawSyntaxTokenTests.cs | 549 ++++++++++++++++++ .../Syntax/RawSyntaxTriviaTests.cs | 108 +++- 10 files changed, 1002 insertions(+), 37 deletions(-) create mode 100644 src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxToken.cs create mode 100644 src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxFacts.cs create mode 100644 src/OffDotNet.CodeAnalysis/Utils/Boxing.cs create mode 100644 tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTokenTests.cs diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxNode.cs b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxNode.cs index 037d76e..bc2aea7 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxNode.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxNode.cs @@ -7,23 +7,44 @@ namespace OffDotNet.CodeAnalysis.Pdf.Syntax; using OffDotNet.CodeAnalysis.Syntax; +/// +/// Represents a raw syntax node in the syntax tree. +/// internal abstract class RawSyntaxNode : AbstractNode { + /// + /// Initializes a new instance of the class with the specified kind. + /// + /// The kind of the syntax node. protected RawSyntaxNode(SyntaxKind kind) : base((ushort)kind) { Kind = kind; } + /// + /// Initializes a new instance of the class with the specified kind and full width. + /// + /// The kind of the syntax node. + /// The full width of the syntax node. protected RawSyntaxNode(SyntaxKind kind, int fullWidth) : base((ushort)kind, fullWidth) { Kind = kind; } + /// + /// Gets the kind of the syntax node. + /// public SyntaxKind Kind { get; } + /// + /// Gets the text representation of the kind of the syntax node. + /// public override string KindText => this.Kind.ToString(); + /// + /// Gets the language of the syntax node. + /// public string Language => "PDF"; } diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxToken.cs b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxToken.cs new file mode 100644 index 0000000..71c866c --- /dev/null +++ b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxToken.cs @@ -0,0 +1,164 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Syntax; + +using OffDotNet.CodeAnalysis.Syntax; +using Utils; + +/// Represents a raw syntax token in the syntax tree. +internal sealed class RawSyntaxToken : RawSyntaxNode +{ + /// Initializes a new instance of the class with the specified kind. + /// The kind of the token. + private RawSyntaxToken(SyntaxKind kind) + : base(kind) + { + Debug.Assert(!kind.IsTrivia(), "Invalid token kind"); + Text = kind.GetText(); + Value = GetValue(); + ValueText = Text; + FullWidth = Text.Length; + } + + /// Initializes a new instance of the class with the specified kind and trivia. + /// The kind of the token. + /// The leading trivia of the token. + /// The trailing trivia of the token. + private RawSyntaxToken(SyntaxKind kind, Option leadingTrivia, Option trailingTrivia) + : this(kind) + { + LeadingTrivia = leadingTrivia; + TrailingTrivia = trailingTrivia; + FullWidth = Text.Length + + LeadingTrivia.Match(static t => t.FullWidth, static () => 0) + + TrailingTrivia.Match(static t => t.FullWidth, static () => 0); + } + + /// Initializes a new instance of the class with the specified kind and text. + /// The kind of the token. + /// The text of the token. + private RawSyntaxToken(SyntaxKind kind, object text) + : base(kind) + { + Text = text.ToString() ?? string.Empty; + Value = GetValue(); + ValueText = Text; + FullWidth = Text.Length; + } + + /// Initializes a new instance of the class with the specified kind, text, and trivia. + /// The kind of the token. + /// The text of the token. + /// The leading trivia of the token. + /// The trailing trivia of the token. + private RawSyntaxToken(SyntaxKind kind, object text, Option leadingTrivia, Option trailingTrivia) + : this(kind, text) + { + LeadingTrivia = leadingTrivia; + TrailingTrivia = trailingTrivia; + FullWidth = Text.Length + + LeadingTrivia.Match(static t => t.FullWidth, static () => 0) + + TrailingTrivia.Match(static t => t.FullWidth, static () => 0); + } + + /// Gets a value indicating whether this instance represents a token. + public override bool IsToken => true; + + /// Gets the text of the token. + public string Text { get; } + + /// Gets the value of the token. + public override Option Value { get; } + + /// Gets the text representation of the value of the token. + public override string ValueText { get; } + + /// Gets the width of the token. + public override int Width => Text.Length; + + /// Gets the leading trivia of the token. + public override Option LeadingTrivia { get; } + + /// Gets the width of the leading trivia. + public override int LeadingTriviaWidth => LeadingTrivia.Match(static t => t.FullWidth, static () => 0); + + /// Gets the trailing trivia of the token. + public override Option TrailingTrivia { get; } + + /// Gets the width of the trailing trivia. + public override int TrailingTriviaWidth => TrailingTrivia.Match(static t => t.FullWidth, static () => 0); + + /// Creates a new instance of the class with the specified kind. + /// The kind of the token. + /// A new instance of the class. + internal static RawSyntaxToken Create(SyntaxKind kind) => new(kind); + + /// Creates a new instance of the class with the specified kind and text. + /// The type of the text. + /// The kind of the token. + /// The text of the token. + /// A new instance of the class. + internal static RawSyntaxToken Create(SyntaxKind kind, T text) + where T : notnull + { + return new RawSyntaxToken(kind, text); + } + + /// Creates a new instance of the class with the specified kind, text, and trivia. + /// The type of the text. + /// The kind of the token. + /// The text of the token. + /// The leading trivia of the token. + /// The trailing trivia of the token. + /// A new instance of the class. + internal static RawSyntaxToken Create(SyntaxKind kind, T text, Option leadingTrivia, Option trailingTrivia) + where T : notnull + { + return new RawSyntaxToken(kind, text, leadingTrivia, trailingTrivia); + } + + /// Creates a new instance of the class with the specified kind and trivia. + /// The kind of the token. + /// The leading trivia of the token. + /// The trailing trivia of the token. + /// A new instance of the class. + internal static RawSyntaxToken Create(SyntaxKind kind, Option leadingTrivia, Option trailingTrivia) => + new(kind, leadingTrivia, trailingTrivia); + + /// Gets the slot at the specified index. + /// The index of the slot. + /// An option containing the slot if it exists, otherwise none. + internal override Option GetSlot(int index) => Option.None; + + /// Writes the token to the specified . + /// The writer to write to. + /// Whether to include leading trivia. + /// Whether to include trailing trivia. + protected override void WriteTokenTo(TextWriter writer, bool leading, bool trailing) + { + if (leading && LeadingTrivia.IsSome(out var leadingTrivia)) + { + leadingTrivia.WriteTo(writer, leading: true, trailing: false); + } + + writer.Write(this.Text); + + if (trailing && TrailingTrivia.IsSome(out var trailingTrivia)) + { + trailingTrivia.WriteTo(writer, leading: false, trailing: true); + } + } + + /// Gets the value of the token based on its kind. + /// An option containing the value of the token. + private Option GetValue() => this.Kind switch + { + SyntaxKind.TrueKeyword => Boxing.True, + SyntaxKind.FalseKeyword => Boxing.False, + SyntaxKind.NullKeyword => Option.None, + _ => Text, + }; +} diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxTrivia.cs b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxTrivia.cs index 33fbfb0..777b526 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxTrivia.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxTrivia.cs @@ -8,21 +8,77 @@ namespace OffDotNet.CodeAnalysis.Pdf.Syntax; using OffDotNet.CodeAnalysis.Syntax; using Utils; +/// +/// Represents a raw syntax trivia node in the syntax tree. +/// internal sealed class RawSyntaxTrivia : RawSyntaxNode { - internal RawSyntaxTrivia(SyntaxKind kind, ReadOnlySpan text) + /// + /// Initializes a new instance of the class. + /// + /// The kind of trivia. + /// The text of the trivia. + private RawSyntaxTrivia(SyntaxKind kind, ReadOnlySpan text) : base(kind, text.Length) { Debug.Assert(kind.IsTrivia(), "Invalid trivia kind"); Text = Encoding.ASCII.GetString(text); } + /// + /// Gets the text of the trivia. + /// public string Text { get; } + /// + /// Gets a value indicating whether this instance represents trivia. + /// public override bool IsTrivia => true; - internal override Option GetSlot(int index) + /// + /// Gets the width of the trailing trivia. + /// + public override int TrailingTriviaWidth => 0; + + /// + /// Gets the width of the leading trivia. + /// + public override int LeadingTriviaWidth => 0; + + /// + /// Gets the width of the trivia. + /// + public override int Width => FullWidth; + + /// + /// Converts the trivia to its full string representation. + /// + /// The full string representation of the trivia. + public override string ToFullString() => this.Text; + + /// + /// Converts the trivia to its string representation. + /// + /// The string representation of the trivia. + public override string ToString() => this.Text; + + /// Creates a new instance of the class. + /// The kind of trivia. + /// The text of the trivia. + /// A new instance of the class. + internal static RawSyntaxTrivia Create(SyntaxKind kind, ReadOnlySpan text) => new(kind, text); + + /// + /// Gets the slot at the specified index. + /// + /// The index of the slot. + /// An option containing the slot if it exists, otherwise none. + internal override Option GetSlot(int index) => Option.None; + + /// Writes the trivia of the node to the specified writer. + /// The writer to which the trivia will be written. + protected override void WriteTriviaTo(TextWriter writer) { - throw new NotImplementedException(); + writer.Write(this.Text); } } diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxFacts.cs b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxFacts.cs new file mode 100644 index 0000000..6dc1b41 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxFacts.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Syntax; + +/// +/// Provides utility methods for syntax kinds. +/// +internal static class SyntaxFacts +{ + /// + /// Gets the text representation of the specified . + /// + /// The syntax kind. + /// The text representation of the syntax kind. + public static string GetText(this SyntaxKind kind) + { + return kind switch + { + // Tokens + SyntaxKind.MinusToken => "-", + + // Keywords + SyntaxKind.TrueKeyword => "true", + SyntaxKind.FalseKeyword => "false", + SyntaxKind.NullKeyword => "null", + _ => string.Empty, + }; + } +} diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKind.cs b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKind.cs index 1383968..a801ec0 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKind.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKind.cs @@ -7,13 +7,42 @@ namespace OffDotNet.CodeAnalysis.Pdf.Syntax; using OffDotNet.CodeAnalysis.Syntax; +/// Represents the different kinds of syntax elements. public enum SyntaxKind : ushort { + /// Represents no syntax kind. None = AbstractNode.NoneKind, + + /// Represents a list of syntax elements. List = AbstractNode.ListKind, + // Tokens + + /// Represents a minus token ('-'). + MinusToken, + + // Keywords + + /// Represents the 'true' keyword. + TrueKeyword, + + /// Represents the 'false' keyword. + FalseKeyword, + + /// Represents the 'null' keyword. + NullKeyword, + + /// Represents a bad token. + BadToken, + // Trivia + + /// Represents end-of-line trivia. EndOfLineTrivia, + + /// Represents whitespace trivia. WhitespaceTrivia, + + /// Represents single-line comment trivia. SingleLineCommentTrivia, } diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKindFacts.cs b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKindFacts.cs index dc040a0..e34e780 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKindFacts.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/SyntaxKindFacts.cs @@ -5,8 +5,12 @@ namespace OffDotNet.CodeAnalysis.Pdf.Syntax; -public static class SyntaxKindFacts +/// Provides utility methods for . +internal static class SyntaxKindFacts { + /// Determines whether the specified is trivia. + /// The syntax kind to check. + /// true if the specified syntax kind is trivia; otherwise, false. public static bool IsTrivia(this SyntaxKind kind) { switch (kind) diff --git a/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs b/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs index 5cfb2f5..ea4faa3 100644 --- a/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs +++ b/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs @@ -20,7 +20,7 @@ internal abstract class AbstractNode internal const int ListKind = 1; /// Indicates that the slot count is too large. - protected const int SlotCountTooLarge = 0b0000000000001111; + private const int SlotCountTooLarge = 0b0000000000001111; private readonly NodeFlagsAndSlotCount _nodeFlagsAndSlotCount; @@ -62,7 +62,7 @@ protected AbstractNode(ushort rawKind, int fullWidth) public bool HasTrailingTrivia => TrailingTriviaWidth != 0; /// Gets the full width of the node. - public int FullWidth { get; } + public int FullWidth { get; protected init; } /// Gets the width of the node, excluding leading and trailing trivia. public virtual int Width => FullWidth - LeadingTriviaWidth - TrailingTriviaWidth; @@ -155,32 +155,12 @@ public virtual string ToFullString() /// The slot at the specified index. internal abstract Option GetSlot(int index); - /// Gets the number of slots in the node. - /// The number of slots. - protected virtual int GetSlotCount() => 0; - - /// Writes the trivia of the node to the specified writer. - /// The writer to which the trivia will be written. - protected virtual void WriteTriviaTo(TextWriter writer) - { - Debug.Fail("Should not get here."); - } - - /// Writes the token of the node to the specified writer. - /// The writer to which the token will be written. - /// Indicates whether to include leading trivia. - /// Indicates whether to include trailing trivia. - protected virtual void WriteTokenTo(TextWriter writer, bool leading, bool trailing) - { - Debug.Fail("Should not get here."); - } - /// Writes the node to the specified writer. /// The writer to which the node will be written. /// Indicates whether to include leading trivia. /// Indicates whether to include trailing trivia. [SuppressMessage("Minor Code Smell", "S3626:Jump statements should not be redundant", Justification = "False positive.")] - protected void WriteTo(TextWriter writer, bool leading, bool trailing) + protected internal void WriteTo(TextWriter writer, bool leading, bool trailing) { var stack = SharedObjectPools.AbstractNodesCache.Get(); stack.Push((this, leading, trailing)); @@ -211,6 +191,26 @@ static void ProcessStack(TextWriter writer, Stack stack } } + /// Gets the number of slots in the node. + /// The number of slots. + protected virtual int GetSlotCount() => 0; + + /// Writes the trivia of the node to the specified writer. + /// The writer to which the trivia will be written. + protected virtual void WriteTriviaTo(TextWriter writer) + { + Debug.Fail("Should not get here."); + } + + /// Writes the token of the node to the specified writer. + /// The writer to which the token will be written. + /// Indicates whether to include leading trivia. + /// Indicates whether to include trailing trivia. + protected virtual void WriteTokenTo(TextWriter writer, bool leading, bool trailing) + { + Debug.Fail("Should not get here."); + } + /// Gets the first terminal node in the tree. /// The first terminal node. private Option GetFirstTerminal() diff --git a/src/OffDotNet.CodeAnalysis/Utils/Boxing.cs b/src/OffDotNet.CodeAnalysis/Utils/Boxing.cs new file mode 100644 index 0000000..147f4b7 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Utils/Boxing.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Utils; + +/// +/// Provides boxed versions of common value types. +/// +internal static class Boxing +{ + /// + /// A boxed representation of the boolean value true. + /// + public static readonly object True = true; + + /// + /// A boxed representation of the boolean value false. + /// + public static readonly object False = false; +} diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTokenTests.cs b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTokenTests.cs new file mode 100644 index 0000000..43f3e2b --- /dev/null +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTokenTests.cs @@ -0,0 +1,549 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Tests.Syntax; + +using OffDotNet.CodeAnalysis.Pdf.Syntax; +using OffDotNet.CodeAnalysis.Syntax; +using Utils; + +[WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] +public class RawSyntaxTokenTests +{ + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"Class should inherit from {nameof(RawSyntaxTrivia)}")] + public void Class_ShouldInheritFromAbstractNode1() + { + // Arrange + + // Act + var actual = RawSyntaxToken.Create(SyntaxKind.BadToken); + + // Assert + Assert.IsAssignableFrom(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"Class should inherit from {nameof(RawSyntaxNode)}")] + public void Class_ShouldInheritFromAbstractNode2() + { + // Arrange + + // Act + var actual = RawSyntaxToken.Create(SyntaxKind.BadToken); + + // Assert + Assert.IsAssignableFrom(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.IsToken)} property should return true by default")] + public void IsTokenProperty_ShouldReturnTrueByDefault() + { + // Arrange + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken); + + // Act + var isToken = rawNode.IsToken; + + // Assert + Assert.True(isToken); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.IsTrivia)} property should return false by default")] + public void IsTriviaProperty_ShouldReturnFalseByDefault() + { + // Arrange + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken); + + // Act + var isTrivia = rawNode.IsTrivia; + + // Assert + Assert.False(isTrivia); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.IsList)} property should return false by default")] + public void IsListProperty_ShouldReturnFalseByDefault() + { + // Arrange + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken); + + // Act + var isList = rawNode.IsList; + + // Assert + Assert.False(isList); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.GetSlot)} method should return None")] + public void GetSlotMethod_ShouldReturnNone() + { + // Arrange + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken); + + // Act + var actual = rawNode.GetSlot(0); + + // Assert + Assert.Equal(Option.None, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(RawSyntaxToken.Text)} property should return the default value")] + [InlineData(SyntaxKind.BadToken, "")] + [InlineData(SyntaxKind.MinusToken, "-")] + [InlineData(SyntaxKind.TrueKeyword, "true")] + [InlineData(SyntaxKind.FalseKeyword, "false")] + [InlineData(SyntaxKind.NullKeyword, "null")] + public void TextProperty_ShouldReturnTheDefaultValue(SyntaxKind kind, string expected) + { + // Arrange + var rawNode = RawSyntaxToken.Create(kind); + + // Act + var actual = rawNode.Text; + + // Assert + Assert.Equal(expected, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.Value)} property should return the {nameof(Option.None)} by default")] + public void ValueProperty_ShouldReturnTheNone() + { + // Arrange + var rawNode = RawSyntaxToken.Create(SyntaxKind.NullKeyword); + + // Act + var actual = rawNode.Value; + + // Assert + Assert.Equal(Option.None, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(RawSyntaxToken.Value)} property should return the default value")] + [InlineData(SyntaxKind.MinusToken, "-")] + [InlineData(SyntaxKind.TrueKeyword, true)] + [InlineData(SyntaxKind.FalseKeyword, false)] + public void ValueProperty_ShouldReturnTheDefaultValue(SyntaxKind kind, object expected) + { + // Arrange + var rawNode = RawSyntaxToken.Create(kind); + + // Act + var actual = rawNode.Value; + var isSome = actual.IsSome(out var value); + + // Assert + Assert.True(isSome); + Assert.Equal(expected, value); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.Value)} property should return the text from the constructor")] + public void ValueProperty_ShouldReturnTheTextFromTheConstructor() + { + // Arrange + const string Expected = "abc"; + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken, Expected); + + // Act + var actual = rawNode.Value; + var isSome = actual.IsSome(out var value); + + // Assert + Assert.True(isSome); + Assert.Equal(rawNode.Text, value); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.ValueText)} property should return the empty string by default")] + public void ValueTextProperty_ShouldReturnTheEmptyStringByDefault() + { + // Arrange + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken); + + // Act + var actual = rawNode.ValueText; + + // Assert + Assert.Equal(string.Empty, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.ValueText)} property should return the text from the constructor")] + public void ValueTextProperty_ShouldReturnTheTextFromTheConstructor() + { + // Arrange + const string Expected = "abc"; + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken, Expected); + + // Act + var actual = rawNode.ValueText; + + // Assert + Assert.Equal(Expected, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(RawSyntaxToken.ValueText)} property should return the default value")] + [InlineData(SyntaxKind.BadToken, "")] + [InlineData(SyntaxKind.MinusToken, "-")] + [InlineData(SyntaxKind.TrueKeyword, "true")] + [InlineData(SyntaxKind.FalseKeyword, "false")] + [InlineData(SyntaxKind.NullKeyword, "null")] + public void ValueTextProperty_ShouldReturnTheDefaultValue(SyntaxKind kind, string expected) + { + // Arrange + var rawNode = RawSyntaxToken.Create(kind); + + // Act + var actual = rawNode.ValueText; + + // Assert + Assert.Equal(expected, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.Width)} property should return the length of the text")] + public void WidthProperty_ShouldReturnTheLengthOfTheText() + { + // Arrange + var rawNode = RawSyntaxToken.Create(SyntaxKind.TrueKeyword); + + // Act + var actual = rawNode.Width; + + // Assert + Assert.Equal(4, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.LeadingTrivia)} property should return the default value")] + public void LeadingTriviaProperty_ShouldReturnTheDefaultValue() + { + // Arrange + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken); + + // Act + var actual = rawNode.LeadingTrivia; + + // Assert + Assert.Equal(Option.None, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.LeadingTrivia)} property should return the value from the constructor")] + public void LeadingTriviaProperty_ShouldReturnValueFromTheConstructor() + { + // Arrange + var leadingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken, leadingTrivia: leadingTrivia, trailingTrivia: Option.None); + + // Act + var actual = rawNode.LeadingTrivia; + var isSome = actual.IsSome(out var value); + + // Assert + Assert.True(isSome); + Assert.Same(leadingTrivia, value); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.TrailingTrivia)} property should return the default value")] + public void TrailingTriviaProperty_ShouldReturnTheDefaultValue() + { + // Arrange + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken); + + // Act + var actual = rawNode.TrailingTrivia; + + // Assert + Assert.Equal(Option.None, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.TrailingTrivia)} property should return the value from the constructor")] + public void TrailingTriviaProperty_ShouldReturnValueFromTheConstructor() + { + // Arrange + var trailingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken, leadingTrivia: Option.None, trailingTrivia: trailingTrivia); + + // Act + var actual = rawNode.TrailingTrivia; + var isSome = actual.IsSome(out var value); + + // Assert + Assert.True(isSome); + Assert.Same(trailingTrivia, value); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.LeadingTriviaWidth)} property should return 0 by default")] + public void LeadingTriviaWidthProperty_ShouldReturnZeroByDefault() + { + // Arrange + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken); + + // Act + var actual = rawNode.LeadingTriviaWidth; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.LeadingTriviaWidth)} property should return the length of the leading trivia")] + public void LeadingTriviaWidthProperty_ShouldReturnTheLengthOfTheLeadingTrivia() + { + // Arrange + var leadingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken, leadingTrivia: leadingTrivia, trailingTrivia: Option.None); + + // Act + var actual = rawNode.LeadingTriviaWidth; + + // Assert + Assert.Equal(2, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.TrailingTriviaWidth)} property should return 0 by default")] + public void TrailingTriviaWidthProperty_ShouldReturnZeroByDefault() + { + // Arrange + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken); + + // Act + var actual = rawNode.TrailingTriviaWidth; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.TrailingTriviaWidth)} property should return the length of the trailing trivia")] + public void TrailingTriviaWidthProperty_ShouldReturnTheLengthOfTheTrailingTrivia() + { + // Arrange + var trailingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxToken.Create(SyntaxKind.BadToken, leadingTrivia: Option.None, trailingTrivia: trailingTrivia); + + // Act + var actual = rawNode.TrailingTriviaWidth; + + // Assert + Assert.Equal(2, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.ToString)} method should return the text of the token excluding the trivia")] + public void ToStringMethod_ShouldReturnTheTextOfTheTokenExcludingTheTrivia() + { + // Arrange + var leadingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var trailingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxToken.Create(SyntaxKind.TrueKeyword, leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia); + + // Act + var actual = rawNode.ToString(); + + // Assert + Assert.Equal("true", actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.ToFullString)} method should return the text of the token including the leading trivia")] + public void ToFullStringMethod_ShouldReturnTheTextOfTheTokenIncludingTheLeadingTrivia() + { + // Arrange + var leadingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxToken.Create(SyntaxKind.TrueKeyword, leadingTrivia: leadingTrivia, trailingTrivia: Option.None); + + // Act + var actual = rawNode.ToFullString(); + + // Assert + Assert.Equal(" true", actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.ToFullString)} method should return the text of the token including the trailing trivia")] + public void ToFullStringMethod_ShouldReturnTheTextOfTheTokenIncludingTheTrailingTrivia() + { + // Arrange + var trailingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + const string Text = "true"; + var rawNode = RawSyntaxToken.Create(SyntaxKind.TrueKeyword, text: Text, leadingTrivia: Option.None, trailingTrivia: trailingTrivia); + + // Act + var actual = rawNode.ToFullString(); + + // Assert + Assert.Equal("true ", actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.Kind)} property should return the value from the constructor")] + public void KindProperty_ShouldReturnValueFromTheConstructor() + { + // Arrange + const SyntaxKind Expected = SyntaxKind.BadToken; + var rawNode = RawSyntaxToken.Create(Expected); + + // Act + var actual = rawNode.Kind; + + // Assert + Assert.Equal(Expected, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxToken.RawKind)} property should return the value from the constructor")] + public void RawKindProperty_ShouldReturnValueFromTheConstructor() + { + // Arrange + const SyntaxKind Expected = SyntaxKind.BadToken; + var rawNode = RawSyntaxToken.Create(Expected); + + // Act + var actual = rawNode.RawKind; + + // Assert + Assert.Equal((ushort)Expected, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.HasLeadingTrivia)} property should return false by default")] + public void HasLeadingTriviaProperty_ShouldReturnFalseByDefault() + { + // Arrange + var rawNode = RawSyntaxToken.Create(SyntaxKind.TrueKeyword); + + // Act + var actual = rawNode.HasLeadingTrivia; + + // Assert + Assert.False(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.HasLeadingTrivia)} property should return true if the leading trivia is not None")] + public void HasLeadingTriviaProperty_ShouldReturnTrueIfTheLeadingTriviaIsNotNone() + { + // Arrange + var leadingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxToken.Create(SyntaxKind.TrueKeyword, leadingTrivia: leadingTrivia, trailingTrivia: Option.None); + + // Act + var actual = rawNode.HasLeadingTrivia; + + // Assert + Assert.True(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.HasTrailingTrivia)} property should return false by default")] + public void HasTrailingTriviaProperty_ShouldReturnFalseByDefault() + { + // Arrange + var rawNode = RawSyntaxToken.Create(SyntaxKind.TrueKeyword); + + // Act + var actual = rawNode.HasTrailingTrivia; + + // Assert + Assert.False(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.HasTrailingTrivia)} property should return true if the trailing trivia is not None")] + public void HasTrailingTriviaProperty_ShouldReturnTrueIfTheTrailingTriviaIsNotNone() + { + // Arrange + var trailingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxToken.Create(SyntaxKind.TrueKeyword, leadingTrivia: Option.None, trailingTrivia: trailingTrivia); + + // Act + var actual = rawNode.HasTrailingTrivia; + + // Assert + Assert.True(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.FullWidth)} property should return the sum of the width of the token and the trivia")] + public void FullWidthProperty_ShouldReturnTheSumOfTheWidthOfTheTokenAndTheTrivia() + { + // Arrange + var leadingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var trailingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxToken.Create(SyntaxKind.TrueKeyword, leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia); + + // Act + var actual = rawNode.FullWidth; + + // Assert + Assert.Equal(8, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.Width)} property should return the length of the token")] + public void WidthProperty_ShouldReturnTheLengthOfTheToken() + { + // Arrange + var leadingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var trailingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxToken.Create(SyntaxKind.TrueKeyword, leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia); + + // Act + var actual = rawNode.Width; + + // Assert + Assert.Equal(4, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.Value)} property should return the text of the token")] + public void ValueProperty_ShouldReturnTheTextOfTheToken() + { + // Arrange + var leadingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var trailingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxToken.Create(SyntaxKind.TrueKeyword, leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia); + + // Act + var actual = rawNode.Value; + var isSome = actual.IsSome(out var value); + + // Assert + Assert.True(isSome); + Assert.True(Assert.IsType(value)); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.ValueText)} property should return the text of the token")] + public void ValueTextProperty_ShouldReturnTheTextOfTheToken() + { + // Arrange + const string Expected = "true"; + var leadingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var trailingTrivia = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxToken.Create(SyntaxKind.TrueKeyword, leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia); + + // Act + var actual = rawNode.ValueText; + + // Assert + Assert.Equal(Expected, actual); + } +} diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTriviaTests.cs b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTriviaTests.cs index cfee016..ec99806 100644 --- a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTriviaTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTriviaTests.cs @@ -7,6 +7,7 @@ namespace OffDotNet.CodeAnalysis.Pdf.Tests.Syntax; using OffDotNet.CodeAnalysis.Pdf.Syntax; using OffDotNet.CodeAnalysis.Syntax; +using Utils; [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] public class RawSyntaxTriviaTests @@ -18,7 +19,7 @@ public void Class_ShouldInheritFromAbstractNode1() // Arrange // Act - var actual = new RawSyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "u8); + var actual = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); // Assert Assert.IsAssignableFrom(actual); @@ -31,7 +32,7 @@ public void Class_ShouldInheritFromAbstractNode2() // Arrange // Act - var actual = new RawSyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "u8); + var actual = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); // Assert Assert.IsAssignableFrom(actual); @@ -45,7 +46,7 @@ public void Class_ShouldInheritFromAbstractNode2() public void RawKindProperty_ShouldReturnTheValuePassedToTheConstructor(SyntaxKind rawKind) { // Arrange - var rawNode = new RawSyntaxTrivia(rawKind, " "u8); + var rawNode = RawSyntaxTrivia.Create(rawKind, " "u8); // Act var kind = rawNode.RawKind; @@ -59,7 +60,7 @@ public void RawKindProperty_ShouldReturnTheValuePassedToTheConstructor(SyntaxKin public void IsTokenProperty_ShouldReturnFalseByDefault() { // Arrange - var rawNode = new RawSyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); // Act var isToken = rawNode.IsToken; @@ -73,7 +74,7 @@ public void IsTokenProperty_ShouldReturnFalseByDefault() public void IsTriviaProperty_ShouldReturnFalseByDefault() { // Arrange - var rawNode = new RawSyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); // Act var isTrivia = rawNode.IsTrivia; @@ -91,7 +92,7 @@ public void FullWidthProperty_ShouldReturnTheValuePassedToTheConstructor(string { // Arrange ReadOnlySpan textSpan = Encoding.ASCII.GetBytes(text); - var rawNode = new RawSyntaxTrivia(SyntaxKind.EndOfLineTrivia, textSpan); + var rawNode = RawSyntaxTrivia.Create(SyntaxKind.EndOfLineTrivia, textSpan); // Act var width = rawNode.FullWidth; @@ -108,7 +109,7 @@ public void FullWidthProperty_ShouldReturnTheValuePassedToTheConstructor(string public void KindProperty_ShouldReturnTheValuePassedToTheConstructor(SyntaxKind kind) { // Arrange - var rawNode = new RawSyntaxTrivia(kind, " "u8); + var rawNode = RawSyntaxTrivia.Create(kind, " "u8); // Act var actual = rawNode.Kind; @@ -125,7 +126,7 @@ public void KindProperty_ShouldReturnTheValuePassedToTheConstructor(SyntaxKind k public void KindTextProperty_ShouldReturnTheNameOfTheSyntaxKind(SyntaxKind kind, string expected) { // Arrange - var rawNode = new RawSyntaxTrivia(kind, " "u8); + var rawNode = RawSyntaxTrivia.Create(kind, " "u8); // Act var kindText = rawNode.KindText; @@ -139,7 +140,7 @@ public void KindTextProperty_ShouldReturnTheNameOfTheSyntaxKind(SyntaxKind kind, public void LanguageProperty_ShouldReturnPDF() { // Arrange - var rawNode = new RawSyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "u8); + var rawNode = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); // Act var language = rawNode.Language; @@ -154,7 +155,7 @@ public void TextProperty_ShouldReturnTheValuePassedToTheConstructor() { // Arrange var text = " "u8; - var rawNode = new RawSyntaxTrivia(SyntaxKind.WhitespaceTrivia, text); + var rawNode = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, text); // Act var actual = rawNode.Text; @@ -162,4 +163,91 @@ public void TextProperty_ShouldReturnTheValuePassedToTheConstructor() // Assert Assert.Equal(" ", actual); } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxTrivia.GetSlot)} method should return None")] + public void GetSlotMethod_ShouldReturnNone() + { + // Arrange + var rawNode = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + + // Act + var actual = rawNode.GetSlot(0); + + // Assert + Assert.Equal(Option.None, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxTrivia.LeadingTriviaWidth)} property should return 0")] + public void LeadingTriviaWidthProperty_ShouldReturn0() + { + // Arrange + var rawNode = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + + // Act + var actual = rawNode.LeadingTriviaWidth; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxTrivia.TrailingTriviaWidth)} property should return 0")] + public void TrailingTriviaWidthProperty_ShouldReturn0() + { + // Arrange + var rawNode = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + + // Act + var actual = rawNode.TrailingTriviaWidth; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxTrivia.Width)} property should be the same as {nameof(RawSyntaxTrivia.FullWidth)}")] + public void WidthProperty_ShouldBeTheSameAsFullWidth() + { + // Arrange + const int ExpectedWidth = 1; + var rawNode = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); + + // Act + var width = rawNode.Width; + + // Assert + Assert.Equal(ExpectedWidth, width); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxTrivia.ToString)} method should return the value of the {nameof(RawSyntaxTrivia.Text)} property")] + public void ToStringMethod_ShouldReturnTheValueOfTheTextProperty() + { + // Arrange + var text = " "u8; + var rawNode = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, text); + + // Act + var actual = rawNode.ToString(); + + // Assert + Assert.Equal(" ", actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(RawSyntaxTrivia.ToFullString)} method should return the value of the {nameof(RawSyntaxTrivia.Text)} property")] + public void ToFullStringMethod_ShouldReturnTheValueOfTheTextProperty() + { + // Arrange + var text = " "u8; + var rawNode = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, text); + + // Act + var actual = rawNode.ToFullString(); + + // Assert + Assert.Equal(" ", actual); + } } From 745f9c97070ff89a76542484b311675ceb783aad Mon Sep 17 00:00:00 2001 From: Victor Pogor Date: Sat, 27 Jul 2024 13:31:57 +1000 Subject: [PATCH 06/14] added diagnostics --- .editorconfig | 1 + Directory.Packages.props | 4 +- .../Configurations/DiagnosticOptions.cs | 11 ++ .../Configurations/RootConfigurations.cs | 13 ++ .../Dependencies.cs | 6 + .../Diagnostics/DiagnosticCode.cs | 11 ++ .../Diagnostics/MessageProvider.cs | 43 ++++++ .../packages.lock.json | 48 ++++++- src/OffDotNet.CodeAnalysis/Dependencies.cs | 3 + .../Diagnostics/AbstractMessageProvider.cs | 30 +++++ .../Diagnostics/DiagnosticDescriptor.cs | 44 ++++++ .../Diagnostics/DiagnosticSeverity.cs | 14 ++ .../Diagnostics/IMessageProvider.cs | 10 ++ .../Lexer/CursorFactory.cs | 2 + .../OffDotNet.CodeAnalysis.csproj | 2 + src/OffDotNet.CodeAnalysis/packages.lock.json | 46 ++++++- .../Diagnostics/MessageProviderTests.cs | 124 +++++++++++++++++ .../packages.lock.json | 48 ++++++- .../AbstractMessageProviderTests.cs | 43 ++++++ .../Diagnostics/DiagnosticDescriptorTests.cs | 125 ++++++++++++++++++ .../packages.lock.json | 48 ++++++- 21 files changed, 663 insertions(+), 13 deletions(-) create mode 100644 src/OffDotNet.CodeAnalysis.Pdf/Configurations/DiagnosticOptions.cs create mode 100644 src/OffDotNet.CodeAnalysis.Pdf/Configurations/RootConfigurations.cs create mode 100644 src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/DiagnosticCode.cs create mode 100644 src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/MessageProvider.cs create mode 100644 src/OffDotNet.CodeAnalysis/Diagnostics/AbstractMessageProvider.cs create mode 100644 src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticDescriptor.cs create mode 100644 src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticSeverity.cs create mode 100644 src/OffDotNet.CodeAnalysis/Diagnostics/IMessageProvider.cs create mode 100644 tests/OffDotNet.CodeAnalysis.Pdf.Tests/Diagnostics/MessageProviderTests.cs create mode 100644 tests/OffDotNet.CodeAnalysis.Tests/Diagnostics/AbstractMessageProviderTests.cs create mode 100644 tests/OffDotNet.CodeAnalysis.Tests/Diagnostics/DiagnosticDescriptorTests.cs diff --git a/.editorconfig b/.editorconfig index 1bbf35a..535e7a6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -253,6 +253,7 @@ csharp_preserve_single_line_statements = true dotnet_diagnostic.IDE0060.severity = warning # Nullable are treated as errors +dotnet_diagnostic.CS8618.severity = error dotnet_diagnostic.CS8625.severity = error # SA1500: Braces for multi-line statements should not share line diff --git a/Directory.Packages.props b/Directory.Packages.props index 25ed3a9..1cc0fb6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,8 @@ + + @@ -9,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Configurations/DiagnosticOptions.cs b/src/OffDotNet.CodeAnalysis.Pdf/Configurations/DiagnosticOptions.cs new file mode 100644 index 0000000..2a9d9d8 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis.Pdf/Configurations/DiagnosticOptions.cs @@ -0,0 +1,11 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Configurations; + +public sealed record DiagnosticOptions +{ + public required string HelpLink { get; init; } +} diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Configurations/RootConfigurations.cs b/src/OffDotNet.CodeAnalysis.Pdf/Configurations/RootConfigurations.cs new file mode 100644 index 0000000..b52e540 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis.Pdf/Configurations/RootConfigurations.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Configurations; + +public sealed record RootConfigurations +{ + public const string SectionName = "OffDotNet"; + + public required DiagnosticOptions Diagnostic { get; init; } +} diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs b/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs index 4e14e3a..2b5c6ed 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs @@ -5,7 +5,10 @@ namespace OffDotNet.CodeAnalysis.Pdf; +using Configurations; +using Diagnostics; using Microsoft.Extensions.DependencyInjection; +using OffDotNet.CodeAnalysis.Diagnostics; using OffDotNet.CodeAnalysis.Lexer; /// @@ -20,8 +23,11 @@ public static class Dependencies /// The same service collection so that multiple calls can be chained. public static IServiceCollection AddPdfCodeAnalysis(this IServiceCollection services) { + services.AddOptions(RootConfigurations.SectionName).ValidateOnStart(); + services.AddCoreCodeAnalysis(); services.AddSingleton(); + services.AddSingleton(); return services; } } diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/DiagnosticCode.cs b/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/DiagnosticCode.cs new file mode 100644 index 0000000..dc41a4c --- /dev/null +++ b/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/DiagnosticCode.cs @@ -0,0 +1,11 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Diagnostics; + +public enum DiagnosticCode +{ + Unknown = 0, +} diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/MessageProvider.cs b/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/MessageProvider.cs new file mode 100644 index 0000000..bfdc0cf --- /dev/null +++ b/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/MessageProvider.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Diagnostics; + +using Configurations; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using OffDotNet.CodeAnalysis.Diagnostics; + +internal sealed class MessageProvider : AbstractMessageProvider +{ + private const string TitleSuffix = "_Title"; + private const string DescriptionSuffix = "_Description"; + + private readonly IStringLocalizer _localizer; + private readonly DiagnosticOptions _options; + + public MessageProvider(IStringLocalizer localizer, IOptions options) + { + _localizer = localizer; + _options = options.Value ?? throw new ArgumentNullException(nameof(options)); + } + + public override string LanguagePrefix => "PDF"; + + public override LocalizedString GetTitle(ushort code) => _localizer[$"{(DiagnosticCode)code}{TitleSuffix}"]; + + public override LocalizedString GetDescription(ushort code) => _localizer[$"{(DiagnosticCode)code}{DescriptionSuffix}"]; + + public override string GetHelpLink(ushort code) => string.Format(_options.HelpLink, GetIdForDiagnosticCode(code)); + + public override byte GetSeverity(ushort code) + { + switch (code) + { + default: + return (byte)DiagnosticSeverity.Hidden; + } + } +} diff --git a/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json b/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json index 23d3b7e..fb42a09 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json +++ b/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json @@ -22,9 +22,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.29.0.95321, )", - "resolved": "9.29.0.95321", - "contentHash": "dAvGL7gavcUYk83p1StVzvMFcbwN4hr08hjthsRr/wR/uPFgjd7LUn7QGN/1iH92aA7P72x06RRNtdUAqmVHdw==" + "requested": "[9.30.0.95878, )", + "resolved": "9.30.0.95878", + "contentHash": "P0DylTJphECGE9HD6yho7rb6qs2WTTW36QUu5ezGRJeZpq8ItlRLmzUpxNHVOaudJywcnKO1DdRyZSaU7LXOcA==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -35,6 +35,28 @@ "StyleCop.Analyzers.Unstable": "1.2.0.556" } }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "RIFgaqoaINxkM2KTOw72dmilDmTrYA0ns2KW4lDz4gZ2+o6IQ894CzmdL3StM2oh7QQq44nCWiqKqc4qUI9Jmg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "8.0.2", + "contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" + }, "StyleCop.Analyzers.Unstable": { "type": "Transitive", "resolved": "1.2.0.556", @@ -44,8 +66,28 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", + "Microsoft.Extensions.Localization": "[8.0.7, )", + "Microsoft.Extensions.Localization.Abstractions": "[8.0.7, )", "Microsoft.Extensions.ObjectPool": "[8.0.7, )" } + }, + "Microsoft.Extensions.Localization": { + "type": "CentralTransitive", + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "kdI24IfC1Jk3zfhWvYmuzvkzuKp+5Shjmfq0vk6EKBB+Udt4Gu1SkVSkMWJkYZFag+ndd+sTroUKDC/sud/DYQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", + "Microsoft.Extensions.Localization.Abstractions": "8.0.7", + "Microsoft.Extensions.Logging.Abstractions": "8.0.1", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "CentralTransitive", + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "uCsxqBBJEsSoV52tX/v+bxxN0e1O8WYIRR7tmcEmVVkRSX+0k4ZJcaL79ddus+0HIm3ssjV/dJk56XufDrGDqg==" } } } diff --git a/src/OffDotNet.CodeAnalysis/Dependencies.cs b/src/OffDotNet.CodeAnalysis/Dependencies.cs index 5c68c6e..5a89d49 100644 --- a/src/OffDotNet.CodeAnalysis/Dependencies.cs +++ b/src/OffDotNet.CodeAnalysis/Dependencies.cs @@ -7,6 +7,7 @@ namespace OffDotNet.CodeAnalysis; using Lexer; using Microsoft.Extensions.DependencyInjection; +using OffDotNet.CodeAnalysis.Diagnostics; /// /// Provides extension methods for registering code analysis services. @@ -20,7 +21,9 @@ public static class Dependencies /// The same service collection so that multiple calls can be chained. public static IServiceCollection AddCoreCodeAnalysis(this IServiceCollection services) { + services.AddLocalization(options => options.ResourcesPath = "Resources"); services.AddSingleton(); + services.AddSingleton(); return services; } } diff --git a/src/OffDotNet.CodeAnalysis/Diagnostics/AbstractMessageProvider.cs b/src/OffDotNet.CodeAnalysis/Diagnostics/AbstractMessageProvider.cs new file mode 100644 index 0000000..217d5b1 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Diagnostics/AbstractMessageProvider.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Diagnostics; + +using System.Collections.Concurrent; +using Microsoft.Extensions.Localization; +using DiagnosticCacheTuple = (string LanguagePrefix, ushort Code); + +internal abstract class AbstractMessageProvider : IMessageProvider +{ + private static readonly ConcurrentDictionary s_cache = new(); + + public abstract string LanguagePrefix { get; } + + public abstract LocalizedString GetTitle(ushort code); + + public abstract LocalizedString GetDescription(ushort code); + + public abstract string GetHelpLink(ushort code); + + public abstract byte GetSeverity(ushort code); + + public string GetIdForDiagnosticCode(ushort diagnosticCode) + { + return s_cache.GetOrAdd((LanguagePrefix, diagnosticCode), key => $"{key.LanguagePrefix}{key.Code:0000}"); + } +} diff --git a/src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticDescriptor.cs b/src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticDescriptor.cs new file mode 100644 index 0000000..aedd891 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticDescriptor.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Diagnostics; + +using System.Collections.Immutable; +using Microsoft.Extensions.Localization; + +public sealed record DiagnosticDescriptor +{ + internal DiagnosticDescriptor() + { + } + + private static ImmutableDictionary s_errorCodeToDescriptorMap = ImmutableDictionary.Empty; + + public required string Id { get; init; } + + public required LocalizedString Title { get; init; } + + public required LocalizedString Description { get; init; } + + public required string HelpLink { get; init; } + + public DiagnosticSeverity DefaultSeverity { get; init; } + + internal static DiagnosticDescriptor CreateDescriptor(ushort diagnosticCode, AbstractMessageProvider messageProvider) + { + return ImmutableInterlocked.GetOrAdd( + location: ref s_errorCodeToDescriptorMap, + key: diagnosticCode, + valueFactory: static (code, messageProvider) => new DiagnosticDescriptor + { + Id = messageProvider.GetIdForDiagnosticCode(code), + Title = messageProvider.GetTitle(code), + Description = messageProvider.GetDescription(code), + HelpLink = messageProvider.GetHelpLink(code), + DefaultSeverity = (DiagnosticSeverity)messageProvider.GetSeverity(code), + }, + factoryArgument: messageProvider); + } +} diff --git a/src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticSeverity.cs b/src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticSeverity.cs new file mode 100644 index 0000000..fc57a90 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticSeverity.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Diagnostics; + +public enum DiagnosticSeverity +{ + Hidden = 0, + Info = 1, + Warning = 2, + Error = 3, +} diff --git a/src/OffDotNet.CodeAnalysis/Diagnostics/IMessageProvider.cs b/src/OffDotNet.CodeAnalysis/Diagnostics/IMessageProvider.cs new file mode 100644 index 0000000..eb1f388 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Diagnostics/IMessageProvider.cs @@ -0,0 +1,10 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Diagnostics; + +internal interface IMessageProvider +{ +} diff --git a/src/OffDotNet.CodeAnalysis/Lexer/CursorFactory.cs b/src/OffDotNet.CodeAnalysis/Lexer/CursorFactory.cs index c2da183..3773234 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/CursorFactory.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/CursorFactory.cs @@ -5,6 +5,8 @@ namespace OffDotNet.CodeAnalysis.Lexer; +using Microsoft.Extensions.Localization; + /// /// Factory class for creating instances of . /// diff --git a/src/OffDotNet.CodeAnalysis/OffDotNet.CodeAnalysis.csproj b/src/OffDotNet.CodeAnalysis/OffDotNet.CodeAnalysis.csproj index 6962d5d..73684f2 100644 --- a/src/OffDotNet.CodeAnalysis/OffDotNet.CodeAnalysis.csproj +++ b/src/OffDotNet.CodeAnalysis/OffDotNet.CodeAnalysis.csproj @@ -1,6 +1,8 @@  + + diff --git a/src/OffDotNet.CodeAnalysis/packages.lock.json b/src/OffDotNet.CodeAnalysis/packages.lock.json index fe3a08b..b26b2c1 100644 --- a/src/OffDotNet.CodeAnalysis/packages.lock.json +++ b/src/OffDotNet.CodeAnalysis/packages.lock.json @@ -8,6 +8,24 @@ "resolved": "8.0.1", "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" }, + "Microsoft.Extensions.Localization": { + "type": "Direct", + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "kdI24IfC1Jk3zfhWvYmuzvkzuKp+5Shjmfq0vk6EKBB+Udt4Gu1SkVSkMWJkYZFag+ndd+sTroUKDC/sud/DYQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", + "Microsoft.Extensions.Localization.Abstractions": "8.0.7", + "Microsoft.Extensions.Logging.Abstractions": "8.0.1", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "Direct", + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "uCsxqBBJEsSoV52tX/v+bxxN0e1O8WYIRR7tmcEmVVkRSX+0k4ZJcaL79ddus+0HIm3ssjV/dJk56XufDrGDqg==" + }, "Microsoft.Extensions.ObjectPool": { "type": "Direct", "requested": "[8.0.7, )", @@ -22,9 +40,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.29.0.95321, )", - "resolved": "9.29.0.95321", - "contentHash": "dAvGL7gavcUYk83p1StVzvMFcbwN4hr08hjthsRr/wR/uPFgjd7LUn7QGN/1iH92aA7P72x06RRNtdUAqmVHdw==" + "requested": "[9.30.0.95878, )", + "resolved": "9.30.0.95878", + "contentHash": "P0DylTJphECGE9HD6yho7rb6qs2WTTW36QUu5ezGRJeZpq8ItlRLmzUpxNHVOaudJywcnKO1DdRyZSaU7LXOcA==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -35,6 +53,28 @@ "StyleCop.Analyzers.Unstable": "1.2.0.556" } }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "RIFgaqoaINxkM2KTOw72dmilDmTrYA0ns2KW4lDz4gZ2+o6IQ894CzmdL3StM2oh7QQq44nCWiqKqc4qUI9Jmg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "8.0.2", + "contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" + }, "StyleCop.Analyzers.Unstable": { "type": "Transitive", "resolved": "1.2.0.556", diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Diagnostics/MessageProviderTests.cs b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Diagnostics/MessageProviderTests.cs new file mode 100644 index 0000000..7fbcfd3 --- /dev/null +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Diagnostics/MessageProviderTests.cs @@ -0,0 +1,124 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Pdf.Tests.Diagnostics; + +using Configurations; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using OffDotNet.CodeAnalysis.Diagnostics; +using OffDotNet.CodeAnalysis.Pdf.Diagnostics; + +[WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] +public class MessageProviderTests +{ + private const string HelpLink = "https://help.local/{0}"; + private readonly IStringLocalizer _localizer = Substitute.For>(); + private readonly IOptions _options = Substitute.For>(); + + public MessageProviderTests() + { + _options.Value.Returns(new DiagnosticOptions { HelpLink = HelpLink }); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"Class should implement the {nameof(IMessageProvider)} interface")] + public void Class_ShouldImplementTheIMessageProviderInterface() + { + // Arrange + + // Act + var actual = new MessageProvider(_localizer, _options); + + // Assert + Assert.IsAssignableFrom(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(MessageProvider.LanguagePrefix)} should return the 'PDF' value")] + public void LanguagePrefix_ShouldReturnThePDFValue() + { + // Arrange + const string Expected = "PDF"; + var messageProvider = new MessageProvider(_localizer, _options); + + // Act + var actual = messageProvider.LanguagePrefix; + + // Assert + Assert.Equal(Expected, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(MessageProvider.GetTitle)} should return the localized title")] + public void GetTitle_ShouldReturnTheLocalizedTitle() + { + // Arrange + const string Expected = "Localized Title"; + const DiagnosticCode Code = DiagnosticCode.Unknown; + var stringCode = Code + "_Title"; + + var messageProvider = new MessageProvider(_localizer, _options); + _localizer[stringCode].Returns(new LocalizedString(stringCode, Expected)); + + // Act + var actual = messageProvider.GetTitle((ushort)Code); + + // Assert + Assert.Equal(Expected, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(MessageProvider.GetDescription)} should return the localized description")] + public void GetDescription_ShouldReturnTheLocalizedDescription() + { + // Arrange + const string Expected = "Localized Description"; + const DiagnosticCode Code = DiagnosticCode.Unknown; + var stringCode = Code + "_Description"; + + var messageProvider = new MessageProvider(_localizer, _options); + _localizer[stringCode].Returns(new LocalizedString(stringCode, Expected)); + + // Act + var actual = messageProvider.GetDescription((ushort)Code); + + // Assert + Assert.Equal(Expected, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(MessageProvider.GetHelpLink)} should return the help URL")] + public void GetHelpLink_ShouldReturnTheHelpURL() + { + // Arrange + const int Code = 1234; + const string Expected = "https://help.local/PDF1234"; + + _options.Value.Returns(new DiagnosticOptions { HelpLink = HelpLink }); + var messageProvider = new MessageProvider(_localizer, _options); + + // Act + var actual = messageProvider.GetHelpLink(Code); + + // Assert + Assert.Equal(Expected, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(MessageProvider.GetSeverity)} should return the severity")] + [InlineData(DiagnosticCode.Unknown, DiagnosticSeverity.Hidden)] + public void GetSeverity_ShouldReturnTheSeverity(DiagnosticCode code, DiagnosticSeverity expected) + { + // Arrange + var messageProvider = new MessageProvider(_localizer, _options); + + // Act + var actual = messageProvider.GetSeverity((ushort)code); + + // Assert + Assert.Equal((byte)expected, actual); + } +} diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json index da788e6..73fbc85 100644 --- a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json @@ -41,9 +41,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.29.0.95321, )", - "resolved": "9.29.0.95321", - "contentHash": "dAvGL7gavcUYk83p1StVzvMFcbwN4hr08hjthsRr/wR/uPFgjd7LUn7QGN/1iH92aA7P72x06RRNtdUAqmVHdw==" + "requested": "[9.30.0.95878, )", + "resolved": "9.30.0.95878", + "contentHash": "P0DylTJphECGE9HD6yho7rb6qs2WTTW36QUu5ezGRJeZpq8ItlRLmzUpxNHVOaudJywcnKO1DdRyZSaU7LXOcA==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -84,6 +84,28 @@ "resolved": "17.10.0", "contentHash": "yC7oSlnR54XO5kOuHlVOKtxomNNN1BWXX8lK1G2jaPXT9sUok7kCOoA4Pgs0qyFaCtMrNsprztYMeoEGqCm4uA==" }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "RIFgaqoaINxkM2KTOw72dmilDmTrYA0ns2KW4lDz4gZ2+o6IQ894CzmdL3StM2oh7QQq44nCWiqKqc4qUI9Jmg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "8.0.2", + "contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" + }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "17.10.0", @@ -165,6 +187,8 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", + "Microsoft.Extensions.Localization": "[8.0.7, )", + "Microsoft.Extensions.Localization.Abstractions": "[8.0.7, )", "Microsoft.Extensions.ObjectPool": "[8.0.7, )" } }, @@ -191,6 +215,24 @@ "resolved": "8.0.1", "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" }, + "Microsoft.Extensions.Localization": { + "type": "CentralTransitive", + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "kdI24IfC1Jk3zfhWvYmuzvkzuKp+5Shjmfq0vk6EKBB+Udt4Gu1SkVSkMWJkYZFag+ndd+sTroUKDC/sud/DYQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", + "Microsoft.Extensions.Localization.Abstractions": "8.0.7", + "Microsoft.Extensions.Logging.Abstractions": "8.0.1", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "CentralTransitive", + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "uCsxqBBJEsSoV52tX/v+bxxN0e1O8WYIRR7tmcEmVVkRSX+0k4ZJcaL79ddus+0HIm3ssjV/dJk56XufDrGDqg==" + }, "Microsoft.Extensions.ObjectPool": { "type": "CentralTransitive", "requested": "[8.0.7, )", diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Diagnostics/AbstractMessageProviderTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Diagnostics/AbstractMessageProviderTests.cs new file mode 100644 index 0000000..f6de16b --- /dev/null +++ b/tests/OffDotNet.CodeAnalysis.Tests/Diagnostics/AbstractMessageProviderTests.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Tests.Diagnostics; + +using OffDotNet.CodeAnalysis.Diagnostics; + +[WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] +public class AbstractMessageProviderTests +{ + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"Class should implement the {nameof(IMessageProvider)} interface")] + public void Class_ShouldImplementTheIMessageProviderInterface() + { + // Arrange + + // Act + var actual = Substitute.For(); + + // Assert + Assert.IsAssignableFrom(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(AbstractMessageProvider.GetIdForDiagnosticCode)} method should return the language prefix and the diagnostic code")] + public void GetIdForDiagnosticCodeMethod_ShouldReturnTheLanguagePrefixAndTheDiagnosticCode() + { + // Arrange + const ushort Code = 1234; + const string LanguagePrefix = "PDF"; + const string ExpectedDiagnosticId = "PDF1234"; + var messageProvider = Substitute.For(); + messageProvider.LanguagePrefix.Returns(LanguagePrefix); + + // Act + var id = messageProvider.GetIdForDiagnosticCode(Code); + + // Assert + Assert.Equal(ExpectedDiagnosticId, id); + } +} diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Diagnostics/DiagnosticDescriptorTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Diagnostics/DiagnosticDescriptorTests.cs new file mode 100644 index 0000000..4ed6236 --- /dev/null +++ b/tests/OffDotNet.CodeAnalysis.Tests/Diagnostics/DiagnosticDescriptorTests.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Tests.Diagnostics; + +using Microsoft.Extensions.Localization; +using OffDotNet.CodeAnalysis.Diagnostics; + +[WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] +public class DiagnosticDescriptorTests +{ + private const string DiagnosticId = "PDF1234"; + private const string LocalizedString = "Diagnostic Description"; + private const string ExpectedHelpLink = "https://help.local/{0}"; + private const DiagnosticSeverity ExpectedSeverity = DiagnosticSeverity.Error; + private readonly LocalizedString _localizedString = new(LocalizedString, LocalizedString); + private readonly DiagnosticDescriptor _descriptor; + + public DiagnosticDescriptorTests() + { + _descriptor = new DiagnosticDescriptor + { + Id = DiagnosticId, + Title = _localizedString, + Description = _localizedString, + HelpLink = ExpectedHelpLink, + DefaultSeverity = ExpectedSeverity, + }; + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"Class should implement the {nameof(IEquatable)} interface")] + public void Class_ShouldImplementTheIEquatableInterface() + { + // Arrange + + // Act + var actual = _descriptor; + + // Assert + Assert.IsAssignableFrom>(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(DiagnosticDescriptor.Id)} property should return the diagnostic id")] + public void IdProperty_ShouldReturnTheDiagnosticId() + { + // Arrange + + // Act + var id = _descriptor.Id; + + // Assert + Assert.Equal(DiagnosticId, id); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(DiagnosticDescriptor.Title)} property should return the diagnostic title")] + public void TitleProperty_ShouldReturnTheDiagnosticTitle() + { + // Arrange + + // Act + var title = _descriptor.Title; + + // Assert + Assert.Equal(LocalizedString, title); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(DiagnosticDescriptor.Description)} property should return the diagnostic description")] + public void DescriptionProperty_ShouldReturnTheDiagnosticDescription() + { + // Arrange + + // Act + var description = _descriptor.Description; + + // Assert + Assert.Equal(LocalizedString, description); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(DiagnosticDescriptor.HelpLink)} property should return the diagnostic help link")] + public void HelpLinkProperty_ShouldReturnTheDiagnosticHelpLink() + { + // Arrange + + // Act + var helpLink = _descriptor.HelpLink; + + // Assert + Assert.Equal(ExpectedHelpLink, helpLink); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(DiagnosticDescriptor.DefaultSeverity)} property should return the diagnostic default severity")] + public void DefaultSeverityProperty_ShouldReturnTheDiagnosticDefaultSeverity() + { + // Arrange + + // Act + var defaultSeverity = _descriptor.DefaultSeverity; + + // Assert + Assert.Equal(ExpectedSeverity, defaultSeverity); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(DiagnosticDescriptor.CreateDescriptor)} method should return a new instance of the {nameof(DiagnosticDescriptor)} class")] + public void CreateDescriptorMethod_ShouldReturnANewInstance() + { + // Arrange + const ushort Id = 1234; + var abstractMessageProvider = Substitute.For(); + + // Act + var actual = DiagnosticDescriptor.CreateDescriptor(Id, abstractMessageProvider); + + // Assert + Assert.IsType(actual); + } +} diff --git a/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json b/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json index 8dc8f96..2259755 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json +++ b/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json @@ -41,9 +41,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.29.0.95321, )", - "resolved": "9.29.0.95321", - "contentHash": "dAvGL7gavcUYk83p1StVzvMFcbwN4hr08hjthsRr/wR/uPFgjd7LUn7QGN/1iH92aA7P72x06RRNtdUAqmVHdw==" + "requested": "[9.30.0.95878, )", + "resolved": "9.30.0.95878", + "contentHash": "P0DylTJphECGE9HD6yho7rb6qs2WTTW36QUu5ezGRJeZpq8ItlRLmzUpxNHVOaudJywcnKO1DdRyZSaU7LXOcA==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -84,6 +84,28 @@ "resolved": "17.10.0", "contentHash": "yC7oSlnR54XO5kOuHlVOKtxomNNN1BWXX8lK1G2jaPXT9sUok7kCOoA4Pgs0qyFaCtMrNsprztYMeoEGqCm4uA==" }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "RIFgaqoaINxkM2KTOw72dmilDmTrYA0ns2KW4lDz4gZ2+o6IQ894CzmdL3StM2oh7QQq44nCWiqKqc4qUI9Jmg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "8.0.2", + "contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" + }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "17.10.0", @@ -165,6 +187,8 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", + "Microsoft.Extensions.Localization": "[8.0.7, )", + "Microsoft.Extensions.Localization.Abstractions": "[8.0.7, )", "Microsoft.Extensions.ObjectPool": "[8.0.7, )" } }, @@ -174,6 +198,24 @@ "resolved": "8.0.1", "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" }, + "Microsoft.Extensions.Localization": { + "type": "CentralTransitive", + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "kdI24IfC1Jk3zfhWvYmuzvkzuKp+5Shjmfq0vk6EKBB+Udt4Gu1SkVSkMWJkYZFag+ndd+sTroUKDC/sud/DYQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", + "Microsoft.Extensions.Localization.Abstractions": "8.0.7", + "Microsoft.Extensions.Logging.Abstractions": "8.0.1", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "CentralTransitive", + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "uCsxqBBJEsSoV52tX/v+bxxN0e1O8WYIRR7tmcEmVVkRSX+0k4ZJcaL79ddus+0HIm3ssjV/dJk56XufDrGDqg==" + }, "Microsoft.Extensions.ObjectPool": { "type": "CentralTransitive", "requested": "[8.0.7, )", From f548f487c5da059d853d87c62c95006f09d2f1b1 Mon Sep 17 00:00:00 2001 From: Victor Pogor Date: Sun, 28 Jul 2024 13:12:51 +1000 Subject: [PATCH 07/14] added source text --- src/OffDotNet.CodeAnalysis/Dependencies.cs | 4 +- .../Lexer/CursorFactory.cs | 44 ---- .../Lexer/ICursorFactory.cs | 33 --- .../Lexer/ISourceText.cs | 17 ++ .../Lexer/StringText.cs | 25 ++ .../Lexer/TextCursor.cs | 77 ++---- .../Lexer/StringTextTests.cs | 98 ++++++++ .../Lexer/TextCursorTests.cs | 235 +++++++++++++----- 8 files changed, 323 insertions(+), 210 deletions(-) delete mode 100644 src/OffDotNet.CodeAnalysis/Lexer/CursorFactory.cs delete mode 100644 src/OffDotNet.CodeAnalysis/Lexer/ICursorFactory.cs create mode 100644 src/OffDotNet.CodeAnalysis/Lexer/ISourceText.cs create mode 100644 src/OffDotNet.CodeAnalysis/Lexer/StringText.cs create mode 100644 tests/OffDotNet.CodeAnalysis.Tests/Lexer/StringTextTests.cs diff --git a/src/OffDotNet.CodeAnalysis/Dependencies.cs b/src/OffDotNet.CodeAnalysis/Dependencies.cs index 5a89d49..b81cdaf 100644 --- a/src/OffDotNet.CodeAnalysis/Dependencies.cs +++ b/src/OffDotNet.CodeAnalysis/Dependencies.cs @@ -5,9 +5,8 @@ namespace OffDotNet.CodeAnalysis; -using Lexer; +using Diagnostics; using Microsoft.Extensions.DependencyInjection; -using OffDotNet.CodeAnalysis.Diagnostics; /// /// Provides extension methods for registering code analysis services. @@ -22,7 +21,6 @@ public static class Dependencies public static IServiceCollection AddCoreCodeAnalysis(this IServiceCollection services) { services.AddLocalization(options => options.ResourcesPath = "Resources"); - services.AddSingleton(); services.AddSingleton(); return services; } diff --git a/src/OffDotNet.CodeAnalysis/Lexer/CursorFactory.cs b/src/OffDotNet.CodeAnalysis/Lexer/CursorFactory.cs deleted file mode 100644 index 3773234..0000000 --- a/src/OffDotNet.CodeAnalysis/Lexer/CursorFactory.cs +++ /dev/null @@ -1,44 +0,0 @@ -// -// Copyright (c) Sunt Programator. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -namespace OffDotNet.CodeAnalysis.Lexer; - -using Microsoft.Extensions.Localization; - -/// -/// Factory class for creating instances of . -/// -internal sealed class CursorFactory : ICursorFactory -{ - /// - /// Creates an instance of from a read-only byte span. - /// - /// The text represented as a read-only byte span. - /// An instance of initialized with the provided text. - public ITextCursor Create(ReadOnlySpan text) - { - return new TextCursor(text); - } - - /// - /// Creates an instance of from a read-only character span. - /// - /// The text represented as a read-only character span. - /// An instance of initialized with the provided text. - public ITextCursor Create(ReadOnlySpan text) - { - return new TextCursor(text); - } - - /// - /// Creates an instance of from a string. - /// - /// The text represented as a string. - /// An instance of initialized with the provided text. - public ITextCursor Create(string text) - { - return new TextCursor(text); - } -} diff --git a/src/OffDotNet.CodeAnalysis/Lexer/ICursorFactory.cs b/src/OffDotNet.CodeAnalysis/Lexer/ICursorFactory.cs deleted file mode 100644 index 384336e..0000000 --- a/src/OffDotNet.CodeAnalysis/Lexer/ICursorFactory.cs +++ /dev/null @@ -1,33 +0,0 @@ -// -// Copyright (c) Sunt Programator. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -namespace OffDotNet.CodeAnalysis.Lexer; - -/// -/// Factory interface for creating instances of . -/// -public interface ICursorFactory -{ - /// - /// Creates an instance of from a read-only byte span. - /// - /// The text represented as a read-only byte span. - /// An instance of initialized with the provided text. - ITextCursor Create(ReadOnlySpan text); - - /// - /// Creates an instance of from a read-only character span. - /// - /// The text represented as a read-only character span. - /// An instance of initialized with the provided text. - ITextCursor Create(ReadOnlySpan text); - - /// - /// Creates an instance of from a string. - /// - /// The text represented as a string. - /// An instance of initialized with the provided text. - ITextCursor Create(string text); -} diff --git a/src/OffDotNet.CodeAnalysis/Lexer/ISourceText.cs b/src/OffDotNet.CodeAnalysis/Lexer/ISourceText.cs new file mode 100644 index 0000000..a58df21 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Lexer/ISourceText.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Lexer; + +public interface ISourceText +{ + ReadOnlyMemory Source { get; } + + int Length { get; } + + byte this[int position] { get; } + + void CopyTo(int sourceIndex, byte[] destination, int destinationIndex, int count); +} diff --git a/src/OffDotNet.CodeAnalysis/Lexer/StringText.cs b/src/OffDotNet.CodeAnalysis/Lexer/StringText.cs new file mode 100644 index 0000000..f75fcec --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Lexer/StringText.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Lexer; + +internal sealed class StringText : ISourceText +{ + internal StringText(in ReadOnlySpan source) + { + Source = source.ToArray(); + } + + public ReadOnlyMemory Source { get; } + + public int Length => Source.Length; + + public byte this[int position] => Source.Span[position]; + + public void CopyTo(int sourceIndex, byte[] destination, int destinationIndex, int count) + { + Source.Span.Slice(sourceIndex, count).CopyTo(destination.AsSpan(destinationIndex)); + } +} diff --git a/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs b/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs index 0a3277f..33b5485 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs @@ -7,80 +7,44 @@ namespace OffDotNet.CodeAnalysis.Lexer; using Utils; -/// -/// Represents a text cursor for navigating and processing text data. -/// +/// Represents a text cursor for navigating and processing text data. internal sealed class TextCursor : ITextCursor { - private readonly IMemoryOwner _textOwner; private int _position; - /// - /// Initializes a new instance of the class with a read-only byte span. - /// + /// Initializes a new instance of the class with a read-only byte span. /// The text represented as a read-only byte span. - public TextCursor(in ReadOnlySpan text) + public TextCursor(ISourceText text) { + SourceText = text; Length = text.Length; _position = 0; - _textOwner = MemoryPool.Shared.Rent(text.Length); - - text.CopyTo(_textOwner.Memory.Span); - } - - /// - /// Initializes a new instance of the class with a read-only character span. - /// - /// The text represented as a read-only character span. - public TextCursor(in ReadOnlySpan text) - { - _position = 0; - - var maxByteCount = Encoding.ASCII.GetMaxByteCount(text.Length); - _textOwner = MemoryPool.Shared.Rent(maxByteCount); - Length = Encoding.ASCII.GetBytes(text, _textOwner.Memory.Span); } - /// - /// Initializes a new instance of the class with a string. - /// - /// The text represented as a string. - public TextCursor(string text) - : this(text.AsSpan()) - { - } + /// Gets the source text. + public ISourceText SourceText { get; } - /// - /// Gets the current byte at the cursor position. - /// + /// Gets the current byte at the cursor position. public Option Current => Peek(); - /// - /// Gets the length of the text. - /// + /// Gets the length of the text. public int Length { get; } - /// - /// Gets a value indicating whether the cursor is at the end of the text. - /// + /// Gets a value indicating whether the cursor is at the end of the text. public bool IsAtEnd => _position >= Length; - /// - /// Peeks at the byte at the specified delta from the current position. - /// + /// Peeks at the byte at the specified delta from the current position. /// The delta from the current position. /// The byte at the specified delta if available; otherwise, . public Option Peek(int delta = 0) { Debug.Assert(delta >= 0, "Delta should be positive"); return !IsAtEnd - ? Option.Some(_textOwner.Memory.Span[_position + delta]) + ? Option.Some(SourceText[_position + delta]) : Option.None; } - /// - /// Advances the cursor by the specified delta. - /// + /// Advances the cursor by the specified delta. /// The delta by which to advance the cursor. public void Advance(int delta = 1) { @@ -88,9 +52,7 @@ public void Advance(int delta = 1) _position += delta; } - /// - /// Advances the cursor while the specified predicate is true. - /// + /// Advances the cursor while the specified predicate is true. /// The predicate to test each byte against. public void Advance(Predicate predicate) { @@ -100,9 +62,7 @@ public void Advance(Predicate predicate) } } - /// - /// Tries to advance the cursor if the current byte matches the specified byte. - /// + /// Tries to advance the cursor if the current byte matches the specified byte. /// The byte to match against. /// True if the cursor was advanced; otherwise, false. public bool TryAdvance(byte b) @@ -116,9 +76,7 @@ public bool TryAdvance(byte b) return true; } - /// - /// Tries to advance the cursor if the subsequent bytes match the specified subtext. - /// + /// Tries to advance the cursor if the subsequent bytes match the specified subtext. /// The subtext to match against. /// True if the cursor was advanced; otherwise, false. public bool TryAdvance(ReadOnlySpan subtext) @@ -147,11 +105,8 @@ public bool TryAdvance(ReadOnlySpan subtext) return true; } - /// - /// Releases the resources used by the class. - /// + /// Releases the resources used by the class. public void Dispose() { - _textOwner.Dispose(); } } diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/StringTextTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/StringTextTests.cs new file mode 100644 index 0000000..da7e4b4 --- /dev/null +++ b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/StringTextTests.cs @@ -0,0 +1,98 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Tests.Lexer; + +using OffDotNet.CodeAnalysis.Lexer; + +[WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] +public class StringTextTests +{ + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"Class should implement {nameof(ISourceText)} interface")] + public void Class_ShouldImplementISourceText() + { + // Arrange + var text = "123"u8; + + // Act + var actual = new StringText(text); + + // Assert + Assert.IsAssignableFrom(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(StringText.Source)} property should return the value passed to the constructor")] + public void SourceProperty_ShouldReturnTheValuePassedToTheConstructor() + { + // Arrange + var text = "123"u8; + var source = text.ToArray(); + var stringText = new StringText(text); + + // Act + var actual = stringText.Source.ToArray(); + + // Assert + Assert.Equal(source, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = $"{nameof(StringText.Length)} property should return the length of the source text")] + public void LengthProperty_ShouldReturnTheLengthOfTheSourceText() + { + // Arrange + var text = "123"u8; + var stringText = new StringText(text); + + // Act + var actual = stringText.Length; + + // Assert + Assert.Equal(text.Length, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Fact(DisplayName = "Indexer should return the byte at the specified position")] + public void Indexer_ShouldReturnTheByteAtTheSpecifiedPosition() + { + // Arrange + var text = "123"u8; + const byte Expected = (byte)'2'; + var stringText = new StringText(text); + + // Act + var actual = stringText[1]; + + // Assert + Assert.Equal(Expected, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] + [Theory(DisplayName = $"{nameof(StringText.CopyTo)} method should copy the specified number of bytes from the source text to the destination")] + [InlineData(0, 0, 3, "123")] + [InlineData(0, 0, 2, "12\0")] + [InlineData(1, 0, 2, "23\0")] + [InlineData(0, 1, 2, "\012")] + [InlineData(1, 1, 1, "\02\0")] + public void CopyToMethod_ShouldCopyTheSpecifiedNumberOfBytesFromTheSourceTextToTheDestination( + int sourceIndex, + int destinationIndex, + int count, + string expected) + { + // Arrange + var text = "123"u8; + var destination = new byte[3]; + var stringText = new StringText(text); + + // Act + stringText.CopyTo(sourceIndex, destination, destinationIndex, count); + + // Assert + Assert.Equal(expected, Encoding.ASCII.GetString(destination)); + } +} diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs index 6a00feb..5f6f525 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs @@ -11,6 +11,8 @@ namespace OffDotNet.CodeAnalysis.Tests.Lexer; [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] public class TextCursorTests { + private readonly ISourceText _sourceText = Substitute.For(); + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] [Fact(DisplayName = $"Class should implement {nameof(IDisposable)} interface")] public void Class_ShouldImplementIDisposableInterface() @@ -18,7 +20,7 @@ public void Class_ShouldImplementIDisposableInterface() // Arrange // Act - var cursor = new TextCursor(string.Empty); + var cursor = new TextCursor(_sourceText); // Assert Assert.IsAssignableFrom(cursor); @@ -31,7 +33,7 @@ public void Class_ShouldImplementITextCursorInterface() // Arrange // Act - var cursor = new TextCursor(string.Empty); + var cursor = new TextCursor(_sourceText); // Assert Assert.IsAssignableFrom(cursor); @@ -42,7 +44,7 @@ public void Class_ShouldImplementITextCursorInterface() public void Dispose_ShouldNotThrowAnyException() { // Arrange - var cursor = new TextCursor(string.Empty); + var cursor = new TextCursor(_sourceText); // Act cursor.Dispose(); @@ -52,29 +54,17 @@ public void Dispose_ShouldNotThrowAnyException() } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] - [Fact(DisplayName = $"Constructor with {nameof(ReadOnlySpan)} parameter should copy the input text")] + [Fact(DisplayName = "Constructor should copy the input text")] public void Constructor_WithReadOnlySpanByteParameter_ShouldCopyInputText() { // Arrange - var text = "ABCD"u8.ToArray(); - var expected = text[0]; - var cursor = new TextCursor(text); + const int TextLength = 4; + const byte Expected = (byte)'A'; - // Act - var actual = cursor.Current; + _sourceText[0].Returns(Expected); + _sourceText.Length.Returns(TextLength); - // Assert - Assert.Equal(expected, actual.GetValueOrDefault(0x0)); - } - - [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] - [Fact(DisplayName = $"Constructor with {nameof(ReadOnlySpan)} parameter should copy the input text")] - public void Constructor_WithReadOnlySpanCharParameter_ShouldCopyInputText() - { - // Arrange - const string Text = "ABCD"; - const byte Expected = (byte)'A'; - var cursor = new TextCursor(Text.AsSpan()); + var cursor = new TextCursor(_sourceText); // Act var actual = cursor.Current; @@ -89,7 +79,7 @@ public void Current_ShouldReturnNone_WhenInputStringIsEmpty() { // Arrange var expected = Option.None; - var cursor = new TextCursor(string.Empty); + var cursor = new TextCursor(_sourceText); // Act var current = cursor.Current; @@ -103,15 +93,19 @@ public void Current_ShouldReturnNone_WhenInputStringIsEmpty() public void Current_ShouldReturnFirstCharacterOfInputString() { // Arrange - var text = "CDEF"u8.ToArray(); - var expected = text[0]; - var cursor = new TextCursor(text); + const int TextLength = 4; + const byte Expected = (byte)'C'; + + _sourceText[0].Returns(Expected); + _sourceText.Length.Returns(TextLength); + + var cursor = new TextCursor(_sourceText); // Act var actual = cursor.Current; // Assert - Assert.Equal(expected, actual.GetValueOrDefault(0x0)); + Assert.Equal(Expected, actual.GetValueOrDefault(0x0)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -119,9 +113,11 @@ public void Current_ShouldReturnFirstCharacterOfInputString() public void Current_ShouldReturnNone_WhenCursorIsAtTheEnd() { // Arrange - var text = "CDEF"u8.ToArray(); - var cursor = new TextCursor(text); - cursor.Advance(4); + const int TextLength = 4; + _sourceText.Length.Returns(TextLength); + + var cursor = new TextCursor(_sourceText); + cursor.Advance(TextLength); // Act var current = cursor.Current; @@ -136,7 +132,7 @@ public void IsAtEnd_ShouldReturnTrue_WhenInputStringIsEmpty() { // Arrange const bool Expected = true; - var cursor = new TextCursor(string.Empty); + var cursor = new TextCursor(_sourceText); // Act var isAtEnd = cursor.IsAtEnd; @@ -151,7 +147,11 @@ public void IsAtEnd_ShouldReturnFalse_WhenInputStringIsNotEmpty_AndCursorIsAtThe { // Arrange const bool Expected = false; - var cursor = new TextCursor("ABCD"); + const int TextLength = 4; + + _sourceText.Length.Returns(TextLength); + + var cursor = new TextCursor(_sourceText); // Act var isAtEnd = cursor.IsAtEnd; @@ -166,8 +166,12 @@ public void IsAtEnd_ShouldReturnTrue_WhenCursorIsAtTheEnd() { // Arrange const bool Expected = true; - var cursor = new TextCursor("ABCD"); - cursor.Advance(4); + const int TextLength = 4; + + _sourceText.Length.Returns(TextLength); + + var cursor = new TextCursor(_sourceText); + cursor.Advance(TextLength); // Act var isAtEnd = cursor.IsAtEnd; @@ -182,7 +186,10 @@ public void Length_ShouldReturnLengthOfInputString() { // Arrange const int Expected = 4; - var cursor = new TextCursor("ABCD"); + + _sourceText.Length.Returns(Expected); + + var cursor = new TextCursor(_sourceText); // Act var length = cursor.Length; @@ -197,7 +204,7 @@ public void Peek_ShouldReturnNone_WhenInputStringIsEmpty() { // Arrange var expected = Option.None; - var cursor = new TextCursor(string.Empty); + var cursor = new TextCursor(_sourceText); // Act var peek = cursor.Peek(); @@ -211,15 +218,19 @@ public void Peek_ShouldReturnNone_WhenInputStringIsEmpty() public void Peek_ShouldReturnFirstCharacterOfInputString() { // Arrange - var text = "CDEF"u8.ToArray(); - var expected = text[0]; - var cursor = new TextCursor(text); + const int TextLength = 4; + const byte Expected = (byte)'C'; + + _sourceText[0].Returns(Expected); + _sourceText.Length.Returns(TextLength); + + var cursor = new TextCursor(_sourceText); // Act var actual = cursor.Peek(); // Assert - Assert.Equal(expected, actual.GetValueOrDefault(0x0)); + Assert.Equal(Expected, actual.GetValueOrDefault(0x0)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -227,15 +238,20 @@ public void Peek_ShouldReturnFirstCharacterOfInputString() public void Peek_ShouldReturnNthCharacterOfInputString() { // Arrange - var text = "CDEF"u8.ToArray(); - var expected = text[2]; - var cursor = new TextCursor(text); + const int Index = 2; + const int TextLength = 4; + const byte Expected = (byte)'E'; + + _sourceText[Index].Returns(Expected); + _sourceText.Length.Returns(TextLength); + + var cursor = new TextCursor(_sourceText); // Act - var actual = cursor.Peek(2); + var actual = cursor.Peek(Index); // Assert - Assert.Equal(expected, actual.GetValueOrDefault(0x0)); + Assert.Equal(Expected, actual.GetValueOrDefault(0x0)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -243,9 +259,12 @@ public void Peek_ShouldReturnNthCharacterOfInputString() public void Peek_ShouldReturnNone_WhenCursorIsAtTheEnd() { // Arrange - var text = "CDEF"u8.ToArray(); - var cursor = new TextCursor(text); - cursor.Advance(4); + const int TextLength = 4; + + _sourceText.Length.Returns(TextLength); + + var cursor = new TextCursor(_sourceText); + cursor.Advance(TextLength); // Act var peek = cursor.Peek(); @@ -259,16 +278,20 @@ public void Peek_ShouldReturnNone_WhenCursorIsAtTheEnd() public void Advance_ShouldAdvanceCursorByOnePosition() { // Arrange - var text = "CDEF"u8.ToArray(); - var expected = text[1]; - var cursor = new TextCursor(text); + const int TextLength = 4; + const byte Expected = (byte)'D'; + + _sourceText.Length.Returns(TextLength); + _sourceText[1].Returns(Expected); + + var cursor = new TextCursor(_sourceText); // Act cursor.Advance(); var actual = cursor.Current; // Assert - Assert.Equal(expected, actual.GetValueOrDefault(0x0)); + Assert.Equal(Expected, actual.GetValueOrDefault(0x0)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -276,16 +299,21 @@ public void Advance_ShouldAdvanceCursorByOnePosition() public void Advance_ShouldAdvanceCursorByNPositions() { // Arrange - var text = "CDEF"u8.ToArray(); - var expected = text[2]; - var cursor = new TextCursor(text); + const int TextLength = 4; + const int Index = 2; + const byte Expected = (byte)'E'; + + _sourceText.Length.Returns(TextLength); + _sourceText[Index].Returns(Expected); + + var cursor = new TextCursor(_sourceText); // Act - cursor.Advance(2); + cursor.Advance(Index); var actual = cursor.Current; // Assert - Assert.Equal(expected, actual.GetValueOrDefault(0x0)); + Assert.Equal(Expected, actual.GetValueOrDefault(0x0)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -293,15 +321,23 @@ public void Advance_ShouldAdvanceCursorByNPositions() public void Advance_WithPredicate_ShouldAdvanceCursor_IfPredicateIsTrue() { // Arrange + const byte Expected = (byte)'E'; var text = "CDEF"u8.ToArray(); - var cursor = new TextCursor(text); + + _sourceText.Length.Returns(text.Length); + _sourceText[0].Returns(text[0]); + _sourceText[1].Returns(text[1]); + _sourceText[2].Returns(text[2]); + _sourceText[3].Returns(text[3]); + + var cursor = new TextCursor(_sourceText); // Act cursor.Advance(static x => x is (byte)'C' or (byte)'D'); var peek = cursor.Current; // Assert - Assert.Equal((byte)'E', peek.GetValueOrDefault(0x0)); + Assert.Equal(Expected, peek.GetValueOrDefault(0x0)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -309,15 +345,23 @@ public void Advance_WithPredicate_ShouldAdvanceCursor_IfPredicateIsTrue() public void Advance_WithPredicate_ShouldNotAdvanceCursor_IfPredicateIsFalse() { // Arrange + const byte Expected = (byte)'C'; var text = "CDEF"u8.ToArray(); - var cursor = new TextCursor(text); + + _sourceText.Length.Returns(text.Length); + _sourceText[0].Returns(text[0]); + _sourceText[1].Returns(text[1]); + _sourceText[2].Returns(text[2]); + _sourceText[3].Returns(text[3]); + + var cursor = new TextCursor(_sourceText); // Act cursor.Advance(static x => x is (byte)'A' or (byte)'B'); var peek = cursor.Current; // Assert - Assert.Equal((byte)'C', peek.GetValueOrDefault(0x0)); + Assert.Equal(Expected, peek.GetValueOrDefault(0x0)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -325,8 +369,16 @@ public void Advance_WithPredicate_ShouldNotAdvanceCursor_IfPredicateIsFalse() public void TryAdvance_ShouldReturnFalse_IfCharacterAtCurrentPositionIsNotTheExpectedOne() { // Arrange + const byte Expected = (byte)'C'; var text = "CDEF"u8.ToArray(); - var cursor = new TextCursor(text); + + _sourceText.Length.Returns(text.Length); + _sourceText[0].Returns(text[0]); + _sourceText[1].Returns(text[1]); + _sourceText[2].Returns(text[2]); + _sourceText[3].Returns(text[3]); + + var cursor = new TextCursor(_sourceText); // Act var result = cursor.TryAdvance((byte)'A'); @@ -334,7 +386,7 @@ public void TryAdvance_ShouldReturnFalse_IfCharacterAtCurrentPositionIsNotTheExp // Assert Assert.False(result); - Assert.Equal((byte)'C', peek.GetValueOrDefault(0x0)); + Assert.Equal(Expected, peek.GetValueOrDefault(0x0)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -342,8 +394,16 @@ public void TryAdvance_ShouldReturnFalse_IfCharacterAtCurrentPositionIsNotTheExp public void TryAdvance_ShouldReturnTrue_IfCharacterAtCurrentPositionIsTheExpectedOne() { // Arrange + const byte Expected = (byte)'D'; var text = "CDEF"u8.ToArray(); - var cursor = new TextCursor(text); + + _sourceText.Length.Returns(text.Length); + _sourceText[0].Returns(text[0]); + _sourceText[1].Returns(text[1]); + _sourceText[2].Returns(text[2]); + _sourceText[3].Returns(text[3]); + + var cursor = new TextCursor(_sourceText); // Act var result = cursor.TryAdvance((byte)'C'); @@ -351,7 +411,7 @@ public void TryAdvance_ShouldReturnTrue_IfCharacterAtCurrentPositionIsTheExpecte // Assert Assert.True(result); - Assert.Equal((byte)'D', peek.GetValueOrDefault(0x0)); + Assert.Equal(Expected, peek.GetValueOrDefault(0x0)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -359,8 +419,16 @@ public void TryAdvance_ShouldReturnTrue_IfCharacterAtCurrentPositionIsTheExpecte public void TryAdvance_WithEmptySubText_ShouldReturnFalse() { // Arrange + const byte Expected = (byte)'C'; var text = "CDEF"u8.ToArray(); - var cursor = new TextCursor(text); + + _sourceText.Length.Returns(text.Length); + _sourceText[0].Returns(text[0]); + _sourceText[1].Returns(text[1]); + _sourceText[2].Returns(text[2]); + _sourceText[3].Returns(text[3]); + + var cursor = new TextCursor(_sourceText); // Act var result = cursor.TryAdvance(ReadOnlySpan.Empty); @@ -368,7 +436,7 @@ public void TryAdvance_WithEmptySubText_ShouldReturnFalse() // Assert Assert.False(result); - Assert.Equal((byte)'C', peek.GetValueOrDefault(0x0)); + Assert.Equal(Expected, peek.GetValueOrDefault(0x0)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -380,8 +448,16 @@ public void TryAdvance_WithEmptySubText_ShouldReturnFalse() public void TryAdvance_WithSubText_ShouldReturnFalse_IfSubTextIsNotFound(string subtext) { // Arrange + const byte Expected = (byte)'C'; var text = "CDEF"u8.ToArray(); - var cursor = new TextCursor(text); + + _sourceText.Length.Returns(text.Length); + _sourceText[0].Returns(text[0]); + _sourceText[1].Returns(text[1]); + _sourceText[2].Returns(text[2]); + _sourceText[3].Returns(text[3]); + + var cursor = new TextCursor(_sourceText); // Act var result = cursor.TryAdvance(Encoding.ASCII.GetBytes(subtext)); @@ -389,7 +465,7 @@ public void TryAdvance_WithSubText_ShouldReturnFalse_IfSubTextIsNotFound(string // Assert Assert.False(result); - Assert.Equal((byte)'C', peek.GetValueOrDefault(0x0)); + Assert.Equal(Expected, peek.GetValueOrDefault(0x0)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -402,7 +478,14 @@ public void TryAdvance_WithSubText_ShouldReturnTrue_IfSubTextIsFound(string subt { // Arrange var text = "CDEF"u8.ToArray(); - var cursor = new TextCursor(text); + + _sourceText.Length.Returns(text.Length); + _sourceText[0].Returns(text[0]); + _sourceText[1].Returns(text[1]); + _sourceText[2].Returns(text[2]); + _sourceText[3].Returns(text[3]); + + var cursor = new TextCursor(_sourceText); // Act var result = cursor.TryAdvance(Encoding.ASCII.GetBytes(subtext)); @@ -412,4 +495,18 @@ public void TryAdvance_WithSubText_ShouldReturnTrue_IfSubTextIsFound(string subt Assert.True(result); Assert.Equal(nextChar, peek.GetValueOrDefault(0x0)); } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.SourceText)} property should return the source text")] + public void SourceText_ShouldReturnSourceText() + { + // Arrange + var cursor = new TextCursor(_sourceText); + + // Act + var actual = cursor.SourceText; + + // Assert + Assert.Same(_sourceText, actual); + } } From 57844195b522c3407fce6888b2c169f94f7760c5 Mon Sep 17 00:00:00 2001 From: Victor Pogor Date: Sun, 28 Jul 2024 13:18:44 +1000 Subject: [PATCH 08/14] added docs --- .../Lexer/ITextCursor.cs | 19 +++++++++++++++++++ .../Lexer/TextCursor.cs | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs b/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs index 4119925..5398f4b 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs @@ -8,6 +8,25 @@ namespace OffDotNet.CodeAnalysis.Lexer; using Utils; /// Represents a text cursor for navigating and processing text data. +/// +/// +/// SLIDING position +/// WINDOW | +/// -------- + -------- +/// | | +/// basis =======>> offset +/// | | +/// ------------------------------- +/// SRC: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +/// ------------------------------- +/// | | +/// basis ==>> offset +/// | | +/// ---- + ---- +/// LEXEME | +/// SUB-WINDOW position +/// +/// public interface ITextCursor : IDisposable { /// diff --git a/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs b/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs index 33b5485..78b8bcd 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs @@ -8,6 +8,25 @@ namespace OffDotNet.CodeAnalysis.Lexer; using Utils; /// Represents a text cursor for navigating and processing text data. +/// +/// +/// SLIDING position +/// WINDOW | +/// -------- + -------- +/// | | +/// basis =======>> offset +/// | | +/// ------------------------------- +/// SRC: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +/// ------------------------------- +/// | | +/// basis ==>> offset +/// | | +/// ---- + ---- +/// LEXEME | +/// SUB-WINDOW position +/// +/// internal sealed class TextCursor : ITextCursor { private int _position; From 611260e098314cb65d643f03a89e9beb47655750 Mon Sep 17 00:00:00 2001 From: Victor Pogor Date: Sun, 28 Jul 2024 18:53:46 +1000 Subject: [PATCH 09/14] enhanced text cursor --- .../Lexer/ITextCursor.cs | 38 +- .../Lexer/TextCursor.cs | 113 ++++- .../PooledObjects/ArrayPooledObjectPolicy.cs | 53 ++ .../PooledObjects/SharedObjectPools.cs | 7 +- .../Lexer/TextCursorTests.cs | 467 ++++++++++++------ 5 files changed, 500 insertions(+), 178 deletions(-) create mode 100644 src/OffDotNet.CodeAnalysis/PooledObjects/ArrayPooledObjectPolicy.cs diff --git a/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs b/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs index 5398f4b..d00a33a 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs @@ -29,21 +29,43 @@ namespace OffDotNet.CodeAnalysis.Lexer; /// public interface ITextCursor : IDisposable { + /// Gets the source text. + ISourceText SourceText { get; } + /// /// Gets the current byte at the cursor position. /// Option Current { get; } - /// - /// Gets the length of the text. - /// - int Length { get; } - /// /// Gets a value indicating whether the cursor is at the end of the text. /// bool IsAtEnd { get; } + /// Gets the start offset inside the window (relative to the start). + int Basis { get; } + + /// Gets the end offset inside the window (relative to the ). + int Offset { get; } + + /// Gets the absolute position in the . + int Position => Basis + Offset; + + /// Gets a value indicating whether the window is in parsing lexeme mode. + bool IsLexemeMode { get; } + + /// Gets the lexeme start offset relative to the window start. + int LexemeBasis { get; } + + /// Gets the absolute position of the lexeme in the . + int LexemePosition => Basis + LexemeBasis; + + /// Gets the width of the lexeme. + int LexemeWidth => Offset - LexemeBasis; + + /// Gets the number of characters in the window. + int WindowCount { get; } + /// /// Peeks at the byte at the specified delta from the current position. /// @@ -76,4 +98,10 @@ public interface ITextCursor : IDisposable /// The subtext to match against. /// True if the cursor was advanced; otherwise, false. bool TryAdvance(ReadOnlySpan subtext); + + /// Starts parsing a lexeme and sets the to the current value. + public void StartLexemeMode(); + + /// Stops parsing a lexeme. + public void StopLexemeMode(); } diff --git a/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs b/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs index 78b8bcd..aaacc27 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs @@ -5,6 +5,7 @@ namespace OffDotNet.CodeAnalysis.Lexer; +using PooledObjects; using Utils; /// Represents a text cursor for navigating and processing text data. @@ -29,15 +30,17 @@ namespace OffDotNet.CodeAnalysis.Lexer; /// internal sealed class TextCursor : ITextCursor { - private int _position; + private byte[] _characterWindow; /// Initializes a new instance of the class with a read-only byte span. /// The text represented as a read-only byte span. - public TextCursor(ISourceText text) + public TextCursor(in ISourceText text) { + _characterWindow = SharedObjectPools.WindowPool.Get(); SourceText = text; - Length = text.Length; - _position = 0; + Basis = 0; + Offset = 0; + LexemeBasis = 0; } /// Gets the source text. @@ -46,11 +49,32 @@ public TextCursor(ISourceText text) /// Gets the current byte at the cursor position. public Option Current => Peek(); - /// Gets the length of the text. - public int Length { get; } - /// Gets a value indicating whether the cursor is at the end of the text. - public bool IsAtEnd => _position >= Length; + public bool IsAtEnd => Offset >= WindowCount && !HasMoreBytes(); + + /// Gets the start offset inside the window (relative to the start). + public int Basis { get; private set; } + + /// Gets the end offset inside the window (relative to the ). + public int Offset { get; private set; } + + /// Gets the absolute position in the . + public int Position => Basis + Offset; + + /// Gets a value indicating whether the window is in parsing lexeme mode. + public bool IsLexemeMode { get; private set; } + + /// Gets the lexeme start offset relative to the window start. + public int LexemeBasis { get; private set; } + + /// Gets the absolute position of the lexeme in the . + public int LexemePosition => Basis + LexemeBasis; + + /// Gets the width of the lexeme. + public int LexemeWidth => Offset - LexemeBasis; + + /// Gets the number of characters in the window. + public int WindowCount { get; private set; } /// Peeks at the byte at the specified delta from the current position. /// The delta from the current position. @@ -59,7 +83,7 @@ public Option Peek(int delta = 0) { Debug.Assert(delta >= 0, "Delta should be positive"); return !IsAtEnd - ? Option.Some(SourceText[_position + delta]) + ? Option.Some(_characterWindow[Offset + delta]) : Option.None; } @@ -68,7 +92,7 @@ public Option Peek(int delta = 0) public void Advance(int delta = 1) { Debug.Assert(delta > 0, "Delta should greater than 0"); - _position += delta; + Offset += delta; } /// Advances the cursor while the specified predicate is true. @@ -124,8 +148,77 @@ public bool TryAdvance(ReadOnlySpan subtext) return true; } + /// Starts parsing a lexeme and sets the to the current value. + public void StartLexemeMode() + { + LexemeBasis = Offset; + IsLexemeMode = true; + } + + /// Stops parsing a lexeme. + public void StopLexemeMode() + { + LexemeBasis = 0; + IsLexemeMode = false; + } + /// Releases the resources used by the class. public void Dispose() { + SharedObjectPools.WindowPool.Return(_characterWindow); + } + + private bool HasMoreBytes() + { + if (Offset < WindowCount) + { + return true; + } + + if (Position >= SourceText.Length) + { + return false; + } + + // if lexeme scanning is sufficiently into the char buffer, + // then refocus the window onto the lexeme + if (LexemeBasis > WindowCount / 4) + { + Array.Copy( + sourceArray: _characterWindow, + sourceIndex: LexemeBasis, + destinationArray: _characterWindow, + destinationIndex: 0, + length: WindowCount - LexemeBasis); + + WindowCount -= LexemeBasis; + Offset -= LexemeBasis; + Basis += LexemeBasis; + StopLexemeMode(); + } + + if (WindowCount >= _characterWindow.Length) + { + // grow char array, since we need more contiguous space + var oldWindow = _characterWindow; + var newWindow = new byte[_characterWindow.Length * 2]; + + Array.Copy( + sourceArray: oldWindow, + sourceIndex: 0, + destinationArray: newWindow, + destinationIndex: 0, + length: WindowCount); + + _characterWindow = newWindow; + } + + var amountToRead = Math.Min( + SourceText.Length - (Basis + WindowCount), + _characterWindow.Length - WindowCount); + + SourceText.CopyTo(Basis + WindowCount, _characterWindow, WindowCount, amountToRead); + WindowCount += amountToRead; + return amountToRead > 0; } } diff --git a/src/OffDotNet.CodeAnalysis/PooledObjects/ArrayPooledObjectPolicy.cs b/src/OffDotNet.CodeAnalysis/PooledObjects/ArrayPooledObjectPolicy.cs new file mode 100644 index 0000000..189f36f --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/PooledObjects/ArrayPooledObjectPolicy.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.PooledObjects; + +using Microsoft.Extensions.ObjectPool; + +internal sealed class ArrayPooledObjectPolicy : PooledObjectPolicy +{ + public ArrayPooledObjectPolicy() + { + } + + public ArrayPooledObjectPolicy(int initialCapacity, int maximumRetainedCapacity) + { + InitialCapacity = initialCapacity; + MaximumRetainedCapacity = maximumRetainedCapacity; + } + + /// + /// Gets the initial capacity of pooled byte array instances. + /// + /// Defaults to 8. + public int InitialCapacity { get; init; } = 8; + + /// + /// Gets the maximum capacity of a single byte array instance that is allowed to be + /// retained, when is invoked. + /// + /// Defaults to 128. + public int MaximumRetainedCapacity { get; init; } = 128; + + /// + public override T[] Create() + { + return new T[InitialCapacity]; + } + + /// + public override bool Return(T[] obj) + { + if (obj.Length > this.MaximumRetainedCapacity) + { + // Too big. Discard this one. + return false; + } + + Array.Clear(obj); + return true; + } +} diff --git a/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs b/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs index d880c1c..919bdab 100644 --- a/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs +++ b/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs @@ -17,6 +17,9 @@ internal static class SharedObjectPools /// The string builder pool. internal static readonly ObjectPool StringBuilderPool; + /// The sliding window pool. + internal static readonly ObjectPool WindowPool; + static SharedObjectPools() { var defaultObjectPoolProvider = new DefaultObjectPoolProvider(); @@ -30,9 +33,11 @@ static SharedObjectPools() var stringBuilderPoolProvider = defaultStringBuilderPoolProvider; #endif + var arrayPooledObject = new ArrayPooledObjectPolicy(initialCapacity: 2048, maximumRetainedCapacity: 2048); var stackPooledObjectPolicy = new StackPooledObjectPolicy(initialCapacity: 2, maximumRetainedCapacity: 16); - AbstractNodesCache = objectPoolProvider.Create(stackPooledObjectPolicy); StringBuilderPool = stringBuilderPoolProvider.CreateStringBuilderPool(); + AbstractNodesCache = objectPoolProvider.Create(stackPooledObjectPolicy); + WindowPool = objectPoolProvider.Create(arrayPooledObject); } } diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs index 5f6f525..1ad8656 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs @@ -12,6 +12,20 @@ namespace OffDotNet.CodeAnalysis.Tests.Lexer; public class TextCursorTests { private readonly ISourceText _sourceText = Substitute.For(); + private readonly byte[] _text = "CDEF"u8.ToArray(); + + public TextCursorTests() + { + _sourceText.Length.Returns(_text.Length); + _sourceText[0].Returns(_text[0]); + _sourceText[1].Returns(_text[1]); + _sourceText[2].Returns(_text[2]); + _sourceText[3].Returns(_text[3]); + + _sourceText + .When(x => x.CopyTo(0, Arg.Any(), 0, _text.Length)) + .Do(x => _text.CopyTo(x.ArgAt(1), 0)); + } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] [Fact(DisplayName = $"Class should implement {nameof(IDisposable)} interface")] @@ -20,7 +34,7 @@ public void Class_ShouldImplementIDisposableInterface() // Arrange // Act - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Assert Assert.IsAssignableFrom(cursor); @@ -33,7 +47,7 @@ public void Class_ShouldImplementITextCursorInterface() // Arrange // Act - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Assert Assert.IsAssignableFrom(cursor); @@ -44,7 +58,7 @@ public void Class_ShouldImplementITextCursorInterface() public void Dispose_ShouldNotThrowAnyException() { // Arrange - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Act cursor.Dispose(); @@ -55,16 +69,11 @@ public void Dispose_ShouldNotThrowAnyException() [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] [Fact(DisplayName = "Constructor should copy the input text")] - public void Constructor_WithReadOnlySpanByteParameter_ShouldCopyInputText() + public void Constructor_ShouldCopyInputText() { // Arrange - const int TextLength = 4; - const byte Expected = (byte)'A'; - - _sourceText[0].Returns(Expected); - _sourceText.Length.Returns(TextLength); - - var cursor = new TextCursor(_sourceText); + const byte Expected = (byte)'C'; + ITextCursor cursor = new TextCursor(_sourceText); // Act var actual = cursor.Current; @@ -79,7 +88,8 @@ public void Current_ShouldReturnNone_WhenInputStringIsEmpty() { // Arrange var expected = Option.None; - var cursor = new TextCursor(_sourceText); + _sourceText.Length.Returns(0); + ITextCursor cursor = new TextCursor(_sourceText); // Act var current = cursor.Current; @@ -88,36 +98,13 @@ public void Current_ShouldReturnNone_WhenInputStringIsEmpty() Assert.Equal(expected, current); } - [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] - [Fact(DisplayName = $"{nameof(TextCursor.Current)} property should return the first character of the input string")] - public void Current_ShouldReturnFirstCharacterOfInputString() - { - // Arrange - const int TextLength = 4; - const byte Expected = (byte)'C'; - - _sourceText[0].Returns(Expected); - _sourceText.Length.Returns(TextLength); - - var cursor = new TextCursor(_sourceText); - - // Act - var actual = cursor.Current; - - // Assert - Assert.Equal(Expected, actual.GetValueOrDefault(0x0)); - } - [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] [Fact(DisplayName = $"{nameof(TextCursor.Current)} property should return none when the cursor is at the end of the input string")] public void Current_ShouldReturnNone_WhenCursorIsAtTheEnd() { // Arrange - const int TextLength = 4; - _sourceText.Length.Returns(TextLength); - - var cursor = new TextCursor(_sourceText); - cursor.Advance(TextLength); + ITextCursor cursor = new TextCursor(_sourceText); + cursor.Advance(_text.Length); // Act var current = cursor.Current; @@ -132,7 +119,8 @@ public void IsAtEnd_ShouldReturnTrue_WhenInputStringIsEmpty() { // Arrange const bool Expected = true; - var cursor = new TextCursor(_sourceText); + _sourceText.Length.Returns(0); + ITextCursor cursor = new TextCursor(_sourceText); // Act var isAtEnd = cursor.IsAtEnd; @@ -147,11 +135,7 @@ public void IsAtEnd_ShouldReturnFalse_WhenInputStringIsNotEmpty_AndCursorIsAtThe { // Arrange const bool Expected = false; - const int TextLength = 4; - - _sourceText.Length.Returns(TextLength); - - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Act var isAtEnd = cursor.IsAtEnd; @@ -166,12 +150,8 @@ public void IsAtEnd_ShouldReturnTrue_WhenCursorIsAtTheEnd() { // Arrange const bool Expected = true; - const int TextLength = 4; - - _sourceText.Length.Returns(TextLength); - - var cursor = new TextCursor(_sourceText); - cursor.Advance(TextLength); + ITextCursor cursor = new TextCursor(_sourceText); + cursor.Advance(_text.Length); // Act var isAtEnd = cursor.IsAtEnd; @@ -180,31 +160,14 @@ public void IsAtEnd_ShouldReturnTrue_WhenCursorIsAtTheEnd() Assert.Equal(Expected, isAtEnd); } - [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] - [Fact(DisplayName = $"{nameof(TextCursor.Length)} property should return the length of the input string")] - public void Length_ShouldReturnLengthOfInputString() - { - // Arrange - const int Expected = 4; - - _sourceText.Length.Returns(Expected); - - var cursor = new TextCursor(_sourceText); - - // Act - var length = cursor.Length; - - // Assert - Assert.Equal(Expected, length); - } - [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] [Fact(DisplayName = $"{nameof(TextCursor.Peek)} method should return none when the input string is empty")] public void Peek_ShouldReturnNone_WhenInputStringIsEmpty() { // Arrange var expected = Option.None; - var cursor = new TextCursor(_sourceText); + _sourceText.Length.Returns(0); + ITextCursor cursor = new TextCursor(_sourceText); // Act var peek = cursor.Peek(); @@ -218,13 +181,8 @@ public void Peek_ShouldReturnNone_WhenInputStringIsEmpty() public void Peek_ShouldReturnFirstCharacterOfInputString() { // Arrange - const int TextLength = 4; const byte Expected = (byte)'C'; - - _sourceText[0].Returns(Expected); - _sourceText.Length.Returns(TextLength); - - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Act var actual = cursor.Peek(); @@ -239,13 +197,8 @@ public void Peek_ShouldReturnNthCharacterOfInputString() { // Arrange const int Index = 2; - const int TextLength = 4; const byte Expected = (byte)'E'; - - _sourceText[Index].Returns(Expected); - _sourceText.Length.Returns(TextLength); - - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Act var actual = cursor.Peek(Index); @@ -259,12 +212,8 @@ public void Peek_ShouldReturnNthCharacterOfInputString() public void Peek_ShouldReturnNone_WhenCursorIsAtTheEnd() { // Arrange - const int TextLength = 4; - - _sourceText.Length.Returns(TextLength); - - var cursor = new TextCursor(_sourceText); - cursor.Advance(TextLength); + ITextCursor cursor = new TextCursor(_sourceText); + cursor.Advance(_text.Length); // Act var peek = cursor.Peek(); @@ -278,13 +227,8 @@ public void Peek_ShouldReturnNone_WhenCursorIsAtTheEnd() public void Advance_ShouldAdvanceCursorByOnePosition() { // Arrange - const int TextLength = 4; const byte Expected = (byte)'D'; - - _sourceText.Length.Returns(TextLength); - _sourceText[1].Returns(Expected); - - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Act cursor.Advance(); @@ -299,14 +243,9 @@ public void Advance_ShouldAdvanceCursorByOnePosition() public void Advance_ShouldAdvanceCursorByNPositions() { // Arrange - const int TextLength = 4; const int Index = 2; const byte Expected = (byte)'E'; - - _sourceText.Length.Returns(TextLength); - _sourceText[Index].Returns(Expected); - - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Act cursor.Advance(Index); @@ -322,15 +261,7 @@ public void Advance_WithPredicate_ShouldAdvanceCursor_IfPredicateIsTrue() { // Arrange const byte Expected = (byte)'E'; - var text = "CDEF"u8.ToArray(); - - _sourceText.Length.Returns(text.Length); - _sourceText[0].Returns(text[0]); - _sourceText[1].Returns(text[1]); - _sourceText[2].Returns(text[2]); - _sourceText[3].Returns(text[3]); - - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Act cursor.Advance(static x => x is (byte)'C' or (byte)'D'); @@ -346,15 +277,7 @@ public void Advance_WithPredicate_ShouldNotAdvanceCursor_IfPredicateIsFalse() { // Arrange const byte Expected = (byte)'C'; - var text = "CDEF"u8.ToArray(); - - _sourceText.Length.Returns(text.Length); - _sourceText[0].Returns(text[0]); - _sourceText[1].Returns(text[1]); - _sourceText[2].Returns(text[2]); - _sourceText[3].Returns(text[3]); - - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Act cursor.Advance(static x => x is (byte)'A' or (byte)'B'); @@ -370,15 +293,7 @@ public void TryAdvance_ShouldReturnFalse_IfCharacterAtCurrentPositionIsNotTheExp { // Arrange const byte Expected = (byte)'C'; - var text = "CDEF"u8.ToArray(); - - _sourceText.Length.Returns(text.Length); - _sourceText[0].Returns(text[0]); - _sourceText[1].Returns(text[1]); - _sourceText[2].Returns(text[2]); - _sourceText[3].Returns(text[3]); - - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Act var result = cursor.TryAdvance((byte)'A'); @@ -395,15 +310,7 @@ public void TryAdvance_ShouldReturnTrue_IfCharacterAtCurrentPositionIsTheExpecte { // Arrange const byte Expected = (byte)'D'; - var text = "CDEF"u8.ToArray(); - - _sourceText.Length.Returns(text.Length); - _sourceText[0].Returns(text[0]); - _sourceText[1].Returns(text[1]); - _sourceText[2].Returns(text[2]); - _sourceText[3].Returns(text[3]); - - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Act var result = cursor.TryAdvance((byte)'C'); @@ -420,15 +327,7 @@ public void TryAdvance_WithEmptySubText_ShouldReturnFalse() { // Arrange const byte Expected = (byte)'C'; - var text = "CDEF"u8.ToArray(); - - _sourceText.Length.Returns(text.Length); - _sourceText[0].Returns(text[0]); - _sourceText[1].Returns(text[1]); - _sourceText[2].Returns(text[2]); - _sourceText[3].Returns(text[3]); - - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Act var result = cursor.TryAdvance(ReadOnlySpan.Empty); @@ -449,15 +348,7 @@ public void TryAdvance_WithSubText_ShouldReturnFalse_IfSubTextIsNotFound(string { // Arrange const byte Expected = (byte)'C'; - var text = "CDEF"u8.ToArray(); - - _sourceText.Length.Returns(text.Length); - _sourceText[0].Returns(text[0]); - _sourceText[1].Returns(text[1]); - _sourceText[2].Returns(text[2]); - _sourceText[3].Returns(text[3]); - - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Act var result = cursor.TryAdvance(Encoding.ASCII.GetBytes(subtext)); @@ -477,15 +368,7 @@ public void TryAdvance_WithSubText_ShouldReturnFalse_IfSubTextIsNotFound(string public void TryAdvance_WithSubText_ShouldReturnTrue_IfSubTextIsFound(string subtext, byte nextChar) { // Arrange - var text = "CDEF"u8.ToArray(); - - _sourceText.Length.Returns(text.Length); - _sourceText[0].Returns(text[0]); - _sourceText[1].Returns(text[1]); - _sourceText[2].Returns(text[2]); - _sourceText[3].Returns(text[3]); - - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Act var result = cursor.TryAdvance(Encoding.ASCII.GetBytes(subtext)); @@ -501,7 +384,7 @@ public void TryAdvance_WithSubText_ShouldReturnTrue_IfSubTextIsFound(string subt public void SourceText_ShouldReturnSourceText() { // Arrange - var cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(_sourceText); // Act var actual = cursor.SourceText; @@ -509,4 +392,264 @@ public void SourceText_ShouldReturnSourceText() // Assert Assert.Same(_sourceText, actual); } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.Basis)} property should return 0 by default")] + public void Basis_ShouldReturnZero_ByDefault() + { + // Arrange + ITextCursor cursor = new TextCursor(_sourceText); + + // Act + var actual = cursor.Basis; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.Offset)} property should return 0 by default")] + public void Offset_ShouldReturnZero_ByDefault() + { + // Arrange + ITextCursor cursor = new TextCursor(_sourceText); + + // Act + var actual = cursor.Offset; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.Position)} property should return 0 by default")] + public void Position_ShouldReturnZero_ByDefault() + { + // Arrange + ITextCursor cursor = new TextCursor(_sourceText); + + // Act + var actual = cursor.Position; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.IsLexemeMode)} property should return false by default")] + public void IsLexemeMode_ShouldReturnFalse_ByDefault() + { + // Arrange + ITextCursor cursor = new TextCursor(_sourceText); + + // Act + var actual = cursor.IsLexemeMode; + + // Assert + Assert.False(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.LexemeBasis)} property should return 0 by default")] + public void LexemeBasis_ShouldReturnZero_ByDefault() + { + // Arrange + ITextCursor cursor = new TextCursor(_sourceText); + + // Act + var actual = cursor.LexemeBasis; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.LexemePosition)} property should return 0 by default")] + public void LexemePosition_ShouldReturnZero_ByDefault() + { + // Arrange + ITextCursor cursor = new TextCursor(_sourceText); + + // Act + var actual = cursor.LexemePosition; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.LexemeWidth)} property should return 0 by default")] + public void LexemeWidth_ShouldReturnZero_ByDefault() + { + // Arrange + ITextCursor cursor = new TextCursor(_sourceText); + + // Act + var actual = cursor.LexemeWidth; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.WindowCount)} property should return 0 by default")] + public void WindowCount_ShouldReturnZero_ByDefault() + { + // Arrange + ITextCursor cursor = new TextCursor(_sourceText); + + // Act + var actual = cursor.WindowCount; + + // Assert + Assert.Equal(0, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.Offset)} property should correspond to the number of characters advanced")] + public void Offset_ShouldCorrespondToNumberOfCharactersAdvanced() + { + // Arrange + const int Expected = 3; + var text = "CDEF"u8.ToArray(); + + _sourceText.Length.Returns(text.Length); + _sourceText[0].Returns(text[0]); + _sourceText[1].Returns(text[1]); + _sourceText[2].Returns(text[2]); + _sourceText[3].Returns(text[3]); + + ITextCursor cursor = new TextCursor(_sourceText); + + // Act + cursor.Advance(3); + var actual = cursor.Offset; + + // Assert + Assert.Equal(Expected, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.Position)} property should correspond to the number of characters advanced")] + public void Position_ShouldCorrespondToNumberOfCharactersAdvanced() + { + // Arrange + const int Expected = 3; + var text = "CDEF"u8.ToArray(); + + _sourceText.Length.Returns(text.Length); + _sourceText[0].Returns(text[0]); + _sourceText[1].Returns(text[1]); + _sourceText[2].Returns(text[2]); + _sourceText[3].Returns(text[3]); + + ITextCursor cursor = new TextCursor(_sourceText); + + // Act + cursor.Advance(3); + var actual = cursor.Position; + + // Assert + Assert.Equal(Expected, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.StartLexemeMode)} method should set the lexeme basis to the current offset")] + public void StartLexemeMode_ShouldSetLexemeBasisToCurrentOffset() + { + // Arrange + const int Expected = 3; + var text = "CDEF"u8.ToArray(); + + _sourceText.Length.Returns(text.Length); + _sourceText[0].Returns(text[0]); + _sourceText[1].Returns(text[1]); + _sourceText[2].Returns(text[2]); + _sourceText[3].Returns(text[3]); + + ITextCursor cursor = new TextCursor(_sourceText); + + // Act + cursor.Advance(3); + cursor.StartLexemeMode(); + var actual = cursor.LexemeBasis; + + // Assert + Assert.Equal(Expected, actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.StartLexemeMode)} method should set the {nameof(TextCursor.IsLexemeMode)} property to true")] + public void StartLexemeMode_ShouldSetIsLexemeModeToTrue() + { + // Arrange + var text = "CDEF"u8.ToArray(); + + _sourceText.Length.Returns(text.Length); + _sourceText[0].Returns(text[0]); + _sourceText[1].Returns(text[1]); + _sourceText[2].Returns(text[2]); + _sourceText[3].Returns(text[3]); + + ITextCursor cursor = new TextCursor(_sourceText); + + // Act + cursor.Advance(3); + cursor.StartLexemeMode(); + var actual = cursor.IsLexemeMode; + + // Assert + Assert.True(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.StopLexemeMode)} method should set the {nameof(TextCursor.IsLexemeMode)} property to false")] + public void StopLexemeMode_ShouldSetIsLexemeModeToFalse() + { + // Arrange + var text = "CDEF"u8.ToArray(); + + _sourceText.Length.Returns(text.Length); + _sourceText[0].Returns(text[0]); + _sourceText[1].Returns(text[1]); + _sourceText[2].Returns(text[2]); + _sourceText[3].Returns(text[3]); + + ITextCursor cursor = new TextCursor(_sourceText); + + // Act + cursor.Advance(3); + cursor.StartLexemeMode(); + cursor.StopLexemeMode(); + var actual = cursor.IsLexemeMode; + + // Assert + Assert.False(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.StopLexemeMode)} method should set the {nameof(TextCursor.LexemeBasis)} property to 0")] + public void StopLexemeMode_ShouldSetLexemeBasisToZero() + { + // Arrange + const int Expected = 0; + var text = "CDEF"u8.ToArray(); + + _sourceText.Length.Returns(text.Length); + _sourceText[0].Returns(text[0]); + _sourceText[1].Returns(text[1]); + _sourceText[2].Returns(text[2]); + _sourceText[3].Returns(text[3]); + + ITextCursor cursor = new TextCursor(_sourceText); + + // Act + cursor.Advance(3); + cursor.StartLexemeMode(); + cursor.StopLexemeMode(); + var actual = cursor.LexemeBasis; + + // Assert + Assert.Equal(Expected, actual); + } } From f17b961f45bff293c713bbb4a0bef6b2723938cb Mon Sep 17 00:00:00 2001 From: Victor Pogor Date: Sun, 13 Oct 2024 12:25:41 +1000 Subject: [PATCH 10/14] improved test cursor --- .editorconfig | 3 + OffDotNet.slnx | 47 ++ .../DiagnosticOptions.cs | 2 +- .../RootConfigurations.cs | 6 +- .../Dependencies.cs | 3 +- .../Diagnostics/MessageProvider.cs | 3 +- .../Syntax/RawSyntaxToken.cs | 4 +- .../Configs/TextCursorOptions.cs | 11 + .../Lexer/ISourceText.cs | 2 +- .../Lexer/ITextCursor.cs | 26 +- .../Lexer/StringText.cs | 4 +- .../Lexer/TextCursor.cs | 148 +++--- .../PooledObjects/SharedObjectPools.cs | 5 - .../Syntax/AbstractNode.cs | 18 +- src/OffDotNet.CodeAnalysis/Utils/Option.cs | 12 +- .../Utils/OptionExtensions.cs | 6 +- .../Diagnostics/MessageProviderTests.cs | 3 +- .../Syntax/RawSyntaxTokenTests.cs | 10 +- .../Lexer/TextCursorTests.cs | 502 ++++++++++++++---- .../Syntax/AbstractNodeTests.cs | 8 +- .../Utils/OptionExtensionsTests.cs | 14 +- .../Utils/OptionTests.cs | 45 +- 22 files changed, 626 insertions(+), 256 deletions(-) create mode 100644 OffDotNet.slnx rename src/OffDotNet.CodeAnalysis.Pdf/{Configurations => Configs}/DiagnosticOptions.cs (86%) rename src/OffDotNet.CodeAnalysis.Pdf/{Configurations => Configs}/RootConfigurations.cs (73%) create mode 100644 src/OffDotNet.CodeAnalysis/Configs/TextCursorOptions.cs diff --git a/.editorconfig b/.editorconfig index 535e7a6..52ac571 100644 --- a/.editorconfig +++ b/.editorconfig @@ -171,6 +171,9 @@ dotnet_diagnostic.SA1308.severity = none # SA1311: Static readonly fields should begin with upper-case letter dotnet_diagnostic.SA1311.severity = none +# SA1503: Braces should not be omitted +dotnet_diagnostic.SA1503.severity = none + # CSharp code style settings: [*.cs] # Newline settings diff --git a/OffDotNet.slnx b/OffDotNet.slnx new file mode 100644 index 0000000..03a9205 --- /dev/null +++ b/OffDotNet.slnx @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Configurations/DiagnosticOptions.cs b/src/OffDotNet.CodeAnalysis.Pdf/Configs/DiagnosticOptions.cs similarity index 86% rename from src/OffDotNet.CodeAnalysis.Pdf/Configurations/DiagnosticOptions.cs rename to src/OffDotNet.CodeAnalysis.Pdf/Configs/DiagnosticOptions.cs index 2a9d9d8..4db5662 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Configurations/DiagnosticOptions.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Configs/DiagnosticOptions.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace OffDotNet.CodeAnalysis.Pdf.Configurations; +namespace OffDotNet.CodeAnalysis.Pdf.Configs; public sealed record DiagnosticOptions { diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Configurations/RootConfigurations.cs b/src/OffDotNet.CodeAnalysis.Pdf/Configs/RootConfigurations.cs similarity index 73% rename from src/OffDotNet.CodeAnalysis.Pdf/Configurations/RootConfigurations.cs rename to src/OffDotNet.CodeAnalysis.Pdf/Configs/RootConfigurations.cs index b52e540..a10de3e 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Configurations/RootConfigurations.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Configs/RootConfigurations.cs @@ -3,11 +3,15 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace OffDotNet.CodeAnalysis.Pdf.Configurations; +namespace OffDotNet.CodeAnalysis.Pdf.Configs; + +using OffDotNet.CodeAnalysis.Configs; public sealed record RootConfigurations { public const string SectionName = "OffDotNet"; public required DiagnosticOptions Diagnostic { get; init; } + + public required TextCursorOptions TextCursor { get; init; } } diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs b/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs index 2b5c6ed..0451e5b 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs @@ -3,9 +3,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using OffDotNet.CodeAnalysis.Pdf.Configs; + namespace OffDotNet.CodeAnalysis.Pdf; -using Configurations; using Diagnostics; using Microsoft.Extensions.DependencyInjection; using OffDotNet.CodeAnalysis.Diagnostics; diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/MessageProvider.cs b/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/MessageProvider.cs index bfdc0cf..a146ce5 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/MessageProvider.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/MessageProvider.cs @@ -3,9 +3,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using OffDotNet.CodeAnalysis.Pdf.Configs; + namespace OffDotNet.CodeAnalysis.Pdf.Diagnostics; -using Configurations; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using OffDotNet.CodeAnalysis.Diagnostics; diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxToken.cs b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxToken.cs index 71c866c..5a24c80 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxToken.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxToken.cs @@ -139,14 +139,14 @@ internal static RawSyntaxToken Create(SyntaxKind kind, Option lead /// Whether to include trailing trivia. protected override void WriteTokenTo(TextWriter writer, bool leading, bool trailing) { - if (leading && LeadingTrivia.IsSome(out var leadingTrivia)) + if (leading && LeadingTrivia.TryGetValue(out var leadingTrivia)) { leadingTrivia.WriteTo(writer, leading: true, trailing: false); } writer.Write(this.Text); - if (trailing && TrailingTrivia.IsSome(out var trailingTrivia)) + if (trailing && TrailingTrivia.TryGetValue(out var trailingTrivia)) { trailingTrivia.WriteTo(writer, leading: false, trailing: true); } diff --git a/src/OffDotNet.CodeAnalysis/Configs/TextCursorOptions.cs b/src/OffDotNet.CodeAnalysis/Configs/TextCursorOptions.cs new file mode 100644 index 0000000..c0c7786 --- /dev/null +++ b/src/OffDotNet.CodeAnalysis/Configs/TextCursorOptions.cs @@ -0,0 +1,11 @@ +// +// Copyright (c) Sunt Programator. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace OffDotNet.CodeAnalysis.Configs; + +public sealed record TextCursorOptions +{ + public int WindowSize { get; init; } = 2048; +} diff --git a/src/OffDotNet.CodeAnalysis/Lexer/ISourceText.cs b/src/OffDotNet.CodeAnalysis/Lexer/ISourceText.cs index a58df21..1591843 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/ISourceText.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/ISourceText.cs @@ -13,5 +13,5 @@ public interface ISourceText byte this[int position] { get; } - void CopyTo(int sourceIndex, byte[] destination, int destinationIndex, int count); + void CopyTo(int sourceIndex, Span destination, int destinationIndex, int count); } diff --git a/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs b/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs index d00a33a..bba4621 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs @@ -43,28 +43,31 @@ public interface ITextCursor : IDisposable bool IsAtEnd { get; } /// Gets the start offset inside the window (relative to the start). - int Basis { get; } + int WindowStart { get; } - /// Gets the end offset inside the window (relative to the ). + /// Gets the end offset inside the window (relative to the ). int Offset { get; } /// Gets the absolute position in the . - int Position => Basis + Offset; + int Position => WindowStart + Offset; /// Gets a value indicating whether the window is in parsing lexeme mode. bool IsLexemeMode { get; } - /// Gets the lexeme start offset relative to the window start. - int LexemeBasis { get; } + /// Gets the lexeme start offset relative to the window start. + int LexemeStart { get; } /// Gets the absolute position of the lexeme in the . - int LexemePosition => Basis + LexemeBasis; + int LexemePosition => WindowStart + LexemeStart; /// Gets the width of the lexeme. - int LexemeWidth => Offset - LexemeBasis; + int LexemeWidth => Offset - LexemeStart; + + /// Gets the text window. + ReadOnlyMemory Window { get; } /// Gets the number of characters in the window. - int WindowCount { get; } + int WindowSize { get; } /// /// Peeks at the byte at the specified delta from the current position. @@ -99,7 +102,12 @@ public interface ITextCursor : IDisposable /// True if the cursor was advanced; otherwise, false. bool TryAdvance(ReadOnlySpan subtext); - /// Starts parsing a lexeme and sets the to the current value. + /// Slides the text window to the specified start position and size. + /// The start position of the window. + /// The size of the window. + void SlideTextWindow(int windowStart = -1, int windowSize = -1); + + /// Starts parsing a lexeme and sets the to the current value. public void StartLexemeMode(); /// Stops parsing a lexeme. diff --git a/src/OffDotNet.CodeAnalysis/Lexer/StringText.cs b/src/OffDotNet.CodeAnalysis/Lexer/StringText.cs index f75fcec..f6ec03e 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/StringText.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/StringText.cs @@ -18,8 +18,8 @@ internal StringText(in ReadOnlySpan source) public byte this[int position] => Source.Span[position]; - public void CopyTo(int sourceIndex, byte[] destination, int destinationIndex, int count) + public void CopyTo(int sourceIndex, Span destination, int destinationIndex, int count) { - Source.Span.Slice(sourceIndex, count).CopyTo(destination.AsSpan(destinationIndex)); + Source.Span.Slice(sourceIndex, count).CopyTo(destination[destinationIndex..]); } } diff --git a/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs b/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs index aaacc27..d07d196 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs @@ -5,7 +5,8 @@ namespace OffDotNet.CodeAnalysis.Lexer; -using PooledObjects; +using Configs; +using Microsoft.Extensions.Options; using Utils; /// Represents a text cursor for navigating and processing text data. @@ -30,17 +31,19 @@ namespace OffDotNet.CodeAnalysis.Lexer; /// internal sealed class TextCursor : ITextCursor { - private byte[] _characterWindow; + private IMemoryOwner _characterWindowMemoryOwner; /// Initializes a new instance of the class with a read-only byte span. /// The text represented as a read-only byte span. - public TextCursor(in ISourceText text) + /// The text cursor options. + public TextCursor(in ISourceText text, IOptions options) { - _characterWindow = SharedObjectPools.WindowPool.Get(); + _characterWindowMemoryOwner = MemoryPool.Shared.Rent(options.Value.WindowSize); + SourceText = text; - Basis = 0; + WindowStart = 0; Offset = 0; - LexemeBasis = 0; + LexemeStart = 0; } /// Gets the source text. @@ -50,31 +53,34 @@ public TextCursor(in ISourceText text) public Option Current => Peek(); /// Gets a value indicating whether the cursor is at the end of the text. - public bool IsAtEnd => Offset >= WindowCount && !HasMoreBytes(); + public bool IsAtEnd => !HasMoreBytes(); /// Gets the start offset inside the window (relative to the start). - public int Basis { get; private set; } + public int WindowStart { get; private set; } - /// Gets the end offset inside the window (relative to the ). + /// Gets the end offset inside the window (relative to the ). public int Offset { get; private set; } /// Gets the absolute position in the . - public int Position => Basis + Offset; + public int Position => WindowStart + Offset; /// Gets a value indicating whether the window is in parsing lexeme mode. public bool IsLexemeMode { get; private set; } - /// Gets the lexeme start offset relative to the window start. - public int LexemeBasis { get; private set; } + /// Gets the lexeme start offset relative to the window start. + public int LexemeStart { get; private set; } /// Gets the absolute position of the lexeme in the . - public int LexemePosition => Basis + LexemeBasis; + public int LexemePosition => WindowStart + LexemeStart; /// Gets the width of the lexeme. - public int LexemeWidth => Offset - LexemeBasis; + public int LexemeWidth => Offset - LexemeStart; + + /// Gets the text window. + public ReadOnlyMemory Window => _characterWindowMemoryOwner.Memory; - /// Gets the number of characters in the window. - public int WindowCount { get; private set; } + /// Gets the number of valid characters in the text window. + public int WindowSize { get; private set; } /// Peeks at the byte at the specified delta from the current position. /// The delta from the current position. @@ -83,7 +89,7 @@ public Option Peek(int delta = 0) { Debug.Assert(delta >= 0, "Delta should be positive"); return !IsAtEnd - ? Option.Some(_characterWindow[Offset + delta]) + ? Option.Some(_characterWindowMemoryOwner.Memory.Span[Offset + delta]) : Option.None; } @@ -99,7 +105,7 @@ public void Advance(int delta = 1) /// The predicate to test each byte against. public void Advance(Predicate predicate) { - while (Current.Where(predicate).IsSome(out _)) + while (Current.Where(predicate).IsSome) { Advance(); } @@ -110,10 +116,8 @@ public void Advance(Predicate predicate) /// True if the cursor was advanced; otherwise, false. public bool TryAdvance(byte b) { - if (!Current.Where(x => x == b).IsSome(out _)) - { + if (!Current.Where(x => x == b).IsSome) return false; - } Advance(); return true; @@ -125,100 +129,84 @@ public bool TryAdvance(byte b) public bool TryAdvance(ReadOnlySpan subtext) { if (subtext.IsEmpty) - { return false; - } - - var pool = ArrayPool.Shared; - var buffer = pool.Rent(subtext.Length); - subtext.CopyTo(buffer); for (var i = 0; i < subtext.Length; i++) { - var i1 = i; - if (!Peek(i).Where(x => x == buffer[i1]).IsSome(out _)) + if (!Peek(i).TryGetValue(out var value) || value != subtext[i]) { - pool.Return(buffer); return false; } } - pool.Return(buffer); Advance(subtext.Length); return true; } - /// Starts parsing a lexeme and sets the to the current value. + /// Slides the text window to the specified start position and size. + /// The start position of the window. + /// The size of the window. + public void SlideTextWindow(int windowStart = -1, int windowSize = -1) + { + if (windowStart < 0) + return; + + if (windowStart >= SourceText.Length) + return; + + if (windowSize >= 0) + { + var oldCharacterWindowMemoryOwner = _characterWindowMemoryOwner; + + // the new memory owner size is not guaranteed to match the requested window size as it is rounded up to the nearest power of 2 + _characterWindowMemoryOwner = MemoryPool.Shared.Rent(windowSize); + var newWindowSize = Math.Min(windowSize, WindowSize); + oldCharacterWindowMemoryOwner.Memory.Span[..newWindowSize].CopyTo(_characterWindowMemoryOwner.Memory.Span); + oldCharacterWindowMemoryOwner.Dispose(); + } + + var count = Math.Min(SourceText.Length - windowStart, _characterWindowMemoryOwner.Memory.Span.Length); + + if (count > 0) + { + SourceText.CopyTo(windowStart, _characterWindowMemoryOwner.Memory.Span, 0, count); + } + + Offset = 0; + WindowStart = windowStart; + WindowSize = count; + StopLexemeMode(); + } + + /// Starts parsing a lexeme and sets the to the current value. public void StartLexemeMode() { - LexemeBasis = Offset; + LexemeStart = Offset; IsLexemeMode = true; } /// Stops parsing a lexeme. public void StopLexemeMode() { - LexemeBasis = 0; + LexemeStart = 0; IsLexemeMode = false; } /// Releases the resources used by the class. public void Dispose() { - SharedObjectPools.WindowPool.Return(_characterWindow); + _characterWindowMemoryOwner.Dispose(); } private bool HasMoreBytes() { - if (Offset < WindowCount) - { + if (Offset < WindowSize) return true; - } if (Position >= SourceText.Length) - { return false; - } - - // if lexeme scanning is sufficiently into the char buffer, - // then refocus the window onto the lexeme - if (LexemeBasis > WindowCount / 4) - { - Array.Copy( - sourceArray: _characterWindow, - sourceIndex: LexemeBasis, - destinationArray: _characterWindow, - destinationIndex: 0, - length: WindowCount - LexemeBasis); - - WindowCount -= LexemeBasis; - Offset -= LexemeBasis; - Basis += LexemeBasis; - StopLexemeMode(); - } - - if (WindowCount >= _characterWindow.Length) - { - // grow char array, since we need more contiguous space - var oldWindow = _characterWindow; - var newWindow = new byte[_characterWindow.Length * 2]; - - Array.Copy( - sourceArray: oldWindow, - sourceIndex: 0, - destinationArray: newWindow, - destinationIndex: 0, - length: WindowCount); - - _characterWindow = newWindow; - } - - var amountToRead = Math.Min( - SourceText.Length - (Basis + WindowCount), - _characterWindow.Length - WindowCount); - SourceText.CopyTo(Basis + WindowCount, _characterWindow, WindowCount, amountToRead); - WindowCount += amountToRead; - return amountToRead > 0; + SlideTextWindow(Position); + return WindowSize > 0; } } diff --git a/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs b/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs index 919bdab..3646a7b 100644 --- a/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs +++ b/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs @@ -17,9 +17,6 @@ internal static class SharedObjectPools /// The string builder pool. internal static readonly ObjectPool StringBuilderPool; - /// The sliding window pool. - internal static readonly ObjectPool WindowPool; - static SharedObjectPools() { var defaultObjectPoolProvider = new DefaultObjectPoolProvider(); @@ -33,11 +30,9 @@ static SharedObjectPools() var stringBuilderPoolProvider = defaultStringBuilderPoolProvider; #endif - var arrayPooledObject = new ArrayPooledObjectPolicy(initialCapacity: 2048, maximumRetainedCapacity: 2048); var stackPooledObjectPolicy = new StackPooledObjectPolicy(initialCapacity: 2, maximumRetainedCapacity: 16); StringBuilderPool = stringBuilderPoolProvider.CreateStringBuilderPool(); AbstractNodesCache = objectPoolProvider.Create(stackPooledObjectPolicy); - WindowPool = objectPoolProvider.Create(arrayPooledObject); } } diff --git a/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs b/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs index ea4faa3..bffac95 100644 --- a/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs +++ b/src/OffDotNet.CodeAnalysis/Syntax/AbstractNode.cs @@ -80,12 +80,12 @@ protected AbstractNode(ushort rawKind, int fullWidth) public virtual Option TrailingTrivia => default; /// Gets the width of the leading trivia. - public virtual int LeadingTriviaWidth => this.FullWidth != 0 && this.GetFirstTerminal().IsSome(out var firstTerminal) + public virtual int LeadingTriviaWidth => this.FullWidth != 0 && this.GetFirstTerminal().TryGetValue(out var firstTerminal) ? firstTerminal.LeadingTriviaWidth : 0; /// Gets the width of the trailing trivia. - public virtual int TrailingTriviaWidth => this.FullWidth != 0 && this.GetLastTerminal().IsSome(out var lastTerminal) + public virtual int TrailingTriviaWidth => this.FullWidth != 0 && this.GetLastTerminal().TryGetValue(out var lastTerminal) ? lastTerminal.TrailingTriviaWidth : 0; @@ -113,7 +113,7 @@ public virtual int GetSlotOffset(int index) var offset = 0; for (var i = 0; i < index; i++) { - if (this.GetSlot(i).IsSome(out var slot)) + if (this.GetSlot(i).TryGetValue(out var slot)) { offset += slot.FullWidth; } @@ -220,12 +220,12 @@ private Option GetFirstTerminal() do { Option firstChild = default; - _ = node.IsSome(out var nodeValue); + _ = node.TryGetValue(out var nodeValue); Debug.Assert(nodeValue != null, "Node is null."); for (int i = 0, n = nodeValue.SlotCount; i < n; i++) { - if (nodeValue.GetSlot(i).IsSome(out var child)) + if (nodeValue.GetSlot(i).TryGetValue(out var child)) { firstChild = child; break; @@ -233,7 +233,7 @@ private Option GetFirstTerminal() } node = firstChild; - } while (node.IsSome(out var nd) && nd._nodeFlagsAndSlotCount.SmallSlotCount > 0); + } while (node.TryGetValue(out var nd) && nd._nodeFlagsAndSlotCount.SmallSlotCount > 0); return node; } @@ -247,12 +247,12 @@ private Option GetLastTerminal() do { Option lastChild = default; - _ = node.IsSome(out var nodeValue); + _ = node.TryGetValue(out var nodeValue); Debug.Assert(nodeValue != null, "Node is null."); for (var i = nodeValue.SlotCount - 1; i >= 0; i--) { - if (nodeValue.GetSlot(i).IsSome(out var child)) + if (nodeValue.GetSlot(i).TryGetValue(out var child)) { lastChild = child; break; @@ -260,7 +260,7 @@ private Option GetLastTerminal() } node = lastChild; - } while (node.IsSome(out var nd) && nd._nodeFlagsAndSlotCount.SmallSlotCount > 0); + } while (node.TryGetValue(out var nd) && nd._nodeFlagsAndSlotCount.SmallSlotCount > 0); return node; } diff --git a/src/OffDotNet.CodeAnalysis/Utils/Option.cs b/src/OffDotNet.CodeAnalysis/Utils/Option.cs index 4837cff..cf47f54 100644 --- a/src/OffDotNet.CodeAnalysis/Utils/Option.cs +++ b/src/OffDotNet.CodeAnalysis/Utils/Option.cs @@ -19,7 +19,6 @@ namespace OffDotNet.CodeAnalysis.Utils; public readonly struct Option where T : notnull { - private readonly bool _isSome; private readonly T _value; /// Initializes a new instance of the struct with no value. @@ -31,12 +30,15 @@ public Option() private Option(T value) { _value = value; - _isSome = true; + IsSome = true; } /// Gets an option with no value. public static Option None => new(); + /// Gets a value indicating whether the option has a value. + public bool IsSome { get; } + public static implicit operator Option(T value) => new(value); /// Creates an option with the specified value. @@ -47,9 +49,9 @@ private Option(T value) /// Determines whether the option has a value and returns that value if it exists. /// The returned value if the option has a value. /// if the option has a value; otherwise, . - public bool IsSome([MaybeNullWhen(false)] out T value) + public bool TryGetValue([MaybeNullWhen(false)] out T value) { - if (_isSome) + if (IsSome) { value = _value; return true; @@ -61,7 +63,7 @@ public bool IsSome([MaybeNullWhen(false)] out T value) /// Returns a string that represents the current option. /// A string that represents the current option. - public override string ToString() => _isSome ? $"Some({_value})" : "None"; + public override string ToString() => IsSome ? $"Some({_value})" : "None"; private string DebuggerDisplay() => ToString(); } diff --git a/src/OffDotNet.CodeAnalysis/Utils/OptionExtensions.cs b/src/OffDotNet.CodeAnalysis/Utils/OptionExtensions.cs index 9e9b88d..fcccb66 100644 --- a/src/OffDotNet.CodeAnalysis/Utils/OptionExtensions.cs +++ b/src/OffDotNet.CodeAnalysis/Utils/OptionExtensions.cs @@ -21,17 +21,17 @@ public static class OptionExtensions /// The result of either or . /// /// - /// This method is preferred over using directly because it enforces handling + /// This method is preferred over using directly because it enforces handling /// of both the and cases, ensuring that no case is missed. /// /// - /// Use when you do not need to return a value but want to perform side effects instead. + /// Use when you do not need to return a value but want to perform side effects instead. /// /// public static TOut Match(this Option option, Func some, Func none) where TIn : notnull { - return option.IsSome(out var value) ? some(value) : none(); + return option.TryGetValue(out var value) ? some(value) : none(); } /// diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Diagnostics/MessageProviderTests.cs b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Diagnostics/MessageProviderTests.cs index 7fbcfd3..6c836e6 100644 --- a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Diagnostics/MessageProviderTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Diagnostics/MessageProviderTests.cs @@ -3,9 +3,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using OffDotNet.CodeAnalysis.Pdf.Configs; + namespace OffDotNet.CodeAnalysis.Pdf.Tests.Diagnostics; -using Configurations; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using OffDotNet.CodeAnalysis.Diagnostics; diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTokenTests.cs b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTokenTests.cs index 43f3e2b..7065916 100644 --- a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTokenTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTokenTests.cs @@ -139,7 +139,7 @@ public void ValueProperty_ShouldReturnTheDefaultValue(SyntaxKind kind, object ex // Act var actual = rawNode.Value; - var isSome = actual.IsSome(out var value); + var isSome = actual.TryGetValue(out var value); // Assert Assert.True(isSome); @@ -156,7 +156,7 @@ public void ValueProperty_ShouldReturnTheTextFromTheConstructor() // Act var actual = rawNode.Value; - var isSome = actual.IsSome(out var value); + var isSome = actual.TryGetValue(out var value); // Assert Assert.True(isSome); @@ -249,7 +249,7 @@ public void LeadingTriviaProperty_ShouldReturnValueFromTheConstructor() // Act var actual = rawNode.LeadingTrivia; - var isSome = actual.IsSome(out var value); + var isSome = actual.TryGetValue(out var value); // Assert Assert.True(isSome); @@ -280,7 +280,7 @@ public void TrailingTriviaProperty_ShouldReturnValueFromTheConstructor() // Act var actual = rawNode.TrailingTrivia; - var isSome = actual.IsSome(out var value); + var isSome = actual.TryGetValue(out var value); // Assert Assert.True(isSome); @@ -523,7 +523,7 @@ public void ValueProperty_ShouldReturnTheTextOfTheToken() // Act var actual = rawNode.Value; - var isSome = actual.IsSome(out var value); + var isSome = actual.TryGetValue(out var value); // Assert Assert.True(isSome); diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs index 1ad8656..a13deff 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs @@ -5,26 +5,19 @@ namespace OffDotNet.CodeAnalysis.Tests.Lexer; +using Configs; +using Microsoft.Extensions.Options; using OffDotNet.CodeAnalysis.Lexer; using OffDotNet.CodeAnalysis.Utils; [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] public class TextCursorTests { - private readonly ISourceText _sourceText = Substitute.For(); - private readonly byte[] _text = "CDEF"u8.ToArray(); + private readonly IOptions _options = Substitute.For>(); public TextCursorTests() { - _sourceText.Length.Returns(_text.Length); - _sourceText[0].Returns(_text[0]); - _sourceText[1].Returns(_text[1]); - _sourceText[2].Returns(_text[2]); - _sourceText[3].Returns(_text[3]); - - _sourceText - .When(x => x.CopyTo(0, Arg.Any(), 0, _text.Length)) - .Do(x => _text.CopyTo(x.ArgAt(1), 0)); + _options.Value.Returns(new TextCursorOptions { WindowSize = 2048 }); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -32,9 +25,11 @@ public TextCursorTests() public void Class_ShouldImplementIDisposableInterface() { // Arrange + const string Text = "CDEF"; + var sourceText = GetSourceText(Text); // Act - ITextCursor cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(sourceText, _options); // Assert Assert.IsAssignableFrom(cursor); @@ -45,9 +40,11 @@ public void Class_ShouldImplementIDisposableInterface() public void Class_ShouldImplementITextCursorInterface() { // Arrange + const string Text = "CDEF"; + var sourceText = GetSourceText(Text); // Act - ITextCursor cursor = new TextCursor(_sourceText); + ITextCursor cursor = new TextCursor(sourceText, _options); // Assert Assert.IsAssignableFrom(cursor); @@ -58,7 +55,9 @@ public void Class_ShouldImplementITextCursorInterface() public void Dispose_ShouldNotThrowAnyException() { // Arrange - ITextCursor cursor = new TextCursor(_sourceText); + const string Text = "CDEF"; + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act cursor.Dispose(); @@ -72,8 +71,11 @@ public void Dispose_ShouldNotThrowAnyException() public void Constructor_ShouldCopyInputText() { // Arrange + const string Text = "CDEF"; const byte Expected = (byte)'C'; - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var actual = cursor.Current; @@ -87,9 +89,11 @@ public void Constructor_ShouldCopyInputText() public void Current_ShouldReturnNone_WhenInputStringIsEmpty() { // Arrange + const string Text = ""; var expected = Option.None; - _sourceText.Length.Returns(0); - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var current = cursor.Current; @@ -103,8 +107,11 @@ public void Current_ShouldReturnNone_WhenInputStringIsEmpty() public void Current_ShouldReturnNone_WhenCursorIsAtTheEnd() { // Arrange - ITextCursor cursor = new TextCursor(_sourceText); - cursor.Advance(_text.Length); + const string Text = "CDEF"; + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); + cursor.Advance(Text.Length); // Act var current = cursor.Current; @@ -118,9 +125,11 @@ public void Current_ShouldReturnNone_WhenCursorIsAtTheEnd() public void IsAtEnd_ShouldReturnTrue_WhenInputStringIsEmpty() { // Arrange + const string Text = ""; const bool Expected = true; - _sourceText.Length.Returns(0); - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var isAtEnd = cursor.IsAtEnd; @@ -134,8 +143,11 @@ public void IsAtEnd_ShouldReturnTrue_WhenInputStringIsEmpty() public void IsAtEnd_ShouldReturnFalse_WhenInputStringIsNotEmpty_AndCursorIsAtTheBeginning() { // Arrange + const string Text = "CDEF"; const bool Expected = false; - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var isAtEnd = cursor.IsAtEnd; @@ -149,11 +161,14 @@ public void IsAtEnd_ShouldReturnFalse_WhenInputStringIsNotEmpty_AndCursorIsAtThe public void IsAtEnd_ShouldReturnTrue_WhenCursorIsAtTheEnd() { // Arrange + const string Text = "CDEF"; const bool Expected = true; - ITextCursor cursor = new TextCursor(_sourceText); - cursor.Advance(_text.Length); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act + cursor.Advance(Text.Length); var isAtEnd = cursor.IsAtEnd; // Assert @@ -165,9 +180,11 @@ public void IsAtEnd_ShouldReturnTrue_WhenCursorIsAtTheEnd() public void Peek_ShouldReturnNone_WhenInputStringIsEmpty() { // Arrange + const string Text = ""; var expected = Option.None; - _sourceText.Length.Returns(0); - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var peek = cursor.Peek(); @@ -181,8 +198,11 @@ public void Peek_ShouldReturnNone_WhenInputStringIsEmpty() public void Peek_ShouldReturnFirstCharacterOfInputString() { // Arrange + const string Text = "CDEF"; const byte Expected = (byte)'C'; - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var actual = cursor.Peek(); @@ -196,9 +216,12 @@ public void Peek_ShouldReturnFirstCharacterOfInputString() public void Peek_ShouldReturnNthCharacterOfInputString() { // Arrange + const string Text = "CDEF"; const int Index = 2; const byte Expected = (byte)'E'; - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var actual = cursor.Peek(Index); @@ -212,10 +235,13 @@ public void Peek_ShouldReturnNthCharacterOfInputString() public void Peek_ShouldReturnNone_WhenCursorIsAtTheEnd() { // Arrange - ITextCursor cursor = new TextCursor(_sourceText); - cursor.Advance(_text.Length); + const string Text = "CDEF"; + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act + cursor.Advance(Text.Length); var peek = cursor.Peek(); // Assert @@ -227,10 +253,14 @@ public void Peek_ShouldReturnNone_WhenCursorIsAtTheEnd() public void Advance_ShouldAdvanceCursorByOnePosition() { // Arrange + const string Text = "CDEF"; const byte Expected = (byte)'D'; - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act + cursor.Peek(); cursor.Advance(); var actual = cursor.Current; @@ -243,11 +273,15 @@ public void Advance_ShouldAdvanceCursorByOnePosition() public void Advance_ShouldAdvanceCursorByNPositions() { // Arrange + const string Text = "CDEF"; const int Index = 2; const byte Expected = (byte)'E'; - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act + cursor.Peek(); cursor.Advance(Index); var actual = cursor.Current; @@ -260,8 +294,11 @@ public void Advance_ShouldAdvanceCursorByNPositions() public void Advance_WithPredicate_ShouldAdvanceCursor_IfPredicateIsTrue() { // Arrange + const string Text = "CDEF"; const byte Expected = (byte)'E'; - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act cursor.Advance(static x => x is (byte)'C' or (byte)'D'); @@ -276,8 +313,11 @@ public void Advance_WithPredicate_ShouldAdvanceCursor_IfPredicateIsTrue() public void Advance_WithPredicate_ShouldNotAdvanceCursor_IfPredicateIsFalse() { // Arrange + const string Text = "CDEF"; const byte Expected = (byte)'C'; - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act cursor.Advance(static x => x is (byte)'A' or (byte)'B'); @@ -292,8 +332,11 @@ public void Advance_WithPredicate_ShouldNotAdvanceCursor_IfPredicateIsFalse() public void TryAdvance_ShouldReturnFalse_IfCharacterAtCurrentPositionIsNotTheExpectedOne() { // Arrange + const string Text = "CDEF"; const byte Expected = (byte)'C'; - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var result = cursor.TryAdvance((byte)'A'); @@ -309,8 +352,11 @@ public void TryAdvance_ShouldReturnFalse_IfCharacterAtCurrentPositionIsNotTheExp public void TryAdvance_ShouldReturnTrue_IfCharacterAtCurrentPositionIsTheExpectedOne() { // Arrange + const string Text = "CDEF"; const byte Expected = (byte)'D'; - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var result = cursor.TryAdvance((byte)'C'); @@ -326,8 +372,11 @@ public void TryAdvance_ShouldReturnTrue_IfCharacterAtCurrentPositionIsTheExpecte public void TryAdvance_WithEmptySubText_ShouldReturnFalse() { // Arrange + const string Text = "CDEF"; const byte Expected = (byte)'C'; - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var result = cursor.TryAdvance(ReadOnlySpan.Empty); @@ -347,8 +396,11 @@ public void TryAdvance_WithEmptySubText_ShouldReturnFalse() public void TryAdvance_WithSubText_ShouldReturnFalse_IfSubTextIsNotFound(string subtext) { // Arrange + const string Text = "CDEF"; const byte Expected = (byte)'C'; - ITextCursor cursor = new TextCursor(_sourceText); + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var result = cursor.TryAdvance(Encoding.ASCII.GetBytes(subtext)); @@ -368,7 +420,10 @@ public void TryAdvance_WithSubText_ShouldReturnFalse_IfSubTextIsNotFound(string public void TryAdvance_WithSubText_ShouldReturnTrue_IfSubTextIsFound(string subtext, byte nextChar) { // Arrange - ITextCursor cursor = new TextCursor(_sourceText); + const string Text = "CDEF"; + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var result = cursor.TryAdvance(Encoding.ASCII.GetBytes(subtext)); @@ -384,24 +439,30 @@ public void TryAdvance_WithSubText_ShouldReturnTrue_IfSubTextIsFound(string subt public void SourceText_ShouldReturnSourceText() { // Arrange - ITextCursor cursor = new TextCursor(_sourceText); + const string Text = "CDEF"; + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var actual = cursor.SourceText; // Assert - Assert.Same(_sourceText, actual); + Assert.Same(sourceText, actual); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] - [Fact(DisplayName = $"{nameof(TextCursor.Basis)} property should return 0 by default")] - public void Basis_ShouldReturnZero_ByDefault() + [Fact(DisplayName = $"{nameof(TextCursor.WindowStart)} property should return 0 by default")] + public void WindowStart_ShouldReturnZero_ByDefault() { // Arrange - ITextCursor cursor = new TextCursor(_sourceText); + const string Text = "CDEF"; + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act - var actual = cursor.Basis; + var actual = cursor.WindowStart; // Assert Assert.Equal(0, actual); @@ -412,7 +473,10 @@ public void Basis_ShouldReturnZero_ByDefault() public void Offset_ShouldReturnZero_ByDefault() { // Arrange - ITextCursor cursor = new TextCursor(_sourceText); + const string Text = "CDEF"; + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var actual = cursor.Offset; @@ -426,7 +490,10 @@ public void Offset_ShouldReturnZero_ByDefault() public void Position_ShouldReturnZero_ByDefault() { // Arrange - ITextCursor cursor = new TextCursor(_sourceText); + const string Text = "CDEF"; + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var actual = cursor.Position; @@ -440,7 +507,10 @@ public void Position_ShouldReturnZero_ByDefault() public void IsLexemeMode_ShouldReturnFalse_ByDefault() { // Arrange - ITextCursor cursor = new TextCursor(_sourceText); + const string Text = "CDEF"; + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var actual = cursor.IsLexemeMode; @@ -450,14 +520,17 @@ public void IsLexemeMode_ShouldReturnFalse_ByDefault() } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] - [Fact(DisplayName = $"{nameof(TextCursor.LexemeBasis)} property should return 0 by default")] - public void LexemeBasis_ShouldReturnZero_ByDefault() + [Fact(DisplayName = $"{nameof(TextCursor.LexemeStart)} property should return 0 by default")] + public void LexemeStart_ShouldReturnZero_ByDefault() { // Arrange - ITextCursor cursor = new TextCursor(_sourceText); + const string Text = "CDEF"; + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act - var actual = cursor.LexemeBasis; + var actual = cursor.LexemeStart; // Assert Assert.Equal(0, actual); @@ -468,7 +541,10 @@ public void LexemeBasis_ShouldReturnZero_ByDefault() public void LexemePosition_ShouldReturnZero_ByDefault() { // Arrange - ITextCursor cursor = new TextCursor(_sourceText); + const string Text = "CDEF"; + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var actual = cursor.LexemePosition; @@ -482,7 +558,10 @@ public void LexemePosition_ShouldReturnZero_ByDefault() public void LexemeWidth_ShouldReturnZero_ByDefault() { // Arrange - ITextCursor cursor = new TextCursor(_sourceText); + const string Text = "CDEF"; + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act var actual = cursor.LexemeWidth; @@ -492,34 +571,55 @@ public void LexemeWidth_ShouldReturnZero_ByDefault() } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] - [Fact(DisplayName = $"{nameof(TextCursor.WindowCount)} property should return 0 by default")] - public void WindowCount_ShouldReturnZero_ByDefault() + [Fact(DisplayName = $"{nameof(TextCursor.WindowSize)} property should return 0 by default")] + public void WindowSize_ShouldReturnZero_ByDefault() { // Arrange - ITextCursor cursor = new TextCursor(_sourceText); + const string Text = "CDEF"; + + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act - var actual = cursor.WindowCount; + var actual = cursor.WindowSize; // Assert Assert.Equal(0, actual); } + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Theory(DisplayName = $"{nameof(TextCursor.WindowSize)} property should update when peeking a character")] + [InlineData(2048, 24)] + [InlineData(16, 16)] + public void WindowSize_ShouldUpdate_WhenPeekingACharacter(int windowSize, int expectedWindowCount) + { + // Arrange + const int TextLength = 24; + + _options.Value.Returns(new TextCursorOptions { WindowSize = windowSize }); + var text = new string('A', TextLength); + + var sourceText = GetSourceText(text); + ITextCursor cursor = new TextCursor(sourceText, _options); + + // Act + cursor.Peek(); + var actual = cursor.WindowSize; + + // Assert + Assert.Equal(expectedWindowCount, actual); + } + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] [Fact(DisplayName = $"{nameof(TextCursor.Offset)} property should correspond to the number of characters advanced")] public void Offset_ShouldCorrespondToNumberOfCharactersAdvanced() { // Arrange + const string Text = "CDEF"; const int Expected = 3; - var text = "CDEF"u8.ToArray(); - _sourceText.Length.Returns(text.Length); - _sourceText[0].Returns(text[0]); - _sourceText[1].Returns(text[1]); - _sourceText[2].Returns(text[2]); - _sourceText[3].Returns(text[3]); - - ITextCursor cursor = new TextCursor(_sourceText); + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act cursor.Advance(3); @@ -534,16 +634,11 @@ public void Offset_ShouldCorrespondToNumberOfCharactersAdvanced() public void Position_ShouldCorrespondToNumberOfCharactersAdvanced() { // Arrange + const string Text = "CDEF"; const int Expected = 3; - var text = "CDEF"u8.ToArray(); - - _sourceText.Length.Returns(text.Length); - _sourceText[0].Returns(text[0]); - _sourceText[1].Returns(text[1]); - _sourceText[2].Returns(text[2]); - _sourceText[3].Returns(text[3]); - ITextCursor cursor = new TextCursor(_sourceText); + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act cursor.Advance(3); @@ -555,24 +650,19 @@ public void Position_ShouldCorrespondToNumberOfCharactersAdvanced() [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] [Fact(DisplayName = $"{nameof(TextCursor.StartLexemeMode)} method should set the lexeme basis to the current offset")] - public void StartLexemeMode_ShouldSetLexemeBasisToCurrentOffset() + public void StartLexemeMode_ShouldSetLexemeStartToCurrentOffset() { // Arrange + const string Text = "CDEF"; const int Expected = 3; - var text = "CDEF"u8.ToArray(); - _sourceText.Length.Returns(text.Length); - _sourceText[0].Returns(text[0]); - _sourceText[1].Returns(text[1]); - _sourceText[2].Returns(text[2]); - _sourceText[3].Returns(text[3]); - - ITextCursor cursor = new TextCursor(_sourceText); + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act cursor.Advance(3); cursor.StartLexemeMode(); - var actual = cursor.LexemeBasis; + var actual = cursor.LexemeStart; // Assert Assert.Equal(Expected, actual); @@ -583,15 +673,10 @@ public void StartLexemeMode_ShouldSetLexemeBasisToCurrentOffset() public void StartLexemeMode_ShouldSetIsLexemeModeToTrue() { // Arrange - var text = "CDEF"u8.ToArray(); - - _sourceText.Length.Returns(text.Length); - _sourceText[0].Returns(text[0]); - _sourceText[1].Returns(text[1]); - _sourceText[2].Returns(text[2]); - _sourceText[3].Returns(text[3]); + const string Text = "CDEF"; - ITextCursor cursor = new TextCursor(_sourceText); + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act cursor.Advance(3); @@ -607,15 +692,10 @@ public void StartLexemeMode_ShouldSetIsLexemeModeToTrue() public void StopLexemeMode_ShouldSetIsLexemeModeToFalse() { // Arrange - var text = "CDEF"u8.ToArray(); + const string Text = "CDEF"; - _sourceText.Length.Returns(text.Length); - _sourceText[0].Returns(text[0]); - _sourceText[1].Returns(text[1]); - _sourceText[2].Returns(text[2]); - _sourceText[3].Returns(text[3]); - - ITextCursor cursor = new TextCursor(_sourceText); + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act cursor.Advance(3); @@ -628,28 +708,228 @@ public void StopLexemeMode_ShouldSetIsLexemeModeToFalse() } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] - [Fact(DisplayName = $"{nameof(TextCursor.StopLexemeMode)} method should set the {nameof(TextCursor.LexemeBasis)} property to 0")] - public void StopLexemeMode_ShouldSetLexemeBasisToZero() + [Fact(DisplayName = $"{nameof(TextCursor.StopLexemeMode)} method should set the {nameof(TextCursor.LexemeStart)} property to 0")] + public void StopLexemeMode_ShouldSetLexemeStartToZero() { // Arrange + const string Text = "CDEF"; const int Expected = 0; - var text = "CDEF"u8.ToArray(); - - _sourceText.Length.Returns(text.Length); - _sourceText[0].Returns(text[0]); - _sourceText[1].Returns(text[1]); - _sourceText[2].Returns(text[2]); - _sourceText[3].Returns(text[3]); - ITextCursor cursor = new TextCursor(_sourceText); + var sourceText = GetSourceText(Text); + ITextCursor cursor = new TextCursor(sourceText, _options); // Act cursor.Advance(3); cursor.StartLexemeMode(); cursor.StopLexemeMode(); - var actual = cursor.LexemeBasis; + var actual = cursor.LexemeStart; // Assert Assert.Equal(Expected, actual); } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Theory(DisplayName = $"{nameof(TextCursor.SlideTextWindow)} method should slide the text window and compute the window size")] + [InlineData(0, 4, 2048, 4)] + [InlineData(1, 4, 2048, 3)] + [InlineData(2, 4, 2048, 2)] + [InlineData(3, 4, 2048, 1)] + [InlineData(0, 256, 16, 16)] + [InlineData(17, 256, 16, 16)] + [InlineData(-1, 256, 16, 0)] + public void SlideTextWindow_NotInitialized_ShouldComputeWindowSize(int windowStart, int sourceTextLength, int defaultWindowSize, int expectedWindowSize) + { + // Arrange + var sourceText = GetSourceText(new string('A', sourceTextLength)); + _options.Value.Returns(new TextCursorOptions { WindowSize = defaultWindowSize }); + ITextCursor cursor = new TextCursor(sourceText, _options); + + // Act + cursor.SlideTextWindow(windowStart); + + // Assert + Assert.Equal(expectedWindowSize, cursor.WindowSize); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Theory(DisplayName = $"{nameof(TextCursor.SlideTextWindow)} method should slide the text window and compute the window start position")] + [InlineData(0, 0)] + [InlineData(15, 15)] + [InlineData(-1, 0)] + public void SlideTextWindow_NotInitialized_ShouldComputeWindowStart(int windowStart, int expectedWindowStart) + { + // Arrange + const int WindowSize = 2048; + const int TextLength = 16; + + var sourceText = GetSourceText(new string('A', TextLength)); + _options.Value.Returns(new TextCursorOptions { WindowSize = WindowSize }); + ITextCursor cursor = new TextCursor(sourceText, _options); + + // Act + cursor.SlideTextWindow(windowStart); + + // Assert + Assert.Equal(expectedWindowStart, cursor.WindowStart); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.SlideTextWindow)} method should shrink the window size")] + public void SlideTextWindow_NotInitialized_ShouldShrinkWindowSize() + { + // Arrange + const int TextLength = 256; + const int Position = 0; + const int DefaultWindowSize = 2048; + const int WindowSize = 48; + const int ExpectedWindowSize = 64; + + var sourceText = GetSourceText(GenerateIncrementingString(TextLength)); + _options.Value.Returns(new TextCursorOptions { WindowSize = DefaultWindowSize }); + ITextCursor cursor = new TextCursor(sourceText, _options); + + // Act + cursor.SlideTextWindow(Position, WindowSize); + var character = cursor.Peek(delta: 0); + + // Assert + Assert.Equal(ExpectedWindowSize, cursor.WindowSize); + Assert.Equal((byte)'a', character); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.SlideTextWindow)} method should extend the window size")] + public void SlideTextWindow_NotInitialized_ShouldExtendWindowSize() + { + // Arrange + const int TextLength = 256; + const int Position = 0; + const int DefaultWindowSize = 16; + const int WindowSize = 48; + const int ExpectedWindowSize = 64; + + var sourceText = GetSourceText(GenerateIncrementingString(TextLength)); + _options.Value.Returns(new TextCursorOptions { WindowSize = DefaultWindowSize }); + ITextCursor cursor = new TextCursor(sourceText, _options); + + // Act + cursor.SlideTextWindow(Position, WindowSize); + var character = cursor.Peek(delta: 0); + + // Assert + Assert.Equal(ExpectedWindowSize, cursor.WindowSize); + Assert.Equal((byte)'a', character); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.SlideTextWindow)} method should copy the data from old window when extending new window")] + public void SlideTextWindow_WhenInitialized_WhenExtending_ShouldCopyOldWindowToNewWindow() + { + // Arrange + const int TextLength = 256; + const int Position = 0; + const int DefaultWindowSize = 16; + const int WindowSize = 48; + const int ExpectedWindowSize = 64; + + var sourceText = GetSourceText(GenerateIncrementingString(TextLength)); + _options.Value.Returns(new TextCursorOptions { WindowSize = DefaultWindowSize }); + ITextCursor cursor = new TextCursor(sourceText, _options); + + // Act + cursor.SlideTextWindow(Position); + cursor.SlideTextWindow(Position, WindowSize); + + // Assert + Assert.Equal(ExpectedWindowSize, cursor.WindowSize); + Assert.Equal(sourceText.Source[Position..ExpectedWindowSize], cursor.Window[..ExpectedWindowSize]); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.SlideTextWindow)} method should copy the data from old window when shrinking new window")] + public void SlideTextWindow_WhenInitialized_WhenShrinking_ShouldCopyOldWindowToNewWindow() + { + // Arrange + const int TextLength = 256; + const int Position = 0; + const int DefaultWindowSize = 128; + const int WindowSize = 48; + const int ExpectedWindowSize = 64; + + var sourceText = GetSourceText(GenerateIncrementingString(TextLength)); + _options.Value.Returns(new TextCursorOptions { WindowSize = DefaultWindowSize }); + ITextCursor cursor = new TextCursor(sourceText, _options); + + // Act + cursor.SlideTextWindow(Position); + cursor.SlideTextWindow(Position, WindowSize); + + // Assert + Assert.Equal(ExpectedWindowSize, cursor.WindowSize); + Assert.Equal(sourceText.Source[Position..ExpectedWindowSize], cursor.Window[..ExpectedWindowSize]); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.SlideTextWindow)} method should set {nameof(TextCursor.Offset)} to 0 when sliding the window")] + public void SlideTextWindow_WhenSliding_ShouldSetOffsetToZero() + { + // Arrange + const int TextLength = 256; + const int Position = 0; + const int DefaultWindowSize = 128; + const int WindowSize = 48; + const int ExpectedOffset = 0; + + var sourceText = GetSourceText(GenerateIncrementingString(TextLength)); + _options.Value.Returns(new TextCursorOptions { WindowSize = DefaultWindowSize }); + ITextCursor cursor = new TextCursor(sourceText, _options); + + // Act + cursor.Peek(); + cursor.Advance(5); + cursor.SlideTextWindow(Position, WindowSize); + + // Assert + Assert.Equal(ExpectedOffset, cursor.Offset); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.SlideTextWindow)} method should set {nameof(TextCursor.LexemeStart)} to 0 when sliding the window")] + public void SlideTextWindow_WhenSliding_ShouldSetLexemeStartToZero() + { + // Arrange + const int TextLength = 256; + const int Position = 0; + const int DefaultWindowSize = 128; + const int WindowSize = 48; + const int ExpectedLexemeStart = 0; + + var sourceText = GetSourceText(GenerateIncrementingString(TextLength)); + _options.Value.Returns(new TextCursorOptions { WindowSize = DefaultWindowSize }); + ITextCursor cursor = new TextCursor(sourceText, _options); + + // Act + cursor.Peek(); + cursor.Advance(5); + cursor.SlideTextWindow(Position, WindowSize); + + // Assert + Assert.Equal(ExpectedLexemeStart, cursor.LexemeStart); + Assert.False(cursor.IsLexemeMode); + } + + private static ISourceText GetSourceText(string text) => new StringText(Encoding.ASCII.GetBytes(text)); + + private static string GenerateIncrementingString(int n) + { + const char StartChar = 'a'; + var result = new char[n]; + + for (var i = 0; i < n; i++) + { + result[i] = (char)(StartChar + i); + } + + return new string(result); + } } diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Syntax/AbstractNodeTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Syntax/AbstractNodeTests.cs index 5354833..521c750 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/Syntax/AbstractNodeTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Tests/Syntax/AbstractNodeTests.cs @@ -361,7 +361,7 @@ public void LeadingTriviaProperty_ShouldReturnNoneByDefault() var rawNode = new MockAbstractNode(0); // Act - var actual = rawNode.LeadingTrivia.IsSome(out _); + var actual = rawNode.LeadingTrivia.TryGetValue(out _); // Assert Assert.False(actual); @@ -375,7 +375,7 @@ public void TrailingTriviaProperty_ShouldReturnNoneByDefault() var rawNode = new MockAbstractNode(0); // Act - var actual = rawNode.TrailingTrivia.IsSome(out _); + var actual = rawNode.TrailingTrivia.TryGetValue(out _); // Assert Assert.False(actual); @@ -389,7 +389,7 @@ public void ValueProperty_ShouldReturnNoneByDefault() var rawNode = new MockAbstractNode(0); // Act - var actual = rawNode.Value.IsSome(out _); + var actual = rawNode.Value.TryGetValue(out _); // Assert Assert.False(actual); @@ -407,7 +407,7 @@ public void ValueProperty_ShouldReturnThePopulatedValue() var actual = rawNode.Value; // Assert - Assert.True(actual.IsSome(out var actualValue)); + Assert.True(actual.TryGetValue(out var actualValue)); Assert.Equal(expected, actualValue); } diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Utils/OptionExtensionsTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Utils/OptionExtensionsTests.cs index 6b883ce..e55912c 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/Utils/OptionExtensionsTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Tests/Utils/OptionExtensionsTests.cs @@ -54,7 +54,7 @@ public void Bind_ShouldReturnNoneIfOptionHasNoValue() var actual = option.Bind(static value => Option.Some(value * 2)); // Assert - Assert.False(actual.IsSome(out _)); + Assert.False(actual.TryGetValue(out _)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -69,7 +69,7 @@ public void Bind_ShouldTransformValueIfOptionHasValue() var actual = option.Bind(static value => Option.Some(value * 2)); // Assert - Assert.True(actual.IsSome(out var actualValue)); + Assert.True(actual.TryGetValue(out var actualValue)); Assert.Equal(Expected * 2, actualValue); } @@ -84,7 +84,7 @@ public void Select_ShouldReturnNoneIfOptionHasNoValue() var actual = option.Select(static value => value * 2); // Assert - Assert.False(actual.IsSome(out _)); + Assert.False(actual.TryGetValue(out _)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -99,7 +99,7 @@ public void Select_ShouldTransformValueIfOptionHasValue() var actual = option.Select(static value => value * 2); // Assert - Assert.True(actual.IsSome(out var actualValue)); + Assert.True(actual.TryGetValue(out var actualValue)); Assert.Equal(Expected * 2, actualValue); } @@ -114,7 +114,7 @@ public void Where_ShouldReturnNoneIfOptionHasNoValue() var actual = option.Where(static value => value > 0); // Assert - Assert.False(actual.IsSome(out _)); + Assert.False(actual.TryGetValue(out _)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -129,7 +129,7 @@ public void Where_ShouldReturnNoneIfPredicateReturnsFalse() var actual = option.Where(static value => value < 0); // Assert - Assert.False(actual.IsSome(out _)); + Assert.False(actual.TryGetValue(out _)); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -144,7 +144,7 @@ public void Where_ShouldReturnOptionIfPredicateReturnsTrue() var actual = option.Where(static value => value > 0); // Assert - Assert.True(actual.IsSome(out var actualValue)); + Assert.True(actual.TryGetValue(out var actualValue)); Assert.Equal(Expected, actualValue); } diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Utils/OptionTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Utils/OptionTests.cs index 887fc19..2116129 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/Utils/OptionTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Tests/Utils/OptionTests.cs @@ -47,7 +47,7 @@ public void None_ShouldReturnDefaultInstance() var option = Option.None; // Act - var actual = option.IsSome(out var value); + var actual = option.TryGetValue(out var value); // Assert Assert.False(actual); @@ -63,7 +63,7 @@ public void Some_ShouldReturnInstanceWithValue() var option = Option.Some(Expected); // Act - var actual = option.IsSome(out var value); + var actual = option.TryGetValue(out var value); // Assert Assert.True(actual); @@ -71,14 +71,14 @@ public void Some_ShouldReturnInstanceWithValue() } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] - [Fact(DisplayName = $"{nameof(Option.IsSome)} method should return false for the default instance of {nameof(Option)}")] - public void IsSome_ShouldReturnFalseForDefaultInstance() + [Fact(DisplayName = $"{nameof(Option.TryGetValue)} method should return false for the default instance of {nameof(Option)}")] + public void TryGetValue_ShouldReturnFalseForDefaultInstance() { // Arrange var option = default(Option); // Act - var actual = option.IsSome(out var value); + var actual = option.TryGetValue(out var value); // Assert Assert.False(actual); @@ -86,18 +86,47 @@ public void IsSome_ShouldReturnFalseForDefaultInstance() } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] - [Fact(DisplayName = $"{nameof(Option.IsSome)} method should return true for the instance of {nameof(Option)} with value set")] - public void IsSome_ShouldReturnTrueForInstanceWithValueSet() + [Fact(DisplayName = $"{nameof(Option.TryGetValue)} method should return true for the instance of {nameof(Option)} with value set")] + public void TryGetValue_ShouldReturnTrueForInstanceWithValueSet() { // Arrange const int Expected = 42; Option option = Expected; // Act - var actual = option.IsSome(out var value); + var actual = option.TryGetValue(out var value); // Assert Assert.True(actual); Assert.Equal(Expected, value); } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(Option.IsSome)} property should return false for the default instance of {nameof(Option)}")] + public void IsSome_ShouldReturnFalseForDefaultInstance() + { + // Arrange + var option = default(Option); + + // Act + var actual = option.IsSome; + + // Assert + Assert.False(actual); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(Option.IsSome)} property should return true for the instance of {nameof(Option)} with value set")] + public void IsSome_ShouldReturnTrueForInstanceWithValueSet() + { + // Arrange + const int Expected = 42; + Option option = Expected; + + // Act + var actual = option.IsSome; + + // Assert + Assert.True(actual); + } } From 2b08521aaf34528ef980a4547316ae2cffadace1 Mon Sep 17 00:00:00 2001 From: Victor Pogor Date: Mon, 14 Oct 2024 20:27:04 +1000 Subject: [PATCH 11/14] fixed peek method --- .../Lexer/TextCursor.cs | 17 +++- .../Lexer/TextCursorTests.cs | 80 +++++++++++++++---- 2 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs b/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs index d07d196..bac2bef 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs @@ -88,16 +88,21 @@ public TextCursor(in ISourceText text, IOptions options) public Option Peek(int delta = 0) { Debug.Assert(delta >= 0, "Delta should be positive"); - return !IsAtEnd - ? Option.Some(_characterWindowMemoryOwner.Memory.Span[Offset + delta]) - : Option.None; + var windowStart = WindowStart; + var offset = Offset; + + Advance(delta); + var b = !HasMoreBytes() ? Option.None : _characterWindowMemoryOwner.Memory.Span[Offset]; + + SlideTextWindow(windowStart); + Advance(offset); + return b; } /// Advances the cursor by the specified delta. /// The delta by which to advance the cursor. public void Advance(int delta = 1) { - Debug.Assert(delta > 0, "Delta should greater than 0"); Offset += delta; } @@ -154,6 +159,10 @@ public void SlideTextWindow(int windowStart = -1, int windowSize = -1) if (windowStart >= SourceText.Length) return; + // do not slide the window if the window start and size are the same + if (windowStart == WindowStart && windowSize == WindowSize) + return; + if (windowSize >= 0) { var oldCharacterWindowMemoryOwner = _characterWindowMemoryOwner; diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs index a13deff..1b0251a 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs @@ -779,7 +779,7 @@ public void SlideTextWindow_NotInitialized_ShouldShrinkWindowSize() { // Arrange const int TextLength = 256; - const int Position = 0; + const int WindowStart = 0; const int DefaultWindowSize = 2048; const int WindowSize = 48; const int ExpectedWindowSize = 64; @@ -789,7 +789,7 @@ public void SlideTextWindow_NotInitialized_ShouldShrinkWindowSize() ITextCursor cursor = new TextCursor(sourceText, _options); // Act - cursor.SlideTextWindow(Position, WindowSize); + cursor.SlideTextWindow(WindowStart, WindowSize); var character = cursor.Peek(delta: 0); // Assert @@ -803,7 +803,7 @@ public void SlideTextWindow_NotInitialized_ShouldExtendWindowSize() { // Arrange const int TextLength = 256; - const int Position = 0; + const int WindowStart = 0; const int DefaultWindowSize = 16; const int WindowSize = 48; const int ExpectedWindowSize = 64; @@ -813,7 +813,7 @@ public void SlideTextWindow_NotInitialized_ShouldExtendWindowSize() ITextCursor cursor = new TextCursor(sourceText, _options); // Act - cursor.SlideTextWindow(Position, WindowSize); + cursor.SlideTextWindow(WindowStart, WindowSize); var character = cursor.Peek(delta: 0); // Assert @@ -827,7 +827,7 @@ public void SlideTextWindow_WhenInitialized_WhenExtending_ShouldCopyOldWindowToN { // Arrange const int TextLength = 256; - const int Position = 0; + const int WindowStart = 0; const int DefaultWindowSize = 16; const int WindowSize = 48; const int ExpectedWindowSize = 64; @@ -837,12 +837,12 @@ public void SlideTextWindow_WhenInitialized_WhenExtending_ShouldCopyOldWindowToN ITextCursor cursor = new TextCursor(sourceText, _options); // Act - cursor.SlideTextWindow(Position); - cursor.SlideTextWindow(Position, WindowSize); + cursor.SlideTextWindow(WindowStart); + cursor.SlideTextWindow(WindowStart, WindowSize); // Assert Assert.Equal(ExpectedWindowSize, cursor.WindowSize); - Assert.Equal(sourceText.Source[Position..ExpectedWindowSize], cursor.Window[..ExpectedWindowSize]); + Assert.Equal(sourceText.Source[WindowStart..ExpectedWindowSize], cursor.Window[..ExpectedWindowSize]); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -851,7 +851,7 @@ public void SlideTextWindow_WhenInitialized_WhenShrinking_ShouldCopyOldWindowToN { // Arrange const int TextLength = 256; - const int Position = 0; + const int WindowStart = 0; const int DefaultWindowSize = 128; const int WindowSize = 48; const int ExpectedWindowSize = 64; @@ -861,12 +861,12 @@ public void SlideTextWindow_WhenInitialized_WhenShrinking_ShouldCopyOldWindowToN ITextCursor cursor = new TextCursor(sourceText, _options); // Act - cursor.SlideTextWindow(Position); - cursor.SlideTextWindow(Position, WindowSize); + cursor.SlideTextWindow(WindowStart); + cursor.SlideTextWindow(WindowStart, WindowSize); // Assert Assert.Equal(ExpectedWindowSize, cursor.WindowSize); - Assert.Equal(sourceText.Source[Position..ExpectedWindowSize], cursor.Window[..ExpectedWindowSize]); + Assert.Equal(sourceText.Source[WindowStart..ExpectedWindowSize], cursor.Window[..ExpectedWindowSize]); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -875,7 +875,7 @@ public void SlideTextWindow_WhenSliding_ShouldSetOffsetToZero() { // Arrange const int TextLength = 256; - const int Position = 0; + const int WindowStart = 0; const int DefaultWindowSize = 128; const int WindowSize = 48; const int ExpectedOffset = 0; @@ -887,7 +887,7 @@ public void SlideTextWindow_WhenSliding_ShouldSetOffsetToZero() // Act cursor.Peek(); cursor.Advance(5); - cursor.SlideTextWindow(Position, WindowSize); + cursor.SlideTextWindow(WindowStart, WindowSize); // Assert Assert.Equal(ExpectedOffset, cursor.Offset); @@ -899,7 +899,7 @@ public void SlideTextWindow_WhenSliding_ShouldSetLexemeStartToZero() { // Arrange const int TextLength = 256; - const int Position = 0; + const int WindowStart = 0; const int DefaultWindowSize = 128; const int WindowSize = 48; const int ExpectedLexemeStart = 0; @@ -911,13 +911,61 @@ public void SlideTextWindow_WhenSliding_ShouldSetLexemeStartToZero() // Act cursor.Peek(); cursor.Advance(5); - cursor.SlideTextWindow(Position, WindowSize); + cursor.SlideTextWindow(WindowStart, WindowSize); // Assert Assert.Equal(ExpectedLexemeStart, cursor.LexemeStart); Assert.False(cursor.IsLexemeMode); } + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.SlideTextWindow)} method should not slide the window if the start position and size are the same")] + public void SlideTextWindow_WhenStartAndSizeAreTheSame_ShouldNotSlideWindow() + { + // Arrange + const int TextLength = 256; + const int WindowStart = 0; + const int DefaultWindowSize = 128; + const int WindowSize = 128; + + var sourceText = GetSourceText(GenerateIncrementingString(TextLength)); + _options.Value.Returns(new TextCursorOptions { WindowSize = DefaultWindowSize }); + ITextCursor cursor = new TextCursor(sourceText, _options); + + // Act + cursor.SlideTextWindow(WindowStart, WindowSize); + cursor.SlideTextWindow(); + + // Assert + Assert.Equal(DefaultWindowSize, cursor.WindowSize); + } + + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Theory(DisplayName = $"{nameof(TextCursor.Peek)} method should restore the {nameof(TextCursor.WindowStart)} if the delta is outside window")] + [InlineData(16, (byte)'s')] + [InlineData(17, (byte)'t')] + [InlineData(18, (byte)'u')] + [InlineData(19, (byte)'v')] + public void Peek_CustomWindowStart_WhenDeltaIsOutsideWindow_ShouldRestoreWindowStart(int peekDelta, byte expectedByte) + { + // Arrange + const int TextLength = 256; + const int WindowStart = 2; + const int DefaultWindowSize = 16; + + var sourceText = GetSourceText(GenerateIncrementingString(TextLength)); + _options.Value.Returns(new TextCursorOptions { WindowSize = DefaultWindowSize }); + ITextCursor cursor = new TextCursor(sourceText, _options); + + // Act + cursor.SlideTextWindow(WindowStart); + var actual = cursor.Peek(peekDelta); + + // Assert + Assert.Equal(WindowStart, cursor.WindowStart); + Assert.Equal(expectedByte, actual); + } + private static ISourceText GetSourceText(string text) => new StringText(Encoding.ASCII.GetBytes(text)); private static string GenerateIncrementingString(int n) From 86ba75029e96fd057156c280231e3c45f6a59bf5 Mon Sep 17 00:00:00 2001 From: Victor Pogor Date: Tue, 19 Nov 2024 19:37:19 +1000 Subject: [PATCH 12/14] enhanced text cursor --- .config/dotnet-tools.json | 15 +- Directory.Packages.props | 18 +- .../packages.lock.json | 76 +++++---- .../Lexer/ITextCursor.cs | 92 +++-------- .../Lexer/TextCursor.cs | 150 +++++++++++------ src/OffDotNet.CodeAnalysis/Utils/Option.cs | 15 +- src/OffDotNet.CodeAnalysis/packages.lock.json | 68 ++++---- .../packages.lock.json | 154 +++++++++--------- .../Lexer/TextCursorTests.cs | 56 +++++-- .../packages.lock.json | 144 ++++++++-------- 10 files changed, 433 insertions(+), 355 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 739a9d3..87059be 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,22 +3,25 @@ "isRoot": true, "tools": { "dotnet-reportgenerator-globaltool": { - "version": "5.3.7", + "version": "5.3.11", "commands": [ "reportgenerator" - ] + ], + "rollForward": false }, "dotnet-config": { - "version": "1.0.6", + "version": "1.2.0", "commands": [ "dotnet-config" - ] + ], + "rollForward": false }, "dotnet-stryker": { - "version": "4.0.6", + "version": "4.3.0", "commands": [ "dotnet-stryker" - ] + ], + "rollForward": false } } } \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 1cc0fb6..c465bcc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,9 +1,9 @@ - - - - + + + + @@ -11,11 +11,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -26,8 +26,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json b/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json index fb42a09..4c6556c 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json +++ b/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json @@ -4,27 +4,27 @@ "net8.0": { "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Direct", - "requested": "[8.0.1, )", - "resolved": "8.0.1", - "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==" }, "Microsoft.Extensions.ObjectPool": { "type": "Direct", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "2yLweyqmpuuFSRo+I3sLHMxmnAgNcI537kBJiyv49U2ZEqo00jZcG8lrnD8uCiOJp9IklYyTZULtbsXoFVzsjQ==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "UbsU/gYe4nv1DeqMXIVzDfNNek7Sk2kKuAOXL/Y+sLcAR0HwFUqzg1EPiU88jeHNe0g81aPvvHbvHarQr3r9IA==" }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.12.4, )", - "resolved": "4.12.4", - "contentHash": "isl8hAh7yFNjyBEC4YlTSi+xGBblqBUC/2MCMmnBPwuXPewb7XYnMRzT3vXbP/gOGwT8hZUOy1g/aRH3lAF/NQ==" + "requested": "[4.12.9, )", + "resolved": "4.12.9", + "contentHash": "X6lDpN/D5wuinq37KIx+l3GSUe9No+8bCjGBTI5sEEtxapLztkHg6gzNVhMXpXw8P+/5gFYxTXJ5Pf8O4iNz/w==" }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.30.0.95878, )", - "resolved": "9.30.0.95878", - "contentHash": "P0DylTJphECGE9HD6yho7rb6qs2WTTW36QUu5ezGRJeZpq8ItlRLmzUpxNHVOaudJywcnKO1DdRyZSaU7LXOcA==" + "requested": "[9.32.0.97167, )", + "resolved": "9.32.0.97167", + "contentHash": "Yxk86RV+8ynJpUhku1Yw2hITFmnmXKkXJ73cIFSy85ol5SnWREQg9RuTyV8nI7V7+pyLKpCfRmD7P0widsgjkg==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -37,57 +37,63 @@ }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "RIFgaqoaINxkM2KTOw72dmilDmTrYA0ns2KW4lDz4gZ2+o6IQ894CzmdL3StM2oh7QQq44nCWiqKqc4qUI9Jmg==", + "resolved": "9.0.0", + "contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "System.Diagnostics.DiagnosticSource": "9.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "8.0.2", - "contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==", + "resolved": "9.0.0", + "contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" + "resolved": "9.0.0", + "contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg==" }, "StyleCop.Analyzers.Unstable": { "type": "Transitive", "resolved": "1.2.0.556", "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "ddppcFpnbohLWdYKr/ZeLZHmmI+DXFgZ3Snq+/E7SwcdW4UnvxmaugkwGywvGVWkHPGCSZjCP+MLzu23AL5SDw==" + }, "offdotnet.codeanalysis": { "type": "Project", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", - "Microsoft.Extensions.Localization": "[8.0.7, )", - "Microsoft.Extensions.Localization.Abstractions": "[8.0.7, )", - "Microsoft.Extensions.ObjectPool": "[8.0.7, )" + "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.0, )", + "Microsoft.Extensions.Localization": "[9.0.0, )", + "Microsoft.Extensions.Localization.Abstractions": "[9.0.0, )", + "Microsoft.Extensions.ObjectPool": "[9.0.0, )" } }, "Microsoft.Extensions.Localization": { "type": "CentralTransitive", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "kdI24IfC1Jk3zfhWvYmuzvkzuKp+5Shjmfq0vk6EKBB+Udt4Gu1SkVSkMWJkYZFag+ndd+sTroUKDC/sud/DYQ==", + "requested": "[8.0.10, )", + "resolved": "9.0.0", + "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", - "Microsoft.Extensions.Localization.Abstractions": "8.0.7", - "Microsoft.Extensions.Logging.Abstractions": "8.0.1", - "Microsoft.Extensions.Options": "8.0.2" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Localization.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" } }, "Microsoft.Extensions.Localization.Abstractions": { "type": "CentralTransitive", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "uCsxqBBJEsSoV52tX/v+bxxN0e1O8WYIRR7tmcEmVVkRSX+0k4ZJcaL79ddus+0HIm3ssjV/dJk56XufDrGDqg==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "wc7PaRhPOnio5Csj80b3UgBWA5l6bp28EhGem7gtfpVopcwbkfPb2Sk8Cu6eBnIW3ZNf1YUgYJzwtjzZEM8+iw==" } } } diff --git a/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs b/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs index bba4621..a7ddc65 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/ITextCursor.cs @@ -7,109 +7,69 @@ namespace OffDotNet.CodeAnalysis.Lexer; using Utils; -/// Represents a text cursor for navigating and processing text data. -/// -/// -/// SLIDING position -/// WINDOW | -/// -------- + -------- -/// | | -/// basis =======>> offset -/// | | -/// ------------------------------- -/// SRC: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | -/// ------------------------------- -/// | | -/// basis ==>> offset -/// | | -/// ---- + ---- -/// LEXEME | -/// SUB-WINDOW position -/// -/// +/// public interface ITextCursor : IDisposable { - /// Gets the source text. + /// ISourceText SourceText { get; } - /// - /// Gets the current byte at the cursor position. - /// + /// Option Current { get; } - /// - /// Gets a value indicating whether the cursor is at the end of the text. - /// + /// bool IsAtEnd { get; } - /// Gets the start offset inside the window (relative to the start). + /// int WindowStart { get; } - /// Gets the end offset inside the window (relative to the ). + /// int Offset { get; } - /// Gets the absolute position in the . + /// int Position => WindowStart + Offset; - /// Gets a value indicating whether the window is in parsing lexeme mode. + /// bool IsLexemeMode { get; } - /// Gets the lexeme start offset relative to the window start. + /// int LexemeStart { get; } - /// Gets the absolute position of the lexeme in the . + /// int LexemePosition => WindowStart + LexemeStart; - /// Gets the width of the lexeme. + /// int LexemeWidth => Offset - LexemeStart; - /// Gets the text window. + /// ReadOnlyMemory Window { get; } - /// Gets the number of characters in the window. + /// int WindowSize { get; } - /// - /// Peeks at the byte at the specified delta from the current position. - /// - /// The delta from the current position. - /// The byte at the specified delta if available; otherwise, . - Option Peek(int delta = 0); - - /// - /// Advances the cursor by the specified delta. - /// - /// The delta by which to advance the cursor. + /// + Option Peek(); + + /// + Option Peek(int delta); + + /// void Advance(int delta = 1); - /// - /// Advances the cursor while the specified predicate is true. - /// - /// The predicate to test each byte against. + /// void Advance(Predicate predicate); - /// - /// Tries to advance the cursor if the current byte matches the specified byte. - /// - /// The byte to match against. - /// True if the cursor was advanced; otherwise, false. + /// bool TryAdvance(byte b); - /// - /// Tries to advance the cursor if the subsequent bytes match the specified subtext. - /// - /// The subtext to match against. - /// True if the cursor was advanced; otherwise, false. + /// bool TryAdvance(ReadOnlySpan subtext); - /// Slides the text window to the specified start position and size. - /// The start position of the window. - /// The size of the window. + /// void SlideTextWindow(int windowStart = -1, int windowSize = -1); - /// Starts parsing a lexeme and sets the to the current value. + /// public void StartLexemeMode(); - /// Stops parsing a lexeme. + /// public void StopLexemeMode(); } diff --git a/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs b/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs index bac2bef..20b3f25 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/TextCursor.cs @@ -12,21 +12,21 @@ namespace OffDotNet.CodeAnalysis.Lexer; /// Represents a text cursor for navigating and processing text data. /// /// -/// SLIDING position -/// WINDOW | -/// -------- + -------- -/// | | -/// basis =======>> offset -/// | | +/// SLIDING position +/// WINDOW | +/// ------ size ------ +/// | | +/// start =======>> offset +/// | | /// ------------------------------- -/// SRC: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +/// TXT: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | /// ------------------------------- -/// | | -/// basis ==>> offset -/// | | -/// ---- + ---- -/// LEXEME | -/// SUB-WINDOW position +/// | | | +/// | start | +/// | | | +/// - position | width - +/// LEXEME +/// SUB-WINDOW /// /// internal sealed class TextCursor : ITextCursor @@ -82,20 +82,25 @@ public TextCursor(in ISourceText text, IOptions options) /// Gets the number of valid characters in the text window. public int WindowSize { get; private set; } + /// Peeks at the byte at the specified delta from the current position. + /// The byte at the specified delta if available; otherwise, . + public Option Peek() + { + return !HasMoreBytes() ? Option.None : _characterWindowMemoryOwner.Memory.Span[Offset]; + } + /// Peeks at the byte at the specified delta from the current position. /// The delta from the current position. /// The byte at the specified delta if available; otherwise, . - public Option Peek(int delta = 0) + public Option Peek(int delta) { Debug.Assert(delta >= 0, "Delta should be positive"); - var windowStart = WindowStart; - var offset = Offset; + var position = Position; Advance(delta); var b = !HasMoreBytes() ? Option.None : _characterWindowMemoryOwner.Memory.Span[Offset]; - SlideTextWindow(windowStart); - Advance(offset); + SlideTextWindow(position); return b; } @@ -153,38 +158,9 @@ public bool TryAdvance(ReadOnlySpan subtext) /// The size of the window. public void SlideTextWindow(int windowStart = -1, int windowSize = -1) { - if (windowStart < 0) - return; - - if (windowStart >= SourceText.Length) - return; - - // do not slide the window if the window start and size are the same - if (windowStart == WindowStart && windowSize == WindowSize) - return; - - if (windowSize >= 0) - { - var oldCharacterWindowMemoryOwner = _characterWindowMemoryOwner; - - // the new memory owner size is not guaranteed to match the requested window size as it is rounded up to the nearest power of 2 - _characterWindowMemoryOwner = MemoryPool.Shared.Rent(windowSize); - var newWindowSize = Math.Min(windowSize, WindowSize); - oldCharacterWindowMemoryOwner.Memory.Span[..newWindowSize].CopyTo(_characterWindowMemoryOwner.Memory.Span); - oldCharacterWindowMemoryOwner.Dispose(); - } - - var count = Math.Min(SourceText.Length - windowStart, _characterWindowMemoryOwner.Memory.Span.Length); - - if (count > 0) - { - SourceText.CopyTo(windowStart, _characterWindowMemoryOwner.Memory.Span, 0, count); - } - - Offset = 0; - WindowStart = windowStart; - WindowSize = count; - StopLexemeMode(); + ChangeWindowSize(windowSize); + ChangeWindowStart(windowStart); + AdjustTextWindowCursors(windowStart); } /// Starts parsing a lexeme and sets the to the current value. @@ -215,7 +191,79 @@ private bool HasMoreBytes() if (Position >= SourceText.Length) return false; - SlideTextWindow(Position); + if (IsLexemeMode) + { + // grow the window to include the entire lexeme + var offset = Offset; + var size = WindowSize == 0 ? _characterWindowMemoryOwner.Memory.Span.Length : WindowSize; + var windowSize = size * 2; + + SlideTextWindow(WindowStart, windowSize); + Offset = offset; + } + else + { + SlideTextWindow(Position); + } + return WindowSize > 0; } + + private void ChangeWindowSize(int windowSize) + { + if (windowSize < 0) + return; + + if (windowSize == WindowSize) + return; + + // the new memory owner size is not guaranteed to match the requested window size as it is rounded up to the nearest power of 2 + var newCharacterWindowMemoryOwner = MemoryPool.Shared.Rent(windowSize); + var oldCharacterWindowMemoryOwner = _characterWindowMemoryOwner; + + var newWindowSize = Math.Min(windowSize, WindowSize); + oldCharacterWindowMemoryOwner.Memory.Span[..newWindowSize].CopyTo(newCharacterWindowMemoryOwner.Memory.Span); + oldCharacterWindowMemoryOwner.Dispose(); + + _characterWindowMemoryOwner = newCharacterWindowMemoryOwner; + } + + private void ChangeWindowStart(int windowStart) + { + if (windowStart < 0) + return; + + if (windowStart >= SourceText.Length) + return; + + var windowSpan = _characterWindowMemoryOwner.Memory.Span; + var count = Math.Min(SourceText.Length - windowStart, windowSpan.Length); + + if (count > 0) + { + SourceText.CopyTo(windowStart, windowSpan, 0, count); + } + + WindowStart = windowStart; + WindowSize = count; + } + + private void AdjustTextWindowCursors(int windowStart) + { + var windowStartChanged = windowStart != WindowStart; + var relative = windowStart - WindowStart; + var isRelativeWithinBounds = relative >= 0 && relative <= WindowSize; + + if (windowStartChanged || Offset >= WindowSize) + { + Offset = 0; + StopLexemeMode(); + return; + } + + if (isRelativeWithinBounds && relative != Offset) + { + Offset = relative; + } + } } diff --git a/src/OffDotNet.CodeAnalysis/Utils/Option.cs b/src/OffDotNet.CodeAnalysis/Utils/Option.cs index cf47f54..7b08acf 100644 --- a/src/OffDotNet.CodeAnalysis/Utils/Option.cs +++ b/src/OffDotNet.CodeAnalysis/Utils/Option.cs @@ -63,7 +63,20 @@ public bool TryGetValue([MaybeNullWhen(false)] out T value) /// Returns a string that represents the current option. /// A string that represents the current option. - public override string ToString() => IsSome ? $"Some({_value})" : "None"; + public override string ToString() + { + if (!IsSome) + { + return "None"; + } + + if (_value is byte b) + { + return $"Some('{Convert.ToChar(b)}' | 0x{b:X})"; + } + + return $"Some({_value})"; + } private string DebuggerDisplay() => ToString(); } diff --git a/src/OffDotNet.CodeAnalysis/packages.lock.json b/src/OffDotNet.CodeAnalysis/packages.lock.json index b26b2c1..505c039 100644 --- a/src/OffDotNet.CodeAnalysis/packages.lock.json +++ b/src/OffDotNet.CodeAnalysis/packages.lock.json @@ -4,45 +4,45 @@ "net8.0": { "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Direct", - "requested": "[8.0.1, )", - "resolved": "8.0.1", - "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==" }, "Microsoft.Extensions.Localization": { "type": "Direct", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "kdI24IfC1Jk3zfhWvYmuzvkzuKp+5Shjmfq0vk6EKBB+Udt4Gu1SkVSkMWJkYZFag+ndd+sTroUKDC/sud/DYQ==", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", - "Microsoft.Extensions.Localization.Abstractions": "8.0.7", - "Microsoft.Extensions.Logging.Abstractions": "8.0.1", - "Microsoft.Extensions.Options": "8.0.2" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Localization.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" } }, "Microsoft.Extensions.Localization.Abstractions": { "type": "Direct", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "uCsxqBBJEsSoV52tX/v+bxxN0e1O8WYIRR7tmcEmVVkRSX+0k4ZJcaL79ddus+0HIm3ssjV/dJk56XufDrGDqg==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "wc7PaRhPOnio5Csj80b3UgBWA5l6bp28EhGem7gtfpVopcwbkfPb2Sk8Cu6eBnIW3ZNf1YUgYJzwtjzZEM8+iw==" }, "Microsoft.Extensions.ObjectPool": { "type": "Direct", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "2yLweyqmpuuFSRo+I3sLHMxmnAgNcI537kBJiyv49U2ZEqo00jZcG8lrnD8uCiOJp9IklYyTZULtbsXoFVzsjQ==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "UbsU/gYe4nv1DeqMXIVzDfNNek7Sk2kKuAOXL/Y+sLcAR0HwFUqzg1EPiU88jeHNe0g81aPvvHbvHarQr3r9IA==" }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.12.4, )", - "resolved": "4.12.4", - "contentHash": "isl8hAh7yFNjyBEC4YlTSi+xGBblqBUC/2MCMmnBPwuXPewb7XYnMRzT3vXbP/gOGwT8hZUOy1g/aRH3lAF/NQ==" + "requested": "[4.12.9, )", + "resolved": "4.12.9", + "contentHash": "X6lDpN/D5wuinq37KIx+l3GSUe9No+8bCjGBTI5sEEtxapLztkHg6gzNVhMXpXw8P+/5gFYxTXJ5Pf8O4iNz/w==" }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.30.0.95878, )", - "resolved": "9.30.0.95878", - "contentHash": "P0DylTJphECGE9HD6yho7rb6qs2WTTW36QUu5ezGRJeZpq8ItlRLmzUpxNHVOaudJywcnKO1DdRyZSaU7LXOcA==" + "requested": "[9.32.0.97167, )", + "resolved": "9.32.0.97167", + "contentHash": "Yxk86RV+8ynJpUhku1Yw2hITFmnmXKkXJ73cIFSy85ol5SnWREQg9RuTyV8nI7V7+pyLKpCfRmD7P0widsgjkg==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -55,30 +55,36 @@ }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "RIFgaqoaINxkM2KTOw72dmilDmTrYA0ns2KW4lDz4gZ2+o6IQ894CzmdL3StM2oh7QQq44nCWiqKqc4qUI9Jmg==", + "resolved": "9.0.0", + "contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "System.Diagnostics.DiagnosticSource": "9.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "8.0.2", - "contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==", + "resolved": "9.0.0", + "contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" + "resolved": "9.0.0", + "contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg==" }, "StyleCop.Analyzers.Unstable": { "type": "Transitive", "resolved": "1.2.0.556", "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "ddppcFpnbohLWdYKr/ZeLZHmmI+DXFgZ3Snq+/E7SwcdW4UnvxmaugkwGywvGVWkHPGCSZjCP+MLzu23AL5SDw==" } } } diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json index 73fbc85..152804b 100644 --- a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json @@ -10,19 +10,19 @@ }, "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[17.10.0, )", - "resolved": "17.10.0", - "contentHash": "0/2HeACkaHEYU3wc83YlcD2Fi4LMtECJjqrtvw0lPi9DCEa35zSPt1j4fuvM8NagjDqJuh1Ja35WcRtn1Um6/A==", + "requested": "[17.11.1, )", + "resolved": "17.11.1", + "contentHash": "U3Ty4BaGoEu+T2bwSko9tWqWUOU16WzSFkq6U8zve75oRBMSLTBdMAZrVNNz1Tq12aCdDom9fcOcM9QZaFHqFg==", "dependencies": { - "Microsoft.CodeCoverage": "17.10.0", - "Microsoft.TestPlatform.TestHost": "17.10.0" + "Microsoft.CodeCoverage": "17.11.1", + "Microsoft.TestPlatform.TestHost": "17.11.1" } }, "NSubstitute": { "type": "Direct", - "requested": "[5.1.0, )", - "resolved": "5.1.0", - "contentHash": "ZCqOP3Kpp2ea7QcLyjMU4wzE+0wmrMN35PQMsdPOHYc2IrvjmusG9hICOiqiOTPKN0gJon6wyCn6ZuGHdNs9hQ==", + "requested": "[5.3.0, )", + "resolved": "5.3.0", + "contentHash": "lJ47Cps5Qzr86N99lcwd+OUvQma7+fBgr8+Mn+aOC0WrlqMNkdivaYD9IvnZ5Mqo6Ky3LS7ZI+tUq1/s9ERd0Q==", "dependencies": { "Castle.Core": "5.1.1" } @@ -35,15 +35,15 @@ }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.12.4, )", - "resolved": "4.12.4", - "contentHash": "isl8hAh7yFNjyBEC4YlTSi+xGBblqBUC/2MCMmnBPwuXPewb7XYnMRzT3vXbP/gOGwT8hZUOy1g/aRH3lAF/NQ==" + "requested": "[4.12.9, )", + "resolved": "4.12.9", + "contentHash": "X6lDpN/D5wuinq37KIx+l3GSUe9No+8bCjGBTI5sEEtxapLztkHg6gzNVhMXpXw8P+/5gFYxTXJ5Pf8O4iNz/w==" }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.30.0.95878, )", - "resolved": "9.30.0.95878", - "contentHash": "P0DylTJphECGE9HD6yho7rb6qs2WTTW36QUu5ezGRJeZpq8ItlRLmzUpxNHVOaudJywcnKO1DdRyZSaU7LXOcA==" + "requested": "[9.32.0.97167, )", + "resolved": "9.32.0.97167", + "contentHash": "Yxk86RV+8ynJpUhku1Yw2hITFmnmXKkXJ73cIFSy85ol5SnWREQg9RuTyV8nI7V7+pyLKpCfRmD7P0widsgjkg==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -56,13 +56,13 @@ }, "xunit": { "type": "Direct", - "requested": "[2.9.0, )", - "resolved": "2.9.0", - "contentHash": "PtU3rZ0ThdmdJqTbK7GkgFf6iBaCR6Q0uvJHznID+XEYk2v6O/b7sRxqnbi3B2gRDXxjTqMkVNayzwsqsFUxRw==", + "requested": "[2.9.2, )", + "resolved": "2.9.2", + "contentHash": "7LhFS2N9Z6Xgg8aE5lY95cneYivRMfRI8v+4PATa4S64D5Z/Plkg0qa8dTRHSiGRgVZ/CL2gEfJDE5AUhOX+2Q==", "dependencies": { - "xunit.analyzers": "1.15.0", - "xunit.assert": "2.9.0", - "xunit.core": "[2.9.0]" + "xunit.analyzers": "1.16.0", + "xunit.assert": "2.9.2", + "xunit.core": "[2.9.2]" } }, "xunit.runner.visualstudio": { @@ -81,45 +81,46 @@ }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "yC7oSlnR54XO5kOuHlVOKtxomNNN1BWXX8lK1G2jaPXT9sUok7kCOoA4Pgs0qyFaCtMrNsprztYMeoEGqCm4uA==" + "resolved": "17.11.1", + "contentHash": "nPJqrcA5iX+Y0kqoT3a+pD/8lrW/V7ayqnEJQsTonSoPz59J8bmoQhcSN4G8+UJ64Hkuf0zuxnfuj2lkHOq4cA==" }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "RIFgaqoaINxkM2KTOw72dmilDmTrYA0ns2KW4lDz4gZ2+o6IQ894CzmdL3StM2oh7QQq44nCWiqKqc4qUI9Jmg==", + "resolved": "9.0.0", + "contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "System.Diagnostics.DiagnosticSource": "9.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "8.0.2", - "contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==", + "resolved": "9.0.0", + "contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" + "resolved": "9.0.0", + "contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg==" }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "KkwhjQevuDj0aBRoPLY6OLAhGqbPUEBuKLbaCs0kUVw29qiOYncdORd4mLVJbn9vGZ7/iFGQ/+AoJl0Tu5Umdg==", + "resolved": "17.11.1", + "contentHash": "E2jZqAU6JeWEVsyOEOrSW1o1bpHLgb25ypvKNB/moBXPVsFYBPd/Jwi7OrYahG50J83LfHzezYI+GaEkpAotiA==", "dependencies": { "System.Reflection.Metadata": "1.6.0" } }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "LWpMdfqhHvcUkeMCvNYJO8QlPLlYz9XPPb+ZbaXIKhdmjAV0wqTSrTiW5FLaf7RRZT50AQADDOYMOe0HxDxNgA==", + "resolved": "17.11.1", + "contentHash": "DnG+GOqJXO/CkoqlJWeDFTgPhqD/V6VqUIL3vINizCWZ3X+HshCtbbyDdSHQQEjrc2Sl/K3yaxX6s+5LFEdYuw==", "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "17.10.0", + "Microsoft.TestPlatform.ObjectModel": "17.11.1", "Newtonsoft.Json": "13.0.1" } }, @@ -133,6 +134,11 @@ "resolved": "1.2.0.556", "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "ddppcFpnbohLWdYKr/ZeLZHmmI+DXFgZ3Snq+/E7SwcdW4UnvxmaugkwGywvGVWkHPGCSZjCP+MLzu23AL5SDw==" + }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", @@ -150,94 +156,94 @@ }, "xunit.analyzers": { "type": "Transitive", - "resolved": "1.15.0", - "contentHash": "s+M8K/Rtlgr6CmD7AYQKrNTvT5sh0l0ZKDoZ3Z/ExhlIwfV9mGAMR4f7KqIB7SSK7ZOhqDTgTUMYPmKfmvWUWQ==" + "resolved": "1.16.0", + "contentHash": "hptYM7vGr46GUIgZt21YHO4rfuBAQS2eINbFo16CV/Dqq+24Tp+P5gDCACu1AbFfW4Sp/WRfDPSK8fmUUb8s0Q==" }, "xunit.assert": { "type": "Transitive", - "resolved": "2.9.0", - "contentHash": "Z/1pyia//860wEYTKn6Q5dmgikJdRjgE4t5AoxJkK8oTmidzPLEPG574kmm7LFkMLbH6Frwmgb750kcyR+hwoA==" + "resolved": "2.9.2", + "contentHash": "QkNBAQG4pa66cholm28AxijBjrmki98/vsEh4Sx5iplzotvPgpiotcxqJQMRC8d7RV7nIT8ozh97957hDnZwsQ==" }, "xunit.core": { "type": "Transitive", - "resolved": "2.9.0", - "contentHash": "uRaop9tZsZMCaUS4AfbSPGYHtvywWnm8XXFNUqII7ShWyDBgdchY6gyDNgO4AK1Lv/1NNW61Zq63CsDV6oH6Jg==", + "resolved": "2.9.2", + "contentHash": "O6RrNSdmZ0xgEn5kT927PNwog5vxTtKrWMihhhrT0Sg9jQ7iBDciYOwzBgP2krBEk5/GBXI18R1lKvmnxGcb4w==", "dependencies": { - "xunit.extensibility.core": "[2.9.0]", - "xunit.extensibility.execution": "[2.9.0]" + "xunit.extensibility.core": "[2.9.2]", + "xunit.extensibility.execution": "[2.9.2]" } }, "xunit.extensibility.core": { "type": "Transitive", - "resolved": "2.9.0", - "contentHash": "zjDEUSxsr6UNij4gIwCgMqQox+oLDPRZ+mubwWLci+SssPBFQD1xeRR4SvgBuXqbE0QXCJ/STVTp+lxiB5NLVA==", + "resolved": "2.9.2", + "contentHash": "Ol+KlBJz1x8BrdnhN2DeOuLrr1I/cTwtHCggL9BvYqFuVd/TUSzxNT5O0NxCIXth30bsKxgMfdqLTcORtM52yQ==", "dependencies": { "xunit.abstractions": "2.0.3" } }, "xunit.extensibility.execution": { "type": "Transitive", - "resolved": "2.9.0", - "contentHash": "5ZTQZvmPLlBw6QzCOwM0KnMsZw6eGjbmC176QHZlcbQoMhGIeGcYzYwn5w9yXxf+4phtplMuVqTpTbFDQh2bqQ==", + "resolved": "2.9.2", + "contentHash": "rKMpq4GsIUIJibXuZoZ8lYp5EpROlnYaRpwu9Zr0sRZXE7JqJfEEbCsUriZqB+ByXCLFBJyjkTRULMdC+U566g==", "dependencies": { - "xunit.extensibility.core": "[2.9.0]" + "xunit.extensibility.core": "[2.9.2]" } }, "offdotnet.codeanalysis": { "type": "Project", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", - "Microsoft.Extensions.Localization": "[8.0.7, )", - "Microsoft.Extensions.Localization.Abstractions": "[8.0.7, )", - "Microsoft.Extensions.ObjectPool": "[8.0.7, )" + "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.0, )", + "Microsoft.Extensions.Localization": "[9.0.0, )", + "Microsoft.Extensions.Localization.Abstractions": "[9.0.0, )", + "Microsoft.Extensions.ObjectPool": "[9.0.0, )" } }, "offdotnet.codeanalysis.pdf": { "type": "Project", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", - "Microsoft.Extensions.ObjectPool": "[8.0.7, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.0, )", + "Microsoft.Extensions.ObjectPool": "[9.0.0, )", "OffDotNet.CodeAnalysis": "[1.0.0, )" } }, "offdotnet.codeanalysis.tests": { "type": "Project", "dependencies": { - "Microsoft.NET.Test.Sdk": "[17.10.0, )", - "NSubstitute": "[5.1.0, )", + "Microsoft.NET.Test.Sdk": "[17.11.1, )", + "NSubstitute": "[5.3.0, )", "OffDotNet.CodeAnalysis": "[1.0.0, )", - "xunit": "[2.9.0, )" + "xunit": "[2.9.2, )" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "CentralTransitive", - "requested": "[8.0.1, )", - "resolved": "8.0.1", - "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==" }, "Microsoft.Extensions.Localization": { "type": "CentralTransitive", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "kdI24IfC1Jk3zfhWvYmuzvkzuKp+5Shjmfq0vk6EKBB+Udt4Gu1SkVSkMWJkYZFag+ndd+sTroUKDC/sud/DYQ==", + "requested": "[8.0.10, )", + "resolved": "9.0.0", + "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", - "Microsoft.Extensions.Localization.Abstractions": "8.0.7", - "Microsoft.Extensions.Logging.Abstractions": "8.0.1", - "Microsoft.Extensions.Options": "8.0.2" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Localization.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" } }, "Microsoft.Extensions.Localization.Abstractions": { "type": "CentralTransitive", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "uCsxqBBJEsSoV52tX/v+bxxN0e1O8WYIRR7tmcEmVVkRSX+0k4ZJcaL79ddus+0HIm3ssjV/dJk56XufDrGDqg==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "wc7PaRhPOnio5Csj80b3UgBWA5l6bp28EhGem7gtfpVopcwbkfPb2Sk8Cu6eBnIW3ZNf1YUgYJzwtjzZEM8+iw==" }, "Microsoft.Extensions.ObjectPool": { "type": "CentralTransitive", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "2yLweyqmpuuFSRo+I3sLHMxmnAgNcI537kBJiyv49U2ZEqo00jZcG8lrnD8uCiOJp9IklYyTZULtbsXoFVzsjQ==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "UbsU/gYe4nv1DeqMXIVzDfNNek7Sk2kKuAOXL/Y+sLcAR0HwFUqzg1EPiU88jeHNe0g81aPvvHbvHarQr3r9IA==" } } } diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs index 1b0251a..c469133 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs @@ -870,15 +870,17 @@ public void SlideTextWindow_WhenInitialized_WhenShrinking_ShouldCopyOldWindowToN } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] - [Fact(DisplayName = $"{nameof(TextCursor.SlideTextWindow)} method should set {nameof(TextCursor.Offset)} to 0 when sliding the window")] - public void SlideTextWindow_WhenSliding_ShouldSetOffsetToZero() + [Theory(DisplayName = $"{nameof(TextCursor.SlideTextWindow)} method should adjust {nameof(TextCursor.Offset)} when sliding the window")] + [InlineData(5, 0)] + [InlineData(16, 0)] + [InlineData(17, 0)] + public void SlideTextWindow_WhenSliding_ShouldSetOffset(int advanceDelta, int expectedOffset) { // Arrange const int TextLength = 256; const int WindowStart = 0; const int DefaultWindowSize = 128; - const int WindowSize = 48; - const int ExpectedOffset = 0; + const int WindowSize = 16; var sourceText = GetSourceText(GenerateIncrementingString(TextLength)); _options.Value.Returns(new TextCursorOptions { WindowSize = DefaultWindowSize }); @@ -886,23 +888,25 @@ public void SlideTextWindow_WhenSliding_ShouldSetOffsetToZero() // Act cursor.Peek(); - cursor.Advance(5); + cursor.Advance(advanceDelta); cursor.SlideTextWindow(WindowStart, WindowSize); // Assert - Assert.Equal(ExpectedOffset, cursor.Offset); + Assert.Equal(expectedOffset, cursor.Offset); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] - [Fact(DisplayName = $"{nameof(TextCursor.SlideTextWindow)} method should set {nameof(TextCursor.LexemeStart)} to 0 when sliding the window")] - public void SlideTextWindow_WhenSliding_ShouldSetLexemeStartToZero() + [Theory(DisplayName = $"{nameof(TextCursor.SlideTextWindow)} method should adjust {nameof(TextCursor.LexemeStart)} when sliding the window")] + [InlineData(5, 5, true)] + [InlineData(16, 0, false)] + [InlineData(17, 0, false)] + public void SlideTextWindow_WhenSliding_ShouldSetLexemeStartToZero(int advanceDelta, int expectedLexemeStart, bool expectedLexemeMode) { // Arrange const int TextLength = 256; const int WindowStart = 0; const int DefaultWindowSize = 128; - const int WindowSize = 48; - const int ExpectedLexemeStart = 0; + const int WindowSize = 16; var sourceText = GetSourceText(GenerateIncrementingString(TextLength)); _options.Value.Returns(new TextCursorOptions { WindowSize = DefaultWindowSize }); @@ -910,12 +914,13 @@ public void SlideTextWindow_WhenSliding_ShouldSetLexemeStartToZero() // Act cursor.Peek(); - cursor.Advance(5); + cursor.Advance(advanceDelta); + cursor.StartLexemeMode(); cursor.SlideTextWindow(WindowStart, WindowSize); // Assert - Assert.Equal(ExpectedLexemeStart, cursor.LexemeStart); - Assert.False(cursor.IsLexemeMode); + Assert.Equal(expectedLexemeStart, cursor.LexemeStart); + Assert.Equal(expectedLexemeMode, cursor.IsLexemeMode); } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] @@ -966,6 +971,31 @@ public void Peek_CustomWindowStart_WhenDeltaIsOutsideWindow_ShouldRestoreWindowS Assert.Equal(expectedByte, actual); } + [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] + [Fact(DisplayName = $"{nameof(TextCursor.WindowSize)} property should be increased " + + $"when {nameof(TextCursor.IsLexemeMode)} is true and {nameof(TextCursor.Offset)} is outside the window")] + public void WindowSize_WhenLexemeModeIsTrue_WhenOffsetIsOutsideWindow_ShouldIncreaseWindowSize() + { + // Arrange + const int TextLength = 256; + const int DefaultWindowSize = 16; + const int ExpectedWindowSize = 32; + const byte ExpectedByte = (byte)'q'; + + var sourceText = GetSourceText(GenerateIncrementingString(TextLength)); + _options.Value.Returns(new TextCursorOptions { WindowSize = DefaultWindowSize }); + ITextCursor cursor = new TextCursor(sourceText, _options); + + // Act + cursor.StartLexemeMode(); + var actual = cursor.Peek(DefaultWindowSize); + + // Assert + Assert.True(cursor.IsLexemeMode); + Assert.Equal(ExpectedWindowSize, cursor.WindowSize); + Assert.Equal(ExpectedByte, actual); + } + private static ISourceText GetSourceText(string text) => new StringText(Encoding.ASCII.GetBytes(text)); private static string GenerateIncrementingString(int n) diff --git a/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json b/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json index 2259755..38a53bc 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json +++ b/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json @@ -10,19 +10,19 @@ }, "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[17.10.0, )", - "resolved": "17.10.0", - "contentHash": "0/2HeACkaHEYU3wc83YlcD2Fi4LMtECJjqrtvw0lPi9DCEa35zSPt1j4fuvM8NagjDqJuh1Ja35WcRtn1Um6/A==", + "requested": "[17.11.1, )", + "resolved": "17.11.1", + "contentHash": "U3Ty4BaGoEu+T2bwSko9tWqWUOU16WzSFkq6U8zve75oRBMSLTBdMAZrVNNz1Tq12aCdDom9fcOcM9QZaFHqFg==", "dependencies": { - "Microsoft.CodeCoverage": "17.10.0", - "Microsoft.TestPlatform.TestHost": "17.10.0" + "Microsoft.CodeCoverage": "17.11.1", + "Microsoft.TestPlatform.TestHost": "17.11.1" } }, "NSubstitute": { "type": "Direct", - "requested": "[5.1.0, )", - "resolved": "5.1.0", - "contentHash": "ZCqOP3Kpp2ea7QcLyjMU4wzE+0wmrMN35PQMsdPOHYc2IrvjmusG9hICOiqiOTPKN0gJon6wyCn6ZuGHdNs9hQ==", + "requested": "[5.3.0, )", + "resolved": "5.3.0", + "contentHash": "lJ47Cps5Qzr86N99lcwd+OUvQma7+fBgr8+Mn+aOC0WrlqMNkdivaYD9IvnZ5Mqo6Ky3LS7ZI+tUq1/s9ERd0Q==", "dependencies": { "Castle.Core": "5.1.1" } @@ -35,15 +35,15 @@ }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.12.4, )", - "resolved": "4.12.4", - "contentHash": "isl8hAh7yFNjyBEC4YlTSi+xGBblqBUC/2MCMmnBPwuXPewb7XYnMRzT3vXbP/gOGwT8hZUOy1g/aRH3lAF/NQ==" + "requested": "[4.12.9, )", + "resolved": "4.12.9", + "contentHash": "X6lDpN/D5wuinq37KIx+l3GSUe9No+8bCjGBTI5sEEtxapLztkHg6gzNVhMXpXw8P+/5gFYxTXJ5Pf8O4iNz/w==" }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.30.0.95878, )", - "resolved": "9.30.0.95878", - "contentHash": "P0DylTJphECGE9HD6yho7rb6qs2WTTW36QUu5ezGRJeZpq8ItlRLmzUpxNHVOaudJywcnKO1DdRyZSaU7LXOcA==" + "requested": "[9.32.0.97167, )", + "resolved": "9.32.0.97167", + "contentHash": "Yxk86RV+8ynJpUhku1Yw2hITFmnmXKkXJ73cIFSy85ol5SnWREQg9RuTyV8nI7V7+pyLKpCfRmD7P0widsgjkg==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -56,13 +56,13 @@ }, "xunit": { "type": "Direct", - "requested": "[2.9.0, )", - "resolved": "2.9.0", - "contentHash": "PtU3rZ0ThdmdJqTbK7GkgFf6iBaCR6Q0uvJHznID+XEYk2v6O/b7sRxqnbi3B2gRDXxjTqMkVNayzwsqsFUxRw==", + "requested": "[2.9.2, )", + "resolved": "2.9.2", + "contentHash": "7LhFS2N9Z6Xgg8aE5lY95cneYivRMfRI8v+4PATa4S64D5Z/Plkg0qa8dTRHSiGRgVZ/CL2gEfJDE5AUhOX+2Q==", "dependencies": { - "xunit.analyzers": "1.15.0", - "xunit.assert": "2.9.0", - "xunit.core": "[2.9.0]" + "xunit.analyzers": "1.16.0", + "xunit.assert": "2.9.2", + "xunit.core": "[2.9.2]" } }, "xunit.runner.visualstudio": { @@ -81,45 +81,46 @@ }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "yC7oSlnR54XO5kOuHlVOKtxomNNN1BWXX8lK1G2jaPXT9sUok7kCOoA4Pgs0qyFaCtMrNsprztYMeoEGqCm4uA==" + "resolved": "17.11.1", + "contentHash": "nPJqrcA5iX+Y0kqoT3a+pD/8lrW/V7ayqnEJQsTonSoPz59J8bmoQhcSN4G8+UJ64Hkuf0zuxnfuj2lkHOq4cA==" }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "RIFgaqoaINxkM2KTOw72dmilDmTrYA0ns2KW4lDz4gZ2+o6IQ894CzmdL3StM2oh7QQq44nCWiqKqc4qUI9Jmg==", + "resolved": "9.0.0", + "contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "System.Diagnostics.DiagnosticSource": "9.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "8.0.2", - "contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==", + "resolved": "9.0.0", + "contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" + "resolved": "9.0.0", + "contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg==" }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "KkwhjQevuDj0aBRoPLY6OLAhGqbPUEBuKLbaCs0kUVw29qiOYncdORd4mLVJbn9vGZ7/iFGQ/+AoJl0Tu5Umdg==", + "resolved": "17.11.1", + "contentHash": "E2jZqAU6JeWEVsyOEOrSW1o1bpHLgb25ypvKNB/moBXPVsFYBPd/Jwi7OrYahG50J83LfHzezYI+GaEkpAotiA==", "dependencies": { "System.Reflection.Metadata": "1.6.0" } }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "LWpMdfqhHvcUkeMCvNYJO8QlPLlYz9XPPb+ZbaXIKhdmjAV0wqTSrTiW5FLaf7RRZT50AQADDOYMOe0HxDxNgA==", + "resolved": "17.11.1", + "contentHash": "DnG+GOqJXO/CkoqlJWeDFTgPhqD/V6VqUIL3vINizCWZ3X+HshCtbbyDdSHQQEjrc2Sl/K3yaxX6s+5LFEdYuw==", "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "17.10.0", + "Microsoft.TestPlatform.ObjectModel": "17.11.1", "Newtonsoft.Json": "13.0.1" } }, @@ -133,6 +134,11 @@ "resolved": "1.2.0.556", "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "ddppcFpnbohLWdYKr/ZeLZHmmI+DXFgZ3Snq+/E7SwcdW4UnvxmaugkwGywvGVWkHPGCSZjCP+MLzu23AL5SDw==" + }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", @@ -150,77 +156,77 @@ }, "xunit.analyzers": { "type": "Transitive", - "resolved": "1.15.0", - "contentHash": "s+M8K/Rtlgr6CmD7AYQKrNTvT5sh0l0ZKDoZ3Z/ExhlIwfV9mGAMR4f7KqIB7SSK7ZOhqDTgTUMYPmKfmvWUWQ==" + "resolved": "1.16.0", + "contentHash": "hptYM7vGr46GUIgZt21YHO4rfuBAQS2eINbFo16CV/Dqq+24Tp+P5gDCACu1AbFfW4Sp/WRfDPSK8fmUUb8s0Q==" }, "xunit.assert": { "type": "Transitive", - "resolved": "2.9.0", - "contentHash": "Z/1pyia//860wEYTKn6Q5dmgikJdRjgE4t5AoxJkK8oTmidzPLEPG574kmm7LFkMLbH6Frwmgb750kcyR+hwoA==" + "resolved": "2.9.2", + "contentHash": "QkNBAQG4pa66cholm28AxijBjrmki98/vsEh4Sx5iplzotvPgpiotcxqJQMRC8d7RV7nIT8ozh97957hDnZwsQ==" }, "xunit.core": { "type": "Transitive", - "resolved": "2.9.0", - "contentHash": "uRaop9tZsZMCaUS4AfbSPGYHtvywWnm8XXFNUqII7ShWyDBgdchY6gyDNgO4AK1Lv/1NNW61Zq63CsDV6oH6Jg==", + "resolved": "2.9.2", + "contentHash": "O6RrNSdmZ0xgEn5kT927PNwog5vxTtKrWMihhhrT0Sg9jQ7iBDciYOwzBgP2krBEk5/GBXI18R1lKvmnxGcb4w==", "dependencies": { - "xunit.extensibility.core": "[2.9.0]", - "xunit.extensibility.execution": "[2.9.0]" + "xunit.extensibility.core": "[2.9.2]", + "xunit.extensibility.execution": "[2.9.2]" } }, "xunit.extensibility.core": { "type": "Transitive", - "resolved": "2.9.0", - "contentHash": "zjDEUSxsr6UNij4gIwCgMqQox+oLDPRZ+mubwWLci+SssPBFQD1xeRR4SvgBuXqbE0QXCJ/STVTp+lxiB5NLVA==", + "resolved": "2.9.2", + "contentHash": "Ol+KlBJz1x8BrdnhN2DeOuLrr1I/cTwtHCggL9BvYqFuVd/TUSzxNT5O0NxCIXth30bsKxgMfdqLTcORtM52yQ==", "dependencies": { "xunit.abstractions": "2.0.3" } }, "xunit.extensibility.execution": { "type": "Transitive", - "resolved": "2.9.0", - "contentHash": "5ZTQZvmPLlBw6QzCOwM0KnMsZw6eGjbmC176QHZlcbQoMhGIeGcYzYwn5w9yXxf+4phtplMuVqTpTbFDQh2bqQ==", + "resolved": "2.9.2", + "contentHash": "rKMpq4GsIUIJibXuZoZ8lYp5EpROlnYaRpwu9Zr0sRZXE7JqJfEEbCsUriZqB+ByXCLFBJyjkTRULMdC+U566g==", "dependencies": { - "xunit.extensibility.core": "[2.9.0]" + "xunit.extensibility.core": "[2.9.2]" } }, "offdotnet.codeanalysis": { "type": "Project", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", - "Microsoft.Extensions.Localization": "[8.0.7, )", - "Microsoft.Extensions.Localization.Abstractions": "[8.0.7, )", - "Microsoft.Extensions.ObjectPool": "[8.0.7, )" + "Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.0, )", + "Microsoft.Extensions.Localization": "[9.0.0, )", + "Microsoft.Extensions.Localization.Abstractions": "[9.0.0, )", + "Microsoft.Extensions.ObjectPool": "[9.0.0, )" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "CentralTransitive", - "requested": "[8.0.1, )", - "resolved": "8.0.1", - "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==" }, "Microsoft.Extensions.Localization": { "type": "CentralTransitive", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "kdI24IfC1Jk3zfhWvYmuzvkzuKp+5Shjmfq0vk6EKBB+Udt4Gu1SkVSkMWJkYZFag+ndd+sTroUKDC/sud/DYQ==", + "requested": "[8.0.10, )", + "resolved": "9.0.0", + "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", - "Microsoft.Extensions.Localization.Abstractions": "8.0.7", - "Microsoft.Extensions.Logging.Abstractions": "8.0.1", - "Microsoft.Extensions.Options": "8.0.2" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Localization.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" } }, "Microsoft.Extensions.Localization.Abstractions": { "type": "CentralTransitive", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "uCsxqBBJEsSoV52tX/v+bxxN0e1O8WYIRR7tmcEmVVkRSX+0k4ZJcaL79ddus+0HIm3ssjV/dJk56XufDrGDqg==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "wc7PaRhPOnio5Csj80b3UgBWA5l6bp28EhGem7gtfpVopcwbkfPb2Sk8Cu6eBnIW3ZNf1YUgYJzwtjzZEM8+iw==" }, "Microsoft.Extensions.ObjectPool": { "type": "CentralTransitive", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "2yLweyqmpuuFSRo+I3sLHMxmnAgNcI537kBJiyv49U2ZEqo00jZcG8lrnD8uCiOJp9IklYyTZULtbsXoFVzsjQ==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "UbsU/gYe4nv1DeqMXIVzDfNNek7Sk2kKuAOXL/Y+sLcAR0HwFUqzg1EPiU88jeHNe0g81aPvvHbvHarQr3r9IA==" } } } From 205def07db42027e6fc001c618ba204e411db0eb Mon Sep 17 00:00:00 2001 From: Victor Pogor Date: Tue, 19 Nov 2024 19:47:07 +1000 Subject: [PATCH 13/14] fixed the lock files --- src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json | 2 +- tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json | 2 +- tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json b/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json index 4c6556c..3d3d9c3 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json +++ b/src/OffDotNet.CodeAnalysis.Pdf/packages.lock.json @@ -79,7 +79,7 @@ }, "Microsoft.Extensions.Localization": { "type": "CentralTransitive", - "requested": "[8.0.10, )", + "requested": "[9.0.0, )", "resolved": "9.0.0", "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", "dependencies": { diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json index 152804b..e874a48 100644 --- a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/packages.lock.json @@ -223,7 +223,7 @@ }, "Microsoft.Extensions.Localization": { "type": "CentralTransitive", - "requested": "[8.0.10, )", + "requested": "[9.0.0, )", "resolved": "9.0.0", "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", "dependencies": { diff --git a/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json b/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json index 38a53bc..61c8f90 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json +++ b/tests/OffDotNet.CodeAnalysis.Tests/packages.lock.json @@ -206,7 +206,7 @@ }, "Microsoft.Extensions.Localization": { "type": "CentralTransitive", - "requested": "[8.0.10, )", + "requested": "[9.0.0, )", "resolved": "9.0.0", "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", "dependencies": { From 473698dc649198b8990b9a3ca048a298f670b0b6 Mon Sep 17 00:00:00 2001 From: Victor Pogor Date: Tue, 19 Nov 2024 20:18:14 +1000 Subject: [PATCH 14/14] fixed sonar comments --- .../Configs/DiagnosticOptions.cs | 2 ++ .../Configs/RootConfigurations.cs | 4 +++ .../Dependencies.cs | 3 +- .../Diagnostics/DiagnosticCode.cs | 6 ++++ .../Diagnostics/MessageProvider.cs | 34 +++++++++++++++++-- src/OffDotNet.CodeAnalysis.Pdf/Lexer/Lexer.cs | 7 ++-- .../Syntax/RawSyntaxNode.cs | 10 +++--- .../Configs/TextCursorOptions.cs | 6 ++++ .../Diagnostics/AbstractMessageProvider.cs | 34 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptor.cs | 31 +++++++++++++++-- .../Diagnostics/DiagnosticSeverity.cs | 18 ++++++++++ .../Diagnostics/IMessageProvider.cs | 7 ++-- src/OffDotNet.CodeAnalysis/Lexer/ILexer.cs | 7 ++-- .../Lexer/ISourceText.cs | 21 ++++++++++++ .../Lexer/StringText.cs | 25 ++++++++++++++ .../PooledObjects/ArrayPooledObjectPolicy.cs | 15 ++++++-- .../PooledObjects/SharedObjectPools.cs | 15 ++++++-- .../PooledObjects/StackPooledObjectPolicy.cs | 17 ++++++++-- .../Utils/ExceptionUtilities.cs | 9 +++++ .../Diagnostics/MessageProviderTests.cs | 3 +- .../Syntax/RawSyntaxNodeTests.cs | 3 +- .../Syntax/RawSyntaxTriviaTests.cs | 5 ++- .../Lexer/TextCursorTests.cs | 2 ++ 23 files changed, 250 insertions(+), 34 deletions(-) diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Configs/DiagnosticOptions.cs b/src/OffDotNet.CodeAnalysis.Pdf/Configs/DiagnosticOptions.cs index 4db5662..c11ff1b 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Configs/DiagnosticOptions.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Configs/DiagnosticOptions.cs @@ -5,7 +5,9 @@ namespace OffDotNet.CodeAnalysis.Pdf.Configs; +/// Represents the diagnostic options for the PDF analysis. public sealed record DiagnosticOptions { + /// Gets the help link associated with the diagnostic. public required string HelpLink { get; init; } } diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Configs/RootConfigurations.cs b/src/OffDotNet.CodeAnalysis.Pdf/Configs/RootConfigurations.cs index a10de3e..92069db 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Configs/RootConfigurations.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Configs/RootConfigurations.cs @@ -7,11 +7,15 @@ namespace OffDotNet.CodeAnalysis.Pdf.Configs; using OffDotNet.CodeAnalysis.Configs; +/// Represents the root configurations for the OffDotNet analysis. public sealed record RootConfigurations { + /// The section name for the OffDotNet configurations. public const string SectionName = "OffDotNet"; + /// Gets the diagnostic options for the PDF analysis. public required DiagnosticOptions Diagnostic { get; init; } + /// Gets the text cursor options for the PDF analysis. public required TextCursorOptions TextCursor { get; init; } } diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs b/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs index 0451e5b..7dfa648 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Dependencies.cs @@ -3,10 +3,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using OffDotNet.CodeAnalysis.Pdf.Configs; - namespace OffDotNet.CodeAnalysis.Pdf; +using Configs; using Diagnostics; using Microsoft.Extensions.DependencyInjection; using OffDotNet.CodeAnalysis.Diagnostics; diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/DiagnosticCode.cs b/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/DiagnosticCode.cs index dc41a4c..091e553 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/DiagnosticCode.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/DiagnosticCode.cs @@ -5,7 +5,13 @@ namespace OffDotNet.CodeAnalysis.Pdf.Diagnostics; +/// +/// Represents the diagnostic codes for the PDF analysis. +/// public enum DiagnosticCode { + /// + /// Represents an unknown diagnostic code. + /// Unknown = 0, } diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/MessageProvider.cs b/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/MessageProvider.cs index a146ce5..dd54cd0 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/MessageProvider.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Diagnostics/MessageProvider.cs @@ -3,14 +3,16 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using OffDotNet.CodeAnalysis.Pdf.Configs; - namespace OffDotNet.CodeAnalysis.Pdf.Diagnostics; +using Configs; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using OffDotNet.CodeAnalysis.Diagnostics; +/// +/// Provides localized messages for diagnostics in PDF analysis. +/// internal sealed class MessageProvider : AbstractMessageProvider { private const string TitleSuffix = "_Title"; @@ -19,20 +21,48 @@ internal sealed class MessageProvider : AbstractMessageProvider private readonly IStringLocalizer _localizer; private readonly DiagnosticOptions _options; + /// + /// Initializes a new instance of the class. + /// + /// The localizer for retrieving localized strings. + /// The diagnostic options. public MessageProvider(IStringLocalizer localizer, IOptions options) { _localizer = localizer; _options = options.Value ?? throw new ArgumentNullException(nameof(options)); } + /// + /// Gets the language prefix for the diagnostics. + /// public override string LanguagePrefix => "PDF"; + /// + /// Gets the localized title for the specified diagnostic code. + /// + /// The diagnostic code. + /// The localized title. public override LocalizedString GetTitle(ushort code) => _localizer[$"{(DiagnosticCode)code}{TitleSuffix}"]; + /// + /// Gets the localized description for the specified diagnostic code. + /// + /// The diagnostic code. + /// The localized description. public override LocalizedString GetDescription(ushort code) => _localizer[$"{(DiagnosticCode)code}{DescriptionSuffix}"]; + /// + /// Gets the help link for the specified diagnostic code. + /// + /// The diagnostic code. + /// The help link. public override string GetHelpLink(ushort code) => string.Format(_options.HelpLink, GetIdForDiagnosticCode(code)); + /// + /// Gets the severity for the specified diagnostic code. + /// + /// The diagnostic code. + /// The severity as a byte value. public override byte GetSeverity(ushort code) { switch (code) diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Lexer/Lexer.cs b/src/OffDotNet.CodeAnalysis.Pdf/Lexer/Lexer.cs index d65de7d..c575ef3 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Lexer/Lexer.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Lexer/Lexer.cs @@ -7,6 +7,7 @@ namespace OffDotNet.CodeAnalysis.Pdf.Lexer; using OffDotNet.CodeAnalysis.Lexer; -public class Lexer : ILexer -{ -} +/// +/// Represents the lexer for PDF code analysis. +/// +public class Lexer : ILexer; diff --git a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxNode.cs b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxNode.cs index bc2aea7..ada64cb 100644 --- a/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxNode.cs +++ b/src/OffDotNet.CodeAnalysis.Pdf/Syntax/RawSyntaxNode.cs @@ -33,6 +33,11 @@ protected RawSyntaxNode(SyntaxKind kind, int fullWidth) Kind = kind; } + /// + /// Gets the language of the syntax node. + /// + public static string Language => "PDF"; + /// /// Gets the kind of the syntax node. /// @@ -42,9 +47,4 @@ protected RawSyntaxNode(SyntaxKind kind, int fullWidth) /// Gets the text representation of the kind of the syntax node. /// public override string KindText => this.Kind.ToString(); - - /// - /// Gets the language of the syntax node. - /// - public string Language => "PDF"; } diff --git a/src/OffDotNet.CodeAnalysis/Configs/TextCursorOptions.cs b/src/OffDotNet.CodeAnalysis/Configs/TextCursorOptions.cs index c0c7786..03b0cad 100644 --- a/src/OffDotNet.CodeAnalysis/Configs/TextCursorOptions.cs +++ b/src/OffDotNet.CodeAnalysis/Configs/TextCursorOptions.cs @@ -5,7 +5,13 @@ namespace OffDotNet.CodeAnalysis.Configs; +/// +/// Represents the text cursor options for the OffDotNet analysis. +/// public sealed record TextCursorOptions { + /// + /// Gets the window size for the text cursor. + /// public int WindowSize { get; init; } = 2048; } diff --git a/src/OffDotNet.CodeAnalysis/Diagnostics/AbstractMessageProvider.cs b/src/OffDotNet.CodeAnalysis/Diagnostics/AbstractMessageProvider.cs index 217d5b1..40a74c8 100644 --- a/src/OffDotNet.CodeAnalysis/Diagnostics/AbstractMessageProvider.cs +++ b/src/OffDotNet.CodeAnalysis/Diagnostics/AbstractMessageProvider.cs @@ -9,20 +9,54 @@ namespace OffDotNet.CodeAnalysis.Diagnostics; using Microsoft.Extensions.Localization; using DiagnosticCacheTuple = (string LanguagePrefix, ushort Code); +/// +/// Provides an abstract base class for message providers in diagnostics. +/// internal abstract class AbstractMessageProvider : IMessageProvider { + /// + /// A cache for storing diagnostic IDs. + /// private static readonly ConcurrentDictionary s_cache = new(); + /// + /// Gets the language prefix for the diagnostics. + /// public abstract string LanguagePrefix { get; } + /// + /// Gets the localized title for the specified diagnostic code. + /// + /// The diagnostic code. + /// The localized title. public abstract LocalizedString GetTitle(ushort code); + /// + /// Gets the localized description for the specified diagnostic code. + /// + /// The diagnostic code. + /// The localized description. public abstract LocalizedString GetDescription(ushort code); + /// + /// Gets the help link for the specified diagnostic code. + /// + /// The diagnostic code. + /// The help link. public abstract string GetHelpLink(ushort code); + /// + /// Gets the severity for the specified diagnostic code. + /// + /// The diagnostic code. + /// The severity as a byte value. public abstract byte GetSeverity(ushort code); + /// + /// Gets the ID for the specified diagnostic code. + /// + /// The diagnostic code. + /// The ID for the diagnostic code. public string GetIdForDiagnosticCode(ushort diagnosticCode) { return s_cache.GetOrAdd((LanguagePrefix, diagnosticCode), key => $"{key.LanguagePrefix}{key.Code:0000}"); diff --git a/src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticDescriptor.cs b/src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticDescriptor.cs index aedd891..20210d1 100644 --- a/src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticDescriptor.cs +++ b/src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticDescriptor.cs @@ -8,24 +8,51 @@ namespace OffDotNet.CodeAnalysis.Diagnostics; using System.Collections.Immutable; using Microsoft.Extensions.Localization; +/// +/// Represents a diagnostic descriptor which contains information about a diagnostic. +/// public sealed record DiagnosticDescriptor { + private static ImmutableDictionary s_errorCodeToDescriptorMap = ImmutableDictionary.Empty; + + /// + /// Initializes a new instance of the class. + /// internal DiagnosticDescriptor() { } - private static ImmutableDictionary s_errorCodeToDescriptorMap = ImmutableDictionary.Empty; - + /// + /// Gets the ID of the diagnostic. + /// public required string Id { get; init; } + /// + /// Gets the title of the diagnostic. + /// public required LocalizedString Title { get; init; } + /// + /// Gets the description of the diagnostic. + /// public required LocalizedString Description { get; init; } + /// + /// Gets the help link for the diagnostic. + /// public required string HelpLink { get; init; } + /// + /// Gets the default severity of the diagnostic. + /// public DiagnosticSeverity DefaultSeverity { get; init; } + /// + /// Creates a diagnostic descriptor for the specified diagnostic code using the provided message provider. + /// + /// The diagnostic code. + /// The message provider. + /// The created diagnostic descriptor. internal static DiagnosticDescriptor CreateDescriptor(ushort diagnosticCode, AbstractMessageProvider messageProvider) { return ImmutableInterlocked.GetOrAdd( diff --git a/src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticSeverity.cs b/src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticSeverity.cs index fc57a90..9eb5573 100644 --- a/src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticSeverity.cs +++ b/src/OffDotNet.CodeAnalysis/Diagnostics/DiagnosticSeverity.cs @@ -5,10 +5,28 @@ namespace OffDotNet.CodeAnalysis.Diagnostics; +/// +/// Specifies the severity of a diagnostic. +/// public enum DiagnosticSeverity { + /// + /// Hidden severity, used for diagnostics that should not be shown to the user. + /// Hidden = 0, + + /// + /// Informational severity, used for diagnostics that provide information to the user. + /// Info = 1, + + /// + /// Warning severity, used for diagnostics that indicate a potential issue. + /// Warning = 2, + + /// + /// Error severity, used for diagnostics that indicate a definite issue. + /// Error = 3, } diff --git a/src/OffDotNet.CodeAnalysis/Diagnostics/IMessageProvider.cs b/src/OffDotNet.CodeAnalysis/Diagnostics/IMessageProvider.cs index eb1f388..8e188fe 100644 --- a/src/OffDotNet.CodeAnalysis/Diagnostics/IMessageProvider.cs +++ b/src/OffDotNet.CodeAnalysis/Diagnostics/IMessageProvider.cs @@ -5,6 +5,7 @@ namespace OffDotNet.CodeAnalysis.Diagnostics; -internal interface IMessageProvider -{ -} +/// +/// Defines the interface for message providers in diagnostics. +/// +internal interface IMessageProvider; diff --git a/src/OffDotNet.CodeAnalysis/Lexer/ILexer.cs b/src/OffDotNet.CodeAnalysis/Lexer/ILexer.cs index 262fff1..bcd03d4 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/ILexer.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/ILexer.cs @@ -5,6 +5,7 @@ namespace OffDotNet.CodeAnalysis.Lexer; -public interface ILexer -{ -} +/// +/// Defines the interface for a lexer in the OffDotNet analysis. +/// +public interface ILexer; diff --git a/src/OffDotNet.CodeAnalysis/Lexer/ISourceText.cs b/src/OffDotNet.CodeAnalysis/Lexer/ISourceText.cs index 1591843..225372b 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/ISourceText.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/ISourceText.cs @@ -5,13 +5,34 @@ namespace OffDotNet.CodeAnalysis.Lexer; +/// +/// Represents the source text for the lexer. +/// public interface ISourceText { + /// + /// Gets the source text as a read-only memory of bytes. + /// ReadOnlyMemory Source { get; } + /// + /// Gets the length of the source text. + /// int Length { get; } + /// + /// Gets the byte at the specified position in the source text. + /// + /// The position of the byte. + /// The byte at the specified position. byte this[int position] { get; } + /// + /// Copies a range of bytes from the source text to a destination span. + /// + /// The starting index in the source text. + /// The destination span. + /// The starting index in the destination span. + /// The number of bytes to copy. void CopyTo(int sourceIndex, Span destination, int destinationIndex, int count); } diff --git a/src/OffDotNet.CodeAnalysis/Lexer/StringText.cs b/src/OffDotNet.CodeAnalysis/Lexer/StringText.cs index f6ec03e..2c32a09 100644 --- a/src/OffDotNet.CodeAnalysis/Lexer/StringText.cs +++ b/src/OffDotNet.CodeAnalysis/Lexer/StringText.cs @@ -5,19 +5,44 @@ namespace OffDotNet.CodeAnalysis.Lexer; +/// +/// Represents a string-based source text for the lexer. +/// internal sealed class StringText : ISourceText { + /// + /// Initializes a new instance of the class with the specified source text. + /// + /// The source text as a read-only span of bytes. internal StringText(in ReadOnlySpan source) { Source = source.ToArray(); } + /// + /// Gets the source text as a read-only memory of bytes. + /// public ReadOnlyMemory Source { get; } + /// + /// Gets the length of the source text. + /// public int Length => Source.Length; + /// + /// Gets the byte at the specified position in the source text. + /// + /// The position of the byte. + /// The byte at the specified position. public byte this[int position] => Source.Span[position]; + /// + /// Copies a range of bytes from the source text to a destination span. + /// + /// The starting index in the source text. + /// The destination span. + /// The starting index in the destination span. + /// The number of bytes to copy. public void CopyTo(int sourceIndex, Span destination, int destinationIndex, int count) { Source.Span.Slice(sourceIndex, count).CopyTo(destination[destinationIndex..]); diff --git a/src/OffDotNet.CodeAnalysis/PooledObjects/ArrayPooledObjectPolicy.cs b/src/OffDotNet.CodeAnalysis/PooledObjects/ArrayPooledObjectPolicy.cs index 189f36f..0d763ad 100644 --- a/src/OffDotNet.CodeAnalysis/PooledObjects/ArrayPooledObjectPolicy.cs +++ b/src/OffDotNet.CodeAnalysis/PooledObjects/ArrayPooledObjectPolicy.cs @@ -7,12 +7,24 @@ namespace OffDotNet.CodeAnalysis.PooledObjects; using Microsoft.Extensions.ObjectPool; +/// +/// Provides a policy for pooling arrays with specified initial and maximum retained capacities. +/// +/// The type of elements in the array. internal sealed class ArrayPooledObjectPolicy : PooledObjectPolicy { + /// + /// Initializes a new instance of the class. + /// public ArrayPooledObjectPolicy() { } + /// + /// Initializes a new instance of the class with the specified capacities. + /// + /// The initial capacity of pooled byte array instances. + /// The maximum capacity of a single byte array instance that is allowed to be retained. public ArrayPooledObjectPolicy(int initialCapacity, int maximumRetainedCapacity) { InitialCapacity = initialCapacity; @@ -26,8 +38,7 @@ public ArrayPooledObjectPolicy(int initialCapacity, int maximumRetainedCapacity) public int InitialCapacity { get; init; } = 8; /// - /// Gets the maximum capacity of a single byte array instance that is allowed to be - /// retained, when is invoked. + /// Gets the maximum capacity of a single byte array instance that is allowed to be retained, when is invoked. /// /// Defaults to 128. public int MaximumRetainedCapacity { get; init; } = 128; diff --git a/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs b/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs index 3646a7b..e81eca5 100644 --- a/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs +++ b/src/OffDotNet.CodeAnalysis/PooledObjects/SharedObjectPools.cs @@ -9,14 +9,25 @@ namespace OffDotNet.CodeAnalysis.PooledObjects; using Microsoft.Extensions.ObjectPool; using AbstractNodesCacheTuple = (Syntax.AbstractNode Node, bool Leading, bool Trailing); +/// +/// Provides shared object pools for various types used in the OffDotNet analysis. +/// internal static class SharedObjectPools { - /// The abstract nodes cache. + /// + /// Gets the object pool for caching abstract nodes. + /// internal static readonly ObjectPool> AbstractNodesCache; - /// The string builder pool. + /// + /// Gets the object pool for instances. + /// internal static readonly ObjectPool StringBuilderPool; + /// + /// Initializes static members of the class. + /// + [SuppressMessage("Minor Code Smell", "S3963:\"static\" fields should be initialized inline", Justification = "Reviewed")] static SharedObjectPools() { var defaultObjectPoolProvider = new DefaultObjectPoolProvider(); diff --git a/src/OffDotNet.CodeAnalysis/PooledObjects/StackPooledObjectPolicy.cs b/src/OffDotNet.CodeAnalysis/PooledObjects/StackPooledObjectPolicy.cs index 8c9c572..6ac99ab 100644 --- a/src/OffDotNet.CodeAnalysis/PooledObjects/StackPooledObjectPolicy.cs +++ b/src/OffDotNet.CodeAnalysis/PooledObjects/StackPooledObjectPolicy.cs @@ -7,12 +7,24 @@ namespace OffDotNet.CodeAnalysis.PooledObjects; using Microsoft.Extensions.ObjectPool; +/// +/// Provides a policy for pooling stacks with specified initial and maximum retained capacities. +/// +/// The type of elements in the stack. internal sealed class StackPooledObjectPolicy : PooledObjectPolicy> { + /// + /// Initializes a new instance of the class. + /// public StackPooledObjectPolicy() { } + /// + /// Initializes a new instance of the class with the specified capacities. + /// + /// The initial capacity of pooled stack instances. + /// The maximum capacity of a single stack instance that is allowed to be retained. public StackPooledObjectPolicy(int initialCapacity, int maximumRetainedCapacity) { InitialCapacity = initialCapacity; @@ -20,14 +32,13 @@ public StackPooledObjectPolicy(int initialCapacity, int maximumRetainedCapacity) } /// - /// Gets the initial capacity of pooled byte array instances. + /// Gets the initial capacity of pooled stack instances. /// /// Defaults to 8. public int InitialCapacity { get; init; } = 8; /// - /// Gets the maximum capacity of a single byte array instance that is allowed to be - /// retained, when is invoked. + /// Gets the maximum capacity of a single stack instance that is allowed to be retained, when is invoked. /// /// Defaults to 128. public int MaximumRetainedCapacity { get; init; } = 128; diff --git a/src/OffDotNet.CodeAnalysis/Utils/ExceptionUtilities.cs b/src/OffDotNet.CodeAnalysis/Utils/ExceptionUtilities.cs index 7bbe160..9343ea2 100644 --- a/src/OffDotNet.CodeAnalysis/Utils/ExceptionUtilities.cs +++ b/src/OffDotNet.CodeAnalysis/Utils/ExceptionUtilities.cs @@ -7,8 +7,17 @@ namespace OffDotNet.CodeAnalysis.Utils; using System.Runtime.CompilerServices; +/// +/// Provides utility methods for handling exceptions. +/// internal static class ExceptionUtilities { + /// + /// Throws an indicating that the program location is thought to be unreachable. + /// + /// The file path of the caller. This is automatically provided by the compiler. + /// The line number of the caller. This is automatically provided by the compiler. + /// An indicating an unreachable program location. internal static Exception Unreachable([CallerFilePath] string? path = null, [CallerLineNumber] int line = 0) => new InvalidOperationException($"This program location is thought to be unreachable. File='{path}' Line={line}"); } diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Diagnostics/MessageProviderTests.cs b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Diagnostics/MessageProviderTests.cs index 6c836e6..92f54a0 100644 --- a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Diagnostics/MessageProviderTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Diagnostics/MessageProviderTests.cs @@ -3,10 +3,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using OffDotNet.CodeAnalysis.Pdf.Configs; - namespace OffDotNet.CodeAnalysis.Pdf.Tests.Diagnostics; +using Configs; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using OffDotNet.CodeAnalysis.Diagnostics; diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxNodeTests.cs b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxNodeTests.cs index c12875c..599960e 100644 --- a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxNodeTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxNodeTests.cs @@ -122,10 +122,9 @@ public void KindTextProperty_ShouldReturnTheNameOfTheSyntaxKind(SyntaxKind kind, public void LanguageProperty_ShouldReturnPDF() { // Arrange - var rawNode = Substitute.For(SyntaxKind.None); // Act - var language = rawNode.Language; + var language = RawSyntaxNode.Language; // Assert Assert.Equal("PDF", language); diff --git a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTriviaTests.cs b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTriviaTests.cs index ec99806..156c282 100644 --- a/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTriviaTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Pdf.Tests/Syntax/RawSyntaxTriviaTests.cs @@ -136,14 +136,13 @@ public void KindTextProperty_ShouldReturnTheNameOfTheSyntaxKind(SyntaxKind kind, } [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/339")] - [Fact(DisplayName = $"{nameof(RawSyntaxTrivia.Language)} property should return PDF")] + [Fact(DisplayName = $"{nameof(RawSyntaxNode.Language)} property should return PDF")] public void LanguageProperty_ShouldReturnPDF() { // Arrange - var rawNode = RawSyntaxTrivia.Create(SyntaxKind.WhitespaceTrivia, " "u8); // Act - var language = rawNode.Language; + var language = RawSyntaxNode.Language; // Assert Assert.Equal("PDF", language); diff --git a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs index c469133..f323025 100644 --- a/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs +++ b/tests/OffDotNet.CodeAnalysis.Tests/Lexer/TextCursorTests.cs @@ -5,12 +5,14 @@ namespace OffDotNet.CodeAnalysis.Tests.Lexer; +using System.Diagnostics.CodeAnalysis; using Configs; using Microsoft.Extensions.Options; using OffDotNet.CodeAnalysis.Lexer; using OffDotNet.CodeAnalysis.Utils; [WorkItem("https://github.com/sunt-programator/off-dotnet/issues/335")] +[SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance", Justification = "Test purposes")] public class TextCursorTests { private readonly IOptions _options = Substitute.For>();