lib/functions/Private/Get-SqlSyntaxDiagnostic.ps1
|
function Get-SqlSyntaxDiagnostic { <# .SYNOPSIS Parses a T-SQL script with ScriptDom and returns the parse errors as editor diagnostics. .DESCRIPTION The first of the three validation passes described in issue #61. Everything here is local: ScriptDom is the parser SQL Server's own tooling uses, so a parse error carries the wording the server would have returned, without the round trip and without the opaque "External error (ref. no. ...)" that a server-side failure comes back as. The result is a status object rather than a bare collection, because "no diagnostics" and "could not look" are different answers and callers must be able to tell them apart: Status Ok the script was parsed; Diagnostic holds what came back Unavailable ScriptDom is not loaded, so nothing was parsed Diagnostic the parse errors, as marker-shaped objects; empty for a clean or empty script, and always empty when Status is not Ok ParserVersion the TSqlNNNParser actually used, or $null Unavailable is a return value, never an exception. This dependency is optional by design (issue #61 acceptance criterion 6): with it missing the application must behave exactly as it did before the feature existed. The application itself reaches the syntax pass through Get-SqlDiagnostic, which parses once and feeds all three passes from the same tree. This function stays because the syntax pass is meaningful on its own - and because it is the narrowest thing to test. .PARAMETER SqlText The script to parse. Null, empty and whitespace-only input are valid and yield no diagnostics - an empty editor is not a syntax error. .PARAMETER ParserVersion An explicit TSqlNNNParser to use. Omit to take the newest parser the loaded assembly ships. .PARAMETER Source The value written to each diagnostic's Source field, which the editor shows next to the message and which distinguishes this pass from the other two that share the same diagnostics channel. .OUTPUTS [PSCustomObject] with Status, Diagnostic and ParserVersion. #> [CmdletBinding()] param( [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true)] [AllowNull()] [AllowEmptyString()] [string]$SqlText, [Parameter(Mandatory = $false)] [AllowNull()] [AllowEmptyString()] [string]$ParserVersion, [Parameter(Mandatory = $false)] [string]$Source = "T-SQL syntax" ) # DELIBERATELY no $Script:Tracer preamble. Every other function in this module opens with one, # which writes ConvertTo-RedactedLogString -InputObject $PSBoundParameters. Here $PSBoundParameters # IS the user's query text, and query text routinely contains identity data (issue #61 section 5). # The preamble would copy it into the trace on every debounced keystroke. Nothing in this function # logs the script, a fragment of it, or a parse message: the counts below are all that is ever # written, and only at DEBUG. process { try { $Parsed = Get-SqlScriptFragment -SqlText $SqlText -ParserVersion $ParserVersion if ($Parsed.Status -ne "Ok") { return [PSCustomObject]@{ Status = "Unavailable" Diagnostic = @() ParserVersion = $null } } $Diagnostic = @(ConvertTo-SqlSyntaxDiagnostic -ParseError $Parsed.ParseError -SqlText $SqlText -Source $Source) # Count only. The messages carry identifiers lifted straight out of the script # ("Incorrect syntax near 'Person'."), so they are never logged here at any level. "Parsed script with {0}: {1} syntax diagnostic(s)" -f $Parsed.ParserVersion, $Diagnostic.Count | Write-LogOutput -LogType DEBUG return [PSCustomObject]@{ Status = "Ok" Diagnostic = $Diagnostic ParserVersion = $Parsed.ParserVersion } } catch { # A parser that throws must not be worse than a parser that is missing. The exception # message can quote the script, so it is not logged either. "The T-SQL parser failed while checking the query; syntax diagnostics are unavailable for this run." | Write-LogOutput -LogType DEBUG return [PSCustomObject]@{ Status = "Unavailable" Diagnostic = @() ParserVersion = $null } } } } function ConvertTo-SqlSyntaxDiagnostic { <# .SYNOPSIS Maps ScriptDom parse errors onto the shared marker shape. .DESCRIPTION Kept separate from the parse so the same mapping serves both callers: Get-SqlSyntaxDiagnostic, which parses for itself, and Get-SqlDiagnostic, which parses once for all three passes. Two copies of this would be two chances for the syntax markers to land in different places depending on which entry point the application happened to use. .PARAMETER ParseError The errors ScriptDom reported. .PARAMETER SqlText The script they refer to, used only to widen each marker onto the offending token. .PARAMETER Source The pass label written to each diagnostic. .OUTPUTS Marker-shaped [PSCustomObject]s. #> [CmdletBinding()] param( [Parameter(Mandatory = $false, Position = 0)] [AllowNull()] [AllowEmptyCollection()] $ParseError, [Parameter(Mandatory = $false)] [AllowNull()] [AllowEmptyString()] [string]$SqlText, [Parameter(Mandatory = $false)] [string]$Source = "T-SQL syntax" ) # No tracer preamble: the parse messages quote the user's query (issue #61 section 5). return @(foreach ($ParseErrorItem in @($ParseError)) { if ($null -eq $ParseErrorItem) { continue } [PSCustomObject][Ordered]@{ Line = [int]$ParseErrorItem.Line Column = [int]$ParseErrorItem.Column EndLine = [int]$ParseErrorItem.Line EndColumn = Get-SqlDiagnosticEndColumn -SqlText $SqlText -Offset $ParseErrorItem.Offset -Column $ParseErrorItem.Column Severity = "Error" Message = [string]$ParseErrorItem.Message Source = $Source Number = [int]$ParseErrorItem.Number } }) } |