Skip to content

Commit 0e5f629

Browse files
authoredNov 8, 2023
Report compiler errors in machine readable format (#2182)
* Added gRPC protocol for compiler diagnostics reporting * Builder now support parsing of compiler output * Added gcc output parser * Implementation of compile output parser in gRPC command * Tell prettier to ignore testdata files * Added proper result structure for diagnostics * Added integration test * Fixed parser bug and added unit test
1 parent 7a9be52 commit 0e5f629

File tree

23 files changed

+1646
-47
lines changed

23 files changed

+1646
-47
lines changed
 

‎.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
.vs/
55
.ionide/
66

7+
testdata
78
/arduino/libraries/librariesindex/testdata/invalid.json
89
/arduino/security/testdata/
910
/.licenses/

‎arduino/builder/builder.go

+34-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525

2626
"github.com/arduino/arduino-cli/arduino/builder/internal/compilation"
2727
"github.com/arduino/arduino-cli/arduino/builder/internal/detector"
28+
"github.com/arduino/arduino-cli/arduino/builder/internal/diagnostics"
2829
"github.com/arduino/arduino-cli/arduino/builder/internal/logger"
2930
"github.com/arduino/arduino-cli/arduino/builder/internal/progress"
3031
"github.com/arduino/arduino-cli/arduino/builder/internal/utils"
@@ -36,6 +37,7 @@ import (
3637
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
3738
"github.com/arduino/go-paths-helper"
3839
"github.com/arduino/go-properties-orderedmap"
40+
"github.com/sirupsen/logrus"
3941
)
4042

4143
// ErrSketchCannotBeLocatedInBuildPath fixdoc
@@ -90,6 +92,12 @@ type Builder struct {
9092
buildOptions *buildOptions
9193

9294
libsDetector *detector.SketchLibrariesDetector
95+
96+
// This is a function used to parse the output of the compiler
97+
// It is used to extract errors and warnings
98+
compilerOutputParser diagnostics.CompilerOutputParserCB
99+
// and here are the diagnostics parsed from the compiler
100+
compilerDiagnostics diagnostics.Diagnostics
93101
}
94102

95103
// buildArtifacts contains the result of various build
@@ -189,7 +197,7 @@ func NewBuilder(
189197
logger.Warn(string(verboseOut))
190198
}
191199

192-
return &Builder{
200+
b := &Builder{
193201
sketch: sk,
194202
buildProperties: buildProperties,
195203
buildPath: buildPath,
@@ -226,7 +234,26 @@ func NewBuilder(
226234
buildProperties.GetPath("runtime.platform.path"),
227235
buildProperties.GetPath("build.core.path"), // TODO can we buildCorePath ?
228236
),
229-
}, nil
237+
}
238+
239+
b.compilerOutputParser = func(cmdline []string, out []byte) {
240+
compiler := diagnostics.DetectCompilerFromCommandLine(
241+
cmdline,
242+
false, // at the moment compiler-probing is not required
243+
)
244+
if compiler == nil {
245+
logrus.Warnf("Could not detect compiler from: %s", cmdline)
246+
return
247+
}
248+
diags, err := diagnostics.ParseCompilerOutput(compiler, out)
249+
if err != nil {
250+
logrus.Warnf("Error parsing compiler output: %s", err)
251+
return
252+
}
253+
b.compilerDiagnostics = append(b.compilerDiagnostics, diags...)
254+
}
255+
256+
return b, nil
230257
}
231258

232259
// GetBuildProperties returns the build properties for running this build
@@ -249,6 +276,11 @@ func (b *Builder) ImportedLibraries() libraries.List {
249276
return b.libsDetector.ImportedLibraries()
250277
}
251278

279+
// CompilerDiagnostics returns the parsed compiler diagnostics
280+
func (b *Builder) CompilerDiagnostics() diagnostics.Diagnostics {
281+
return b.compilerDiagnostics
282+
}
283+
252284
// Preprocess fixdoc
253285
func (b *Builder) Preprocess() ([]byte, error) {
254286
b.Progress.AddSubSteps(6)

‎arduino/builder/compilation.go

+6
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ func (b *Builder) compileFileWithRecipe(
166166
}
167167
b.logger.WriteStderr(commandStderr.Bytes())
168168

169+
// Parse the output of the compiler to gather errors and warnings...
170+
if b.compilerOutputParser != nil {
171+
b.compilerOutputParser(command.GetArgs(), commandStdout.Bytes())
172+
b.compilerOutputParser(command.GetArgs(), commandStderr.Bytes())
173+
}
174+
169175
// ...and then return the error
170176
if err != nil {
171177
return nil, errors.WithStack(err)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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 license@arduino.cc.
15+
16+
package diagnostics
17+
18+
import (
19+
"bytes"
20+
"path/filepath"
21+
"strings"
22+
23+
"github.com/arduino/arduino-cli/executils"
24+
semver "go.bug.st/relaxed-semver"
25+
)
26+
27+
// DetectedCompiler represents a compiler detected from a given command line
28+
type DetectedCompiler struct {
29+
Name string
30+
Family string
31+
Version *semver.Version
32+
DetailedVersion []string
33+
}
34+
35+
// This function is overridden for mocking unit tests
36+
var runProcess = func(args ...string) []string {
37+
if cmd, err := executils.NewProcess(nil, args...); err == nil {
38+
out := &bytes.Buffer{}
39+
cmd.RedirectStdoutTo(out)
40+
cmd.Run()
41+
return splitLines(out.Bytes())
42+
}
43+
return nil
44+
}
45+
46+
// DetectCompilerFromCommandLine tries to detect a compiler from a given command line.
47+
// If probeCompiler is true, the compiler may be executed with different flags to
48+
// infer the version or capabilities.
49+
func DetectCompilerFromCommandLine(args []string, probeCompiler bool) *DetectedCompiler {
50+
if len(args) == 0 {
51+
return nil
52+
}
53+
basename := filepath.Base(args[0])
54+
family := ""
55+
if strings.Contains(basename, "g++") || strings.Contains(basename, "gcc") {
56+
family = "gcc"
57+
}
58+
res := &DetectedCompiler{
59+
Name: basename,
60+
Family: family,
61+
}
62+
63+
if family == "gcc" && probeCompiler {
64+
// Run "gcc --version" to obtain more info
65+
res.DetailedVersion = runProcess(args[0], "--version")
66+
67+
// Usually on the first line we get the compiler architecture and
68+
// version (as last field), followed by the compiler license, for
69+
// example:
70+
//
71+
// g++ (Ubuntu 12.2.0-3ubuntu1) 12.2.0
72+
// Copyright (C) 2022 Free Software Foundation, Inc.
73+
// This is free software; see the source for copying conditions. There is NO
74+
// warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
75+
//
76+
if len(res.DetailedVersion) > 0 {
77+
split := strings.Split(res.DetailedVersion[0], " ")
78+
if len(split) >= 3 {
79+
res.Name = split[0]
80+
res.Version, _ = semver.Parse(split[len(split)-1])
81+
}
82+
}
83+
}
84+
return res
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 license@arduino.cc.
15+
16+
package diagnostics
17+
18+
import (
19+
"strings"
20+
"testing"
21+
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func init() {
26+
runProcess = mockedRunProcessToGetCompilerVersion
27+
}
28+
29+
func mockedRunProcessToGetCompilerVersion(args ...string) []string {
30+
if strings.HasSuffix(args[0], "7.3.0-atmel3.6.1-arduino7/bin/avr-g++") && args[1] == "--version" {
31+
return []string{
32+
"avr-g++ (GCC) 7.3.0",
33+
"Copyright (C) 2017 Free Software Foundation, Inc.",
34+
"This is free software; see the source for copying conditions. There is NO",
35+
"warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.",
36+
"",
37+
}
38+
}
39+
if strings.HasSuffix(args[0], "7.3.0-atmel3.6.1-arduino7/bin/avr-gcc") && args[1] == "--version" {
40+
return []string{
41+
"avr-gcc (GCC) 7.3.0",
42+
"Copyright (C) 2017 Free Software Foundation, Inc.",
43+
"This is free software; see the source for copying conditions. There is NO",
44+
"warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.",
45+
"",
46+
}
47+
}
48+
if strings.HasSuffix(args[0], "xtensa-esp32-elf-gcc/gcc8_4_0-esp-2021r2-patch3/bin/xtensa-esp32-elf-g++") && args[1] == "--version" {
49+
return []string{
50+
"xtensa-esp32-elf-g++ (crosstool-NG esp-2021r2-patch3) 8.4.0",
51+
"Copyright (C) 2018 Free Software Foundation, Inc.",
52+
"This is free software; see the source for copying conditions. There is NO",
53+
"warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.",
54+
"",
55+
}
56+
}
57+
58+
panic("missing mock for command line: " + strings.Join(args, " "))
59+
}
60+
61+
func TestCompilerDetection(t *testing.T) {
62+
comp := DetectCompilerFromCommandLine([]string{"~/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/bin/avr-g++"}, true)
63+
require.NotNil(t, comp)
64+
require.Equal(t, "gcc", comp.Family)
65+
require.Equal(t, "avr-g++", comp.Name)
66+
require.Equal(t, "7.3.0", comp.Version.String())
67+
68+
comp = DetectCompilerFromCommandLine([]string{"~/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/bin/avr-gcc"}, true)
69+
require.NotNil(t, comp)
70+
require.Equal(t, "gcc", comp.Family)
71+
require.Equal(t, "avr-gcc", comp.Name)
72+
require.Equal(t, "7.3.0", comp.Version.String())
73+
74+
comp = DetectCompilerFromCommandLine([]string{"/home/megabug/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/gcc8_4_0-esp-2021r2-patch3/bin/xtensa-esp32-elf-g++"}, true)
75+
require.NotNil(t, comp)
76+
require.Equal(t, "gcc", comp.Family)
77+
require.Equal(t, "xtensa-esp32-elf-g++", comp.Name)
78+
require.Equal(t, "8.4.0", comp.Version.String())
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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 license@arduino.cc.
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+
// CompilerOutputParserCB is a callback function that is called to feed a parser
26+
// with the plain-text compiler output.
27+
type CompilerOutputParserCB func(cmdline []string, out []byte)
28+
29+
// Diagnostics represents a list of diagnostics
30+
type Diagnostics []*Diagnostic
31+
32+
// Diagnostic represents a diagnostic (a compiler error, warning, note, etc.)
33+
type Diagnostic struct {
34+
Severity Severity `json:"severity,omitempty"`
35+
Message string `json:"message"`
36+
File string `json:"file,omitempty"`
37+
Line int `json:"line,omitempty"`
38+
Column int `json:"col,omitempty"`
39+
Context FullContext `json:"context,omitempty"`
40+
Suggestions Notes `json:"suggestions,omitempty"`
41+
}
42+
43+
// Severity is a diagnostic severity
44+
type Severity string
45+
46+
const (
47+
// SeverityUnspecified is the undefined severity
48+
SeverityUnspecified Severity = ""
49+
// SeverityWarning is a warning
50+
SeverityWarning = "WARNING"
51+
// SeverityError is an error
52+
SeverityError = "ERROR"
53+
// SeverityFatal is a fatal error
54+
SeverityFatal = "FATAL"
55+
)
56+
57+
// Notes represents a list of Note
58+
type Notes []*Note
59+
60+
// Note represents a compiler annotation or suggestion
61+
type Note struct {
62+
Message string `json:"message"`
63+
File string `json:"file,omitempty"`
64+
Line int `json:"line,omitempty"`
65+
Column int `json:"col,omitempty"`
66+
}
67+
68+
// FullContext represents a list of Context
69+
type FullContext []*Context
70+
71+
// Context represents a context, i.e. a reference to a file, line and column
72+
// or a part of the code that a Diagnostic refers to.
73+
type Context struct {
74+
Message string `json:"message"`
75+
File string `json:"file,omitempty"`
76+
Line int `json:"line,omitempty"`
77+
Column int `json:"col,omitempty"`
78+
}
79+
80+
// ParseCompilerOutput parses the output of a compiler and returns a list of
81+
// diagnostics.
82+
func ParseCompilerOutput(compiler *DetectedCompiler, out []byte) ([]*Diagnostic, error) {
83+
lines := splitLines(out)
84+
switch compiler.Family {
85+
case "gcc":
86+
return parseGccOutput(lines)
87+
default:
88+
return nil, fmt.Errorf("unsupported compiler: %s", compiler)
89+
}
90+
}
91+
92+
func splitLines(in []byte) []string {
93+
res := strings.Split(string(in), "\n")
94+
for i, line := range res {
95+
res[i] = strings.TrimSuffix(line, "\r")
96+
}
97+
if l := len(res) - 1; res[l] == "" {
98+
res = res[:l]
99+
}
100+
return res
101+
}
102+
103+
// ToRPC converts a Diagnostics to a slice of rpc.CompileDiagnostic
104+
func (d Diagnostics) ToRPC() []*rpc.CompileDiagnostic {
105+
if len(d) == 0 {
106+
return nil
107+
}
108+
var res []*rpc.CompileDiagnostic
109+
for _, diag := range d {
110+
res = append(res, diag.ToRPC())
111+
}
112+
return res
113+
}
114+
115+
// ToRPC converts a Diagnostic to a rpc.CompileDiagnostic
116+
func (d *Diagnostic) ToRPC() *rpc.CompileDiagnostic {
117+
if d == nil {
118+
return nil
119+
}
120+
return &rpc.CompileDiagnostic{
121+
Severity: string(d.Severity),
122+
Message: d.Message,
123+
File: d.File,
124+
Line: int64(d.Line),
125+
Column: int64(d.Column),
126+
Context: d.Context.ToRPC(),
127+
Notes: d.Suggestions.ToRPC(),
128+
}
129+
}
130+
131+
// ToRPC converts a Notes to a slice of rpc.CompileDiagnosticNote
132+
func (s Notes) ToRPC() []*rpc.CompileDiagnosticNote {
133+
var res []*rpc.CompileDiagnosticNote
134+
for _, suggestion := range s {
135+
res = append(res, suggestion.ToRPC())
136+
}
137+
return res
138+
}
139+
140+
// ToRPC converts a Note to a rpc.CompileDiagnosticNote
141+
func (s *Note) ToRPC() *rpc.CompileDiagnosticNote {
142+
if s == nil {
143+
return nil
144+
}
145+
return &rpc.CompileDiagnosticNote{
146+
File: s.File,
147+
Line: int64(s.Line),
148+
Column: int64(s.Column),
149+
Message: s.Message,
150+
}
151+
}
152+
153+
// ToRPC converts a FullContext to a slice of rpc.CompileDiagnosticContext
154+
func (t FullContext) ToRPC() []*rpc.CompileDiagnosticContext {
155+
var res []*rpc.CompileDiagnosticContext
156+
for _, trace := range t {
157+
res = append(res, trace.ToRPC())
158+
}
159+
return res
160+
}
161+
162+
// ToRPC converts a Context to a rpc.CompileDiagnosticContext
163+
func (d *Context) ToRPC() *rpc.CompileDiagnosticContext {
164+
if d == nil {
165+
return nil
166+
}
167+
return &rpc.CompileDiagnosticContext{
168+
File: d.File,
169+
Line: int64(d.Line),
170+
Column: int64(d.Column),
171+
Message: d.Message,
172+
}
173+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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 license@arduino.cc.
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+
177+
// Special case: handle Windows drive letter `C:\...`
178+
if len(split) > 1 && len(file) == 1 && strings.HasPrefix(split[1], `\`) {
179+
file += ":" + split[1]
180+
split = split[1:]
181+
182+
if len(split) == 1 {
183+
return file, 0, 0
184+
}
185+
}
186+
187+
line, err := strconv.Atoi(split[1])
188+
if err != nil || len(split) == 2 {
189+
return file, line, 0
190+
}
191+
column, _ := strconv.Atoi(split[2])
192+
return file, line, column
193+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 license@arduino.cc.
15+
16+
package diagnostics
17+
18+
import (
19+
"bytes"
20+
"encoding/json"
21+
"strings"
22+
"testing"
23+
24+
"github.com/arduino/go-paths-helper"
25+
"github.com/stretchr/testify/require"
26+
)
27+
28+
func TestParser(t *testing.T) {
29+
t.Run("Generic001", func(t *testing.T) { runParserTest(t, "test001.txt") })
30+
t.Run("Generic002", func(t *testing.T) { runParserTest(t, "test002.txt") })
31+
t.Run("Generic003", func(t *testing.T) { runParserTest(t, "test003.txt") })
32+
t.Run("Generic004", func(t *testing.T) { runParserTest(t, "test004.txt") })
33+
}
34+
35+
func runParserTest(t *testing.T, testFile string) {
36+
testData, err := paths.New("testdata", "compiler_outputs", testFile).ReadFile()
37+
require.NoError(t, err)
38+
// The first line contains the compiler arguments
39+
idx := bytes.Index(testData, []byte("\n"))
40+
require.NotEqual(t, -1, idx)
41+
args := strings.Split(string(testData[0:idx]), " ")
42+
// The remainder of the file is the compiler output
43+
data := testData[idx:]
44+
45+
// Run compiler detection and parse compiler output
46+
detectedCompiler := DetectCompilerFromCommandLine(args, true)
47+
require.NotNil(t, detectedCompiler)
48+
diags, err := ParseCompilerOutput(detectedCompiler, data)
49+
require.NoError(t, err)
50+
51+
// Check if the parsed data match the expected output
52+
output, err := json.MarshalIndent(diags, "", " ")
53+
require.NoError(t, err)
54+
golden, err := paths.New("testdata", "compiler_outputs", testFile+".json").ReadFile()
55+
require.NoError(t, err)
56+
require.Equal(t, string(golden), string(output))
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/home/megabug/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/bin/avr-g++ -c -g -Os -w -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -Wno-error=narrowing -MMD -flto -mmcu=atmega32u4 -DF_CPU=16000000L -DARDUINO=10607 -DARDUINO_AVR_LEONARDO -DARDUINO_ARCH_AVR -DUSB_VID=0x2341 -DUSB_PID=0x8036 "-DUSB_MANUFACTURER=\"Unknown\"" "-DUSB_PRODUCT=\"Arduino Leonardo\"" -I/home/megabug/.arduino15/packages/arduino/hardware/avr/1.8.6/cores/arduino -I/home/megabug/.arduino15/packages/arduino/hardware/avr/1.8.6/variants/leonardo /tmp/arduino/sketches/002050EAA7EFB9A4FC451CDFBC0FA2D3/sketch/Blink.ino.cpp -o /tmp/arduino/sketches/002050EAA7EFB9A4FC451CDFBC0FA2D3/sketch/Blink.ino.cpp.o
2+
/home/megabug/Arduino/Blink/Blink.ino:1:14: error: expected initializer before 'asd'
3+
void setup() asd {
4+
^~~
5+
/home/megabug/Arduino/Blink/Blink.ino: In function 'void loop()':
6+
/home/megabug/Arduino/Blink/Blink.ino:6:1: error: 'asd' was not declared in this scope
7+
asd
8+
^~~
9+
/home/megabug/Arduino/Blink/Blink.ino:6:1: note: suggested alternative: 'rand'
10+
asd
11+
^~~
12+
rand
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[
2+
{
3+
"severity": "ERROR",
4+
"message": "expected initializer before 'asd'\n void setup() asd {\n ^~~",
5+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
6+
"line": 1,
7+
"col": 14
8+
},
9+
{
10+
"severity": "ERROR",
11+
"message": "'asd' was not declared in this scope\n asd\n ^~~",
12+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
13+
"line": 6,
14+
"col": 1,
15+
"context": [
16+
{
17+
"message": "In function 'void loop()':",
18+
"file": "/home/megabug/Arduino/Blink/Blink.ino"
19+
}
20+
],
21+
"suggestions": [
22+
{
23+
"message": "suggested alternative: 'rand'\n asd\n ^~~\n rand",
24+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
25+
"line": 6,
26+
"col": 1
27+
}
28+
]
29+
}
30+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/home/megabug/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/bin/avr-g++ -c -g -Os -w -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -Wno-error=narrowing -MMD -flto -mmcu=atmega328p -DF_CPU=16000000L -DARDUINO=10607 -DARDUINO_AVR_UNO -DARDUINO_ARCH_AVR -I/home/megabug/.arduino15/packages/arduino/hardware/avr/1.8.6/cores/arduino -I/home/megabug/.arduino15/packages/arduino/hardware/avr/1.8.6/variants/standard -I/home/megabug/Arduino/libraries/Audio/src /tmp/arduino/sketches/002050EAA7EFB9A4FC451CDFBC0FA2D3/sketch/Blink.ino.cpp -o /tmp/arduino/sketches/002050EAA7EFB9A4FC451CDFBC0FA2D3/sketch/Blink.ino.cpp.o
2+
In file included from /home/megabug/Arduino/libraries/Audio/src/Audio.h:16:0,
3+
from /home/megabug/Arduino/Blink/Blink.ino:1:
4+
/home/megabug/Arduino/libraries/Audio/src/DAC.h:21:16: error: expected ')' before '*' token
5+
DACClass(Dacc *_dac, uint32_t _dacId, IRQn_Type _isrId) :
6+
^
7+
/home/megabug/Arduino/libraries/Audio/src/DAC.h:35:2: error: 'Dacc' does not name a type
8+
Dacc *dac;
9+
^~~~
10+
/home/megabug/Arduino/libraries/Audio/src/DAC.h:37:2: error: 'IRQn_Type' does not name a type
11+
IRQn_Type isrId;
12+
^~~~~~~~~
13+
/home/megabug/Arduino/libraries/Audio/src/DAC.h: In member function 'void DACClass::enableInterrupts()':
14+
/home/megabug/Arduino/libraries/Audio/src/DAC.h:31:44: error: 'isrId' was not declared in this scope
15+
void enableInterrupts() { NVIC_EnableIRQ(isrId); };
16+
^~~~~
17+
/home/megabug/Arduino/libraries/Audio/src/DAC.h:31:29: error: 'NVIC_EnableIRQ' was not declared in this scope
18+
void enableInterrupts() { NVIC_EnableIRQ(isrId); };
19+
^~~~~~~~~~~~~~
20+
/home/megabug/Arduino/libraries/Audio/src/DAC.h: In member function 'void DACClass::disableInterrupts()':
21+
/home/megabug/Arduino/libraries/Audio/src/DAC.h:32:45: error: 'isrId' was not declared in this scope
22+
void disableInterrupts() { NVIC_DisableIRQ(isrId); };
23+
^~~~~
24+
/home/megabug/Arduino/libraries/Audio/src/DAC.h:32:29: error: 'NVIC_DisableIRQ' was not declared in this scope
25+
void disableInterrupts() { NVIC_DisableIRQ(isrId); };
26+
^~~~~~~~~~~~~~~
27+
/home/megabug/Arduino/Blink/Blink.ino: In function 'void setup()':
28+
/home/megabug/Arduino/Blink/Blink.ino:4:1: error: 'asd' was not declared in this scope
29+
asd;
30+
^~~
31+
/home/megabug/Arduino/Blink/Blink.ino:4:1: note: suggested alternative: 'rand'
32+
asd;
33+
^~~
34+
rand
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
[
2+
{
3+
"severity": "ERROR",
4+
"message": "expected ')' before '*' token\n DACClass(Dacc *_dac, uint32_t _dacId, IRQn_Type _isrId) :\n ^",
5+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h",
6+
"line": 21,
7+
"col": 16,
8+
"context": [
9+
{
10+
"message": "included from here",
11+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
12+
"line": 16
13+
},
14+
{
15+
"message": "included from here",
16+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
17+
"line": 1
18+
}
19+
]
20+
},
21+
{
22+
"severity": "ERROR",
23+
"message": "'Dacc' does not name a type\n Dacc *dac;\n ^~~~",
24+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h",
25+
"line": 35,
26+
"col": 2,
27+
"context": [
28+
{
29+
"message": "included from here",
30+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
31+
"line": 16
32+
},
33+
{
34+
"message": "included from here",
35+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
36+
"line": 1
37+
}
38+
]
39+
},
40+
{
41+
"severity": "ERROR",
42+
"message": "'IRQn_Type' does not name a type\n IRQn_Type isrId;\n ^~~~~~~~~",
43+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h",
44+
"line": 37,
45+
"col": 2,
46+
"context": [
47+
{
48+
"message": "included from here",
49+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
50+
"line": 16
51+
},
52+
{
53+
"message": "included from here",
54+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
55+
"line": 1
56+
}
57+
]
58+
},
59+
{
60+
"severity": "ERROR",
61+
"message": "'isrId' was not declared in this scope\n void enableInterrupts() { NVIC_EnableIRQ(isrId); };\n ^~~~~",
62+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h",
63+
"line": 31,
64+
"col": 44,
65+
"context": [
66+
{
67+
"message": "included from here",
68+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
69+
"line": 16
70+
},
71+
{
72+
"message": "included from here",
73+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
74+
"line": 1
75+
},
76+
{
77+
"message": "In member function 'void DACClass::enableInterrupts()':",
78+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h"
79+
}
80+
]
81+
},
82+
{
83+
"severity": "ERROR",
84+
"message": "'NVIC_EnableIRQ' was not declared in this scope\n void enableInterrupts() { NVIC_EnableIRQ(isrId); };\n ^~~~~~~~~~~~~~",
85+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h",
86+
"line": 31,
87+
"col": 29,
88+
"context": [
89+
{
90+
"message": "included from here",
91+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
92+
"line": 16
93+
},
94+
{
95+
"message": "included from here",
96+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
97+
"line": 1
98+
},
99+
{
100+
"message": "In member function 'void DACClass::enableInterrupts()':",
101+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h"
102+
}
103+
]
104+
},
105+
{
106+
"severity": "ERROR",
107+
"message": "'isrId' was not declared in this scope\n void disableInterrupts() { NVIC_DisableIRQ(isrId); };\n ^~~~~",
108+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h",
109+
"line": 32,
110+
"col": 45,
111+
"context": [
112+
{
113+
"message": "included from here",
114+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
115+
"line": 16
116+
},
117+
{
118+
"message": "included from here",
119+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
120+
"line": 1
121+
},
122+
{
123+
"message": "In member function 'void DACClass::disableInterrupts()':",
124+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h"
125+
}
126+
]
127+
},
128+
{
129+
"severity": "ERROR",
130+
"message": "'NVIC_DisableIRQ' was not declared in this scope\n void disableInterrupts() { NVIC_DisableIRQ(isrId); };\n ^~~~~~~~~~~~~~~",
131+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h",
132+
"line": 32,
133+
"col": 29,
134+
"context": [
135+
{
136+
"message": "included from here",
137+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
138+
"line": 16
139+
},
140+
{
141+
"message": "included from here",
142+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
143+
"line": 1
144+
},
145+
{
146+
"message": "In member function 'void DACClass::disableInterrupts()':",
147+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h"
148+
}
149+
]
150+
},
151+
{
152+
"severity": "ERROR",
153+
"message": "'asd' was not declared in this scope\n asd;\n ^~~",
154+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
155+
"line": 4,
156+
"col": 1,
157+
"context": [
158+
{
159+
"message": "In function 'void setup()':",
160+
"file": "/home/megabug/Arduino/Blink/Blink.ino"
161+
}
162+
],
163+
"suggestions": [
164+
{
165+
"message": "suggested alternative: 'rand'\n asd;\n ^~~\n rand",
166+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
167+
"line": 4,
168+
"col": 1
169+
}
170+
]
171+
}
172+
]

‎arduino/builder/internal/diagnostics/testdata/compiler_outputs/test003.txt

+40
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
[
2+
{
3+
"severity": "ERROR",
4+
"message": "expected ')' before '*' token\n DACClass(Dacc *_dac, uint32_t _dacId, IRQn_Type _isrId) :\n ~ ^~\n )",
5+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h",
6+
"line": 21,
7+
"col": 15,
8+
"context": [
9+
{
10+
"message": "included from here",
11+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
12+
"line": 16
13+
},
14+
{
15+
"message": "included from here",
16+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
17+
"line": 1
18+
}
19+
]
20+
},
21+
{
22+
"severity": "ERROR",
23+
"message": "'Dacc' does not name a type\n Dacc *dac;\n ^~~~",
24+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h",
25+
"line": 35,
26+
"col": 2,
27+
"context": [
28+
{
29+
"message": "included from here",
30+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
31+
"line": 16
32+
},
33+
{
34+
"message": "included from here",
35+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
36+
"line": 1
37+
}
38+
]
39+
},
40+
{
41+
"severity": "ERROR",
42+
"message": "'IRQn_Type' does not name a type\n IRQn_Type isrId;\n ^~~~~~~~~",
43+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h",
44+
"line": 37,
45+
"col": 2,
46+
"context": [
47+
{
48+
"message": "included from here",
49+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
50+
"line": 16
51+
},
52+
{
53+
"message": "included from here",
54+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
55+
"line": 1
56+
}
57+
]
58+
},
59+
{
60+
"severity": "ERROR",
61+
"message": "'isrId' was not declared in this scope\n void enableInterrupts() { NVIC_EnableIRQ(isrId); };\n ^~~~~",
62+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h",
63+
"line": 31,
64+
"col": 43,
65+
"context": [
66+
{
67+
"message": "included from here",
68+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
69+
"line": 16
70+
},
71+
{
72+
"message": "included from here",
73+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
74+
"line": 1
75+
},
76+
{
77+
"message": "In member function 'void DACClass::enableInterrupts()':",
78+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h"
79+
}
80+
]
81+
},
82+
{
83+
"severity": "ERROR",
84+
"message": "'NVIC_EnableIRQ' was not declared in this scope\n void enableInterrupts() { NVIC_EnableIRQ(isrId); };\n ^~~~~~~~~~~~~~",
85+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h",
86+
"line": 31,
87+
"col": 28,
88+
"context": [
89+
{
90+
"message": "included from here",
91+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
92+
"line": 16
93+
},
94+
{
95+
"message": "included from here",
96+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
97+
"line": 1
98+
},
99+
{
100+
"message": "In member function 'void DACClass::enableInterrupts()':",
101+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h"
102+
}
103+
]
104+
},
105+
{
106+
"severity": "ERROR",
107+
"message": "'isrId' was not declared in this scope\n void disableInterrupts() { NVIC_DisableIRQ(isrId); };\n ^~~~~",
108+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h",
109+
"line": 32,
110+
"col": 44,
111+
"context": [
112+
{
113+
"message": "included from here",
114+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
115+
"line": 16
116+
},
117+
{
118+
"message": "included from here",
119+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
120+
"line": 1
121+
},
122+
{
123+
"message": "In member function 'void DACClass::disableInterrupts()':",
124+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h"
125+
}
126+
]
127+
},
128+
{
129+
"severity": "ERROR",
130+
"message": "'NVIC_DisableIRQ' was not declared in this scope\n void disableInterrupts() { NVIC_DisableIRQ(isrId); };\n ^~~~~~~~~~~~~~~",
131+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h",
132+
"line": 32,
133+
"col": 28,
134+
"context": [
135+
{
136+
"message": "included from here",
137+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
138+
"line": 16
139+
},
140+
{
141+
"message": "included from here",
142+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
143+
"line": 1
144+
},
145+
{
146+
"message": "In member function 'void DACClass::disableInterrupts()':",
147+
"file": "/home/megabug/Arduino/libraries/Audio/src/DAC.h"
148+
}
149+
]
150+
},
151+
{
152+
"severity": "WARNING",
153+
"message": "no return statement in function returning non-void [-Wreturn-type]\n virtual size_t write(uint8_t c) { /* not implemented */ };\n ^",
154+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h",
155+
"line": 25,
156+
"col": 82,
157+
"context": [
158+
{
159+
"message": "included from here",
160+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
161+
"line": 1
162+
},
163+
{
164+
"message": "In member function 'virtual size_t AudioClass::write(uint8_t)':",
165+
"file": "/home/megabug/Arduino/libraries/Audio/src/Audio.h"
166+
}
167+
]
168+
},
169+
{
170+
"severity": "ERROR",
171+
"message": "'asd' was not declared in this scope\n asd;\n ^~~",
172+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
173+
"line": 4,
174+
"col": 1,
175+
"context": [
176+
{
177+
"message": "In function 'void setup()':",
178+
"file": "/home/megabug/Arduino/Blink/Blink.ino"
179+
}
180+
],
181+
"suggestions": [
182+
{
183+
"message": "suggested alternative: 'rand'\n asd;\n ^~~\n rand",
184+
"file": "/home/megabug/Arduino/Blink/Blink.ino",
185+
"line": 4,
186+
"col": 1
187+
}
188+
]
189+
}
190+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
C:\Users\runneradmin\AppData\Local\Temp\cli2123776893\A\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7/bin/avr-g++ -c -g -Os -w -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -Wno-error=narrowing -MMD -flto -mmcu=atmega328p -DF_CPU=16000000L -DARDUINO=10607 -DARDUINO_AVR_UNO -DARDUINO_ARCH_AVR -IC:\Users\runneradmin\AppData\Local\Temp\cli2123776893\A\packages\arduino\hardware\avr\1.8.5\cores\arduino -IC:\Users\runneradmin\AppData\Local\Temp\cli2123776893\A\packages\arduino\hardware\avr\1.8.5\variants\standard C:\Users\runneradmin\AppData\Local\Temp\arduino\sketches\BD9E9425D0ACEC4A9F5E44E2417C33A5\sketch\wrong.cpp -o C:\Users\runneradmin\AppData\Local\Temp\arduino\sketches\BD9E9425D0ACEC4A9F5E44E2417C33A5\sketch\wrong.cpp.o
2+
D:\a\arduino-cli\arduino-cli\internal\integrationtest\compile_3\testdata\blink_with_wrong_cpp\wrong.cpp: In function 'void wrong()':
3+
D:\a\arduino-cli\arduino-cli\internal\integrationtest\compile_3\testdata\blink_with_wrong_cpp\wrong.cpp:1:14: error: expected '}' at end of input
4+
void wrong() {
5+
^
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[
2+
{
3+
"severity": "ERROR",
4+
"message": "expected '}' at end of input\n void wrong() {\n ^",
5+
"file": "D:\\a\\arduino-cli\\arduino-cli\\internal\\integrationtest\\compile_3\\testdata\\blink_with_wrong_cpp\\wrong.cpp",
6+
"line": 1,
7+
"col": 14,
8+
"context": [
9+
{
10+
"message": "In function 'void wrong()':",
11+
"file": "D:\\a\\arduino-cli\\arduino-cli\\internal\\integrationtest\\compile_3\\testdata\\blink_with_wrong_cpp\\wrong.cpp"
12+
}
13+
]
14+
}
15+
]

‎commands/compile/compile.go

+5
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
177177
if pme.GetProfile() != nil {
178178
libsManager = lm
179179
}
180+
180181
sketchBuilder, err := builder.NewBuilder(
181182
sk,
182183
boardBuildProperties,
@@ -218,6 +219,10 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
218219
}
219220
}()
220221

222+
defer func() {
223+
r.Diagnostics = sketchBuilder.CompilerDiagnostics().ToRPC()
224+
}()
225+
221226
defer func() {
222227
buildProperties := sketchBuilder.GetBuildProperties()
223228
if buildProperties == nil {

‎internal/cli/compile/compile.go

+9-8
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ func runCompileCommand(cmd *cobra.Command, args []string) {
348348
UpdatedUploadPort: result.NewPort(uploadRes.GetUpdatedUploadPort()),
349349
},
350350
ProfileOut: profileOut,
351+
Diagnostics: result.NewCompileDiagnostics(compileRes.GetDiagnostics()),
351352
Success: compileError == nil,
352353
showPropertiesMode: showProperties,
353354
hideStats: preprocess,
@@ -392,14 +393,14 @@ type updatedUploadPortResult struct {
392393
}
393394

394395
type compileResult struct {
395-
CompilerOut string `json:"compiler_out"`
396-
CompilerErr string `json:"compiler_err"`
397-
BuilderResult *result.CompileResponse `json:"builder_result"`
398-
UploadResult updatedUploadPortResult `json:"upload_result"`
399-
Success bool `json:"success"`
400-
ProfileOut string `json:"profile_out,omitempty"`
401-
Error string `json:"error,omitempty"`
402-
396+
CompilerOut string `json:"compiler_out"`
397+
CompilerErr string `json:"compiler_err"`
398+
BuilderResult *result.CompileResponse `json:"builder_result"`
399+
UploadResult updatedUploadPortResult `json:"upload_result"`
400+
Success bool `json:"success"`
401+
ProfileOut string `json:"profile_out,omitempty"`
402+
Error string `json:"error,omitempty"`
403+
Diagnostics []*result.CompileDiagnostic `json:"diagnostics,omitempty"`
403404
showPropertiesMode arguments.ShowPropertiesMode
404405
hideStats bool
405406
}

‎internal/cli/feedback/result/rpc.go

+61
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package result
1818
import (
1919
"cmp"
2020

21+
f "github.com/arduino/arduino-cli/internal/algorithms"
2122
"github.com/arduino/arduino-cli/internal/orderedmap"
2223
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
2324
semver "go.bug.st/relaxed-semver"
@@ -880,6 +881,7 @@ type CompileResponse struct {
880881
BoardPlatform *InstalledPlatformReference `json:"board_platform,omitempty"`
881882
BuildPlatform *InstalledPlatformReference `json:"build_platform,omitempty"`
882883
BuildProperties []string `json:"build_properties,omitempty"`
884+
Diagnostics []*CompileDiagnostic `json:"diagnostics,omitempty"`
883885
}
884886

885887
func NewCompileResponse(c *rpc.CompileResponse) *CompileResponse {
@@ -904,6 +906,7 @@ func NewCompileResponse(c *rpc.CompileResponse) *CompileResponse {
904906
BoardPlatform: NewInstalledPlatformReference(c.GetBoardPlatform()),
905907
BuildPlatform: NewInstalledPlatformReference(c.GetBuildPlatform()),
906908
BuildProperties: c.GetBuildProperties(),
909+
Diagnostics: NewCompileDiagnostics(c.GetDiagnostics()),
907910
}
908911
}
909912

@@ -959,3 +962,61 @@ func NewBoardListWatchResponse(r *rpc.BoardListWatchResponse) *BoardListWatchRes
959962
Error: r.Error,
960963
}
961964
}
965+
966+
type CompileDiagnostic struct {
967+
Severity string `json:"severity,omitempty"`
968+
Message string `json:"message,omitempty"`
969+
File string `json:"file,omitempty"`
970+
Line int64 `json:"line,omitempty"`
971+
Column int64 `json:"column,omitempty"`
972+
Context []*CompileDiagnosticContext `json:"context,omitempty"`
973+
Notes []*CompileDiagnosticNote `json:"notes,omitempty"`
974+
}
975+
976+
func NewCompileDiagnostics(cd []*rpc.CompileDiagnostic) []*CompileDiagnostic {
977+
return f.Map(cd, NewCompileDiagnostic)
978+
}
979+
980+
func NewCompileDiagnostic(cd *rpc.CompileDiagnostic) *CompileDiagnostic {
981+
return &CompileDiagnostic{
982+
Severity: cd.GetSeverity(),
983+
Message: cd.GetMessage(),
984+
File: cd.GetFile(),
985+
Line: cd.GetLine(),
986+
Column: cd.GetColumn(),
987+
Context: f.Map(cd.GetContext(), NewCompileDiagnosticContext),
988+
Notes: f.Map(cd.GetNotes(), NewCompileDiagnosticNote),
989+
}
990+
}
991+
992+
type CompileDiagnosticContext struct {
993+
Message string `json:"message,omitempty"`
994+
File string `json:"file,omitempty"`
995+
Line int64 `json:"line,omitempty"`
996+
Column int64 `json:"column,omitempty"`
997+
}
998+
999+
func NewCompileDiagnosticContext(cdc *rpc.CompileDiagnosticContext) *CompileDiagnosticContext {
1000+
return &CompileDiagnosticContext{
1001+
Message: cdc.GetMessage(),
1002+
File: cdc.GetFile(),
1003+
Line: cdc.GetLine(),
1004+
Column: cdc.GetColumn(),
1005+
}
1006+
}
1007+
1008+
type CompileDiagnosticNote struct {
1009+
Message string `json:"message,omitempty"`
1010+
File string `json:"file,omitempty"`
1011+
Line int64 `json:"line,omitempty"`
1012+
Column int64 `json:"column,omitempty"`
1013+
}
1014+
1015+
func NewCompileDiagnosticNote(cdn *rpc.CompileDiagnosticNote) *CompileDiagnosticNote {
1016+
return &CompileDiagnosticNote{
1017+
Message: cdn.GetMessage(),
1018+
File: cdn.GetFile(),
1019+
Line: cdn.GetLine(),
1020+
Column: cdn.GetColumn(),
1021+
}
1022+
}

‎internal/cli/feedback/result/rpc_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,18 @@ func TestAllFieldAreMapped(t *testing.T) {
205205
boardListWatchResponseRpc := &rpc.BoardListWatchResponse{}
206206
boardListWatchResponseResult := result.NewBoardListWatchResponse(boardListWatchResponseRpc)
207207
mustContainsAllPropertyOfRpcStruct(t, boardListWatchResponseRpc, boardListWatchResponseResult)
208+
209+
compileDiagnosticRpc := &rpc.CompileDiagnostic{}
210+
compileDiagnosticResult := result.NewCompileDiagnostic(compileDiagnosticRpc)
211+
mustContainsAllPropertyOfRpcStruct(t, compileDiagnosticRpc, compileDiagnosticResult)
212+
213+
compileDiagnosticContextRpc := &rpc.CompileDiagnosticContext{}
214+
compileDiagnosticContextResult := result.NewCompileDiagnosticContext(compileDiagnosticContextRpc)
215+
mustContainsAllPropertyOfRpcStruct(t, compileDiagnosticContextRpc, compileDiagnosticContextResult)
216+
217+
compileDiagnosticNoteRpc := &rpc.CompileDiagnosticNote{}
218+
compileDiagnosticNoteResult := result.NewCompileDiagnosticNote(compileDiagnosticNoteRpc)
219+
mustContainsAllPropertyOfRpcStruct(t, compileDiagnosticNoteRpc, compileDiagnosticNoteResult)
208220
}
209221

210222
func TestEnumsMapsEveryRpcCounterpart(t *testing.T) {

‎internal/integrationtest/compile_3/compile_test.go

+13-3
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,19 @@ func TestCompilerErrOutput(t *testing.T) {
113113
require.NoError(t, err)
114114

115115
// Run compile and catch err stream
116-
out, _, err := cli.Run("compile", "-b", "arduino:avr:uno", "--format", "json", sketch.String())
116+
out, _, err := cli.Run("compile", "-b", "arduino:avr:uno", "-v", "--format", "json", sketch.String())
117117
require.Error(t, err)
118-
compilerErr := requirejson.Parse(t, out).Query(".compiler_err")
119-
compilerErr.MustContain(`"error"`)
118+
outJson := requirejson.Parse(t, out)
119+
outJson.Query(`.compiler_err`).MustContain(`"error"`)
120+
outJson.Query(`.diagnostics`).MustContain(`
121+
[
122+
{
123+
"severity": "ERROR",
124+
"line": 1,
125+
"column": 14,
126+
"context": [ { "message": "In function 'void wrong()':" } ]
127+
}
128+
]`)
120129
}
121130

122131
// Check that library discover do not generate false errors
@@ -132,6 +141,7 @@ func TestCompilerErrOutput(t *testing.T) {
132141
jsonOut := requirejson.Parse(t, out)
133142
jsonOut.Query(".compiler_out").MustNotContain(`"fatal error"`)
134143
jsonOut.Query(".compiler_err").MustNotContain(`"fatal error"`)
144+
jsonOut.MustNotContain(`{ "diagnostics" : [] }`)
135145
}
136146
}
137147

‎rpc/cc/arduino/cli/commands/v1/compile.pb.go

+377-34
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎rpc/cc/arduino/cli/commands/v1/compile.proto

+43
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,53 @@ message CompileResponse {
115115
TaskProgress progress = 8;
116116
// Build properties used for compiling
117117
repeated string build_properties = 9;
118+
// Compiler errors and warnings
119+
repeated CompileDiagnostic diagnostics = 10;
118120
}
119121

120122
message ExecutableSectionSize {
121123
string name = 1;
122124
int64 size = 2;
123125
int64 max_size = 3;
124126
}
127+
128+
message CompileDiagnostic {
129+
// Severity of the diagnostic
130+
string severity = 1;
131+
// The explanation of the diagnostic (it may be multiple preformatted lines)
132+
string message = 2;
133+
// The file containing the diagnostic
134+
string file = 3;
135+
// The line of the diagnostic if available (starts from 1)
136+
int64 line = 4;
137+
// The column of the diagnostic if available (starts from 1)
138+
int64 column = 5;
139+
// The context where this diagnostic is found (it may be multiple files that
140+
// represents a chain of includes, or a text describing where the diagnostic
141+
// is found)
142+
repeated CompileDiagnosticContext context = 6;
143+
// Annotations or suggestions to the diagnostic made by the compiler
144+
repeated CompileDiagnosticNote notes = 7;
145+
}
146+
147+
message CompileDiagnosticContext {
148+
// The message describing the context reference
149+
string message = 1;
150+
// The file of the context reference
151+
string file = 2;
152+
// The line of the context reference
153+
int64 line = 3;
154+
// The column of the context reference
155+
int64 column = 4;
156+
}
157+
158+
message CompileDiagnosticNote {
159+
// The message describing the compiler note
160+
string message = 1;
161+
// The file of the compiler note
162+
string file = 2;
163+
// The line of the compiler note
164+
int64 line = 3;
165+
// The column of the compiler note
166+
int64 column = 4;
167+
}

0 commit comments

Comments
 (0)
Please sign in to comment.