16
16
package preprocessor
17
17
18
18
import (
19
+ "bufio"
20
+ "bytes"
19
21
"context"
20
22
"fmt"
23
+ "io"
24
+ "strconv"
21
25
"strings"
22
26
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"
23
30
"github.com/arduino/arduino-cli/executils"
24
31
"github.com/arduino/arduino-cli/i18n"
25
32
"github.com/arduino/go-paths-helper"
@@ -29,6 +36,145 @@ import (
29
36
30
37
var tr = i18n .Tr
31
38
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
+
32
178
// RunCTags performs a run of ctags on the given source file. Returns the ctags output and the stderr contents.
33
179
func RunCTags (sourceFile * paths.Path , buildProperties * properties.Map ) ([]byte , []byte , error ) {
34
180
ctagsBuildProperties := properties .NewMap ()
@@ -60,3 +206,29 @@ func RunCTags(sourceFile *paths.Path, buildProperties *properties.Map) ([]byte,
60
206
stderr = append ([]byte (args ), stderr ... )
61
207
return stdout , stderr , err
62
208
}
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