Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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 `<Compile Include>` 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.
30 changes: 15 additions & 15 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,35 @@ env:
SolutionPath: 'visual-studio\ProtoAttributor.sln'
Version: '1.2.${{ github.run_number }}'
BaseVersion: '1.2.0.0'

jobs:
build-extension:
runs-on: windows-latest
strategy:
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:
Expand All @@ -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
Expand All @@ -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 }}

10 changes: 4 additions & 6 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
118 changes: 118 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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 }}"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bash.exe.stackdump
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"dotnet.preferCSharpExtension": true
}
Loading
Loading