github.com/cheshirekow/buildtools@v0.0.0-20200224190056-5d637702fe81/buildifier/utils/diagnostics.go (about)

     1  package utils
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"github.com/cheshirekow/buildtools/build"
     7  	"github.com/cheshirekow/buildtools/warn"
     8  	"sort"
     9  	"strings"
    10  )
    11  
    12  // Diagnostics contains diagnostic information returned by formatter and linter
    13  type Diagnostics struct {
    14  	Success bool               `json:"success"` // overall success (whether all files are formatted properly and have no warnings)
    15  	Files   []*FileDiagnostics `json:"files"`   // diagnostics per file
    16  }
    17  
    18  // Format formats a Diagnostics object either as plain text or as json
    19  func (d *Diagnostics) Format(format string, verbose bool) string {
    20  	switch format {
    21  	case "text", "":
    22  		var output strings.Builder
    23  		for _, f := range d.Files {
    24  			for _, w := range f.Warnings {
    25  				formatString := "%s:%d: %s: %s (%s)\n"
    26  				if !w.Actionable {
    27  					formatString = "%s:%d: %s: %s [%s]\n"
    28  				}
    29  				output.WriteString(fmt.Sprintf(formatString,
    30  					f.Filename,
    31  					w.Start.Line,
    32  					w.Category,
    33  					w.Message,
    34  					w.URL))
    35  			}
    36  			if !f.Formatted {
    37  				rewrites := []string{}
    38  				for category, count := range f.Rewrites {
    39  					if count > 0 {
    40  						rewrites = append(rewrites, category)
    41  					}
    42  				}
    43  				log := ""
    44  				if len(rewrites) > 0 {
    45  					sort.Strings(rewrites)
    46  					log = " " + strings.Join(rewrites, " ")
    47  				}
    48  				output.WriteString(fmt.Sprintf("%s # reformat%s\n", f.Filename, log))
    49  			}
    50  		}
    51  		return output.String()
    52  	case "json":
    53  		var result []byte
    54  		if verbose {
    55  			result, _ = json.MarshalIndent(*d, "", "    ")
    56  		} else {
    57  			result, _ = json.Marshal(*d)
    58  		}
    59  		return string(result) + "\n"
    60  	}
    61  	return ""
    62  }
    63  
    64  // FileDiagnostics contains diagnostics information for a file
    65  type FileDiagnostics struct {
    66  	Filename  string         `json:"filename"`
    67  	Formatted bool           `json:"formatted"`
    68  	Valid     bool           `json:"valid"`
    69  	Warnings  []*warning     `json:"warnings"`
    70  	Rewrites  map[string]int `json:"rewrites,omitempty"`
    71  }
    72  
    73  // SetRewrites adds information about rewrites to the diagnostics
    74  func (fd *FileDiagnostics) SetRewrites(categories map[string]int) {
    75  	for category, count := range categories {
    76  		if count > 0 {
    77  			fd.Rewrites[category] = count
    78  		}
    79  	}
    80  }
    81  
    82  type warning struct {
    83  	Start      position `json:"start"`
    84  	End        position `json:"end"`
    85  	Category   string   `json:"category"`
    86  	Actionable bool     `json:"actionable"`
    87  	Message    string   `json:"message"`
    88  	URL        string   `json:"url"`
    89  }
    90  
    91  type position struct {
    92  	Line   int `json:"line"`
    93  	Column int `json:"column"`
    94  }
    95  
    96  // NewDiagnostics returns a new Diagnostics object
    97  func NewDiagnostics(fileDiagnostics ...*FileDiagnostics) *Diagnostics {
    98  	diagnostics := &Diagnostics{
    99  		Success: true,
   100  		Files:   fileDiagnostics,
   101  	}
   102  	for _, file := range diagnostics.Files {
   103  		if !file.Formatted || len(file.Warnings) > 0 {
   104  			diagnostics.Success = false
   105  			break
   106  		}
   107  	}
   108  	return diagnostics
   109  }
   110  
   111  // NewFileDiagnostics returns a new FileDiagnostics object
   112  func NewFileDiagnostics(filename string, warnings []*warn.Finding) *FileDiagnostics {
   113  	fileDiagnostics := FileDiagnostics{
   114  		Filename:  filename,
   115  		Formatted: true,
   116  		Valid:     true,
   117  		Warnings:  []*warning{},
   118  		Rewrites:  map[string]int{},
   119  	}
   120  
   121  	for _, w := range warnings {
   122  		fileDiagnostics.Warnings = append(fileDiagnostics.Warnings, &warning{
   123  			Start:      makePosition(w.Start),
   124  			End:        makePosition(w.End),
   125  			Category:   w.Category,
   126  			Actionable: w.Actionable,
   127  			Message:    w.Message,
   128  			URL:        w.URL,
   129  		})
   130  	}
   131  
   132  	return &fileDiagnostics
   133  }
   134  
   135  // InvalidFileDiagnostics returns a new FileDiagnostics object for an invalid file
   136  func InvalidFileDiagnostics(filename string) *FileDiagnostics {
   137  	fileDiagnostics := &FileDiagnostics{
   138  		Filename:  filename,
   139  		Formatted: false,
   140  		Valid:     false,
   141  		Warnings:  []*warning{},
   142  		Rewrites:  map[string]int{},
   143  	}
   144  	if filename == "" {
   145  		fileDiagnostics.Filename = "<stdin>"
   146  	}
   147  	return fileDiagnostics
   148  }
   149  
   150  func makePosition(p build.Position) position {
   151  	return position{
   152  		Line:   p.Line,
   153  		Column: p.LineRune,
   154  	}
   155  }