Skip to content

Commit 46557b2

Browse files
authored
[skip-changelog] legacy: Arduino preprocess subroutine refactorization (part 5) (#2195)
* Moved CTags parser out of legacy * Moved CTags preprocess subroutine in proper place * Factored all c++ source lines parsers * Removed useless builderCtx field SketchSourceAfterCppPreprocessing * Removed useless builderCtx field SketchSourceAfterArduinoPreprocessing * Removed useless builderCtx field SketchSourceMerged * Moved ctag preprocessor into proper location * Moved ctags parser shenanigans into `internal` package * Fixed linter warnings
1 parent 0585435 commit 46557b2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+515
-552
lines changed

Diff for: arduino/builder/cpp.go renamed to arduino/builder/cpp/cpp.go

+39-5
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,63 @@
1313
// Arduino software without disclosing the source code of your own applications.
1414
// To purchase a commercial license, send an email to [email protected].
1515

16-
package builder
16+
package cpp
1717

1818
import (
19+
"strconv"
1920
"strings"
2021
"unicode/utf8"
22+
23+
"github.com/arduino/go-paths-helper"
2124
)
2225

23-
// QuoteCppString returns the given string as a quoted string for use with the C
26+
// QuoteString returns the given string as a quoted string for use with the C
2427
// preprocessor. This adds double quotes around it and escapes any
2528
// double quotes and backslashes in the string.
26-
func QuoteCppString(str string) string {
29+
func QuoteString(str string) string {
2730
str = strings.Replace(str, "\\", "\\\\", -1)
2831
str = strings.Replace(str, "\"", "\\\"", -1)
2932
return "\"" + str + "\""
3033
}
3134

32-
// ParseCppString parse a C-preprocessor string as emitted by the preprocessor. This
35+
// ParseLineMarker parses the given line as a gcc line marker and returns the contained
36+
// filename.
37+
func ParseLineMarker(line string) *paths.Path {
38+
// A line marker contains the line number and filename and looks like:
39+
// # 123 /path/to/file.cpp
40+
// It can be followed by zero or more flag number that indicate the
41+
// preprocessor state and can be ignored.
42+
// For exact details on this format, see:
43+
// https://github.com/gcc-mirror/gcc/blob/edd716b6b1caa1a5cb320a8cd7f626f30198e098/gcc/c-family/c-ppoutput.c#L413-L415
44+
45+
split := strings.SplitN(line, " ", 3)
46+
if len(split) < 3 || len(split[0]) == 0 || split[0][0] != '#' {
47+
return nil
48+
}
49+
50+
_, err := strconv.Atoi(split[1])
51+
if err != nil {
52+
return nil
53+
}
54+
55+
// If we get here, we found a # followed by a line number, so
56+
// assume this is a line marker and see if the rest of the line
57+
// starts with a string containing the filename
58+
str, rest, ok := ParseString(split[2])
59+
60+
if ok && (rest == "" || rest[0] == ' ') {
61+
return paths.New(str)
62+
}
63+
return nil
64+
}
65+
66+
// ParseString parse a string as emitted by the preprocessor. This
3367
// is a string contained in double quotes, with any backslashes or
3468
// quotes escaped with a backslash. If a valid string was present at the
3569
// start of the given line, returns the unquoted string contents, the
3670
// remainder of the line (everything after the closing "), and true.
3771
// Otherwise, returns the empty string, the entire line and false.
38-
func ParseCppString(line string) (string, string, bool) {
72+
func ParseString(line string) (string, string, bool) {
3973
// For details about how these strings are output by gcc, see:
4074
// https://github.com/gcc-mirror/gcc/blob/a588355ab948cf551bc9d2b89f18e5ae5140f52c/libcpp/macro.c#L491-L511
4175
// Note that the documentation suggests all non-printable

Diff for: arduino/builder/cpp/cpp_test.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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 cpp_test
17+
18+
import (
19+
"testing"
20+
21+
"github.com/arduino/arduino-cli/arduino/builder/cpp"
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func TestParseString(t *testing.T) {
26+
_, _, ok := cpp.ParseString(`foo`)
27+
require.Equal(t, false, ok)
28+
29+
_, _, ok = cpp.ParseString(`"foo`)
30+
require.Equal(t, false, ok)
31+
32+
str, rest, ok := cpp.ParseString(`"foo"`)
33+
require.Equal(t, true, ok)
34+
require.Equal(t, `foo`, str)
35+
require.Equal(t, ``, rest)
36+
37+
str, rest, ok = cpp.ParseString(`"foo\\bar"`)
38+
require.Equal(t, true, ok)
39+
require.Equal(t, `foo\bar`, str)
40+
require.Equal(t, ``, rest)
41+
42+
str, rest, ok = cpp.ParseString(`"foo \"is\" quoted and \\\\bar\"\" escaped\\" and "then" some`)
43+
require.Equal(t, true, ok)
44+
require.Equal(t, `foo "is" quoted and \\bar"" escaped\`, str)
45+
require.Equal(t, ` and "then" some`, rest)
46+
47+
str, rest, ok = cpp.ParseString(`" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~"`)
48+
require.Equal(t, true, ok)
49+
require.Equal(t, ` !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_abcdefghijklmnopqrstuvwxyz{|}~`, str)
50+
require.Equal(t, ``, rest)
51+
52+
str, rest, ok = cpp.ParseString(`"/home/ççç/"`)
53+
require.Equal(t, true, ok)
54+
require.Equal(t, `/home/ççç/`, str)
55+
require.Equal(t, ``, rest)
56+
57+
str, rest, ok = cpp.ParseString(`"/home/ççç/ /$sdsdd\\"`)
58+
require.Equal(t, true, ok)
59+
require.Equal(t, `/home/ççç/ /$sdsdd\`, str)
60+
require.Equal(t, ``, rest)
61+
}
62+
63+
func TestQuoteString(t *testing.T) {
64+
cases := map[string]string{
65+
`foo`: `"foo"`,
66+
`foo\bar`: `"foo\\bar"`,
67+
`foo "is" quoted and \\bar"" escaped\`: `"foo \"is\" quoted and \\\\bar\"\" escaped\\"`,
68+
// ASCII 0x20 - 0x7e, excluding `
69+
` !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_abcdefghijklmnopqrstuvwxyz{|}~`: `" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~"`,
70+
}
71+
for input, expected := range cases {
72+
require.Equal(t, expected, cpp.QuoteString(input))
73+
}
74+
}

Diff for: arduino/builder/cpp_test.go

-46
This file was deleted.

Diff for: arduino/builder/preprocessor/ctags.go

+172
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,17 @@
1616
package preprocessor
1717

1818
import (
19+
"bufio"
20+
"bytes"
1921
"context"
2022
"fmt"
23+
"io"
24+
"strconv"
2125
"strings"
2226

27+
"github.com/arduino/arduino-cli/arduino/builder/cpp"
28+
"github.com/arduino/arduino-cli/arduino/builder/preprocessor/internal/ctags"
29+
"github.com/arduino/arduino-cli/arduino/sketch"
2330
"github.com/arduino/arduino-cli/executils"
2431
"github.com/arduino/arduino-cli/i18n"
2532
"github.com/arduino/go-paths-helper"
@@ -29,6 +36,145 @@ import (
2936

3037
var tr = i18n.Tr
3138

39+
// DebugPreprocessor when set to true the CTags preprocessor will output debugging info to stdout
40+
// this is useful for unit-testing to provide more infos
41+
var DebugPreprocessor bool
42+
43+
// PreprocessSketchWithCtags performs preprocessing of the arduino sketch using CTags.
44+
func PreprocessSketchWithCtags(sketch *sketch.Sketch, buildPath *paths.Path, includes paths.PathList, lineOffset int, buildProperties *properties.Map, onlyUpdateCompilationDatabase bool) ([]byte, []byte, error) {
45+
// Create a temporary working directory
46+
tmpDir, err := paths.MkTempDir("", "")
47+
if err != nil {
48+
return nil, nil, err
49+
}
50+
defer tmpDir.RemoveAll()
51+
ctagsTarget := tmpDir.Join("sketch_merged.cpp")
52+
53+
normalOutput := &bytes.Buffer{}
54+
verboseOutput := &bytes.Buffer{}
55+
56+
// Run GCC preprocessor
57+
sourceFile := buildPath.Join("sketch", sketch.MainFile.Base()+".cpp")
58+
gccStdout, gccStderr, err := GCC(sourceFile, ctagsTarget, includes, buildProperties)
59+
verboseOutput.Write(gccStdout)
60+
verboseOutput.Write(gccStderr)
61+
normalOutput.Write(gccStderr)
62+
if err != nil {
63+
if !onlyUpdateCompilationDatabase {
64+
return normalOutput.Bytes(), verboseOutput.Bytes(), errors.WithStack(err)
65+
}
66+
67+
// Do not bail out if we are generating the compile commands database
68+
normalOutput.WriteString(fmt.Sprintf("%s: %s",
69+
tr("An error occurred adding prototypes"),
70+
tr("the compilation database may be incomplete or inaccurate")))
71+
if err := sourceFile.CopyTo(ctagsTarget); err != nil {
72+
return normalOutput.Bytes(), verboseOutput.Bytes(), errors.WithStack(err)
73+
}
74+
}
75+
76+
if src, err := ctagsTarget.ReadFile(); err == nil {
77+
filteredSource := filterSketchSource(sketch, bytes.NewReader(src), false)
78+
if err := ctagsTarget.WriteFile([]byte(filteredSource)); err != nil {
79+
return normalOutput.Bytes(), verboseOutput.Bytes(), err
80+
}
81+
} else {
82+
return normalOutput.Bytes(), verboseOutput.Bytes(), err
83+
}
84+
85+
// Run CTags on gcc-preprocessed source
86+
ctagsOutput, ctagsStdErr, err := RunCTags(ctagsTarget, buildProperties)
87+
verboseOutput.Write(ctagsStdErr)
88+
if err != nil {
89+
return normalOutput.Bytes(), verboseOutput.Bytes(), err
90+
}
91+
92+
// Parse CTags output
93+
parser := &ctags.Parser{}
94+
prototypes, firstFunctionLine := parser.Parse(ctagsOutput, sketch.MainFile)
95+
if firstFunctionLine == -1 {
96+
firstFunctionLine = 0
97+
}
98+
99+
// Add prototypes to the original sketch source
100+
var source string
101+
if sourceData, err := sourceFile.ReadFile(); err == nil {
102+
source = string(sourceData)
103+
} else {
104+
return normalOutput.Bytes(), verboseOutput.Bytes(), err
105+
}
106+
source = strings.Replace(source, "\r\n", "\n", -1)
107+
source = strings.Replace(source, "\r", "\n", -1)
108+
sourceRows := strings.Split(source, "\n")
109+
if isFirstFunctionOutsideOfSource(firstFunctionLine, sourceRows) {
110+
return normalOutput.Bytes(), verboseOutput.Bytes(), nil
111+
}
112+
113+
insertionLine := firstFunctionLine + lineOffset - 1
114+
firstFunctionChar := len(strings.Join(sourceRows[:insertionLine], "\n")) + 1
115+
prototypeSection := composePrototypeSection(firstFunctionLine, prototypes)
116+
preprocessedSource := source[:firstFunctionChar] + prototypeSection + source[firstFunctionChar:]
117+
118+
if DebugPreprocessor {
119+
fmt.Println("#PREPROCESSED SOURCE")
120+
prototypesRows := strings.Split(prototypeSection, "\n")
121+
prototypesRows = prototypesRows[:len(prototypesRows)-1]
122+
for i := 0; i < len(sourceRows)+len(prototypesRows); i++ {
123+
if i < insertionLine {
124+
fmt.Printf(" |%s\n", sourceRows[i])
125+
} else if i < insertionLine+len(prototypesRows) {
126+
fmt.Printf("PRO|%s\n", prototypesRows[i-insertionLine])
127+
} else {
128+
fmt.Printf(" |%s\n", sourceRows[i-len(prototypesRows)])
129+
}
130+
}
131+
fmt.Println("#END OF PREPROCESSED SOURCE")
132+
}
133+
134+
// Write back arduino-preprocess output to the sourceFile
135+
err = sourceFile.WriteFile([]byte(preprocessedSource))
136+
return normalOutput.Bytes(), verboseOutput.Bytes(), err
137+
}
138+
139+
func composePrototypeSection(line int, prototypes []*ctags.Prototype) string {
140+
if len(prototypes) == 0 {
141+
return ""
142+
}
143+
144+
str := joinPrototypes(prototypes)
145+
str += "\n#line "
146+
str += strconv.Itoa(line)
147+
str += " " + cpp.QuoteString(prototypes[0].File)
148+
str += "\n"
149+
150+
return str
151+
}
152+
153+
func joinPrototypes(prototypes []*ctags.Prototype) string {
154+
prototypesSlice := []string{}
155+
for _, proto := range prototypes {
156+
if signatureContainsaDefaultArg(proto) {
157+
continue
158+
}
159+
prototypesSlice = append(prototypesSlice, "#line "+strconv.Itoa(proto.Line)+" "+cpp.QuoteString(proto.File))
160+
prototypeParts := []string{}
161+
if proto.Modifiers != "" {
162+
prototypeParts = append(prototypeParts, proto.Modifiers)
163+
}
164+
prototypeParts = append(prototypeParts, proto.Prototype)
165+
prototypesSlice = append(prototypesSlice, strings.Join(prototypeParts, " "))
166+
}
167+
return strings.Join(prototypesSlice, "\n")
168+
}
169+
170+
func signatureContainsaDefaultArg(proto *ctags.Prototype) bool {
171+
return strings.Contains(proto.Prototype, "=")
172+
}
173+
174+
func isFirstFunctionOutsideOfSource(firstFunctionLine int, sourceRows []string) bool {
175+
return firstFunctionLine > len(sourceRows)-1
176+
}
177+
32178
// RunCTags performs a run of ctags on the given source file. Returns the ctags output and the stderr contents.
33179
func RunCTags(sourceFile *paths.Path, buildProperties *properties.Map) ([]byte, []byte, error) {
34180
ctagsBuildProperties := properties.NewMap()
@@ -60,3 +206,29 @@ func RunCTags(sourceFile *paths.Path, buildProperties *properties.Map) ([]byte,
60206
stderr = append([]byte(args), stderr...)
61207
return stdout, stderr, err
62208
}
209+
210+
func filterSketchSource(sketch *sketch.Sketch, source io.Reader, removeLineMarkers bool) string {
211+
fileNames := paths.NewPathList()
212+
fileNames.Add(sketch.MainFile)
213+
fileNames.AddAll(sketch.OtherSketchFiles)
214+
215+
inSketch := false
216+
filtered := ""
217+
218+
scanner := bufio.NewScanner(source)
219+
for scanner.Scan() {
220+
line := scanner.Text()
221+
if filename := cpp.ParseLineMarker(line); filename != nil {
222+
inSketch = fileNames.Contains(filename)
223+
if inSketch && removeLineMarkers {
224+
continue
225+
}
226+
}
227+
228+
if inSketch {
229+
filtered += line + "\n"
230+
}
231+
}
232+
233+
return filtered
234+
}

0 commit comments

Comments
 (0)