|
| 1 | +#!/usr/bin/env pwsh |
| 2 | +<# |
| 3 | +.SYNOPSIS |
| 4 | + Reports (and optionally removes) entries in codespell.txt that codespell no longer flags. |
| 5 | +
|
| 6 | +.DESCRIPTION |
| 7 | + Codespell has no built-in way to detect unused entries in an ignore-words file. |
| 8 | + This script runs codespell against the repo with NO ignore-words file, collects the |
| 9 | + set of words it flags, and reports any suppression in codespell.txt that is not in |
| 10 | + that set (i.e., the suppression is no longer needed). |
| 11 | +
|
| 12 | + The pre-commit hook invocation in .pre-commit-config.yaml is mirrored here so the |
| 13 | + scan covers the same files codespell would normally see. |
| 14 | +
|
| 15 | +.PARAMETER Apply |
| 16 | + Rewrite codespell.txt with the unused entries removed. Without this switch the |
| 17 | + script only reports. |
| 18 | +
|
| 19 | +.PARAMETER CodespellCommand |
| 20 | + Override the codespell executable (default: "codespell"). |
| 21 | +#> |
| 22 | +[CmdletBinding()] |
| 23 | +param( |
| 24 | + [switch]$Apply, |
| 25 | + [string]$CodespellCommand = 'codespell' |
| 26 | +) |
| 27 | + |
| 28 | +$ErrorActionPreference = 'Stop' |
| 29 | + |
| 30 | +$repoRoot = (& git -C $PSScriptRoot rev-parse --show-toplevel).Trim() |
| 31 | +$suppressionsFile = Join-Path $repoRoot '.github/linters/codespell.txt' |
| 32 | + |
| 33 | +if (-not (Test-Path $suppressionsFile)) { |
| 34 | + throw "Suppressions file not found: $suppressionsFile" |
| 35 | +} |
| 36 | + |
| 37 | +# Must match the `exclude:` regex for the codespell hook in .pre-commit-config.yaml. |
| 38 | +$excludeRegex = '^src/Lucene\.Net\.Analysis\.Common/Analysis/../.*\.rslp$|^.*Lucene\.Net\.Tests.*$' |
| 39 | + |
| 40 | +$suppressions = Get-Content -LiteralPath $suppressionsFile | |
| 41 | + Where-Object { $_ -ne '' } |
| 42 | + |
| 43 | +Write-Host "Loaded $($suppressions.Count) suppression(s) from $suppressionsFile" |
| 44 | +Write-Host "Enumerating git-tracked files (to match pre-commit's view of the repo)..." |
| 45 | + |
| 46 | +# pre-commit only feeds tracked files to codespell; running codespell directly would |
| 47 | +# also walk bin/, obj/, and other gitignored output. Drive it with `git ls-files` |
| 48 | +# so the two scans cover the same file set. |
| 49 | +Push-Location $repoRoot |
| 50 | +try { |
| 51 | + $trackedFiles = & git ls-files |
| 52 | + Write-Host "Running codespell on $($trackedFiles.Count) tracked file(s) with NO ignore list..." |
| 53 | + |
| 54 | + # Write the file list to a temp argsfile and pass it with codespell's "@PATH" |
| 55 | + # syntax. This avoids OS argv length limits on large file lists. |
| 56 | + $argsFile = New-TemporaryFile |
| 57 | + try { |
| 58 | + Set-Content -LiteralPath $argsFile -Value $trackedFiles -Encoding utf8 |
| 59 | + # We don't care about exit code: a non-zero exit just means it found |
| 60 | + # misspellings, which is exactly what we want. |
| 61 | + $rawOutput = & $CodespellCommand "@$argsFile" 2>&1 |
| 62 | + } |
| 63 | + finally { |
| 64 | + Remove-Item -LiteralPath $argsFile -ErrorAction SilentlyContinue |
| 65 | + } |
| 66 | +} |
| 67 | +finally { |
| 68 | + Pop-Location |
| 69 | +} |
| 70 | + |
| 71 | +# Codespell output format: "<path>:<line>: <flagged> ==> <suggestion>" |
| 72 | +# Filter out paths that match the pre-commit exclude regex, then extract the flagged word. |
| 73 | +$flagged = New-Object System.Collections.Generic.HashSet[string] |
| 74 | +foreach ($line in $rawOutput) { |
| 75 | + if ($line -notmatch '^(?<path>[^:]+):\d+:\s+(?<word>\S+)\s+==>') { continue } |
| 76 | + $path = $Matches['path'] |
| 77 | + if ($path -match $excludeRegex) { continue } |
| 78 | + # Suppressions are case-sensitive and matched against the dictionary entry's case. |
| 79 | + [void]$flagged.Add($Matches['word'].ToLowerInvariant()) |
| 80 | +} |
| 81 | + |
| 82 | +Write-Host "Codespell flagged $($flagged.Count) distinct word(s) (after applying exclude regex)." |
| 83 | + |
| 84 | +$unused = $suppressions | Where-Object { -not $flagged.Contains($_.ToLowerInvariant()) } |
| 85 | + |
| 86 | +if (-not $unused) { |
| 87 | + Write-Host "No unused suppressions found. codespell.txt is clean." -ForegroundColor Green |
| 88 | + return |
| 89 | +} |
| 90 | + |
| 91 | +Write-Host "" |
| 92 | +Write-Host "Unused suppressions ($($unused.Count)):" -ForegroundColor Yellow |
| 93 | +$unused | ForEach-Object { Write-Host " $_" } |
| 94 | + |
| 95 | +if ($Apply) { |
| 96 | + $keep = $suppressions | Where-Object { $flagged.Contains($_.ToLowerInvariant()) } |
| 97 | + # Preserve trailing newline that codespell.txt currently has. |
| 98 | + Set-Content -LiteralPath $suppressionsFile -Value $keep -Encoding utf8 |
| 99 | + Write-Host "" |
| 100 | + Write-Host "Removed $($unused.Count) unused entr$(if ($unused.Count -eq 1) { 'y' } else { 'ies' }) from codespell.txt." -ForegroundColor Green |
| 101 | +} else { |
| 102 | + Write-Host "" |
| 103 | + Write-Host "Re-run with -Apply to remove these from codespell.txt." |
| 104 | + exit 1 |
| 105 | +} |
0 commit comments