File: CommandLine\CommandLineDiagnosticFormatter.vb
Web Access
Project: src\src\Compilers\VisualBasic\Portable\Microsoft.CodeAnalysis.VisualBasic.vbproj (Microsoft.CodeAnalysis.VisualBasic)
' Licensed to the .NET Foundation under one or more agreements.
' The .NET Foundation licenses this file to you under the MIT license.
' See the LICENSE file in the project root for more information.
 
Imports System.Collections.Immutable
Imports System.Runtime.InteropServices
Imports System.Text
Imports Microsoft.CodeAnalysis.Text
 
Namespace Microsoft.CodeAnalysis.VisualBasic
    Friend NotInheritable Class CommandLineDiagnosticFormatter
        Inherits VisualBasicDiagnosticFormatter
 
        Private ReadOnly _baseDirectory As String
        Private ReadOnly _getAdditionalTextFiles As Func(Of ImmutableArray(Of AdditionalTextFile))
 
        Friend Sub New(baseDirectory As String, getAdditionalTextFiles As Func(Of ImmutableArray(Of AdditionalTextFile)))
            _baseDirectory = baseDirectory
            _getAdditionalTextFiles = getAdditionalTextFiles
        End Sub
 
        ' Returns a diagnostic message in string.
        ' VB has a special implementation that prints out a squiggle under the error span as well as a diagnostic message.
        ' e.g.,
        '   c:\Roslyn\Temp\a.vb(5) : warning BC42024: Unused local variable: 'x'.
        '
        '       Dim x As Integer
        '           ~
        '
        Public Overrides Function Format(diagnostic As Diagnostic, Optional formatter As IFormatProvider = Nothing) As String
            ' Builds a diagnostic message
            ' Dev12 vbc prints raw paths -- relative and not normalized, so we don't need to customize the base implementation.
 
            Dim text As SourceText = Nothing
            Dim diagnosticSpanOpt = GetDiagnosticSpanAndFileText(diagnostic, text)
 
            If Not diagnosticSpanOpt.HasValue OrElse
                text Is Nothing OrElse
                text.Length < diagnosticSpanOpt.Value.End Then
                ' Diagnostic either has Location.None OR an invalid location OR invalid file contents.
                ' For all these cases, format the diagnostic as a no-location project level diagnostic.
 
                ' Strip location, if required.
                If diagnostic.Location <> Location.None Then
                    diagnostic = diagnostic.WithLocation(Location.None)
                End If
 
                ' Add "vbc : " command line prefix to the start of the command line diagnostics which do not have a location to match the 
                ' behavior of native compiler.    This allows MSBuild output to be consistent whether Roslyn is installed or not.      
                Return VisualBasicCompiler.VbcCommandLinePrefix &
                    MyBase.Format(diagnostic, formatter)
            End If
 
            Dim baseMessage = MyBase.Format(diagnostic, formatter)
 
            Dim sb As New StringBuilder()
            sb.AppendLine(baseMessage)
 
            ' the squiggles are displayed for the original (unmapped) location
            Dim sourceSpan = diagnosticSpanOpt.Value
            Dim sourceSpanStart = sourceSpan.Start
            Dim sourceSpanEnd = sourceSpan.End
            Dim linenumber = text.Lines.IndexOf(sourceSpanStart)
            Dim line = text.Lines(linenumber)
 
            If sourceSpan.IsEmpty AndAlso line.Start = sourceSpanEnd AndAlso linenumber > 0 Then
                ' Sometimes there is something missing at the end of the line, then the error is reported with an empty span
                ' beyond the end of the line, which makes it appear that the span starts at the beginning of the next line.
                ' Let's go back to the previous line in this case.
                linenumber -= 1
                line = text.Lines(linenumber)
            End If
 
            While (line.Start < sourceSpanEnd)
                ' Builds the original text line
                sb.AppendLine()
                sb.AppendLine(line.ToString().Replace(vbTab, "    ")) ' normalize tabs with 4 spaces
 
                ' Builds leading spaces up to the error span
                For position = Math.Min(sourceSpanStart, line.Start) To Math.Min(line.End, sourceSpanStart) - 1
                    If (text(position) = vbTab) Then
                        ' normalize tabs with 4 spaces
                        sb.Append(" "c, 4)
                    Else
                        sb.Append(" ")
                    End If
                Next
 
                ' Builds squiggles
                If sourceSpan.IsEmpty Then
                    sb.Append("~")
                Else
                    For position = Math.Max(sourceSpanStart, line.Start) To Math.Min(If(sourceSpanEnd = sourceSpanStart, sourceSpanEnd, sourceSpanEnd - 1), line.End - 1)
                        If (text(position) = vbTab) Then
                            ' normalize tabs with 4 spaces
                            sb.Append("~"c, 4)
                        Else
                            sb.Append("~")
                        End If
                    Next
                End If
 
                ' Builds trailing spaces up to the end of this line
                For position = Math.Min(sourceSpanEnd, line.End) To line.End - 1
                    If (text(position) = vbTab) Then
                        ' normalize tabs with 4 spaces
                        sb.Append(" "c, 4)
                    Else
                        sb.Append(" ")
                    End If
                Next
 
                ' The error span can continue over multiple lines
                linenumber = linenumber + 1
                If linenumber >= text.Lines.Count Then
                    ' Exit the loop when we reach the end line (0-based)
                    Exit While
                End If
 
                line = text.Lines(linenumber)
            End While
 
            Return sb.ToString()
        End Function
 
        Friend Overrides Function FormatSourcePath(path As String, basePath As String, formatter As IFormatProvider) As String
            Return If(FileUtilities.NormalizeRelativePath(path, basePath, _baseDirectory), path)
        End Function
 
        Private Function GetDiagnosticSpanAndFileText(diagnostic As Diagnostic, <Out> ByRef text As SourceText) As TextSpan?
            If diagnostic.Location.IsInSource Then
                text = diagnostic.Location.SourceTree.GetText()
                Return diagnostic.Location.SourceSpan
            End If
 
            If diagnostic.Location.Kind = LocationKind.ExternalFile Then
                Dim path = diagnostic.Location.GetLineSpan().Path
                If path IsNot Nothing Then
                    For Each additionalTextFile In _getAdditionalTextFiles()
                        If path.Equals(additionalTextFile.Path) Then
                            Try
                                text = additionalTextFile.GetText()
                            Catch
                                text = Nothing
                            End Try
 
                            Return diagnostic.Location.SourceSpan
                        End If
                    Next
                End If
            End If
 
            text = Nothing
            Return Nothing
        End Function
    End Class
End Namespace