Skip to content

Commit aea5b2c

Browse files
committed
Added gcc output parser
1 parent a0a01d2 commit aea5b2c

File tree

9 files changed

+886
-0
lines changed

9 files changed

+886
-0
lines changed

Diff for: internal/builder/diagnostics/parser.go

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2023 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package diagnostics
17+
18+
import (
19+
"fmt"
20+
"strings"
21+
22+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
23+
)
24+
25+
// Diagnostics represents a list of diagnostics
26+
type Diagnostics []*Diagnostic
27+
28+
// Diagnostic represents a diagnostic (a compiler error, warning, note, etc.)
29+
type Diagnostic struct {
30+
Severity Severity `json:"severity,omitempty"`
31+
Message string `json:"message"`
32+
File string `json:"file,omitempty"`
33+
Line int `json:"line,omitempty"`
34+
Column int `json:"col,omitempty"`
35+
Context FullContext `json:"context,omitempty"`
36+
Suggestions Notes `json:"suggestions,omitempty"`
37+
}
38+
39+
// Severity is a diagnostic severity
40+
type Severity string
41+
42+
const (
43+
// SeverityUnspecified is the undefined severity
44+
SeverityUnspecified Severity = ""
45+
// SeverityWarning is a warning
46+
SeverityWarning = "WARNING"
47+
// SeverityError is an error
48+
SeverityError = "ERROR"
49+
// SeverityFatal is a fatal error
50+
SeverityFatal = "FATAL"
51+
)
52+
53+
// Notes represents a list of Note
54+
type Notes []*Note
55+
56+
// Note represents a compiler annotation or suggestion
57+
type Note struct {
58+
Message string `json:"message"`
59+
File string `json:"file,omitempty"`
60+
Line int `json:"line,omitempty"`
61+
Column int `json:"col,omitempty"`
62+
}
63+
64+
// FullContext represents a list of Context
65+
type FullContext []*Context
66+
67+
// Context represents a context, i.e. a reference to a file, line and column
68+
// or a part of the code that a Diagnostic refers to.
69+
type Context struct {
70+
Message string `json:"message"`
71+
File string `json:"file,omitempty"`
72+
Line int `json:"line,omitempty"`
73+
Column int `json:"col,omitempty"`
74+
}
75+
76+
// ParseCompilerOutput parses the output of a compiler and returns a list of
77+
// diagnostics.
78+
func ParseCompilerOutput(compiler *DetectedCompiler, out []byte) ([]*Diagnostic, error) {
79+
lines := splitLines(out)
80+
switch compiler.Family {
81+
case "gcc":
82+
return parseGccOutput(lines)
83+
default:
84+
return nil, fmt.Errorf("unsupported compiler: %s", compiler)
85+
}
86+
}
87+
88+
func splitLines(in []byte) []string {
89+
res := strings.Split(string(in), "\n")
90+
for i, line := range res {
91+
res[i] = strings.TrimSuffix(line, "\r")
92+
}
93+
if l := len(res) - 1; res[l] == "" {
94+
res = res[:l]
95+
}
96+
return res
97+
}
98+
99+
// ToRPC converts a Diagnostics to a slice of rpc.CompileDiagnostic
100+
func (d Diagnostics) ToRPC() []*rpc.CompileDiagnostic {
101+
if len(d) == 0 {
102+
return nil
103+
}
104+
var res []*rpc.CompileDiagnostic
105+
for _, diag := range d {
106+
res = append(res, diag.ToRPC())
107+
}
108+
return res
109+
}
110+
111+
// ToRPC converts a Diagnostic to a rpc.CompileDiagnostic
112+
func (d *Diagnostic) ToRPC() *rpc.CompileDiagnostic {
113+
if d == nil {
114+
return nil
115+
}
116+
return &rpc.CompileDiagnostic{
117+
Severity: string(d.Severity),
118+
Message: d.Message,
119+
File: d.File,
120+
Line: int64(d.Line),
121+
Column: int64(d.Column),
122+
Context: d.Context.ToRPC(),
123+
Notes: d.Suggestions.ToRPC(),
124+
}
125+
}
126+
127+
// ToRPC converts a Notes to a slice of rpc.CompileDiagnosticNote
128+
func (s Notes) ToRPC() []*rpc.CompileDiagnosticNote {
129+
var res []*rpc.CompileDiagnosticNote
130+
for _, suggestion := range s {
131+
res = append(res, suggestion.ToRPC())
132+
}
133+
return res
134+
}
135+
136+
// ToRPC converts a Note to a rpc.CompileDiagnosticNote
137+
func (s *Note) ToRPC() *rpc.CompileDiagnosticNote {
138+
if s == nil {
139+
return nil
140+
}
141+
return &rpc.CompileDiagnosticNote{
142+
File: s.File,
143+
Line: int64(s.Line),
144+
Column: int64(s.Column),
145+
Message: s.Message,
146+
}
147+
}
148+
149+
// ToRPC converts a FullContext to a slice of rpc.CompileDiagnosticContext
150+
func (t FullContext) ToRPC() []*rpc.CompileDiagnosticContext {
151+
var res []*rpc.CompileDiagnosticContext
152+
for _, trace := range t {
153+
res = append(res, trace.ToRPC())
154+
}
155+
return res
156+
}
157+
158+
// ToRPC converts a Context to a rpc.CompileDiagnosticContext
159+
func (d *Context) ToRPC() *rpc.CompileDiagnosticContext {
160+
if d == nil {
161+
return nil
162+
}
163+
return &rpc.CompileDiagnosticContext{
164+
File: d.File,
165+
Line: int64(d.Line),
166+
Column: int64(d.Column),
167+
Message: d.Message,
168+
}
169+
}

Diff for: internal/builder/diagnostics/parser_gcc.go

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2023 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package diagnostics
17+
18+
import (
19+
"strconv"
20+
"strings"
21+
)
22+
23+
// Parse output from gcc compiler and extract diagnostics
24+
func parseGccOutput(output []string) ([]*Diagnostic, error) {
25+
// Output from gcc is a mix of diagnostics and other information.
26+
//
27+
// 1. include trace lines:
28+
//
29+
// In file included from /home/megabug/Arduino/libraries/Audio/src/Audio.h:16:0,
30+
// ·················from /home/megabug/Arduino/Blink/Blink.ino:1:
31+
//
32+
// 2. in-file context lines:
33+
//
34+
// /home/megabug/Arduino/libraries/Audio/src/DAC.h: In member function 'void DACClass::enableInterrupts()':
35+
//
36+
// 3. actual diagnostic lines:
37+
//
38+
// /home/megabug/Arduino/libraries/Audio/src/DAC.h:31:44: fatal error: 'isrId' was not declared in this scope
39+
//
40+
// /home/megabug/Arduino/libraries/Audio/src/DAC.h:31:44: error: 'isrId' was not declared in this scope
41+
//
42+
// /home/megabug/Arduino/libraries/Audio/src/DAC.h:31:44: warning: 'isrId' was not declared in this scope
43+
//
44+
// 4. annotations or suggestions:
45+
//
46+
// /home/megabug/Arduino/Blink/Blink.ino:4:1: note: suggested alternative: 'rand'
47+
//
48+
// 5. extra context lines with an extract of the code that errors refers to:
49+
//
50+
// ·asd;
51+
// ·^~~
52+
// ·rand
53+
//
54+
// ·void enableInterrupts() { NVIC_EnableIRQ(isrId); };
55+
// ···········································^~~~~
56+
57+
var fullContext FullContext
58+
var fullContextRefersTo string
59+
var inFileContext *Context
60+
var currentDiagnostic *Diagnostic
61+
var currentMessage *string
62+
var res []*Diagnostic
63+
64+
for _, in := range output {
65+
isTrace := false
66+
if strings.HasPrefix(in, "In file included from ") {
67+
in = strings.TrimPrefix(in, "In file included from ")
68+
// 1. include trace
69+
isTrace = true
70+
inFileContext = nil
71+
fullContext = nil
72+
fullContextRefersTo = ""
73+
} else if strings.HasPrefix(in, " from ") {
74+
in = strings.TrimPrefix(in, " from ")
75+
// 1. include trace continuation
76+
isTrace = true
77+
}
78+
if isTrace {
79+
in = strings.TrimSuffix(in, ",")
80+
file, line, col := extractFileLineAndColumn(in)
81+
context := &Context{
82+
File: file,
83+
Line: line,
84+
Column: col,
85+
Message: "included from here",
86+
}
87+
currentMessage = &context.Message
88+
fullContext = append(fullContext, context)
89+
continue
90+
}
91+
92+
if split := strings.SplitN(in, ": ", 2); len(split) == 2 {
93+
file, line, column := extractFileLineAndColumn(split[0])
94+
msg := split[1]
95+
96+
if line == 0 && column == 0 {
97+
// 2. in-file context
98+
inFileContext = &Context{
99+
Message: msg,
100+
File: file,
101+
}
102+
currentMessage = &inFileContext.Message
103+
continue
104+
}
105+
106+
if strings.HasPrefix(msg, "note: ") {
107+
msg = strings.TrimPrefix(msg, "note: ")
108+
// 4. annotations or suggestions
109+
if currentDiagnostic != nil {
110+
suggestion := &Note{
111+
Message: msg,
112+
File: file,
113+
Line: line,
114+
Column: column,
115+
}
116+
currentDiagnostic.Suggestions = append(currentDiagnostic.Suggestions, suggestion)
117+
currentMessage = &suggestion.Message
118+
}
119+
continue
120+
}
121+
122+
severity := SeverityUnspecified
123+
if strings.HasPrefix(msg, "error: ") {
124+
msg = strings.TrimPrefix(msg, "error: ")
125+
severity = SeverityError
126+
} else if strings.HasPrefix(msg, "warning: ") {
127+
msg = strings.TrimPrefix(msg, "warning: ")
128+
severity = SeverityWarning
129+
} else if strings.HasPrefix(msg, "fatal error: ") {
130+
msg = strings.TrimPrefix(msg, "fatal error: ")
131+
severity = SeverityFatal
132+
}
133+
if severity != SeverityUnspecified {
134+
// 3. actual diagnostic lines
135+
currentDiagnostic = &Diagnostic{
136+
Severity: severity,
137+
Message: msg,
138+
File: file,
139+
Line: line,
140+
Column: column,
141+
}
142+
currentMessage = &currentDiagnostic.Message
143+
144+
if len(fullContext) > 0 {
145+
if fullContextRefersTo == "" || fullContextRefersTo == file {
146+
fullContextRefersTo = file
147+
currentDiagnostic.Context = append(currentDiagnostic.Context, fullContext...)
148+
}
149+
}
150+
if inFileContext != nil && inFileContext.File == file {
151+
currentDiagnostic.Context = append(currentDiagnostic.Context, inFileContext)
152+
}
153+
154+
res = append(res, currentDiagnostic)
155+
continue
156+
}
157+
}
158+
159+
// 5. extra context lines
160+
if strings.HasPrefix(in, " ") {
161+
if currentMessage != nil {
162+
*currentMessage += "\n" + in
163+
}
164+
continue
165+
}
166+
}
167+
return res, nil
168+
}
169+
170+
func extractFileLineAndColumn(file string) (string, int, int) {
171+
split := strings.Split(file, ":")
172+
file = split[0]
173+
if len(split) == 1 {
174+
return file, 0, 0
175+
}
176+
line, err := strconv.Atoi(split[1])
177+
if err != nil || len(split) == 2 {
178+
return file, line, 0
179+
}
180+
column, _ := strconv.Atoi(split[2])
181+
return file, line, column
182+
}

0 commit comments

Comments
 (0)