diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..860924a --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,122 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What This Is + +ProtoAttributor is a dual-platform developer tool with two independent extension projects that share a common purpose: automatically managing ProtoBuf (`[ProtoContract]`/`[ProtoMember]`/`[ProtoIgnore]`) and DataContract (`[DataContract]`/`[DataMember]`/`[IgnoreDataMember]`) serialization attributes on C# classes. Both extensions support Add, Reorder, and Remove operations. + +## VS Code Extension (`vscode/`) + +### Commands + +```bash +npm run compile # build (tsc) +npm run watch # watch mode +npm run lint # ESLint +npm run lint-fix # ESLint with auto-fix +npm run pretest # compile + lint +npm run test-jest # unit tests (Jest) +npm run test-jest-watch # unit tests in watch mode +npm run test-jest-coverage # unit tests with coverage (enforces 70% threshold) +npm test # VS Code integration tests +npm run vscode:package # produce .vsix +``` + +Run a single Jest test file: +```bash +npx jest src/path/to/file.test.ts +``` + +Jest config: `vscode/jest.config.js`. Test files match `**/src/**/*.test.+(ts|js)`, excluding `/src/test/` (fixture files) and `/Sample/`. Coverage threshold is 70% across all metrics; `extension.ts` is excluded from coverage. + +### Architecture + +The extension activates on `onLanguage:csharp` and registers three commands (`protoattributor.add`, `.reorder`, `.remove`). Each command prompts the user to choose between Proto or DataContract attribute families via QuickPick. + +**Processing pipeline:** + +1. `extension.ts` - Command registration; calls `getAllPublicMembers()` then dispatches per-member to handlers. +2. `src/utils/csharp-util.ts` - Regex-based C# text parsing. `getAllPublicMembers()` classifies lines as `Class`, `Enum`, `Method`, `LambaProperty`, or `FullProperty` (`SignatureType` enum). Also handles `using` directives and line-ending detection. +3. `src/proto-attributor-csharp.ts` - Core attribute manipulation: add/remove/reorder attributes, compute next index, write back via VS Code `WorkspaceEdit`. +4. `src/utils/constants.ts` - `Proto` and `Data` static classes with all attribute/using name strings. +5. `src/interfaces/window.interface.ts` - `IWindow` interface for testability. +6. `src/utils/workspace-util.ts` - Guards for workspace/editor state. + +**Important:** The VS Code extension uses **regex-based text manipulation**, not an AST. This differs fundamentally from the Visual Studio extension. + +## Visual Studio Extension (`visual-studio/`) + +### Commands + +```bash +# Restore +nuget restore visual-studio\ProtoAttributor.sln + +# Build (requires MSBuild / Visual Studio installed — dotnet CLI cannot build the VSIX project) +msbuild visual-studio\ProtoAttributor.sln /t:Rebuild /p:configuration="Release" /p:DeployExtension=false + +# Test (no MSBuild needed — Core project is SDK-style) +dotnet test -c="Release" --verbosity=normal visual-studio\ProtoAttributor.Tests\ProtoAttributor.Tests.csproj + +# Run a single test class +dotnet test visual-studio\ProtoAttributor.Tests\ProtoAttributor.Tests.csproj --filter "FullyQualifiedName~ClassName" +``` + +See `visual-studio/BuildNotes.md` for manual VS build notes and instructions for adding new VS version targets. + +### Project Structure + +The solution has three projects: + +| Project | Type | Purpose | +|---------|------|---------| +| `ProtoAttributor` | Legacy non-SDK VSIX (ToolsVersion="15.0") | VS extension package — Commands, Services, Executors, Package | +| `ProtoAttributor.Core` | SDK-style netstandard2.0 | Parsers + Constants only; no VS SDK deps; referenced by tests | +| `ProtoAttributor.Tests` | SDK-style net8.0 xUnit | References Core, not the VSIX project | + +**Why Core exists:** The VSIX project uses VS SDK APIs (`EnvDTE`, `Microsoft.VisualStudio.*`) that `dotnet CLI` cannot build. `ProtoAttributor.Core` compiles the pure-Roslyn parser/constant files via relative `` paths so tests can build and run without MSBuild. The VSIX project compiles those same files independently — no files were moved. + +### Architecture + +VSIX AsyncPackage targeting VS 2022 (min 17.0), .NET Framework 4.8 assembly. Uses **Roslyn (Microsoft.CodeAnalysis) AST rewriting**. + +**Layers:** + +- **Package** (`ProtoAttributorPackage.cs`) - Registers two async services and initializes 12 commands (6 Context menu + 6 Tools menu, split between Proto and DataAnno variants). +- **Commands** (`Commands/Context/`, `Commands/Menu/`) - Three command pairs per attribute family (Add, Renumber, Remove). Context commands operate on Solution Explorer selection; Menu commands operate on the open file. +- **Services** (`Services/`) - `ProtoAttributeService` and `DataAnnoAttributeService` via `IAttributeService`. Each wraps three parsers (Adder, Remover, Rewriter) and parses file content into a Roslyn `CSharpSyntaxTree`. Services hold a `Microsoft.VisualStudio.OLE.Interop.IServiceProvider` — this is why they stay in the VSIX project and not Core. +- **Parsers** (in Core) - Core Roslyn `CSharpSyntaxRewriter` subclasses: + - `Parsers/ProtoContracts/BaseProtoRewriter.cs` - Abstract base; handles class/enum declarations and `using` insertion, tracks `StartIndex`. Saves/restores `StartIndex` around nested class visits so inner class counters do not bleed into outer class. + - `ProtoAttributeAdder.cs` - Visits property/enum-member declarations to add missing attributes. Skips `static` and expression-bodied (`=>`) properties. + - `ProtoAttributeReader.cs` - Walks tree to find the highest existing index. Uses `_classDepth` guard to avoid counting properties inside nested classes. + - `ProtoAttributeRewriter.cs` - Renumbers existing attributes sequentially. + - `ProtoAttributeRemover.cs` - Removes all Proto* attributes and usings. + - Mirror classes under `Parsers/DataContracts/` for DataMember support. + - `Parsers/NodeHelper.cs` - Static helpers for Roslyn attribute matching and using-directive insertion. + - `Parsers/TriviaMaintainer.cs` - Preserves leading/trailing whitespace trivia during node mutation. +- **Executors** (`Executors/`) - `AttributeExecutor` iterates `SelectedItems` in Solution Explorer; `TextSelectionExecutor` applies changes to a `TextSelection`. + +### Test Project + +`visual-studio/ProtoAttributor.Tests/` - xUnit targeting .NET 8, assertions via Shouldly. `TestFixure` (note: intentional typo in existing code) provides `LoadTestFile(relativePath)` and `AssertOutputContainsCount(string[] source, string searchTerm, int numOfTimes)`. + +Bug-proving tests live in: +- `ProtoContracts/ProtoAttributeAdderBugTests.cs` +- `DataContracts/DataAttributeReaderBugTests.cs` + +## CI / CD (`/.github/workflows/`) + +**`node.js.yml`** — VS Code extension (ubuntu-latest): +- Runs Jest coverage (enforces 70% threshold) +- On push to main: sets version to `1.0.${{ github.run_number }}` via `npm version`, then publishes via `vsce` using `VSCE_PAT` secret + +**`dotnet.yml`** — VS extension (windows-latest): +- Sets VSIX manifest version to `1.2.${{ github.run_number }}` via `VsixVersionAction` +- MSBuild builds the VSIX; `dotnet test` runs tests (Core builds standalone — no `--no-build` needed) +- On push to main: publishes `.vsix` via `VsixPublisher.exe` (located via `vswhere`) using `VS_MARKETPLACE_PAT` secret +- Publish manifest: `visual-studio/publishManifest.json` + +## Key Symmetry + +Both extensions define the same attribute/using name constants in their respective `Constants.cs` / `constants.ts` files. When adding support for a new attribute family, update both. diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index d98657a..070bb45 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -24,6 +24,7 @@ env: SolutionPath: 'visual-studio\ProtoAttributor.sln' Version: '1.2.${{ github.run_number }}' BaseVersion: '1.2.0.0' + jobs: build-extension: runs-on: windows-latest @@ -31,23 +32,27 @@ jobs: matrix: VsTargetVersion: ['2022'] env: - VsixManifestPath: visual-studio\Manifests\VS${{ matrix.VsTargetVersion }}\source.extension.vsixmanifest - VsTargetVersion: 'VS${{ matrix.VsTargetVersion }}' + VsixManifestPath: visual-studio\Manifests\VS${{ matrix.VsTargetVersion }}\source.extension.vsixmanifest + VsTargetVersion: 'VS${{ matrix.VsTargetVersion }}' + VsixPath: visual-studio\ProtoAttributor\bin\VS${{ matrix.VsTargetVersion }}\Release\ProtoAttributor${{ matrix.VsTargetVersion }}.vsix + steps: - name: Checkout repository uses: actions/checkout@v4.1.1 with: - fetch-depth: 0 # Get all history to allow automatic versioning using MinVer + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.x' - name: Setup MSBuild.exe uses: microsoft/setup-msbuild@v2 - name: Setup NuGet.exe - # You may pin to the exact commit or the version. - # uses: NuGet/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f uses: NuGet/setup-nuget@v2.0.0 - - name: Set version for Visual Studio Extension uses: cezarypiatek/VsixVersionAction@1.2 with: @@ -57,7 +62,7 @@ jobs: - name: Update assembly version run: | (Get-Content -Path visual-studio\ProtoAttributor\Settings\VsixOptions.cs) | - ForEach-Object {$_ -Replace '${{ env.BaseVersion }}', '${{ env.Version }}'} | + ForEach-Object {$_ -Replace [regex]::Escape('${{ env.BaseVersion }}'), '${{ env.Version }}'} | Set-Content -Path visual-studio\ProtoAttributor\Settings\VsixOptions.cs - name: Restore NuGet Packages @@ -66,17 +71,12 @@ jobs: - name: Build extension run: msbuild $env:SolutionPath /t:Rebuild /p:configuration="Release" /p:DeployExtension=false /p:ZipPackageCompressionLevel=normal - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '8.x' - - name: Test extension - run: dotnet test --no-build -c="Release" --verbosity=normal $env:SolutionPath + run: dotnet test -c="Release" --verbosity=normal visual-studio\ProtoAttributor.Tests\ProtoAttributor.Tests.csproj - name: Upload VSIX artifact - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.1 with: name: ProtoAttributor-${{ matrix.VsTargetVersion }}.vsix - path: 'visual-studio\ProtoAttributor\bin\VS${{ matrix.VsTargetVersion }}\Release\ProtoAttributor${{ matrix.VsTargetVersion }}.vsix' + path: ${{ env.VsixPath }} diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 271bfca..e5f565b 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -1,6 +1,3 @@ -# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs - name: ProtoAttributor CI on: @@ -21,24 +18,25 @@ on: jobs: build: - runs-on: ubuntu-latest defaults: run: - working-directory: 'vscode' + working-directory: 'vscode' strategy: matrix: - node-version: [18.x] + node-version: [20.x] steps: - uses: actions/checkout@v4.1.1 + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.0.2 with: node-version: ${{ matrix.node-version }} cache: 'npm' cache-dependency-path: '**/package-lock.json' + - run: npm ci - run: npm run pretest --if-present - run: npm run test-jest-coverage diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7f08d22 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,118 @@ +name: Release to Marketplace + +on: + release: + types: [published] + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + SolutionPath: 'visual-studio\ProtoAttributor.sln' + +jobs: + publish-vscode: + runs-on: ubuntu-latest + defaults: + run: + working-directory: 'vscode' + + steps: + - uses: actions/checkout@v4.1.1 + + - name: Use Node.js 20.x + uses: actions/setup-node@v4.0.2 + with: + node-version: '20.x' + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + + - run: npm ci + + - name: Lint and compile + run: npm run pretest + + - name: Run tests + run: npm run test-jest + + - name: Stamp version from release tag + run: | + TAG="${{ github.event.release.tag_name }}" + VERSION="${TAG#v}" + npm pkg set "version=$VERSION" + echo "Stamped version: $VERSION" + + - name: Publish to VS Code Marketplace + run: npx @vscode/vsce publish -p ${{ secrets.VSCE_PAT }} + + publish-vs: + runs-on: windows-latest + strategy: + matrix: + VsTargetVersion: ['2022'] + env: + VsixManifestPath: visual-studio\Manifests\VS${{ matrix.VsTargetVersion }}\source.extension.vsixmanifest + VsTargetVersion: 'VS${{ matrix.VsTargetVersion }}' + VsixPath: visual-studio\ProtoAttributor\bin\VS${{ matrix.VsTargetVersion }}\Release\ProtoAttributor${{ matrix.VsTargetVersion }}.vsix + BaseVersion: '1.2.0.0' + + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 + + - name: Set version from release tag + shell: pwsh + run: | + $tag = "${{ github.event.release.tag_name }}" -replace '^v', '' + echo "Version=$tag.0" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.x' + + - name: Setup MSBuild.exe + uses: microsoft/setup-msbuild@v2 + + - name: Setup NuGet.exe + uses: NuGet/setup-nuget@v2.0.0 + + - name: Set version for Visual Studio Extension + uses: cezarypiatek/VsixVersionAction@1.2 + with: + version: ${{ env.Version }} + vsix-manifest-file: ${{ env.VsixManifestPath }} + + - name: Update assembly version + run: | + (Get-Content -Path visual-studio\ProtoAttributor\Settings\VsixOptions.cs) | + ForEach-Object {$_ -Replace [regex]::Escape('${{ env.BaseVersion }}'), '${{ env.Version }}'} | + Set-Content -Path visual-studio\ProtoAttributor\Settings\VsixOptions.cs + + - name: Restore NuGet Packages + run: nuget restore $env:SolutionPath + + - name: Build extension + run: msbuild $env:SolutionPath /t:Rebuild /p:configuration="Release" /p:DeployExtension=false /p:ZipPackageCompressionLevel=normal + + - name: Test extension + run: dotnet test -c="Release" --verbosity=normal visual-studio\ProtoAttributor.Tests\ProtoAttributor.Tests.csproj + + - name: Find VsixPublisher + id: find-publisher + shell: pwsh + run: | + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $vsPath = & $vswhere -latest -products * -property installationPath + $publisherPath = Join-Path $vsPath "VSSDK\VisualStudioIntegration\Tools\Bin\VsixPublisher.exe" + if (-not (Test-Path $publisherPath)) { throw "VsixPublisher.exe not found at $publisherPath" } + echo "VSIX_PUBLISHER=$publisherPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Publish VS extension to Marketplace + shell: pwsh + run: | + & "$env:VSIX_PUBLISHER" publish ` + -payload "${{ env.VsixPath }}" ` + -publishManifest "visual-studio\publishManifest.json" ` + -personalAccessToken "${{ secrets.VS_MARKETPLACE_PAT }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45ac3f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bash.exe.stackdump diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7becf85 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.preferCSharpExtension": true +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..974ff56 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,93 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What This Is + +ProtoAttributor is a dual-platform developer tool with two independent extension projects that share a common purpose: automatically managing ProtoBuf (`[ProtoContract]`/`[ProtoMember]`/`[ProtoIgnore]`) and DataContract (`[DataContract]`/`[DataMember]`/`[IgnoreDataMember]`) serialization attributes on C# classes. Both extensions support Add, Reorder, and Remove operations. + +## VS Code Extension (`vscode/`) + +### Commands + +```bash +npm run compile # build (tsc) +npm run watch # watch mode +npm run lint # ESLint +npm run lint-fix # ESLint with auto-fix +npm run pretest # compile + lint +npm run test-jest # unit tests (Jest) +npm run test-jest-watch # unit tests in watch mode +npm run test-jest-coverage # unit tests with coverage +npm test # VS Code integration tests +npm run vscode:package # produce .vsix +``` + +Run a single Jest test file: +```bash +npx jest src/path/to/file.test.ts +``` + +Jest config: `vscode/jest.config.js`. Test files match `**/src/**/*.test.+(ts|js)`, excluding `/src/test/` (fixture files) and `/Sample/`. + +### Architecture + +The extension activates on `onLanguage:csharp` and registers three commands (`protoattributor.add`, `.reorder`, `.remove`). Each command prompts the user to choose between Proto or DataContract attribute families via QuickPick. + +**Processing pipeline:** + +1. `extension.ts` - Command registration; calls `getAllPublicMembers()` then dispatches per-member to handlers. +2. `src/utils/csharp-util.ts` - Regex-based C# text parsing. `getAllPublicMembers()` classifies lines as `Class`, `Enum`, `Method`, `LambaProperty`, or `FullProperty` (`SignatureType` enum). Also handles `using` directives and line-ending detection. +3. `src/proto-attributor-csharp.ts` - Core attribute manipulation: add/remove/reorder attributes, compute next index, write back via VS Code `WorkspaceEdit`. +4. `src/utils/constants.ts` - `Proto` and `Data` static classes with all attribute/using name strings. +5. `src/interfaces/window.interface.ts` - `IWindow` interface for testability. +6. `src/utils/workspace-util.ts` - Guards for workspace/editor state. + +**Important:** The VS Code extension uses **regex-based text manipulation**, not an AST. This differs fundamentally from the Visual Studio extension. + +## Visual Studio Extension (`visual-studio/`) + +### Commands + +```bash +# Restore +nuget restore visual-studio\ProtoAttributor.sln + +# Build +msbuild visual-studio\ProtoAttributor.sln /t:Rebuild /p:configuration="Release" /p:DeployExtension=false + +# Test +dotnet test -c="Release" --verbosity=normal visual-studio\ProtoAttributor.sln + +# Run a single test class +dotnet test visual-studio\ProtoAttributor.sln --filter "FullyQualifiedName~ClassName" +``` + +See `visual-studio/BuildNotes.md` for manual VS build notes and instructions for adding new VS version targets. + +### Architecture + +VSIX AsyncPackage targeting VS 2022 (min 17.0), .NET Framework 4.8 assembly. Uses **Roslyn (Microsoft.CodeAnalysis) AST rewriting** - a proper syntax tree approach. + +**Layers:** + +- **Package** (`ProtoAttributorPackage.cs`) - Registers two async services and initializes 12 commands (6 Context menu + 6 Tools menu, split between Proto and DataAnno variants). +- **Commands** (`Commands/Context/`, `Commands/Menu/`) - Three command pairs per attribute family (Add, Renumber, Remove). Context commands operate on Solution Explorer selection; Menu commands operate on the open file. +- **Services** (`Services/`) - `ProtoAttributeService` and `DataAnnoAttributeService` via `IAttributeService`. Each wraps three parsers (Adder, Remover, Rewriter) and parses file content into a Roslyn `CSharpSyntaxTree`. +- **Parsers** - Core Roslyn `CSharpSyntaxRewriter` subclasses: + - `Parsers/ProtoContracts/BaseProtoRewriter.cs` - Abstract base; handles class/enum declarations and `using` insertion, tracks `StartIndex`. + - `ProtoAttributeAdder.cs` - Visits property/enum-member declarations to add missing attributes. + - `ProtoAttributeRewriter.cs` - Renumbers existing attributes sequentially. + - `ProtoAttributeRemover.cs` - Removes all Proto* attributes and usings. + - Mirror classes under `Parsers/DataContracts/` for DataMember support. + - `Parsers/NodeHelper.cs` - Static helpers for Roslyn attribute matching and using-directive insertion. + - `Parsers/TriviaMaintainer.cs` - Preserves leading/trailing whitespace trivia during node mutation. +- **Executors** (`Executors/`) - `AttributeExecutor` iterates `SelectedItems` in Solution Explorer (supports recursive folder traversal) with a progress dialog; `TextSelectionExecutor` applies changes to a `TextSelection`. + +### Test Project + +`visual-studio/ProtoAttributor.Tests/` - xUnit targeting .NET 8, assertions via Shouldly, coverage via coverlet. Uses protobuf-net in test fixtures. + +## Key Symmetry + +Both extensions define the same attribute/using name constants in their respective `Constants.cs` / `constants.ts` files. When adding support for a new attribute family, update both. diff --git a/visual-studio/Manifests/vs2022/source.extension.vsixmanifest b/visual-studio/Manifests/vs2022/source.extension.vsixmanifest index 87e5a78..cfbe710 100644 --- a/visual-studio/Manifests/vs2022/source.extension.vsixmanifest +++ b/visual-studio/Manifests/vs2022/source.extension.vsixmanifest @@ -11,13 +11,13 @@ ProtoBuf, ProtoBuf-Net, Code, Build, ReFormatting, Organizing, Attributing, Contracts, Serialization - + amd64 - + amd64 - + amd64 @@ -25,7 +25,7 @@ - + diff --git a/visual-studio/ProtoAttributor.Core/ProtoAttributor.Core.csproj b/visual-studio/ProtoAttributor.Core/ProtoAttributor.Core.csproj new file mode 100644 index 0000000..d62222e --- /dev/null +++ b/visual-studio/ProtoAttributor.Core/ProtoAttributor.Core.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + diff --git a/visual-studio/ProtoAttributor.Tests/DataContracts/DataAttributeReaderBugTests.cs b/visual-studio/ProtoAttributor.Tests/DataContracts/DataAttributeReaderBugTests.cs new file mode 100644 index 0000000..d339930 --- /dev/null +++ b/visual-studio/ProtoAttributor.Tests/DataContracts/DataAttributeReaderBugTests.cs @@ -0,0 +1,88 @@ +using Shouldly; +using Microsoft.CodeAnalysis.CSharp; +using ProtoAttributor.Parsers.DataContracts; +using Xunit; + +namespace ProtoAttributor.Tests.DataContracts +{ + public class DataAttributeReaderBugTests + { + // Bug: DataAttributeReader.VisitPropertyDeclaration calls + // item.ArgumentList.Arguments.FirstOrDefault(f => f.NameEquals.Name...) + // without null-guarding ArgumentList. A bare [DataMember] (no parentheses) + // has ArgumentList == null, causing NullReferenceException. + [Fact] + public void Add_DoesNotThrow_WhenExistingDataMemberHasNoArguments() + { + var code = @" +using System.Runtime.Serialization; +namespace Test +{ + [DataContract] + public class Foo + { + [DataMember] + public int Bar { get; set; } + public int Baz { get; set; } + } +}"; + var tree = CSharpSyntaxTree.ParseText(code); + var rewriter = new DataAttributeAdder(); + + var exception = Record.Exception(() => rewriter.Visit(tree.GetRoot())); + + exception.ShouldBeNull(); + } + + // Bug: DataAttributeReader's predicate f => f.NameEquals.Name.Identifier.ValueText.Equals("Order") + // dereferences NameEquals without a null check. A positional argument like [DataMember(1)] + // has NameEquals == null, causing NullReferenceException when the predicate is evaluated. + [Fact] + public void Add_DoesNotThrow_WhenExistingDataMemberHasPositionalArgument() + { + var code = @" +using System.Runtime.Serialization; +namespace Test +{ + [DataContract] + public class Foo + { + [DataMember(1)] + public int Bar { get; set; } + public int Baz { get; set; } + } +}"; + var tree = CSharpSyntaxTree.ParseText(code); + var rewriter = new DataAttributeAdder(); + + var exception = Record.Exception(() => rewriter.Visit(tree.GetRoot())); + + exception.ShouldBeNull(); + } + + // Bug: DataAttributeAdder.VisitPropertyDeclaration does not filter static properties. + // Static properties cannot be serialized by DataContractSerializer, + // but they receive [DataMember] anyway. + [Fact] + public void Add_DoesNotAddAttribute_ToStaticProperty() + { + var code = @" +namespace Test +{ + public class Foo + { + public static string Version { get; } = ""1.0""; + public int RegularProp { get; set; } + } +}"; + var tree = CSharpSyntaxTree.ParseText(code); + var rewriter = new DataAttributeAdder(); + var rewrittenRoot = rewriter.Visit(tree.GetRoot()); + var output = rewrittenRoot.GetText().ToString(); + var source = output.Split(new string[] { " ", "\r\n" }, System.StringSplitOptions.RemoveEmptyEntries); + + // Only RegularProp should be attributed. Bug: Version also gets [DataMember]. + new TestFixure().AssertOutputContainsCount(source, "DataMember", 1); + } + } +} diff --git a/visual-studio/ProtoAttributor.Tests/ProtoAttributor.Tests.csproj b/visual-studio/ProtoAttributor.Tests/ProtoAttributor.Tests.csproj index db2009c..522efd5 100644 --- a/visual-studio/ProtoAttributor.Tests/ProtoAttributor.Tests.csproj +++ b/visual-studio/ProtoAttributor.Tests/ProtoAttributor.Tests.csproj @@ -22,7 +22,7 @@ - + diff --git a/visual-studio/ProtoAttributor.Tests/ProtoContracts/ProtoAttributeAdderBugTests.cs b/visual-studio/ProtoAttributor.Tests/ProtoContracts/ProtoAttributeAdderBugTests.cs new file mode 100644 index 0000000..aa81b72 --- /dev/null +++ b/visual-studio/ProtoAttributor.Tests/ProtoContracts/ProtoAttributeAdderBugTests.cs @@ -0,0 +1,183 @@ +using Shouldly; +using Microsoft.CodeAnalysis.CSharp; +using ProtoAttributor.Parsers.ProtoContracts; +using Xunit; + +namespace ProtoAttributor.Tests.ProtoContracts +{ + public class ProtoAttributeAdderBugTests + { + // Bug: ProtoAttributeReader.VisitPropertyDeclaration dereferences + // item.ArgumentList.Arguments.FirstOrDefault() without null-guarding ArgumentList. + // A bare [ProtoMember] (no parentheses) has ArgumentList == null, causing NullReferenceException. + [Fact] + public void Add_DoesNotThrow_WhenExistingProtoMemberHasNoArguments() + { + var code = @" +using ProtoBuf; +namespace Test +{ + [ProtoContract] + public class Foo + { + [ProtoMember] + public int Bar { get; set; } + public int Baz { get; set; } + } +}"; + var tree = CSharpSyntaxTree.ParseText(code); + var rewriter = new ProtoAttributeAdder(); + + var exception = Record.Exception(() => rewriter.Visit(tree.GetRoot())); + + exception.ShouldBeNull(); + } + + // Bug: ProtoAttributeReader.VisitPropertyDeclaration calls value.GetText() + // where value is the result of FirstOrDefault() on an empty argument list. + // [ProtoMember()] has empty ArgumentList.Arguments, so FirstOrDefault returns null -> NRE. + [Fact] + public void Add_DoesNotThrow_WhenExistingProtoMemberHasEmptyArguments() + { + var code = @" +using ProtoBuf; +namespace Test +{ + [ProtoContract] + public class Foo + { + [ProtoMember()] + public int Bar { get; set; } + public int Baz { get; set; } + } +}"; + var tree = CSharpSyntaxTree.ParseText(code); + var rewriter = new ProtoAttributeAdder(); + + var exception = Record.Exception(() => rewriter.Visit(tree.GetRoot())); + + exception.ShouldBeNull(); + } + + // Bug: BaseProtoRewriter.VisitClassDeclaration resets StartIndex for every class it visits, + // including nested ones. After the inner class is processed, StartIndex is left at the inner + // class's final value. The outer class's remaining properties continue from that value + // instead of the outer class's own next index. + // + // With 2 inner properties (X, Y), StartIndex after inner = 3. + // Outer's PropB incorrectly gets [ProtoMember(3)] instead of [ProtoMember(2)]. + [Fact] + public void Add_AssignsCorrectIndices_WhenClassHasNestedClass() + { + var code = @" +namespace Test +{ + public class Outer + { + public int PropA { get; set; } + public class Inner + { + public int PropX { get; set; } + public int PropY { get; set; } + } + public int PropB { get; set; } + } +}"; + var tree = CSharpSyntaxTree.ParseText(code); + var rewriter = new ProtoAttributeAdder(); + var rewrittenRoot = rewriter.Visit(tree.GetRoot()); + var output = rewrittenRoot.GetText().ToString(); + + // Outer class: PropA=1, PropB=2. Inner class: PropX=1, PropY=2. + // Bug: PropB gets [ProtoMember(3)] because inner's counter bleeds into outer. + output.ShouldNotContain("[ProtoMember(3)]"); + } + + // Bug: ProtoAttributeReader walks the entire syntax subtree including nested classes + // when computing the next available index for a class. Existing [ProtoMember] attributes + // in nested classes inflate the computed max and cause the outer class to start too high. + // + // Inner has [ProtoMember(5)]. Reader finds it when scanning Outer, returns 6. + // Outer's unattributed PropA incorrectly gets [ProtoMember(6)] instead of [ProtoMember(1)]. + [Fact] + public void Add_DoesNotInflateIndex_FromNestedClassAttributes() + { + var code = @" +using ProtoBuf; +namespace Test +{ + public class Outer + { + public int PropA { get; set; } + public class Inner + { + [ProtoMember(5)] + public int PropX { get; set; } + } + public int PropB { get; set; } + } +}"; + var tree = CSharpSyntaxTree.ParseText(code); + var rewriter = new ProtoAttributeAdder(); + var rewrittenRoot = rewriter.Visit(tree.GetRoot()); + var output = rewrittenRoot.GetText().ToString(); + + // Outer's PropA and PropB have no existing attributes so should start at 1. + // Bug: reader sees Inner's [ProtoMember(5)] and starts Outer at 6. + output.ShouldContain("[ProtoMember(1)]"); + output.ShouldNotContain("[ProtoMember(6)]"); + } + + // Bug: ProtoAttributeAdder.VisitPropertyDeclaration does not filter static properties. + // Static properties cannot be serialized by protobuf-net (no instance to read/write), + // but they receive [ProtoMember] anyway. + [Fact] + public void Add_DoesNotAddAttribute_ToStaticProperty() + { + var code = @" +namespace Test +{ + public class Foo + { + public static string Version { get; } = ""1.0""; + public int RegularProp { get; set; } + } +}"; + var tree = CSharpSyntaxTree.ParseText(code); + var rewriter = new ProtoAttributeAdder(); + var rewrittenRoot = rewriter.Visit(tree.GetRoot()); + var output = rewrittenRoot.GetText().ToString(); + var source = output.Split(new string[] { " ", "\r\n" }, System.StringSplitOptions.RemoveEmptyEntries); + + // Only RegularProp should be attributed. Bug: Version also gets [ProtoMember]. + new TestFixure().AssertOutputContainsCount(source, "ProtoMember", 1); + } + + // Bug: ProtoAttributeAdder.VisitPropertyDeclaration does not filter expression-bodied + // (lambda/computed) properties. These have no setter and protobuf-net cannot deserialize them, + // but they receive [ProtoMember] anyway. + [Fact] + public void Add_DoesNotAddAttribute_ToExpressionBodiedProperty() + { + var code = @" +namespace Test +{ + public class Foo + { + public string FirstName { get; set; } + public string LastName { get; set; } + public string FullName => FirstName + "" "" + LastName; + } +}"; + var tree = CSharpSyntaxTree.ParseText(code); + var rewriter = new ProtoAttributeAdder(); + var rewrittenRoot = rewriter.Visit(tree.GetRoot()); + var output = rewrittenRoot.GetText().ToString(); + var source = output.Split(new string[] { " ", "\r\n" }, System.StringSplitOptions.RemoveEmptyEntries); + + // Only FirstName and LastName should be attributed (2 total). + // Bug: FullName also gets [ProtoMember], producing 3. + new TestFixure().AssertOutputContainsCount(source, "ProtoMember", 2); + } + } +} diff --git a/visual-studio/ProtoAttributor.sln b/visual-studio/ProtoAttributor.sln index 822b433..9dd98fa 100644 --- a/visual-studio/ProtoAttributor.sln +++ b/visual-studio/ProtoAttributor.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.0.11205.157 d18.0 +VisualStudioVersion = 18.0.11205.157 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProtoAttributor", "ProtoAttributor\ProtoAttributor.csproj", "{D166A1D7-30C3-4789-8970-FB3CF7DC9097}" EndProject @@ -30,20 +30,54 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ ..\.github\workflows\dotnet.yml = ..\.github\workflows\dotnet.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProtoAttributor.Core", "ProtoAttributor.Core\ProtoAttributor.Core.csproj", "{B221813C-A130-45DD-B28D-4D1949627F94}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {D166A1D7-30C3-4789-8970-FB3CF7DC9097}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D166A1D7-30C3-4789-8970-FB3CF7DC9097}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D166A1D7-30C3-4789-8970-FB3CF7DC9097}.Debug|x64.ActiveCfg = Debug|Any CPU + {D166A1D7-30C3-4789-8970-FB3CF7DC9097}.Debug|x64.Build.0 = Debug|Any CPU + {D166A1D7-30C3-4789-8970-FB3CF7DC9097}.Debug|x86.ActiveCfg = Debug|Any CPU + {D166A1D7-30C3-4789-8970-FB3CF7DC9097}.Debug|x86.Build.0 = Debug|Any CPU {D166A1D7-30C3-4789-8970-FB3CF7DC9097}.Release|Any CPU.ActiveCfg = Release|Any CPU {D166A1D7-30C3-4789-8970-FB3CF7DC9097}.Release|Any CPU.Build.0 = Release|Any CPU + {D166A1D7-30C3-4789-8970-FB3CF7DC9097}.Release|x64.ActiveCfg = Release|Any CPU + {D166A1D7-30C3-4789-8970-FB3CF7DC9097}.Release|x64.Build.0 = Release|Any CPU + {D166A1D7-30C3-4789-8970-FB3CF7DC9097}.Release|x86.ActiveCfg = Release|Any CPU + {D166A1D7-30C3-4789-8970-FB3CF7DC9097}.Release|x86.Build.0 = Release|Any CPU {EF3B9FF1-5273-4A23-83CE-BA15AA9B1FA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EF3B9FF1-5273-4A23-83CE-BA15AA9B1FA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF3B9FF1-5273-4A23-83CE-BA15AA9B1FA3}.Debug|x64.ActiveCfg = Debug|Any CPU + {EF3B9FF1-5273-4A23-83CE-BA15AA9B1FA3}.Debug|x64.Build.0 = Debug|Any CPU + {EF3B9FF1-5273-4A23-83CE-BA15AA9B1FA3}.Debug|x86.ActiveCfg = Debug|Any CPU + {EF3B9FF1-5273-4A23-83CE-BA15AA9B1FA3}.Debug|x86.Build.0 = Debug|Any CPU {EF3B9FF1-5273-4A23-83CE-BA15AA9B1FA3}.Release|Any CPU.ActiveCfg = Release|Any CPU {EF3B9FF1-5273-4A23-83CE-BA15AA9B1FA3}.Release|Any CPU.Build.0 = Release|Any CPU + {EF3B9FF1-5273-4A23-83CE-BA15AA9B1FA3}.Release|x64.ActiveCfg = Release|Any CPU + {EF3B9FF1-5273-4A23-83CE-BA15AA9B1FA3}.Release|x64.Build.0 = Release|Any CPU + {EF3B9FF1-5273-4A23-83CE-BA15AA9B1FA3}.Release|x86.ActiveCfg = Release|Any CPU + {EF3B9FF1-5273-4A23-83CE-BA15AA9B1FA3}.Release|x86.Build.0 = Release|Any CPU + {B221813C-A130-45DD-B28D-4D1949627F94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B221813C-A130-45DD-B28D-4D1949627F94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B221813C-A130-45DD-B28D-4D1949627F94}.Debug|x64.ActiveCfg = Debug|Any CPU + {B221813C-A130-45DD-B28D-4D1949627F94}.Debug|x64.Build.0 = Debug|Any CPU + {B221813C-A130-45DD-B28D-4D1949627F94}.Debug|x86.ActiveCfg = Debug|Any CPU + {B221813C-A130-45DD-B28D-4D1949627F94}.Debug|x86.Build.0 = Debug|Any CPU + {B221813C-A130-45DD-B28D-4D1949627F94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B221813C-A130-45DD-B28D-4D1949627F94}.Release|Any CPU.Build.0 = Release|Any CPU + {B221813C-A130-45DD-B28D-4D1949627F94}.Release|x64.ActiveCfg = Release|Any CPU + {B221813C-A130-45DD-B28D-4D1949627F94}.Release|x64.Build.0 = Release|Any CPU + {B221813C-A130-45DD-B28D-4D1949627F94}.Release|x86.ActiveCfg = Release|Any CPU + {B221813C-A130-45DD-B28D-4D1949627F94}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/visual-studio/ProtoAttributor/Parsers/DataContracts/BaseDataRewriter.cs b/visual-studio/ProtoAttributor/Parsers/DataContracts/BaseDataRewriter.cs index 1278aba..18a2a91 100644 --- a/visual-studio/ProtoAttributor/Parsers/DataContracts/BaseDataRewriter.cs +++ b/visual-studio/ProtoAttributor/Parsers/DataContracts/BaseDataRewriter.cs @@ -64,7 +64,7 @@ public override SyntaxNode VisitEnumDeclaration(EnumDeclarationSyntax node) public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) { - //each class needs to restat with the + var savedIndex = StartIndex; StartIndex = CalculateStartingIndex(node); var hasMatch = NodeHelper.HasMatch(node.AttributeLists, Constants.Data.CLASS_ATTRIBUTE_NAME); @@ -81,7 +81,9 @@ public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) }); } - return base.VisitClassDeclaration(node); + var result = base.VisitClassDeclaration(node); + StartIndex = savedIndex; + return result; } } } diff --git a/visual-studio/ProtoAttributor/Parsers/DataContracts/DataAttributeAdder.cs b/visual-studio/ProtoAttributor/Parsers/DataContracts/DataAttributeAdder.cs index 6150a3e..99b801f 100644 --- a/visual-studio/ProtoAttributor/Parsers/DataContracts/DataAttributeAdder.cs +++ b/visual-studio/ProtoAttributor/Parsers/DataContracts/DataAttributeAdder.cs @@ -1,3 +1,4 @@ +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -40,6 +41,11 @@ public override SyntaxNode VisitEnumMemberDeclaration(EnumMemberDeclarationSynta public override SyntaxNode VisitPropertyDeclaration(PropertyDeclarationSyntax node) { + if (node.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)) || node.ExpressionBody != null) + { + return base.VisitPropertyDeclaration(node); + } + var hasMatch = NodeHelper.HasMatch(node.AttributeLists, Constants.Data.PROPERTY_ATTRIBUTE_NAME, Constants.Data.PROPERTY_IGNORE_ATTRIBUTE_NAME); if (!hasMatch) diff --git a/visual-studio/ProtoAttributor/Parsers/DataContracts/DataAttributeReader.cs b/visual-studio/ProtoAttributor/Parsers/DataContracts/DataAttributeReader.cs index 79d73c9..44cafde 100644 --- a/visual-studio/ProtoAttributor/Parsers/DataContracts/DataAttributeReader.cs +++ b/visual-studio/ProtoAttributor/Parsers/DataContracts/DataAttributeReader.cs @@ -38,7 +38,7 @@ public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) foreach (var item in attrs) { - var argument = item.ArgumentList.Arguments.FirstOrDefault(f=>f.NameEquals.Name.Identifier.ValueText.Equals("Order")); + var argument = item.ArgumentList?.Arguments.FirstOrDefault(f => f.NameEquals?.Name.Identifier.ValueText.Equals("Order") == true); if(argument != null && argument.Expression.Kind() == SyntaxKind.NumericLiteralExpression) { var tokenValue = argument.Expression.ChildTokens().FirstOrDefault(f => f.IsKind(SyntaxKind.NumericLiteralToken)); diff --git a/visual-studio/ProtoAttributor/Parsers/ProtoContracts/BaseProtoRewriter.cs b/visual-studio/ProtoAttributor/Parsers/ProtoContracts/BaseProtoRewriter.cs index b7d4e81..09aae46 100644 --- a/visual-studio/ProtoAttributor/Parsers/ProtoContracts/BaseProtoRewriter.cs +++ b/visual-studio/ProtoAttributor/Parsers/ProtoContracts/BaseProtoRewriter.cs @@ -64,7 +64,7 @@ public override SyntaxNode VisitEnumDeclaration(EnumDeclarationSyntax node) public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) { - //each class needs to restat with the + var savedIndex = StartIndex; StartIndex = CalculateStartingIndex(node); var hasMatch = NodeHelper.HasMatch(node.AttributeLists, Constants.Proto.CLASS_ATTRIBUTE_NAME); @@ -81,7 +81,9 @@ public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) }); } - return base.VisitClassDeclaration(node); + var result = base.VisitClassDeclaration(node); + StartIndex = savedIndex; + return result; } } } diff --git a/visual-studio/ProtoAttributor/Parsers/ProtoContracts/ProtoAttributeAdder.cs b/visual-studio/ProtoAttributor/Parsers/ProtoContracts/ProtoAttributeAdder.cs index 80437b1..dbfe077 100644 --- a/visual-studio/ProtoAttributor/Parsers/ProtoContracts/ProtoAttributeAdder.cs +++ b/visual-studio/ProtoAttributor/Parsers/ProtoContracts/ProtoAttributeAdder.cs @@ -1,3 +1,4 @@ +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -41,6 +42,11 @@ public override SyntaxNode VisitEnumMemberDeclaration(EnumMemberDeclarationSynta public override SyntaxNode VisitPropertyDeclaration(PropertyDeclarationSyntax node) { + if (node.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)) || node.ExpressionBody != null) + { + return base.VisitPropertyDeclaration(node); + } + var hasMatch = NodeHelper.HasMatch(node.AttributeLists, Constants.Proto.PROPERTY_ATTRIBUTE_NAME, Constants.Proto.PROPERTY_IGNORE_ATTRIBUTE_NAME); if (!hasMatch) diff --git a/visual-studio/ProtoAttributor/Parsers/ProtoContracts/ProtoAttributeReader.cs b/visual-studio/ProtoAttributor/Parsers/ProtoContracts/ProtoAttributeReader.cs index efebfb1..2a3edd5 100644 --- a/visual-studio/ProtoAttributor/Parsers/ProtoContracts/ProtoAttributeReader.cs +++ b/visual-studio/ProtoAttributor/Parsers/ProtoContracts/ProtoAttributeReader.cs @@ -8,14 +8,27 @@ namespace ProtoAttributor.Parsers.ProtoContracts public class ProtoAttributeReader: CSharpSyntaxWalker { private int _highestOrder; + private int _classDepth; public int GetProtoNextId(SyntaxNode node) { _highestOrder = 0; + _classDepth = 0; base.Visit(node); return _highestOrder + 1; } + public override void VisitClassDeclaration(ClassDeclarationSyntax node) + { + _classDepth++; + if (_classDepth == 1) + { + base.VisitClassDeclaration(node); + } + + _classDepth--; + } + public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) { if (node.AttributeLists.Count > 0) @@ -33,7 +46,12 @@ public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) foreach (var item in attrs) { - var value = item.ArgumentList.Arguments.FirstOrDefault(); + var value = item.ArgumentList?.Arguments.FirstOrDefault(); + if (value == null) + { + continue; + } + int.TryParse(value.GetText().ToString(), out var order); if (order > _highestOrder) { diff --git a/visual-studio/ProtoAttributor/ProtoAttributor.csproj b/visual-studio/ProtoAttributor/ProtoAttributor.csproj index 72b768b..c93fdfa 100644 --- a/visual-studio/ProtoAttributor/ProtoAttributor.csproj +++ b/visual-studio/ProtoAttributor/ProtoAttributor.csproj @@ -40,10 +40,10 @@ 4.8.0 - + compile; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/visual-studio/ProtoAttributor/ProtoAttributorPackage.cs b/visual-studio/ProtoAttributor/ProtoAttributorPackage.cs index e943545..1222a6f 100644 --- a/visual-studio/ProtoAttributor/ProtoAttributorPackage.cs +++ b/visual-studio/ProtoAttributor/ProtoAttributorPackage.cs @@ -35,7 +35,7 @@ namespace ProtoAttributor public sealed class ProtoAttributorPackage: AsyncPackage { /// ProtoAttributorPackage GUID string. - + #region Package Members @@ -56,7 +56,10 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke var protoCallback = new AsyncServiceCreatorCallback(async (IAsyncServiceContainer container, CancellationToken ct, Type serviceType) => { if (typeof(IProtoAttributeService) == serviceType) + { return new ProtoAttributeService(this, new ProtoAttributeAdder(), new ProtoAttributeRemover(), new ProtoAttributeRewriter()); + } + return null; }); @@ -65,7 +68,10 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke var datAnnoCallback = new AsyncServiceCreatorCallback(async (IAsyncServiceContainer container, CancellationToken ct, Type serviceType) => { if (typeof(IDataAnnoAttributeService) == serviceType) + { return new DataAnnoAttributeService(this, new DataAttributeAdder(), new DataAttributeRemover(), new DataAttributeRewriter()); + } + return null; }); AddService(typeof(IDataAnnoAttributeService), datAnnoCallback, true); diff --git a/visual-studio/publishManifest.json b/visual-studio/publishManifest.json new file mode 100644 index 0000000..2cf391d --- /dev/null +++ b/visual-studio/publishManifest.json @@ -0,0 +1,20 @@ +{ + "categories": [ + "Tools", + "Coding", + "Documentation" + ], + "tags": [ + "Auto Document", + "Coding", + "Documentation", + "Performance", + "XML Comments" + ], + "priceCategory": "free", + "publisher": "DanTurco", + "private": false, + "qna": false, + "repo": "https://github.com/d1820/proto-attributor", + "issueTracker": "https://github.com/d1820/proto-attributor/issues" +} diff --git a/vscode/.vscodeignore b/vscode/.vscodeignore index 33348a1..73d6c0d 100644 --- a/vscode/.vscodeignore +++ b/vscode/.vscodeignore @@ -1,7 +1,9 @@ .vscode/** .vscode-test/** +.claude/** src/** .github/** +__mocks__/** .gitignore .yarnrc vsc-extension-quickstart.md @@ -12,3 +14,9 @@ vsc-extension-quickstart.md **/.vscode-test.* **/Sample/** **/GifInstruction/** +jest.config.js +out/**/*.test.js +out/test/** +out/utils/*.test.js +coverage/** +*.stackdump diff --git a/vscode/jest.config.js b/vscode/jest.config.js index 7682fdf..c1feb4b 100644 --- a/vscode/jest.config.js +++ b/vscode/jest.config.js @@ -19,4 +19,19 @@ module.exports = { '**/src/**/*.test.+(ts|js)', ], preset: 'ts-jest', + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/test/**', + '!src/**/*.test.ts', + '!src/extension.ts', + ], + coverageDirectory: 'coverage', + coverageThreshold: { + global: { + branches: 70, + functions: 70, + lines: 70, + statements: 70, + }, + }, } diff --git a/vscode/package-lock.json b/vscode/package-lock.json index c5ac3fa..080a565 100644 --- a/vscode/package-lock.json +++ b/vscode/package-lock.json @@ -1,26 +1,26 @@ { - "name": "protoattributor", - "version": "1.0.0", + "name": "protoattributor-vscode", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "protoattributor", - "version": "1.0.0", + "name": "protoattributor-vscode", + "version": "1.0.3", "devDependencies": { "@types/jest": "^29.5.12", - "@types/mocha": "^10.0.6", - "@types/node": "18.x", + "@types/mocha": "^10.0.10", + "@types/node": "26.x", "@types/vscode": "^1.85.0", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", - "@vscode/test-cli": "^0.0.4", + "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.3.9", "eslint": "^8.56.0", "jest": "^29.7.0", "jest-mock-vscode": "^2.1.1", - "ts-jest": "^29.1.2", - "typescript": "^5.3.3" + "ts-jest": "^29.4.11", + "typescript": "^6.0.3" }, "engines": { "vscode": "^1.85.0" @@ -863,9 +863,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "engines": { "node": ">=12" @@ -875,12 +875,12 @@ } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -1543,18 +1543,18 @@ "dev": true }, "node_modules/@types/mocha": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", - "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", "dev": true }, "node_modules/@types/node": { - "version": "18.19.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.6.tgz", - "integrity": "sha512-X36s5CXMrrJOs2lQCdDF68apW4Rfx9ixYMawlepwmE4Anezv/AV2LSpKD1Ub8DAc+urp5bk0BGZ6NtmBitfnsg==", + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", + "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", "dev": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~8.3.0" } }, "node_modules/@types/semver": { @@ -1570,9 +1570,9 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.86.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.86.0.tgz", - "integrity": "sha512-DnIXf2ftWv+9LWOB5OJeIeaLigLHF7fdXF6atfc7X5g2w/wVZBgk0amP7b+ub5xAuW1q7qP5YcFvOcit/DtyCQ==", + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", "dev": true }, "node_modules/@types/yargs": { @@ -1787,21 +1787,26 @@ "dev": true }, "node_modules/@vscode/test-cli": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.4.tgz", - "integrity": "sha512-Tx0tfbxeSb2Xlo+jpd+GJrNLgKQHobhRHrYvOipZRZQYWZ82sKiK02VY09UjU1Czc/YnZnqyAnjUfaVGl3h09w==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.12.tgz", + "integrity": "sha512-iYN0fDg29+a2Xelle/Y56Xvv7Nc8Thzq4VwpzAF/SIE6918rDicqfsQxV6w1ttr2+SOm+10laGuY9FG2ptEKsQ==", "dev": true, "dependencies": { - "@types/mocha": "^10.0.2", - "chokidar": "^3.5.3", + "@types/mocha": "^10.0.10", + "c8": "^10.1.3", + "chokidar": "^3.6.0", + "enhanced-resolve": "^5.18.3", "glob": "^10.3.10", "minimatch": "^9.0.3", - "mocha": "^10.2.0", - "supports-color": "^9.4.0", + "mocha": "^11.7.4", + "supports-color": "^10.2.2", "yargs": "^17.7.2" }, "bin": { "vscode-test": "out/bin.mjs" + }, + "engines": { + "node": ">=18" } }, "node_modules/@vscode/test-electron": { @@ -1868,15 +1873,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2079,18 +2075,21 @@ "dev": true }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "dependencies": { "balanced-match": "^1.0.0" @@ -2173,6 +2172,98 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, + "node_modules/c8/node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/c8/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/c8/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/c8/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/c8/node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2252,16 +2343,10 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2274,6 +2359,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -2438,12 +2526,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2505,9 +2593,9 @@ } }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true, "engines": { "node": ">=0.3.1" @@ -2567,6 +2655,19 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/enhanced-resolve": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz", + "integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3046,23 +3147,22 @@ } }, "node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -3079,6 +3179,21 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -3126,6 +3241,27 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3484,16 +3620,13 @@ } }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -4350,13 +4483,10 @@ } }, "node_modules/lru-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true }, "node_modules/make-dir": { "version": "4.0.0", @@ -4440,144 +4570,101 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "11.7.6", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.6.tgz", + "integrity": "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==", + "dev": true, + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", "mocha": "bin/mocha.js" }, "engines": { - "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/mocha/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/mocha/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/mocha/node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "readdirp": "^4.0.1" }, "engines": { - "node": "*" + "node": ">= 14.16.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "url": "https://paulmillr.com/funding/" } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/mocha/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/mocha/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, "engines": { - "node": ">=8" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/mocha/node_modules/supports-color": { @@ -4595,65 +4682,24 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/mocha/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4767,6 +4813,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -4837,16 +4889,16 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4862,9 +4914,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { @@ -5255,13 +5307,10 @@ "dev": true }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -5269,22 +5318,10 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -5451,9 +5488,9 @@ "dev": true }, "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "engines": { "node": ">=12" @@ -5463,12 +5500,12 @@ } }, "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -5533,12 +5570,12 @@ } }, "node_modules/supports-color": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", - "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", "dev": true, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/supports-color?sponsor=1" @@ -5556,6 +5593,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -5658,37 +5708,43 @@ } }, "node_modules/ts-jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", - "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "version": "29.4.11", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.11.tgz", + "integrity": "sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==", "dev": true, "dependencies": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", - "jest-util": "^29.0.0", + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "^7.5.3", - "yargs-parser": "^21.0.1" + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.0", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" }, "bin": { "ts-jest": "cli.js" }, "engines": { - "node": "^16.10.0 || ^18.0.0 || >=20.0.0" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", - "typescript": ">=4.3 <6" + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" }, "peerDependenciesMeta": { "@babel/core": { "optional": true }, + "@jest/transform": { + "optional": true + }, "@jest/types": { "optional": true }, @@ -5697,16 +5753,22 @@ }, "esbuild": { "optional": true + }, + "jest-util": { + "optional": true } } }, - "node_modules/ts-jest/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, "engines": { - "node": ">=12" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/type-check": { @@ -5743,9 +5805,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -5755,10 +5817,23 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true }, "node_modules/update-browserslist-db": { @@ -5850,10 +5925,16 @@ "node": ">= 8" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", "dev": true }, "node_modules/wrap-ansi": { @@ -5912,9 +5993,9 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "engines": { "node": ">=12" @@ -5924,9 +6005,9 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "engines": { "node": ">=12" @@ -5936,12 +6017,12 @@ } }, "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -5984,12 +6065,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -6009,12 +6084,12 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-unparser": { @@ -6052,15 +6127,6 @@ "node": ">=8" } }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/vscode/package.json b/vscode/package.json index fd919dc..8648059 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -62,17 +62,17 @@ }, "devDependencies": { "@types/jest": "^29.5.12", - "@types/mocha": "^10.0.6", - "@types/node": "18.x", + "@types/mocha": "^10.0.10", + "@types/node": "26.x", "@types/vscode": "^1.85.0", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", - "@vscode/test-cli": "^0.0.4", + "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.3.9", "eslint": "^8.56.0", "jest": "^29.7.0", "jest-mock-vscode": "^2.1.1", - "ts-jest": "^29.1.2", - "typescript": "^5.3.3" + "ts-jest": "^29.4.11", + "typescript": "^6.0.3" } } diff --git a/vscode/src/proto-attributor-csharp.test.ts b/vscode/src/proto-attributor-csharp.test.ts index 8627ff4..d8c3eb0 100644 --- a/vscode/src/proto-attributor-csharp.test.ts +++ b/vscode/src/proto-attributor-csharp.test.ts @@ -1,7 +1,22 @@ -import { getNextIndex, handlePropertyAttributeReorder, hasAttribute } from './proto-attributor-csharp'; -import { noProtoExisting, protoExisting, protoReorderExisting, protoReorderExistingExpected } from './test/proto-test-class'; +import { + getNextIndex, + handlePropertyAttributeReorder, + hasAttribute, + hasAttributeInLines, + getAllAttributes, + addAttributeToDocument, + addUsingsToDocument, + removeUsingsFromDocument, + removeClassAttributeFromDocument, + removePropertyAttributeFromDocument, + handleClassAttributes, + handleEnumAttributes, + handlePropertyAttributes, +} from './proto-attributor-csharp'; +import { noProtoExisting, protoExisting, protoReorderExisting, protoReorderExistingExpected, protoEnum } from './test/proto-test-class'; import { dataContractExisting, noDataContractExisting } from './test/data-test-class'; import { Data, Proto } from './utils/constants'; +import { SignatureLineResult, SignatureType } from './utils/csharp-util'; describe('Proto Attributor CSharp', () => { @@ -74,6 +89,277 @@ describe('Proto Attributor CSharp', () => // Assert expect(result).toEqual(protoReorderExistingExpected); }); + + it('should return text unchanged when no attributes exist', () => + { + const result = handlePropertyAttributeReorder(noProtoExisting, Proto.PROPERTY_ATTRIBUTE_NAME); + expect(result).toEqual(noProtoExisting); + }); + }); + + describe('hasAttributeInLines', () => + { + it('should return false when lines is null', () => + { + expect(hasAttributeInLines(null, 'ProtoMember')).toBe(false); + }); + + it('should return true when attribute found in lines', () => + { + expect(hasAttributeInLines(['[ProtoMember(1)]', 'other line'], 'ProtoMember')).toBe(true); + }); + + it('should return false when attribute not in lines', () => + { + expect(hasAttributeInLines(['[DataMember(Order=1)]', 'other line'], 'ProtoMember')).toBe(false); + }); + + it('should return false for empty array', () => + { + expect(hasAttributeInLines([], 'ProtoMember')).toBe(false); + }); + }); + + describe('getAllAttributes', () => + { + it('should return all matching attributes', () => + { + const text = '[ProtoMember(1)]\npublic int A { get; set; }\n[ProtoMember(2)]\npublic int B { get; set; }'; + const result = getAllAttributes(text, 'ProtoMember'); + expect(result).toHaveLength(2); + expect(result[0][1]).toBe('1'); + expect(result[1][1]).toBe('2'); + }); + + it('should return empty array when no attributes exist', () => + { + const result = getAllAttributes('public int A { get; set; }', 'ProtoMember'); + expect(result).toHaveLength(0); + }); + }); + + describe('addAttributeToDocument', () => + { + it('should insert attribute before signature with correct indentation', () => + { + const text = ' public int MyProp { get; set; }'; + const sig = new SignatureLineResult('public int MyProp { get; set; }', SignatureType.FullProperty, 0); + sig.defaultLineIndent = 4; + const result = addAttributeToDocument('\n', text, sig, '[ProtoMember(1)]'); + expect(result).toBe(' [ProtoMember(1)]\n public int MyProp { get; set; }'); + }); + + it('should return text unchanged when signature is null', () => + { + const text = 'some text'; + const sig = new SignatureLineResult(null, SignatureType.FullProperty, 0); + sig.defaultLineIndent = 0; + const result = addAttributeToDocument('\n', text, sig, '[ProtoMember(1)]'); + expect(result).toBe('some text'); + }); + + it('should work with CRLF line endings', () => + { + const text = ' public int MyProp { get; set; }'; + const sig = new SignatureLineResult('public int MyProp { get; set; }', SignatureType.FullProperty, 0); + sig.defaultLineIndent = 4; + const result = addAttributeToDocument('\r\n', text, sig, '[ProtoMember(1)]'); + expect(result).toBe(' [ProtoMember(1)]\r\n public int MyProp { get; set; }'); + }); + }); + + describe('addUsingsToDocument', () => + { + it('should add new using to document', () => + { + const text = 'using System;\n\npublic class Foo {}'; + const result = addUsingsToDocument('\n', text, ['using ProtoBuf;']); + expect(result).toContain('using ProtoBuf;'); + expect(result).toContain('using System;'); + }); + + it('should deduplicate when using already exists', () => + { + const text = 'using System;\nusing ProtoBuf;\n\npublic class Foo {}'; + const result = addUsingsToDocument('\n', text, ['using ProtoBuf;']); + const count = (result.match(/using ProtoBuf;/g) || []).length; + expect(count).toBe(1); + }); + + it('should return empty string unchanged when content is empty', () => + { + const result = addUsingsToDocument('\n', '', ['using ProtoBuf;']); + expect(result).toBe(''); + }); + }); + + describe('removeUsingsFromDocument', () => + { + it('should remove specified using statement', () => + { + const text = 'using System;\nusing ProtoBuf;\n\npublic class Foo {}'; + const result = removeUsingsFromDocument('\n', text, ['using ProtoBuf;']); + expect(result).not.toContain('using ProtoBuf;'); + expect(result).toContain('using System;'); + }); + + it('should remove multiple using statements', () => + { + const text = 'using System;\nusing ProtoBuf;\nusing System.Runtime.Serialization;\n\npublic class Foo {}'; + const result = removeUsingsFromDocument('\n', text, ['using ProtoBuf;', 'using System.Runtime.Serialization;']); + expect(result).not.toContain('using ProtoBuf;'); + expect(result).not.toContain('using System.Runtime.Serialization;'); + expect(result).toContain('using System;'); + }); + + it('should return empty string unchanged when content is empty', () => + { + const result = removeUsingsFromDocument('\n', '', ['using ProtoBuf;']); + expect(result).toBe(''); + }); + }); + + describe('removeClassAttributeFromDocument', () => + { + it('should remove class-level attribute', () => + { + const text = '[ProtoContract]\npublic class Foo {}'; + const result = removeClassAttributeFromDocument('\n', text, 'ProtoContract'); + expect(result).not.toContain('[ProtoContract]'); + expect(result).toContain('public class Foo {}'); + }); + + it('should remove indented class-level attribute', () => + { + const text = ' [ProtoContract]\n public class Foo {}'; + const result = removeClassAttributeFromDocument('\n', text, 'ProtoContract'); + expect(result).not.toContain('[ProtoContract]'); + }); + + it('should return empty string unchanged when content is empty', () => + { + const result = removeClassAttributeFromDocument('\n', '', 'ProtoContract'); + expect(result).toBe(''); + }); + }); + + describe('removePropertyAttributeFromDocument', () => + { + it('should remove property attribute with arguments', () => + { + const text = ' [ProtoMember(1)]\n public int MyProp { get; set; }'; + const result = removePropertyAttributeFromDocument('\n', text, 'ProtoMember'); + expect(result).not.toContain('[ProtoMember(1)]'); + expect(result).toContain('public int MyProp { get; set; }'); + }); + + it('should remove all matching property attributes', () => + { + const text = ' [ProtoMember(1)]\n public int A { get; set; }\n [ProtoMember(2)]\n public int B { get; set; }'; + const result = removePropertyAttributeFromDocument('\n', text, 'ProtoMember'); + expect(result).not.toContain('[ProtoMember'); + }); + + it('should return empty string unchanged when content is empty', () => + { + const result = removePropertyAttributeFromDocument('\n', '', 'ProtoMember'); + expect(result).toBe(''); + }); + }); + + describe('handleClassAttributes', () => + { + it('should add class attribute when not present in leading trivia', () => + { + const sig = new SignatureLineResult('public class Foo {', SignatureType.Class, 0); + sig.leadingTrivia = []; + sig.defaultLineIndent = 0; + const text = 'public class Foo {'; + const result = handleClassAttributes(sig, '\n', text, 'ProtoContract', '[ProtoContract]'); + expect(result).toContain('[ProtoContract]'); + }); + + it('should not add class attribute when already in leading trivia', () => + { + const sig = new SignatureLineResult('public class Foo {', SignatureType.Class, 0); + sig.leadingTrivia = ['[ProtoContract]']; + sig.defaultLineIndent = 0; + const text = '[ProtoContract]\npublic class Foo {'; + const result = handleClassAttributes(sig, '\n', text, 'ProtoContract', '[ProtoContract]'); + const count = (result.match(/\[ProtoContract\]/g) || []).length; + expect(count).toBe(1); + }); + }); + + describe('handlePropertyAttributes', () => + { + it('should add attribute and invoke callback when neither attribute nor ignore present', () => + { + const sig = new SignatureLineResult('public int MyProp { get; set; }', SignatureType.FullProperty, 0); + sig.leadingTrivia = []; + sig.defaultLineIndent = 0; + const text = 'public int MyProp { get; set; }'; + const callback = jest.fn(); + const result = handlePropertyAttributes(sig, '\n', text, 'ProtoMember', '[ProtoMember(1)]', 'ProtoIgnore', callback); + expect(result).toContain('[ProtoMember(1)]'); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should skip and not invoke callback when attribute already in leading trivia', () => + { + const sig = new SignatureLineResult('public int MyProp { get; set; }', SignatureType.FullProperty, 0); + sig.leadingTrivia = ['[ProtoMember(1)]']; + sig.defaultLineIndent = 0; + const text = '[ProtoMember(1)]\npublic int MyProp { get; set; }'; + const callback = jest.fn(); + handlePropertyAttributes(sig, '\n', text, 'ProtoMember', '[ProtoMember(2)]', 'ProtoIgnore', callback); + expect(callback).not.toHaveBeenCalled(); + }); + + it('should skip and not invoke callback when ignore attribute in leading trivia', () => + { + const sig = new SignatureLineResult('public int MyProp { get; set; }', SignatureType.FullProperty, 0); + sig.leadingTrivia = ['[ProtoIgnore]']; + sig.defaultLineIndent = 0; + const text = '[ProtoIgnore]\npublic int MyProp { get; set; }'; + const callback = jest.fn(); + const result = handlePropertyAttributes(sig, '\n', text, 'ProtoMember', '[ProtoMember(1)]', 'ProtoIgnore', callback); + expect(callback).not.toHaveBeenCalled(); + expect(result).not.toContain('[ProtoMember(1)]'); + }); + }); + + describe('handleEnumAttributes', () => + { + it('should add contract attribute to enum and member attribute to each value', () => + { + const sig = new SignatureLineResult('public enum MyEnum', SignatureType.Enum, 5); + sig.leadingTrivia = []; + sig.defaultLineIndent = 2; + const result = handleEnumAttributes(sig, '\n', protoEnum, 'ProtoContract', '[ProtoContract]', 'ProtoEnum', '[ProtoEnum]'); + expect(result).toContain('[ProtoContract]'); + const memberCount = (result.match(/\[ProtoEnum\]/g) || []).length; + expect(memberCount).toBe(3); + }); + + it('should not add contract attribute when already in leading trivia', () => + { + const enumWithContract = `namespace Sample +{ + [ProtoContract] + public enum MyEnum + { + One, + Two + } +}`; + const sig = new SignatureLineResult('public enum MyEnum', SignatureType.Enum, 4); + sig.leadingTrivia = ['[ProtoContract]']; + sig.defaultLineIndent = 2; + const result = handleEnumAttributes(sig, '\n', enumWithContract, 'ProtoContract', '[ProtoContract]', 'ProtoEnum', '[ProtoEnum]'); + const contractCount = (result.match(/\[ProtoContract\]/g) || []).length; + expect(contractCount).toBe(1); + }); }); }); diff --git a/vscode/src/utils/csharp-util.test.ts b/vscode/src/utils/csharp-util.test.ts index 71bce0f..22d2861 100644 --- a/vscode/src/utils/csharp-util.test.ts +++ b/vscode/src/utils/csharp-util.test.ts @@ -1,5 +1,5 @@ import { Position, Selection, Uri } from 'vscode'; -import { getClassName, getNamespace, SignatureType, getUsingStatements, replaceUsingStatementsFromText, getUsingStatementsFromText, getMemberName, getLineEndingFromDoc, getEnumBody } from './csharp-util'; +import { getClassName, getNamespace, SignatureType, getUsingStatements, replaceUsingStatementsFromText, getUsingStatementsFromText, getMemberName, getLineEndingFromDoc, getEnumBody, getBeginningOfLineIndent, cleanString, getAllPublicMembers, getLeadingTrivia, SignatureLineResult } from './csharp-util'; import * as vscodeMock from 'jest-mock-vscode'; import { MockTextEditor } from 'jest-mock-vscode/dist/vscode'; @@ -178,15 +178,13 @@ describe('CSharp Util', () => describe('getLineEnding', () => { - it('should CRLF as line ending', () => + it('should return LF for LF document', () => { - // Act var doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), testFile, 'csharp'); const result = getLineEndingFromDoc(doc); - - // Assert expect(result).toEqual('\n'); }); + }); describe('getUsingStatements', () => @@ -231,6 +229,187 @@ describe('CSharp Util', () => expect(parts[2]).toBe(' One'); }); }); + + describe('getUsingStatementsFromText', () => + { + it('should extract using statements from text', () => + { + const text = 'using System;\nusing ProtoBuf;\n\npublic class Foo {}'; + const result = getUsingStatementsFromText(text, '\n'); + expect(result).toHaveLength(2); + expect(result[0]).toBe('using System;'); + expect(result[1]).toBe('using ProtoBuf;'); + }); + + it('should return empty array when no usings', () => + { + const result = getUsingStatementsFromText('public class Foo {}', '\n'); + expect(result).toHaveLength(0); + }); + }); + + describe('getBeginningOfLineIndent', () => + { + it('should return 0 for no indentation', () => + { + expect(getBeginningOfLineIndent('public class Foo')).toBe(0); + }); + + it('should return 4 for 4-space indent', () => + { + expect(getBeginningOfLineIndent(' public int Prop')).toBe(4); + }); + + it('should return 2 for 2-space indent', () => + { + expect(getBeginningOfLineIndent(' public int Prop')).toBe(2); + }); + + it('should return 0 for empty string', () => + { + expect(getBeginningOfLineIndent('')).toBe(0); + }); + }); + + describe('cleanString', () => + { + it('should return null when input is null', () => + { + expect(cleanString(null)).toBeNull(); + }); + + it('should trim leading and trailing whitespace', () => + { + expect(cleanString(' foo ')).toBe('foo'); + }); + + it('should collapse runs of 2+ whitespace', () => + { + expect(cleanString('foo bar')).toBe('foobar'); + }); + }); + + describe('getLeadingTrivia - bug: crashes when member is on line 0', () => + { + it('should not throw when lineMatchStartsOn is 0', () => + { + // Bug: preSignatureStartingLine = 0 - 1 = -1, document.lineAt(-1) throws RangeError + const text = 'public class Foo {}'; + const doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), text, 'csharp'); + const sig = new SignatureLineResult('public class Foo {}', SignatureType.Class, 0); + expect(() => getLeadingTrivia(doc, sig)).not.toThrow(); + }); + }); + + describe('getEnumBody - bug: crashes when text has no enum', () => + { + it('should return empty array when text contains no public enum', () => + { + // Bug: body! asserts non-null but regex.exec returns null when no match, + // causing TypeError: Cannot read properties of null (reading 'length') + expect(() => getEnumBody('public class Foo {}')).not.toThrow(); + expect(getEnumBody('public class Foo {}')).toHaveLength(0); + }); + }); + + describe('getLeadingTrivia', () => + { + it('should collect attribute lines above the signature', () => + { + const text = 'using System;\n\n[ProtoContract]\npublic class Foo {\n}'; + const doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), text, 'csharp'); + const sig = new SignatureLineResult('public class Foo {', SignatureType.Class, 3); + getLeadingTrivia(doc, sig); + expect(sig.leadingTrivia).toContain('[ProtoContract]'); + }); + + it('should collect xml doc comment lines above the signature', () => + { + const text = 'using System;\n\n///summary\npublic int Prop { get; set; }'; + const doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), text, 'csharp'); + const sig = new SignatureLineResult('public int Prop { get; set; }', SignatureType.FullProperty, 3); + getLeadingTrivia(doc, sig); + expect(sig.leadingTrivia).toContain('///summary'); + }); + + it('should stop at closing brace', () => + { + const text = 'namespace Foo {\n}\npublic class Bar {\n}'; + const doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), text, 'csharp'); + const sig = new SignatureLineResult('public class Bar {', SignatureType.Class, 2); + getLeadingTrivia(doc, sig); + expect(sig.leadingTrivia).toHaveLength(0); + }); + + it('should stop at semicolon line (lambda property above)', () => + { + const text = 'using System;\n\npublic class Foo {\n public int A { get; set; }\n\n public int B { get; set; }\n}'; + const doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), text, 'csharp'); + const sig = new SignatureLineResult('public int B { get; set; }', SignatureType.FullProperty, 5); + getLeadingTrivia(doc, sig); + expect(sig.leadingTrivia).toHaveLength(0); + }); + }); + + describe('getAllPublicMembers', () => + { + it('should classify a class signature', () => + { + const text = 'using System;\n\npublic class Foo {\n}'; + const doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), text, 'csharp'); + const members = getAllPublicMembers(text, doc); + expect(members.some(m => m.signatureType === SignatureType.Class)).toBe(true); + }); + + it('should classify an enum signature', () => + { + const text = 'using System;\n\npublic enum Status {\n Active\n}'; + const doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), text, 'csharp'); + const members = getAllPublicMembers(text, doc); + expect(members.some(m => m.signatureType === SignatureType.Enum)).toBe(true); + }); + + it('should classify a method signature', () => + { + const text = 'using System;\n\npublic class Foo {\n public void Bar() { }\n}'; + const doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), text, 'csharp'); + const members = getAllPublicMembers(text, doc); + expect(members.some(m => m.signatureType === SignatureType.Method)).toBe(true); + }); + + it('should classify a lambda property signature', () => + { + const text = 'using System;\n\npublic class Foo {\n public string Greeting => "Hello";\n}'; + const doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), text, 'csharp'); + const members = getAllPublicMembers(text, doc); + expect(members.some(m => m.signatureType === SignatureType.LambaProperty)).toBe(true); + }); + + it('should classify a full property signature', () => + { + const text = 'using System;\n\npublic class Foo {\n public string Name { get; set; }\n}'; + const doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), text, 'csharp'); + const members = getAllPublicMembers(text, doc); + expect(members.some(m => m.signatureType === SignatureType.FullProperty)).toBe(true); + }); + + it('should return empty array when no public members', () => + { + const text = 'using System;\n\nnamespace Foo {}'; + const doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), text, 'csharp'); + const members = getAllPublicMembers(text, doc); + expect(members).toHaveLength(0); + }); + + it('should attach leading trivia attributes to signatures', () => + { + const text = 'using System;\n\n[ProtoContract]\npublic class Foo {\n}'; + const doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), text, 'csharp'); + const members = getAllPublicMembers(text, doc); + const cls = members.find(m => m.signatureType === SignatureType.Class); + expect(cls?.leadingTrivia).toContain('[ProtoContract]'); + }); + }); }); diff --git a/vscode/src/utils/csharp-util.ts b/vscode/src/utils/csharp-util.ts index 0fe0c07..ec914ce 100644 --- a/vscode/src/utils/csharp-util.ts +++ b/vscode/src/utils/csharp-util.ts @@ -1,6 +1,5 @@ import { EndOfLine, TextDocument, TextEditor } from 'vscode'; import { IWindow } from '../interfaces/window.interface'; -import { match } from 'assert'; export type PublicProtected = 'public' | 'protected'; @@ -34,9 +33,9 @@ export const getEnumBody = (text: string): string[] => { const regEx = new RegExp('public enum[^\\}]*\\{([^\\}]*)\\}', 'gm'); const body = regEx.exec(text); - if (body!.length>1) + if (body && body.length > 1) { - return body![1].split(','); + return body[1].split(','); } return []; @@ -47,7 +46,7 @@ export const getLeadingTrivia = (document: TextDocument, finalSig: SignatureLine let preSignatureStartingLine = finalSig.lineMatchStartsOn - 1; //start at the line just above the signature let preSignatureText = []; let preSignatureLine: string | null = ''; - while (true) + while (preSignatureStartingLine >= 0) { preSignatureLine = (document.lineAt(preSignatureStartingLine).text || '').trim(); @@ -103,7 +102,6 @@ export const getAllPublicMembers = (text: string, document: TextDocument): Signa sig.defaultLineIndent = getBeginningOfLineIndent(textLines[lineNumber]); members.push(sig); }); - console.log(members); return members; }; diff --git a/vscode/tsconfig.json b/vscode/tsconfig.json index 5b7da77..653ed86 100644 --- a/vscode/tsconfig.json +++ b/vscode/tsconfig.json @@ -8,6 +8,7 @@ "lib": [ "ES2022" ], + "types": ["node", "jest"], "sourceMap": true, "rootDir": "src", "strict": true, /* enable all strict type-checking options */ @@ -16,5 +17,6 @@ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ - } + }, + "exclude": ["src/test"] }