From 95f99afc216477c416dede2a6b54544fd6f2cbb7 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Wed, 2 Sep 2026 10:01:31 +0200 Subject: [PATCH 1/4] test: expect LF in SQL fragment assertions The fragments come verbatim from a multi-line raw string literal, so their newlines are the source file's bytes, not the host's. Co-Authored-By: Claude Opus 5 --- .../InterpolatedSqlStatementTests.cs | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs b/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs index 912d5ce..ee3b208 100644 --- a/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs +++ b/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs @@ -35,7 +35,7 @@ public void AppendFormatted_InterpolatedParameter_ShouldSupportComplexExpression statement.Fragments[1].Should().Be(new InterpolatedParameter("BaseDiscount53", baseDiscount * 5 / 3)); - statement.Fragments[2].Should().Be(new Literal($",{Environment.NewLine} ")); + statement.Fragments[2].Should().Be(new Literal(",\n ")); statement .Fragments[3] @@ -67,10 +67,7 @@ public void AppendFormatted_InterpolatedTemporaryTables_ShouldStoreTemporaryTabl table1.ValuesType.Should().Be(typeof(long)); - statement - .Fragments[2] - .Should() - .Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT Id FROM ")); + statement.Fragments[2].Should().Be(new Literal("\nUNION\nSELECT Id FROM ")); var table2 = statement.TemporaryTables[1]; @@ -86,10 +83,7 @@ public void AppendFormatted_InterpolatedTemporaryTables_ShouldStoreTemporaryTabl statement.Fragments[1].Should().Be(table1); - statement - .Fragments[2] - .Should() - .Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT Id FROM ")); + statement.Fragments[2].Should().Be(new Literal("\nUNION\nSELECT Id FROM ")); statement.Fragments[3].Should().Be(table2); } @@ -254,18 +248,15 @@ public void Fragments_ShouldGetFragments() statement.Fragments[1].Should().Be(new InterpolatedParameter("Value1", value1)); - statement.Fragments[2].Should().Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT ")); + statement.Fragments[2].Should().Be(new Literal("\nUNION\nSELECT ")); statement.Fragments[3].Should().Be(new InterpolatedParameter("Value2", value2)); - statement.Fragments[4].Should().Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT ")); + statement.Fragments[4].Should().Be(new Literal("\nUNION\nSELECT ")); statement.Fragments[5].Should().Be(new InterpolatedParameter("Value3", value3)); - statement - .Fragments[6] - .Should() - .Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT Value FROM ")); + statement.Fragments[6].Should().Be(new Literal("\nUNION\nSELECT Value FROM ")); var table1 = statement.Fragments[7].Should().BeOfType().Subject; @@ -275,10 +266,7 @@ public void Fragments_ShouldGetFragments() table1.ValuesType.Should().Be(typeof(long)); - statement - .Fragments[8] - .Should() - .Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT Id FROM ")); + statement.Fragments[8].Should().Be(new Literal("\nUNION\nSELECT Id FROM ")); var table2 = statement.Fragments[9].Should().BeOfType().Subject; From fe952d5367d60c30a90cc8fdba0af05afa36c772 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Wed, 2 Sep 2026 10:02:55 +0200 Subject: [PATCH 2/4] chore: rename tidy-cs.ps1 to tidy-code.ps1 The script covers style and member ordering too, not only formatting of C# files. Co-Authored-By: Claude Opus 5 --- .agents/README.md | 4 ++-- .agents/references/code-style.md | 2 +- .claude/hooks/{tidy-cs.ps1 => tidy-code.ps1} | 8 +++---- .claude/settings.json | 2 +- .codex/hooks.json | 2 +- .codex/hooks/{tidy-cs.ps1 => tidy-code.ps1} | 8 +++---- .editorconfig | 4 ++-- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/workflows/ci.yml | 4 ++-- AGENTS.md | 10 ++++---- CONTRIBUTING.md | 6 ++--- Directory.Build.props | 2 +- scripts/preflight.ps1 | 4 ++-- scripts/{tidy-cs.ps1 => tidy-code.ps1} | 24 ++++++++++---------- 14 files changed, 41 insertions(+), 41 deletions(-) rename .claude/hooks/{tidy-cs.ps1 => tidy-code.ps1} (84%) rename .codex/hooks/{tidy-cs.ps1 => tidy-code.ps1} (86%) rename scripts/{tidy-cs.ps1 => tidy-code.ps1} (93%) diff --git a/.agents/README.md b/.agents/README.md index eb52a69..78b915d 100644 --- a/.agents/README.md +++ b/.agents/README.md @@ -27,12 +27,12 @@ Codex-only UI metadata and explicit-invocation policy. Claude needs thin skill w `disable-model-invocation` policy lives in `SKILL.md` frontmatter. The hook adapters differ because Claude and Codex use different payload and response contracts. Both delegate -all substantive behavior to the same scripts: `scripts/tidy-cs.ps1` and `scripts/public-api-guard.ps1`. +all substantive behavior to the same scripts: `scripts/tidy-code.ps1` and `scripts/public-api-guard.ps1`. The tidy hook runs the **default scope only** — CSharpier, under a second. Code style and member ordering are not run on every edit: `dotnet format style` needs MSBuild and ReSharper loads the whole solution, and neither belongs on the critical path of a single edit. All three are build errors, and `scripts/preflight.ps1` runs -`tidy-cs.ps1 -Scope all` before a commit, so nothing reaches a pull request untidied. +`tidy-code.ps1 -Scope all` before a commit, so nothing reaches a pull request untidied. When changing behavior, edit the canonical file. Keep only required names, descriptions, policies, tool/model settings, and reference instructions in tool-specific files. diff --git a/.agents/references/code-style.md b/.agents/references/code-style.md index 083472b..83f459b 100644 --- a/.agents/references/code-style.md +++ b/.agents/references/code-style.md @@ -63,7 +63,7 @@ explicit field and the compiler's capture field are one field, not two. ## When a tool surprises you - **`cleanupcode` re-indents the content of raw string literals and CSharpier puts it back.** Neither is - idempotent alone; the pair is. Always let CSharpier run last — `tidy-cs.ps1` does. + idempotent alone; the pair is. Always let CSharpier run last — `tidy-code.ps1` does. - **Explicit interface implementations sort first — except events.** StyleCop counts an explicit property, indexer or method as public, so each of those has its own "Explicit interface …" entry in the file layout putting it at the front of its group. An explicit **event** is counted as private, so it stays where diff --git a/.claude/hooks/tidy-cs.ps1 b/.claude/hooks/tidy-code.ps1 similarity index 84% rename from .claude/hooks/tidy-cs.ps1 rename to .claude/hooks/tidy-code.ps1 index b1f45ed..049f017 100644 --- a/.claude/hooks/tidy-cs.ps1 +++ b/.claude/hooks/tidy-code.ps1 @@ -1,6 +1,6 @@ # PostToolUse hook: format an edited C# file with CSharpier. # -# The logic itself lives in scripts/tidy-cs.ps1, so that Codex's hook and a human run exactly the same +# The logic itself lives in scripts/tidy-code.ps1, so that Codex's hook and a human run exactly the same # thing. This file is only the hook wiring: read the tool payload off stdin, pull the edited path out of # it, and delegate. # @@ -21,9 +21,9 @@ try { if ([System.IO.Path]::GetExtension($filePath) -ne '.cs') { exit 0 } if (-not (Test-Path -LiteralPath $filePath)) { exit 0 } - $script = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) 'scripts/tidy-cs.ps1' + $script = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) 'scripts/tidy-code.ps1' if (-not (Test-Path -LiteralPath $script)) { - Write-Output "tidy-cs hook: scripts/tidy-cs.ps1 not found at $script" + Write-Output "tidy-code hook: scripts/tidy-code.ps1 not found at $script" exit 0 } @@ -36,7 +36,7 @@ try { } } catch { - Write-Output "tidy-cs hook error: $($_.Exception.Message)" + Write-Output "tidy-code hook error: $($_.Exception.Message)" } exit 0 diff --git a/.claude/settings.json b/.claude/settings.json index 49ce5d4..66af451 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -7,7 +7,7 @@ "hooks": [ { "type": "command", - "command": "pwsh -NoProfile -NonInteractive -File \"$CLAUDE_PROJECT_DIR/.claude/hooks/tidy-cs.ps1\"", + "command": "pwsh -NoProfile -NonInteractive -File \"$CLAUDE_PROJECT_DIR/.claude/hooks/tidy-code.ps1\"", "timeout": 60 }, { diff --git a/.codex/hooks.json b/.codex/hooks.json index 3d7122c..1e5d3af 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -7,7 +7,7 @@ "hooks": [ { "type": "command", - "command": "pwsh -NoProfile -NonInteractive -Command \"& (Join-Path (git rev-parse --show-toplevel) '.codex/hooks/tidy-cs.ps1')\"", + "command": "pwsh -NoProfile -NonInteractive -Command \"& (Join-Path (git rev-parse --show-toplevel) '.codex/hooks/tidy-code.ps1')\"", "statusMessage": "Formatting changed C# files", "timeout": 120 }, diff --git a/.codex/hooks/tidy-cs.ps1 b/.codex/hooks/tidy-code.ps1 similarity index 86% rename from .codex/hooks/tidy-cs.ps1 rename to .codex/hooks/tidy-code.ps1 index 17570e0..c6676ad 100644 --- a/.codex/hooks/tidy-cs.ps1 +++ b/.codex/hooks/tidy-code.ps1 @@ -1,6 +1,6 @@ # Codex PostToolUse hook: format the C# files an edit just touched, with CSharpier. # -# The logic itself lives in scripts/tidy-cs.ps1, which Claude Code's hook runs too. This file is only +# The logic itself lives in scripts/tidy-code.ps1, which Claude Code's hook runs too. This file is only # the hook wiring. # # Why it does not read a path out of the payload: for a file edit Codex reports tool_name "apply_patch" and @@ -45,9 +45,9 @@ try { $repositoryRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) } - $script = Join-Path $repositoryRoot 'scripts/tidy-cs.ps1' + $script = Join-Path $repositoryRoot 'scripts/tidy-code.ps1' if (-not (Test-Path -LiteralPath $script)) { - Write-HookResult -AdditionalContext "tidy-cs hook: scripts/tidy-cs.ps1 not found at $script" + Write-HookResult -AdditionalContext "tidy-code hook: scripts/tidy-code.ps1 not found at $script" exit 0 } @@ -63,7 +63,7 @@ try { } } catch { - Write-HookResult -AdditionalContext "tidy-cs hook error: $($_.Exception.Message)" + Write-HookResult -AdditionalContext "tidy-code hook error: $($_.Exception.Message)" } exit 0 diff --git a/.editorconfig b/.editorconfig index 8fbb815..1627065 100644 --- a/.editorconfig +++ b/.editorconfig @@ -36,7 +36,7 @@ trim_trailing_whitespace = false # NewStyleCop.Analyzers member ordering (checking only). Fixed by ReSharper, see the ordering # section further down. # -# Run scripts/tidy-cs.ps1 to apply all three. +# Run scripts/tidy-code.ps1 to apply all three. # ====================================================================================================== [*.cs] @@ -221,7 +221,7 @@ dotnet_diagnostic.RCS1037.severity = none # Adding a StyleCop rule to this repository means adding it explicitly below. # # StyleCop can only REPORT a wrong order - its ordering code fix is disabled upstream. ReSharper does the -# fixing: in Rider through Code Cleanup, and on the command line through scripts/tidy-cs.ps1. So the +# fixing: in Rider through Code Cleanup, and on the command line through scripts/tidy-code.ps1. So the # order is defined twice, and the two definitions have to stay in step: # stylecop.json -> what is checked # DbConnectionPlus.slnx.DotSettings -> what is applied diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 96162c2..5f45a2e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -15,6 +15,6 @@ - [ ] Public API changes are declared in the affected `PublicAPI.Unshipped.txt` (`scripts/update-public-api.ps1`). - [ ] XML docs and `README.md` updated for public API changes. - [ ] `CHANGELOG.md` updated, and the version in `src/Directory.Build.props` bumped if this release-bound change needs it. -- [ ] Style, formatting and member ordering applied (`pwsh -File scripts/tidy-cs.ps1 -Scope all`, which +- [ ] Style, formatting and member ordering applied (`pwsh -File scripts/tidy-code.ps1 -Scope all`, which `preflight.ps1` also runs). - [ ] Branch name follows [Conventional Branch](https://conventionalbranch.org/): `/issue--`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da04ec6..96d1c8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: # Enforce the style, formatting and member ordering the repo ships tooling for. A rule that is not # enforced in CI drifts with the first external pull request. # - # This runs scripts/tidy-cs.ps1, the same entry point a developer and both AI agents use, so CI cannot + # This runs scripts/tidy-code.ps1, the same entry point a developer and both AI agents use, so CI cannot # disagree with what the tooling produces locally. -Check -Scope all tidies the checkout for real and # then asks whether anything changed: ReSharper has no check mode, and neither it nor CSharpier is # idempotent alone, so comparing before against after is the only honest question. Writing to the @@ -135,7 +135,7 @@ jobs: run: dotnet tool restore - name: Verify style, formatting and member ordering - run: pwsh -NoProfile -NonInteractive -File scripts/tidy-cs.ps1 -Scope all -Check + run: pwsh -NoProfile -NonInteractive -File scripts/tidy-code.ps1 -Scope all -Check # The check above only says that something is untidy. This says what. - name: Show the changes that would fix it diff --git a/AGENTS.md b/AGENTS.md index 3583628..eeb3f26 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ methods on `DbConnection`, with per-database dialect support from pluggable adap | `docs/` | docfx config, the site landing page and implementation plans. | | `.agents/`, `.codex/`, `.claude/` | Canonical skills and references, plus each tool's agent metadata and hook wiring. | | `.github/workflows/` | `ci.yml` (lint → build/test → package + docs → package-consumption gates → publish), `codeql.yml`, `dependency-review.yml`. | -| `scripts/` | The commands you type: `preflight`, `verify-package-aot`, `benchmarks`, `update-public-api`, `clean-build-artifacts`, `extract-release-notes`. Plus `tidy-cs` and `public-api-guard`, which the editor hooks run for you. | +| `scripts/` | The commands you type: `preflight`, `verify-package-aot`, `benchmarks`, `update-public-api`, `clean-build-artifacts`, `extract-release-notes`. Plus `tidy-code` and `public-api-guard`, which the editor hooks run for you. | The solution file is `DbConnectionPlus.slnx` (XML `.slnx`, not `.sln`). **New projects must be added to it.** @@ -118,9 +118,9 @@ something surprising: [the code-style reference](.agents/references/code-style.m One entry point applies all of it, in three scopes: ```bash -pwsh -File scripts/tidy-cs.ps1 # ~1s formatting, on the files git reports as changed -pwsh -File scripts/tidy-cs.ps1 -Scope style # ~15s + the code-style fixers -pwsh -File scripts/tidy-cs.ps1 -Scope all # ~3min + member ordering, whole solution +pwsh -File scripts/tidy-code.ps1 # ~1s formatting, on the files git reports as changed +pwsh -File scripts/tidy-code.ps1 -Scope style # ~15s + the code-style fixers +pwsh -File scripts/tidy-code.ps1 -Scope all # ~3min + member ordering, whole solution ``` **Before you commit, run `-Scope all`** — or `scripts/preflight.ps1`, which does it for you. That is the only scope @@ -189,7 +189,7 @@ Two skills and two review agents are checked in, each under a Codex and a Claude Claude Code (`.claude/settings.json`) and Codex (`.codex/hooks.json`) fire the same two PostToolUse hooks — formatting and the public-API reminder — and both delegate to `scripts/`. Codex needs those hooks trusted once per -clone (`/hooks`); until then nothing fires and you run `scripts/tidy-cs.ps1` yourself. +clone (`/hooks`); until then nothing fires and you run `scripts/tidy-code.ps1` yourself. Reusable skills and reference material belong in `.agents/`, executable checks in `scripts/`, and only metadata and hook wiring in `.codex/` and `.claude/`. Never fork a procedure or a check into a tool-specific copy — both diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49efa35..64ec1dc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,11 +23,11 @@ Please note we have a code of conduct, please follow it in all your interactions pwsh -File scripts/preflight.ps1 ``` It rewrites files — review what it changed and include it in your commit. To run just the tidying: - `pwsh -File scripts/tidy-cs.ps1 -Scope all`. + `pwsh -File scripts/tidy-code.ps1 -Scope all`. `TreatWarningsAsErrors` is on for **every** project, so the build is also the style, member-ordering, trim-analyzer and public-API gate, and `CSharpier.MsBuild` makes an unformatted file a build error too. - The build never rewrites your files — it fails and names them; `scripts/tidy-cs.ps1` is what fixes them. + The build never rewrites your files — it fails and names them; `scripts/tidy-code.ps1` is what fixes them. **Never suppress an `IL2xxx` warning to get a green build** — it is the only build-time evidence that the trimming annotations are complete. 4. If you touched a reflection path, also run the Native AOT gate. Nothing else in the repository can see @@ -57,7 +57,7 @@ dotnet tool restore git config blame.ignoreRevsFile .git-blame-ignore-revs ``` -The first installs CSharpier, the ReSharper command line tools and docfx, which `scripts/tidy-cs.ps1` and the +The first installs CSharpier, the ReSharper command line tools and docfx, which `scripts/tidy-code.ps1` and the documentation build need. The second makes `git blame` skip the commits listed in `.git-blame-ignore-revs`, which reformatted and reordered the whole repository, so blame points at whoever wrote the logic rather than at the tool that moved it. GitHub already does this on its own. diff --git a/Directory.Build.props b/Directory.Build.props index cfc2df9..e457e58 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -50,7 +50,7 @@ Check, not format. Left at its default the package REWRITES source files during the build, which turns building into editing: the file on disk stops being the file you asked the compiler to compile. With Check the build fails instead and names the file, and fixing it stays a deliberate - act - scripts/tidy-cs.ps1, or the editor hook that already runs on every agent edit. + act - scripts/tidy-code.ps1, or the editor hook that already runs on every agent edit. CSharpier_Bypass=true skips it, for the rare case of needing a build out of a tree mid-edit. --> diff --git a/scripts/preflight.ps1 b/scripts/preflight.ps1 index dfcc322..3bef234 100644 --- a/scripts/preflight.ps1 +++ b/scripts/preflight.ps1 @@ -48,7 +48,7 @@ $repositoryRoot = Split-Path -Parent $PSScriptRoot $solution = Join-Path $repositoryRoot 'DbConnectionPlus.slnx' $unitTests = Join-Path $repositoryRoot 'tests/DbConnectionPlus.UnitTests/DbConnectionPlus.UnitTests.csproj' $publicApiGuard = Join-Path $repositoryRoot 'scripts/public-api-guard.ps1' -$tidy = Join-Path $repositoryRoot 'scripts/tidy-cs.ps1' +$tidy = Join-Path $repositoryRoot 'scripts/tidy-code.ps1' $failures = New-Object System.Collections.Generic.List[String] @@ -93,7 +93,7 @@ if (-not $SkipTidy) { & pwsh -NoProfile -NonInteractive -File $tidy -Scope all if ($LASTEXITCODE -ne 0) { $failures.Add('tidy') - Write-Output 'FAIL - tidy-cs could not finish. Run `dotnet tool restore` if the tools are missing.' + Write-Output 'FAIL - tidy-code could not finish. Run `dotnet tool restore` if the tools are missing.' } else { $dirtyAfter = @(& git -C $repositoryRoot diff --name-only -- '*.cs' 2>$null | Where-Object { $_ }) diff --git a/scripts/tidy-cs.ps1 b/scripts/tidy-code.ps1 similarity index 93% rename from scripts/tidy-cs.ps1 rename to scripts/tidy-code.ps1 index 8043eb6..8e8bef3 100644 --- a/scripts/tidy-cs.ps1 +++ b/scripts/tidy-code.ps1 @@ -41,13 +41,13 @@ tree you are not ready to have tidied. .EXAMPLE - pwsh -File scripts/tidy-cs.ps1 + pwsh -File scripts/tidy-code.ps1 .EXAMPLE - pwsh -File scripts/tidy-cs.ps1 -Scope all + pwsh -File scripts/tidy-code.ps1 -Scope all .EXAMPLE - pwsh -File scripts/tidy-cs.ps1 src/DbConnectionPlus/Entities/EntityHelper.cs + pwsh -File scripts/tidy-code.ps1 src/DbConnectionPlus/Entities/EntityHelper.cs #> [CmdletBinding()] param( @@ -150,7 +150,7 @@ function Invoke-StyleFix { $consumers = [System.IO.Path]::Combine($repositoryRoot, 'tests', 'package-consumption') $skipped = @($Files) | Where-Object { $_.StartsWith($consumers, [StringComparison]::OrdinalIgnoreCase) } if ($skipped) { - Write-Output "tidy-cs: $($skipped.Count) file(s) under tests/package-consumption - no style pass, see AGENTS.md." + Write-Output "tidy-code: $($skipped.Count) file(s) under tests/package-consumption - no style pass, see AGENTS.md." } $Files = @($Files) | Where-Object { -not $_.StartsWith($consumers, [StringComparison]::OrdinalIgnoreCase) } @@ -160,7 +160,7 @@ function Invoke-StyleFix { $Files | Group-Object { Get-OwningProject -FilePath $_ } | ForEach-Object { $project = $_.Name if ([String]::IsNullOrWhiteSpace($project) -or -not (Test-Path -LiteralPath $project)) { - Write-Output "tidy-cs: no owning .csproj for $($_.Group -join ', ') - skipped." + Write-Output "tidy-code: no owning .csproj for $($_.Group -join ', ') - skipped." return } @@ -221,7 +221,7 @@ function Get-CSharpFingerprint { # equal, which would make -Check report a tidy tree without having looked at a single file. This is CI's # only formatting gate; it must not be able to pass by accident. if (-not $relativePaths) { - throw 'tidy-cs: git listed no .cs files. Is this a git repository, and is git on PATH?' + throw 'tidy-code: git listed no .cs files. Is this a git repository, and is git on PATH?' } $sha = [System.Security.Cryptography.SHA256]::Create() @@ -267,7 +267,7 @@ function Invoke-Format { # --- Run ------------------------------------------------------------------------------------------ if ($Scope -eq 'all') { - if ($Path) { Write-Output 'tidy-cs: -Scope all covers the whole solution; the paths given are ignored.' } + if ($Path) { Write-Output 'tidy-code: -Scope all covers the whole solution; the paths given are ignored.' } # -Check does not reach the individual tools here, because two of them are not idempotent on their # own: cleanupcode re-indents the content of raw string literals and CSharpier puts it back. Asking @@ -280,11 +280,11 @@ if ($Scope -eq 'all') { Invoke-Format -Files @() -VerifyOnly $false if ($Check -and -not $failures.Count -and (Get-CSharpFingerprint) -ne $before) { - $failures.Add('the tree is not tidy. Run: pwsh -File scripts/tidy-cs.ps1 -Scope all') + $failures.Add('the tree is not tidy. Run: pwsh -File scripts/tidy-code.ps1 -Scope all') } if (-not $failures.Count) { - Write-Output "tidy-cs: solution $(if ($Check) { 'checked' } else { 'tidied' })." + Write-Output "tidy-code: solution $(if ($Check) { 'checked' } else { 'tidied' })." } } else { @@ -292,7 +292,7 @@ else { $files = Resolve-TargetFile -Candidates $Path if (-not $files) { - Write-Output 'tidy-cs: nothing to do.' + Write-Output 'tidy-code: nothing to do.' exit 0 } @@ -300,12 +300,12 @@ else { Invoke-Format -Files $files -VerifyOnly $Check.IsPresent if (-not $failures.Count) { - Write-Output "tidy-cs: $($files.Count) file(s) $(if ($Check) { 'checked' } else { 'tidied' })." + Write-Output "tidy-code: $($files.Count) file(s) $(if ($Check) { 'checked' } else { 'tidied' })." } } if ($failures.Count) { - $failures | ForEach-Object { Write-Output "tidy-cs: $_" } + $failures | ForEach-Object { Write-Output "tidy-code: $_" } exit 1 } From f327c1e44cc2f81970ca0d820431be6dcff2ea4e Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Wed, 2 Sep 2026 10:03:47 +0200 Subject: [PATCH 3/4] chore: store every text file as LF .gitattributes loses its per-type pins, so a new file type needs no decision, and the explicit eol= overrides whatever core.autocrlf a contributor has. The lint job now fails if a file is stored otherwise. Co-Authored-By: Claude Opus 5 --- .editorconfig | 7 +++--- .gitattributes | 50 ++++++++++++---------------------------- .github/workflows/ci.yml | 13 +++++++++++ AGENTS.md | 3 +++ CONTRIBUTING.md | 21 +++++++++++++++++ 5 files changed, 56 insertions(+), 38 deletions(-) diff --git a/.editorconfig b/.editorconfig index 1627065..b47816e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,13 +3,14 @@ root = true # ====================================================================================================== # Every file # -# Line endings are deliberately NOT set here. .gitattributes owns them: it normalizes to LF in the -# repository and checks each file out with the ending that file type needs. An end_of_line here would -# fight that on Windows. +# Line endings are LF everywhere, on every OS. .gitattributes stores LF and checks out LF - that is the +# guarantee, and it holds whatever core.autocrlf a contributor has. end_of_line below is what makes the +# editors and formatters write LF too, so the working tree never drifts. # ====================================================================================================== [*] charset = utf-8 +end_of_line = lf indent_style = space indent_size = 4 insert_final_newline = true diff --git a/.gitattributes b/.gitattributes index 37a132a..b3d3a21 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,36 +1,16 @@ -# Auto-detect text files, normalize to LF in the repository. -* text=auto +# Every text file is LF, in the repository and in the working tree, on every OS. +# Git auto-detects text vs. binary; the rules below are the exceptions. +# +# The explicit eol= overrides core.autocrlf and core.eol, so a contributor needs no git configuration +# and a wrong one cannot change what lands in the repository. CI fails if a file is ever stored +# otherwise. +* text=auto eol=lf -# Force specific line endings where it matters. -*.cs text -*.csproj text eol=crlf -*.slnx text eol=crlf -*.props text eol=crlf -*.targets text eol=crlf -*.config text eol=crlf -*.resx text eol=crlf -*.editorconfig text eol=crlf -*.DotSettings text eol=crlf -*.json text eol=lf -*.yml text eol=lf -*.yaml text eol=lf -*.md text eol=lf -*.properties text eol=lf - -# PowerShell is the repository's scripting language and runs on both hosts; CRLF is what pwsh writes on -# Windows, which is where these are edited. -*.ps1 text eol=crlf - -# Windows batch files require CRLF; shell scripts require LF. -*.cmd text eol=crlf -*.bat text eol=crlf -*.sh text eol=lf - -# Binary files - never touch. -*.png binary -*.jpg binary -*.ico binary -*.dll binary -*.exe binary -*.snk binary -*.db binary +# Binary files - never convert. text=auto already detects most of these; the list makes it certain. +*.png binary +*.jpg binary +*.gif binary +*.ico binary +*.snk binary +*.dll binary +*.exe binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96d1c8d..32fc060 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,6 +112,19 @@ jobs: echo "All ${count} revision(s) resolve and are reachable." + # .gitattributes only applies when a file is ADDED. A file committed past it, or a rule added + # after the file, stays wrong forever and nothing complains - and commits created server-side on GitHub, + # such as a bot's, bypass the filter entirely. Renormalizing and asking whether anything changed + # is the exact question, and it needs no toolchain. + - name: Verify line endings are normalized + run: | + git add --renormalize . + if ! git diff --cached --quiet; then + git --no-pager diff --cached --name-only + echo "::error title=Line endings::The files above are not stored as .gitattributes requires. Run 'git add --renormalize .' and commit the result." + exit 1 + fi + - name: Setup .NET uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: diff --git a/AGENTS.md b/AGENTS.md index eeb3f26..d16036e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -174,6 +174,9 @@ constraints must not be broken: [the `commit` skill](.agents/skills/commit/SKILL.md). `CHANGELOG.md` follows [Keep a Changelog](https://keepachangelog.com/); versioning is SemVer. - Pull request process: [CONTRIBUTING.md](CONTRIBUTING.md#pull-request-process). +- Line endings are LF everywhere; `.gitattributes` and `.editorconfig` enforce this and CI verifies it. + Never hand-convert line endings, and never compare a multi-line source literal against + `Environment.NewLine` - the literal carries the file's bytes, `Environment.NewLine` carries the host's. ### Releases diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64ec1dc..bb0396d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,6 +66,27 @@ If you use Rider, two settings make the tooling invisible: install the **CSharpi Settings | Tools | CSharpier | Run on Save, and use the shared **ReorderMembers** cleanup profile (from `DbConnectionPlus.slnx.DotSettings`) when you want members put back in order. +## Line endings + +Every text file is LF, in the repository and in the working tree, on every OS. `.gitattributes` +enforces this whatever your `core.autocrlf` is set to, so there is nothing to configure, and CI fails +if a wrongly stored file lands anyway. + +`.editorconfig` also asks editors and formatters to write LF. If one does not, git still stores LF, but +`git status` lists the file as modified while `git diff` shows nothing. Run +`pwsh -File scripts/tidy-code.ps1` to fix it, or `git checkout -- `. + +If you have set `git config core.safecrlf true`, git refuses to add such a file with "CRLF would be +replaced by LF". Run the tidy script first, or use `core.safecrlf warn`. + +To refresh a clone made before this policy (commit or stash your changes first - the second command +discards uncommitted work): + +```shell +git rm -r --cached . -q +git reset --hard +``` + ## Releasing Releases are cut by CI from a pushed tag; nothing is packed or pushed by hand. The versioning scheme is From 89a96f3b37b028c6cecfbfbe1f54a58b3fb38045 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Wed, 2 Sep 2026 10:06:47 +0200 Subject: [PATCH 4/4] test: normalize the expected ToString output to the host newline InterpolatedSqlStatement.ToString builds its output with AppendLine and ReplaceLineEndings, so it is host-native. The expected raw string literal is now always LF, which no longer matches on Windows. Co-Authored-By: Claude Opus 5 --- .../SqlStatements/InterpolatedSqlStatementTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs b/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs index ee3b208..13966bf 100644 --- a/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs +++ b/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs @@ -440,7 +440,7 @@ Statement Temporary Tables '2' (System.Int32) '3' (System.Int32) - """ + """.ReplaceLineEndings() ); } }