Private/Rules/Primitives/Test-AvmRuleGitignoreMustContain.ps1
|
function Test-AvmRuleGitignoreMustContain { <# .SYNOPSIS Primitive: assert that the .gitignore file in the target root contains every required glob from the rule's RequiredGlobs set. .DESCRIPTION Reads <TargetRoot>/.gitignore. Each line of the file is trimmed and compared by exact (ordinal, case-sensitive) match against each glob in Parameters.RequiredGlobs. Lines that begin with '#' (after trim) are treated as comments and never match. Blank lines never match. Pass: every required glob has at least one matching non-comment line. Fail: at least one required glob is absent. Fix path (with -Fix): if the file is missing or any glob is absent, write a new file (or append to the existing one) that ensures every required glob is present at least once. Existing lines are preserved verbatim and the file gets a trailing newline. With -Fix, the result Status is 'fixed' on any change; no Issues are emitted. Without -Fix, every missing glob produces one Issue. The presence check is exact-string-match on a trimmed line; we do not attempt to expand globs or detect semantic overlap (e.g. '*.tfstate' covers '*.tfstate.backup'). That keeps the rule deterministic and the per-glob diagnostic actionable. Parameters honoured on the rule: - RequiredGlobs (required, string[]) : ordered list of globs. .PARAMETER Rule AvmRule pscustomobject (typically produced by New-AvmRule). .PARAMETER TargetRoot Absolute path to the directory the rule applies to. .PARAMETER Fix When set, creates or appends to <TargetRoot>/.gitignore so that every required glob is present. .OUTPUTS [pscustomobject] with Status, Issues, FilesChanged. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] $Rule, [Parameter(Mandatory)] [string] $TargetRoot, [switch] $Fix ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $requiredGlobs = @($Rule.Parameters.RequiredGlobs | ForEach-Object { [string]$_ }) $gitignorePath = Join-Path $TargetRoot '.gitignore' $existingLines = @() if (Test-Path -LiteralPath $gitignorePath -PathType Leaf) { $raw = [System.IO.File]::ReadAllText($gitignorePath, [System.Text.UTF8Encoding]::new($false)) # Normalise CRLF -> LF before split so a CRLF-edited file still parses cleanly, # then preserve original lines verbatim (no trim) so a fix-write keeps the # author's original whitespace and comments untouched. $existingLines = ($raw -replace "`r`n", "`n").Split("`n") # The trailing newline produces an empty final element from Split -- drop it # so we don't emit a spurious blank line on fix-append. if ($existingLines.Length -gt 0 -and $existingLines[-1] -eq '') { $existingLines = $existingLines[0..($existingLines.Length - 2)] } } # Build the trimmed, comment-stripped set used for membership tests. $presentGlobs = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::Ordinal) foreach ($line in $existingLines) { $trimmed = $line.Trim() if ($trimmed.Length -eq 0) { continue } if ($trimmed.StartsWith('#')) { continue } [void]$presentGlobs.Add($trimmed) } $missing = New-Object 'System.Collections.Generic.List[string]' foreach ($glob in $requiredGlobs) { if (-not $presentGlobs.Contains($glob)) { $missing.Add($glob) } } if ($missing.Count -eq 0) { return [pscustomobject][ordered]@{ Status = 'pass' Issues = @() FilesChanged = 0 } } if ($Fix) { $appendBlock = New-Object 'System.Collections.Generic.List[string]' $appendBlock.AddRange([string[]]$existingLines) foreach ($glob in $missing) { $appendBlock.Add($glob) } $newContent = ($appendBlock -join "`n") + "`n" if ($PSCmdlet.ShouldProcess($gitignorePath, 'Append required globs')) { $utf8NoBom = [System.Text.UTF8Encoding]::new($false) [System.IO.File]::WriteAllText($gitignorePath, $newContent, $utf8NoBom) } return [pscustomobject][ordered]@{ Status = 'fixed' Issues = @() FilesChanged = 1 } } $issues = New-Object 'System.Collections.Generic.List[object]' foreach ($glob in $missing) { $issues.Add([pscustomobject][ordered]@{ File = '.gitignore' Line = 0 Column = 0 Severity = $Rule.Severity Code = $Rule.Id Message = "'.gitignore' is missing required glob '$glob' (run with -Fix to append)." }) } return [pscustomobject][ordered]@{ Status = 'fail' Issues = $issues.ToArray() FilesChanged = 0 } } |