Tools/Test-DuplicateParameterBinding.ps1
|
<#
.SYNOPSIS Findet doppelte Parameterbindungen (Splat-Hashtable + gleichnamiger expliziter Parameter). .DESCRIPTION Sucht das Muster, das Windows PowerShell 5.1 mit einer ParameterBindingException (ParameterAlreadyBound) abbricht, waehrend PowerShell 7 es stillschweigend toleriert: $connParams = @{ SqlInstance = $s; ErrorAction = 'Stop' } Invoke-DbaQuery @connParams -Query $q -ErrorAction SilentlyContinue # <- Crash unter 5.1 Weil die Entwicklung ueblicherweise unter PS 7 laeuft, die Module aber PowerShellVersion 5.1 deklarieren und dort ausgeliefert werden, faellt so ein Fehler erst beim Kunden auf. Ein reiner Import-Test findet ihn nicht, da er erst zur Aufrufzeit auftritt. Geprueft werden alle Common Parameter, nicht nur ErrorAction. Erkannt werden sowohl Hashtable-Literale als auch nachtraegliche Index-Zuweisungen ($p['ErrorAction'] = 'Stop'). Parameter-Abkuerzungen (-ea, -ErrorAct) werden ueber einen Praefix-Vergleich mit erfasst. .PARAMETER Root Wurzelverzeichnis der Pruefung. Standard ist das uebergeordnete Verzeichnis dieses Skripts, also das Repository-Wurzelverzeichnis. .PARAMETER ExcludeDirectory Verzeichnisnamen, die uebersprungen werden. Standard: .git, bin, obj, node_modules. .PARAMETER Quiet Unterdrueckt die Ausgabe. Nur der Exit-Code zaehlt. .OUTPUTS Exit-Code 0 = sauber, 1 = mindestens ein Treffer. .NOTES Autor : Uwe Janke - powershelldba.de Anlass: sqmDataTransfer 0.1.17.1, wo genau dieses Muster jeden regulaeren Resume-Lauf unter Windows PowerShell 5.1 abgebrochen hat. #> [CmdletBinding()] param( [string]$Root, [string[]]$ExcludeDirectory = @('.git', 'bin', 'obj', 'node_modules'), [switch]$Quiet ) if (-not $Root) { $Root = Split-Path -Parent $PSScriptRoot } $Root = (Resolve-Path -Path $Root).ProviderPath # Common Parameter, die PowerShell an jedes Advanced Function bindet. Genau diese koennen aus # einer Hashtable und zusaetzlich explizit kommen, ohne dass ein Tippfehler vorliegt. $commonParameters = @( 'ErrorAction', 'WarningAction', 'InformationAction', 'Verbose', 'Debug', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'WhatIf', 'Confirm' ) $files = Get-ChildItem -Path $Root -Recurse -Include '*.ps1', '*.psm1' -File | Where-Object { $relative = $_.FullName.Substring($Root.Length).TrimStart('\', '/') $segments = $relative -split '[\\/]' -not ($segments | Where-Object { $ExcludeDirectory -contains $_ }) } $findings = @() foreach ($file in $files) { $parseErrors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$null, [ref]$parseErrors) if (-not $ast) { continue } # 1. Variablen sammeln, deren Hashtable einen Common Parameter als Schluessel traegt. $tainted = @{ } $assignments = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.AssignmentStatementAst] }, $true) foreach ($assignment in $assignments) { # 1a. $p = @{ ErrorAction = 'Stop' } if ($assignment.Left -is [System.Management.Automation.Language.VariableExpressionAst]) { $variableName = $assignment.Left.VariablePath.UserPath $hashtables = $assignment.Right.FindAll({ $args[0] -is [System.Management.Automation.Language.HashtableAst] }, $true) foreach ($hashtable in $hashtables) { foreach ($pair in $hashtable.KeyValuePairs) { $key = $pair.Item1.Extent.Text.Trim("'", '"', ' ') if ($commonParameters -contains $key) { if (-not $tainted.ContainsKey($variableName)) { $tainted[$variableName] = @() } if ($tainted[$variableName] -notcontains $key) { $tainted[$variableName] += $key } } } } } # 1b. $p['ErrorAction'] = 'Stop' if ($assignment.Left -is [System.Management.Automation.Language.IndexExpressionAst] -and $assignment.Left.Target -is [System.Management.Automation.Language.VariableExpressionAst]) { $variableName = $assignment.Left.Target.VariablePath.UserPath $key = $assignment.Left.Index.Extent.Text.Trim("'", '"', ' ') if ($commonParameters -contains $key) { if (-not $tainted.ContainsKey($variableName)) { $tainted[$variableName] = @() } if ($tainted[$variableName] -notcontains $key) { $tainted[$variableName] += $key } } } } if ($tainted.Count -eq 0) { continue } # 2. Aufrufe finden, die so eine Variable splatten UND denselben Parameter explizit setzen. $commands = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst] }, $true) foreach ($command in $commands) { $splattedVariables = @() $explicitParameters = @() foreach ($element in $command.CommandElements) { if ($element -is [System.Management.Automation.Language.VariableExpressionAst] -and $element.Splatted) { $splattedVariables += $element.VariablePath.UserPath } elseif ($element -is [System.Management.Automation.Language.CommandParameterAst]) { $explicitParameters += $element.ParameterName } } if ($splattedVariables.Count -eq 0 -or $explicitParameters.Count -eq 0) { continue } foreach ($variableName in $splattedVariables) { if (-not $tainted.ContainsKey($variableName)) { continue } foreach ($parameter in $tainted[$variableName]) { # Abkuerzungen mit erfassen: -ea, -ErrorAct, ... binden denselben Parameter. $matched = $explicitParameters | Where-Object { $_.Length -ge 2 -and $parameter -like "$_*" } if ($matched) { $findings += [pscustomobject]@{ File = $file.FullName.Substring($Root.Length).TrimStart('\', '/') Line = $command.Extent.StartLineNumber Command = $command.GetCommandName() Parameter = $parameter Splat = '$' + $variableName } } } } } } if ($findings) { if (-not $Quiet) { Write-Host "" Write-Host "Doppelte Parameterbindung gefunden - crasht unter Windows PowerShell 5.1:" -ForegroundColor Red $findings | Sort-Object File, Line | Format-Table -AutoSize | Out-String -Width 200 | Write-Host Write-Host "Behebung: entweder den Common Parameter aus der Splat-Hashtable entfernen und" -ForegroundColor Yellow Write-Host "explizit am Aufruf setzen, oder den expliziten Parameter weglassen." -ForegroundColor Yellow Write-Host "" } exit 1 } if (-not $Quiet) { Write-Host "Keine doppelten Parameterbindungen in '$Root' ($($files.Count) Datei(en) geprueft)." -ForegroundColor Green } exit 0 |