Skip to content

Commit

Permalink
Added gcc output parser
Browse files Browse the repository at this point in the history
  • Loading branch information
cmaglie committed May 19, 2023
1 parent a0a01d2 commit 1ab5f99
Show file tree
Hide file tree
Showing 9 changed files with 886 additions and 0 deletions.
169 changes: 169 additions & 0 deletions internal/builder/diagnostics/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// This file is part of arduino-cli.
//
// Copyright 2023 ARDUINO SA (http://www.arduino.cc/)
//
// This software is released under the GNU General Public License version 3,
// which covers the main part of arduino-cli.
// The terms of this license can be found at:
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to [email protected].

package diagnostics

import (
"fmt"
"strings"

rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
)

// Diagnostics represents a list of diagnostics
type Diagnostics []*Diagnostic

// Diagnostic represents a diagnostic (a compiler error, warning, note, etc.)
type Diagnostic struct {
Severity Severity `json:"severity,omitempty"`
Message string `json:"message"`
File string `json:"file,omitempty"`
Line int `json:"line,omitempty"`
Column int `json:"col,omitempty"`
Context FullContext `json:"context,omitempty"`
Suggestions Notes `json:"suggestions,omitempty"`
}

// Severity is a diagnostic severity
type Severity string

const (
// SeverityUnspecified is the undefined severity
SeverityUnspecified Severity = ""
// SeverityWarning is a warning
SeverityWarning = "WARNING"
// SeverityError is an error
SeverityError = "ERROR"
// SeverityFatal is a fatal error
SeverityFatal = "FATAL"
)

// Notes represents a list of Note
type Notes []*Note

// Note represents a compiler annotation or suggestion
type Note struct {
Message string `json:"message"`
File string `json:"file,omitempty"`
Line int `json:"line,omitempty"`
Column int `json:"col,omitempty"`
}

// FullContext represents a list of Context
type FullContext []*Context

// Context represents a context, i.e. a reference to a file, line and column
// or a part of the code that a Diagnostic refers to.
type Context struct {
Message string `json:"message"`
File string `json:"file,omitempty"`
Line int `json:"line,omitempty"`
Column int `json:"col,omitempty"`
}

// ParseCompilerOutput parses the output of a compiler and returns a list of
// diagnostics.
func ParseCompilerOutput(compiler *DetectedCompiler, out []byte) ([]*Diagnostic, error) {
lines := splitLines(out)
switch compiler.Family {
case "gcc":
return parseGccOutput(lines)
default:
return nil, fmt.Errorf("unsupported compiler: %s", compiler)
}
}

func splitLines(in []byte) []string {
res := strings.Split(string(in), "\n")
for i, line := range res {
res[i] = strings.TrimSuffix(line, "\r")
}
if l := len(res) - 1; res[l] == "" {
res = res[:l]
}
return res
}

// ToRPC converts a Diagnostics to a slice of rpc.CompileDiagnostic
func (d Diagnostics) ToRPC() []*rpc.CompileDiagnostic {
if len(d) == 0 {
return nil
}
var res []*rpc.CompileDiagnostic
for _, diag := range d {
res = append(res, diag.ToRPC())
}
return res
}

// ToRPC converts a Diagnostic to a rpc.CompileDiagnostic
func (d *Diagnostic) ToRPC() *rpc.CompileDiagnostic {
if d == nil {
return nil
}
return &rpc.CompileDiagnostic{
Severity: string(d.Severity),
Message: d.Message,
File: d.File,
Line: int64(d.Line),
Column: int64(d.Column),
Context: d.Context.ToRPC(),
Notes: d.Suggestions.ToRPC(),
}
}

// ToRPC converts a Notes to a slice of rpc.CompileDiagnosticNote
func (s Notes) ToRPC() []*rpc.CompileDiagnosticNote {
var res []*rpc.CompileDiagnosticNote
for _, suggestion := range s {
res = append(res, suggestion.ToRPC())
}
return res
}

// ToRPC converts a Note to a rpc.CompileDiagnosticNote
func (s *Note) ToRPC() *rpc.CompileDiagnosticNote {
if s == nil {
return nil
}
return &rpc.CompileDiagnosticNote{
File: s.File,
Line: int64(s.Line),
Column: int64(s.Column),
Message: s.Message,
}
}

// ToRPC converts a FullContext to a slice of rpc.CompileDiagnosticContext
func (t FullContext) ToRPC() []*rpc.CompileDiagnosticContext {
var res []*rpc.CompileDiagnosticContext
for _, trace := range t {
res = append(res, trace.ToRPC())
}
return res
}

// ToRPC converts a Context to a rpc.CompileDiagnosticContext
func (d *Context) ToRPC() *rpc.CompileDiagnosticContext {
if d == nil {
return nil
}
return &rpc.CompileDiagnosticContext{
File: d.File,
Line: int64(d.Line),
Column: int64(d.Column),
Message: d.Message,
}
}
182 changes: 182 additions & 0 deletions internal/builder/diagnostics/parser_gcc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// This file is part of arduino-cli.
//
// Copyright 2023 ARDUINO SA (http://www.arduino.cc/)
//
// This software is released under the GNU General Public License version 3,
// which covers the main part of arduino-cli.
// The terms of this license can be found at:
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to [email protected].

package diagnostics

import (
"strconv"
"strings"
)

// Parse output from gcc compiler and extract diagnostics
func parseGccOutput(output []string) ([]*Diagnostic, error) {
// Output from gcc is a mix of diagnostics and other information.
//
// 1. include trace lines:
//
// In file included from /home/megabug/Arduino/libraries/Audio/src/Audio.h:16:0,
// ·················from /home/megabug/Arduino/Blink/Blink.ino:1:
//
// 2. in-file context lines:
//
// /home/megabug/Arduino/libraries/Audio/src/DAC.h: In member function 'void DACClass::enableInterrupts()':
//
// 3. actual diagnostic lines:
//
// /home/megabug/Arduino/libraries/Audio/src/DAC.h:31:44: fatal error: 'isrId' was not declared in this scope
//
// /home/megabug/Arduino/libraries/Audio/src/DAC.h:31:44: error: 'isrId' was not declared in this scope
//
// /home/megabug/Arduino/libraries/Audio/src/DAC.h:31:44: warning: 'isrId' was not declared in this scope
//
// 4. annotations or suggestions:
//
// /home/megabug/Arduino/Blink/Blink.ino:4:1: note: suggested alternative: 'rand'
//
// 5. extra context lines with an extract of the code that errors refers to:
//
// ·asd;
// ·^~~
// ·rand
//
// ·void enableInterrupts() { NVIC_EnableIRQ(isrId); };
// ···········································^~~~~

var fullContext FullContext
var fullContextRefersTo string
var inFileContext *Context
var currentDiagnostic *Diagnostic
var currentMessage *string
var res []*Diagnostic

for _, in := range output {
isTrace := false
if strings.HasPrefix(in, "In file included from ") {
in = strings.TrimPrefix(in, "In file included from ")
// 1. include trace
isTrace = true
inFileContext = nil
fullContext = nil
fullContextRefersTo = ""
} else if strings.HasPrefix(in, " from ") {
in = strings.TrimPrefix(in, " from ")
// 1. include trace continuation
isTrace = true
}
if isTrace {
in = strings.TrimSuffix(in, ",")
file, line, col := extractFileLineAndColumn(in)
context := &Context{
File: file,
Line: line,
Column: col,
Message: "included from here",
}
currentMessage = &context.Message
fullContext = append(fullContext, context)
continue
}

if split := strings.SplitN(in, ": ", 2); len(split) == 2 {
file, line, column := extractFileLineAndColumn(split[0])
msg := split[1]

if line == 0 && column == 0 {
// 2. in-file context
inFileContext = &Context{
Message: msg,
File: file,
}
currentMessage = &inFileContext.Message
continue
}

if strings.HasPrefix(msg, "note: ") {
msg = strings.TrimPrefix(msg, "note: ")
// 4. annotations or suggestions
if currentDiagnostic != nil {
suggestion := &Note{
Message: msg,
File: file,
Line: line,
Column: column,
}
currentDiagnostic.Suggestions = append(currentDiagnostic.Suggestions, suggestion)
currentMessage = &suggestion.Message
}
continue
}

severity := SeverityUnspecified
if strings.HasPrefix(msg, "error: ") {
msg = strings.TrimPrefix(msg, "error: ")
severity = SeverityError
} else if strings.HasPrefix(msg, "warning: ") {
msg = strings.TrimPrefix(msg, "warning: ")
severity = SeverityWarning
} else if strings.HasPrefix(msg, "fatal error: ") {
msg = strings.TrimPrefix(msg, "fatal error: ")
severity = SeverityFatal
}
if severity != SeverityUnspecified {
// 3. actual diagnostic lines
currentDiagnostic = &Diagnostic{
Severity: severity,
Message: msg,
File: file,
Line: line,
Column: column,
}
currentMessage = &currentDiagnostic.Message

if len(fullContext) > 0 {
if fullContextRefersTo == "" || fullContextRefersTo == file {
fullContextRefersTo = file
currentDiagnostic.Context = append(currentDiagnostic.Context, fullContext...)
}
}
if inFileContext != nil && inFileContext.File == file {
currentDiagnostic.Context = append(currentDiagnostic.Context, inFileContext)
}

res = append(res, currentDiagnostic)
continue
}
}

// 5. extra context lines
if strings.HasPrefix(in, " ") {
if currentMessage != nil {
*currentMessage += "\n" + in
}
continue
}
}
return res, nil
}

func extractFileLineAndColumn(file string) (string, int, int) {
split := strings.Split(file, ":")
file = split[0]
if len(split) == 1 {
return file, 0, 0
}
line, err := strconv.Atoi(split[1])
if err != nil || len(split) == 2 {
return file, line, 0
}
column, _ := strconv.Atoi(split[2])
return file, line, column
}
Loading

0 comments on commit 1ab5f99

Please sign in to comment.