lib/functions/Private/Get-SqlParserType.ps1
|
function Get-SqlParserType { <# .SYNOPSIS Resolves the ScriptDom TSqlNNNParser type to parse with, or $null when ScriptDom is not loaded. .DESCRIPTION The parser version is discovered by reflection rather than hard-coded. A literal [Microsoft.SqlServer.TransactSql.ScriptDom.TSql160Parser] in the source would bind the module to one T-SQL grammar for ever: bump the pinned package and the newer grammar is downloaded but never used, and a syntax the tenant accepts would still be flagged. Enumerating the assembly means the newest grammar the pinned package ships is picked up automatically. Choosing the NEWEST parser is deliberate. The parsers are cumulative - each version adds syntax rather than removing it - so the newest one is the most permissive, and a permissive parser is what a false-positive-averse feature wants. It can still disagree with the tenant's actual compatibility level, which is exactly why nothing here ever blocks execution and why SqlParserVersion is configurable. .PARAMETER ParserVersion An explicit parser to use, either as the full type name ("TSql160Parser") or as the bare version number ("160"). Omit to take the newest parser in the loaded assembly. .OUTPUTS [Type] the resolved parser type, or $null when ScriptDom is unavailable or the requested version does not exist in it. #> [CmdletBinding()] param( [Parameter(Mandatory = $false, Position = 0)] [AllowNull()] [AllowEmptyString()] [string]$ParserVersion ) # No tracer preamble and no Write-LogOutput: this is called on every keystroke-debounced parse, # and it is on the path the privacy rule in issue #61 section 5 covers. It carries no query text, but # keeping it silent keeps the whole validation path silent by construction. $ParserAssembly = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq "Microsoft.SqlServer.TransactSql.ScriptDom" } | Select-Object -First 1 if ($null -eq $ParserAssembly) { return $null } # The assembly's exported types are enumerated once per loaded assembly, not per parse (issue # #185): GetExportedTypes() over ScriptDom's thousand-odd types was a large share of every # debounced validation, and a loaded assembly's types cannot change. Keyed by the assembly # itself, so a different ScriptDom loaded later is enumerated afresh rather than served from the # first one's list. if ($null -ne $Script:SqlParserTypeCache -and [object]::ReferenceEquals($Script:SqlParserTypeCache.Assembly, $ParserAssembly)) { $ParserType = $Script:SqlParserTypeCache.ParserType } else { try { $ParserType = @($ParserAssembly.GetExportedTypes() | Where-Object { $_.Name -match '^TSql\d+Parser$' }) } catch { return $null } $Script:SqlParserTypeCache = [PSCustomObject]@{ Assembly = $ParserAssembly ParserType = $ParserType } } if ($ParserType.Count -eq 0) { return $null } if (![string]::IsNullOrWhiteSpace($ParserVersion)) { $RequestedName = $ParserVersion.Trim() if ($RequestedName -match '^\d+$') { $RequestedName = "TSql{0}Parser" -f $RequestedName } return ($ParserType | Where-Object { $_.Name -eq $RequestedName } | Select-Object -First 1) } # Sorted numerically, not lexically: "TSql90Parser" sorts after "TSql180Parser" as a string. return ($ParserType | Sort-Object -Property @{ Expression = { [int]($_.Name -replace '^TSql(\d+)Parser$', '$1') } } -Descending | Select-Object -First 1) } |