1 /*
2 * SessionBuildErrors.cpp
3 *
4 * Copyright (C) 2021 by RStudio, PBC
5 *
6 * Unless you have received this program directly from RStudio pursuant
7 * to the terms of a commercial license agreement with RStudio, then
8 * this program is licensed to you under the terms of version 3 of the
9 * GNU Affero General Public License. This program is distributed WITHOUT
10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13 *
14 */
15
16 #include "SessionBuildErrors.hpp"
17
18 #include <algorithm>
19
20 #include <boost/regex.hpp>
21 #include <boost/format.hpp>
22 #include <boost/algorithm/string/predicate.hpp>
23 #include <boost/bind/bind.hpp>
24
25 #include <shared_core/Error.hpp>
26 #include <shared_core/SafeConvert.hpp>
27
28 #include <core/FileSerializer.hpp>
29 #include <core/Version.hpp>
30
31 #include <r/RExec.hpp>
32
33 #include <session/SessionModuleContext.hpp>
34 #include <session/projects/SessionProjects.hpp>
35
36 #define kAnsiEscapeRegex "(?:\033\\[\\d+m)*"
37
38 using namespace rstudio::core;
39 using namespace boost::placeholders;
40
41 namespace rstudio {
42 namespace session {
43 namespace modules {
44 namespace build {
45
46 namespace {
47
isRSourceFile(const FilePath & filePath)48 bool isRSourceFile(const FilePath& filePath)
49 {
50 return (filePath.getExtensionLowerCase() == ".q" ||
51 filePath.getExtensionLowerCase() == ".s" ||
52 filePath.getExtensionLowerCase() == ".r");
53 }
54
isMatchingFile(const std::vector<std::string> & lines,std::size_t diagLine,const std::string & lineContents,const std::string & nextLineContents)55 bool isMatchingFile(const std::vector<std::string>& lines,
56 std::size_t diagLine,
57 const std::string& lineContents,
58 const std::string& nextLineContents)
59 {
60 // first verify the file has enough lines to match
61 if (lines.size() < (diagLine+1))
62 return false;
63
64 return boost::algorithm::equals(lines[diagLine-1],lineContents) &&
65 boost::algorithm::starts_with(lines[diagLine], nextLineContents);
66 }
67
scanForRSourceFile(const FilePath & basePath,std::size_t diagLine,const std::string & lineContents,const std::string & nextLineContents)68 FilePath scanForRSourceFile(const FilePath& basePath,
69 std::size_t diagLine,
70 const std::string& lineContents,
71 const std::string& nextLineContents)
72 {
73 std::vector<FilePath> children;
74 Error error = basePath.getChildren(children);
75 if (error)
76 {
77 LOG_ERROR(error);
78 return FilePath();
79 }
80
81 for (const FilePath& child : children)
82 {
83 if (isRSourceFile(child))
84 {
85 std::vector<std::string> lines;
86 Error error = core::readStringVectorFromFile(child, &lines, false);
87 if (error)
88 {
89 LOG_ERROR(error);
90 continue;
91 }
92
93 if (isMatchingFile(lines, diagLine, lineContents, nextLineContents))
94 return child;
95 }
96 }
97
98 return FilePath();
99 }
100
parseRErrors(const FilePath & basePath,const std::string & output)101 std::vector<module_context::SourceMarker> parseRErrors(
102 const FilePath& basePath,
103 const std::string& output)
104 {
105 using namespace module_context;
106 std::vector<SourceMarker> errors;
107
108 boost::regex re("^Error in parse\\(outFile\\) : ([0-9]+?):([0-9]+?): (.+?)\\n"
109 "([0-9]+?): (.*?)\\n([0-9]+?): (.+?)$");
110 try
111 {
112 boost::sregex_iterator iter(output.begin(), output.end(), re,
113 boost::regex_constants::match_not_dot_newline);
114 boost::sregex_iterator end;
115 for (; iter != end; iter++)
116 {
117 boost::smatch match = *iter;
118 BOOST_ASSERT(match.size() == 8);
119
120 // first part is straightforward
121 std::string line = match[1];
122 std::string column = match[2];
123 std::string message = match[3];
124
125 // we need to guess the file based on the contextual information
126 // provided in the error message
127 int diagLine = core::safe_convert::stringTo<int>(match[4], -1);
128 if (diagLine != -1)
129 {
130 FilePath rSrcFile = scanForRSourceFile(basePath,
131 diagLine,
132 match[5],
133 match[7]);
134 if (!rSrcFile.isEmpty())
135 {
136 // create error and add it
137 SourceMarker err(SourceMarker::Error,
138 rSrcFile,
139 core::safe_convert::stringTo<int>(line, 1),
140 core::safe_convert::stringTo<int>(column, 1),
141 core::html_utils::HTML(message),
142 false);
143 errors.push_back(err);
144 }
145 }
146
147 }
148 }
149 CATCH_UNEXPECTED_EXCEPTION;
150
151 return errors;
152
153 }
154
155
parseGccErrors(const FilePath & basePath,const std::string & output)156 std::vector<module_context::SourceMarker> parseGccErrors(
157 const FilePath& basePath,
158 const std::string& output)
159 {
160 // check to see if we are in a package
161 std::string pkgInclude;
162 using namespace projects;
163 if (projectContext().hasProject() &&
164 (projectContext().config().buildType == r_util::kBuildTypePackage))
165 {
166 pkgInclude = "/" + projectContext().packageInfo().name() + "/include/";
167 }
168
169 using namespace module_context;
170 std::vector<SourceMarker> errors;
171
172 // parse standard gcc errors and warning lines but also pickup "from"
173 // prefixed errors and substitute the from file for the error/warning file
174 boost::regex re("(?:from (.+?):([0-9]+?).+?\\n)?"
175 "^(.+?):([0-9]+?):(?:([0-9]+?):)? (error|warning): (.+)$");
176 try
177 {
178 boost::sregex_iterator iter(output.begin(), output.end(), re,
179 boost::regex_constants::match_not_dot_newline);
180 boost::sregex_iterator end;
181 for (; iter != end; iter++)
182 {
183 boost::smatch match = *iter;
184 BOOST_ASSERT(match.size() == 8);
185
186 std::string file, line, column, type, message;
187 std::string match1 = match[1];
188 if (!match1.empty() && FilePath::isRootPath(match[1]))
189 {
190 file = match[1];
191 line = match[2];
192 column = "1";
193 }
194 else
195 {
196 file = match[3];
197 line = match[4];
198 column = match[5];
199 if (column.empty())
200 column = "1";
201 }
202 type = match[6];
203 message = match[7];
204
205 // resolve file path
206 FilePath filePath;
207 if (FilePath::isRootPath(file))
208 filePath = FilePath(file);
209 else
210 filePath = basePath.completeChildPath(file);
211
212 // skip if the file doesn't exist
213 if (!filePath.exists())
214 continue;
215
216 FilePath realPath;
217 Error error = core::system::realPath(filePath, &realPath);
218 if (error)
219 LOG_ERROR(error);
220 else
221 filePath = realPath;
222
223 // if we are in a package and the file where the error occurred
224 // has /<package-name>/include/ in it then it might be a template
225 // instantiation error. in that case re-map it to the appropriate
226 // source file within the package
227 if (!pkgInclude.empty())
228 {
229 std::string path = filePath.getAbsolutePath();
230 size_t pos = path.find(pkgInclude);
231 if (pos != std::string::npos)
232 {
233 // advance to end and calculate relative path
234 pos += pkgInclude.length();
235 std::string relativePath = path.substr(pos);
236
237 // does this file exist? if so substitute it
238 FilePath includePath = projectContext().buildTargetPath()
239 .completeChildPath("inst/include/" + relativePath);
240 if (includePath.exists())
241 filePath = includePath;
242 }
243 }
244
245 // don't show warnings from Makeconf
246 if (filePath.getFilename() == "Makeconf")
247 continue;
248
249 // create marker and add it
250 SourceMarker err(module_context::sourceMarkerTypeFromString(type),
251 filePath,
252 core::safe_convert::stringTo<int>(line, 1),
253 core::safe_convert::stringTo<int>(column, 1),
254 core::html_utils::HTML(message),
255 true);
256 errors.push_back(err);
257 }
258 }
259 CATCH_UNEXPECTED_EXCEPTION;
260
261 return errors;
262 }
263
parseTestThatErrors(const FilePath & basePath,const std::string & output,core::Version testthatVersion)264 std::vector<module_context::SourceMarker> parseTestThatErrors(
265 const FilePath& basePath,
266 const std::string& output,
267 core::Version testthatVersion)
268 {
269 using namespace module_context;
270 std::vector<SourceMarker> errors;
271
272 try
273 {
274 FilePath basePathResolved = module_context::resolveAliasedPath(basePath.getAbsolutePath());
275
276 // Error output formats for different testthat versions:
277 //
278 // # testthat (>= 3.0.0)
279 // Failure (test-hello.R:2:3): multiplication works
280 //
281 // # testthat (< 3.0.0)
282 // test-hello.R:2: failure: multiplication works
283 //
284 // Note that ANSI escapes are also used.
285 boost::regex re;
286 if (testthatVersion.versionMajor() >= 3)
287 {
288 re = (
289 kAnsiEscapeRegex // color
290 "([^\\s]+)" // error type (1)
291 kAnsiEscapeRegex // color
292 "\\s+" // separating space
293 "\\(" // opening paren
294 "([^:\\n]+)" // file name (2)
295 ":" // colon separator
296 "([0-9]+)" // file line (3)
297 ":" // colon separator
298 "([0-9]+)" // file column (4)
299 "\\)" // closing paren
300 ":" // colon separator
301 "\\s+" // separating space
302 "([[:print:]]*)" // error message (5)
303 kAnsiEscapeRegex // color
304 );
305 }
306 else
307 {
308 re = (
309 kAnsiEscapeRegex // color
310 "([^:\\n]+):" // file name (1)
311 "([0-9]+):" // file line (2)
312 "\\s*" // spaces
313 kAnsiEscapeRegex // color
314 "([^:\\n]+)" // error type (3)
315 kAnsiEscapeRegex // color
316 ":" // separating colon
317 "\\s*" // spaces
318 "([[:print:]]*)" // error message (4)
319 kAnsiEscapeRegex // color
320 );
321 }
322
323 boost::sregex_iterator iter(output.begin(), output.end(), re);
324 boost::sregex_iterator end;
325 for (; iter != end; iter++)
326 {
327 boost::smatch match = *iter;
328
329 std::string file, line, column, type, message, marker;
330
331 if (testthatVersion.versionMajor() >= 3)
332 {
333 type = match[1];
334 file = match[2];
335 line = match[3];
336 column = match[4];
337 message = match[5];
338 }
339 else
340 {
341 file = match[1];
342 line = match[2];
343 type = match[3];
344 message = match[4];
345 }
346
347 std::string ltype = string_utils::toLower(type);
348 if (ltype.find("error") != std::string::npos) {
349 marker = "error";
350 } else if (ltype.find("failure") != std::string::npos) {
351 marker = "error";
352 } else if (ltype.find("warning") != std::string::npos) {
353 marker = "warning";
354 } else {
355 marker = "info";
356 }
357
358 FilePath testFilePath = basePathResolved.completePath(file);
359 SourceMarker err(module_context::sourceMarkerTypeFromString(marker),
360 testFilePath,
361 core::safe_convert::stringTo<int>(line, 1),
362 core::safe_convert::stringTo<int>(column, 1),
363 core::html_utils::HTML(message),
364 true);
365 errors.push_back(err);
366 }
367 }
368 CATCH_UNEXPECTED_EXCEPTION;
369
370 return errors;
371 }
372
parseShinyTestErrors(const FilePath & basePath,const FilePath & rdsPath,const std::string & output)373 std::vector<module_context::SourceMarker> parseShinyTestErrors(
374 const FilePath& basePath,
375 const FilePath& rdsPath,
376 const std::string& output)
377 {
378 using namespace module_context;
379 std::vector<SourceMarker> errors;
380
381 try
382 {
383 FilePath basePathResolved = module_context::resolveAliasedPath(basePath.getAbsolutePath());
384
385 std::vector<std::string> failed;
386 r::exec::RFunction rFunc(".rs.readShinytestResultRds", rdsPath.getAbsolutePath());
387 Error error = rFunc.call(&failed);
388 if (error)
389 LOG_ERROR(error);
390
391 for (size_t idxFailed = 0; idxFailed < failed.size(); idxFailed++)
392 {
393 std::string file, line, type, message;
394
395 file = failed.at(idxFailed);
396 line = "0";
397 std::string column = "0";
398 type = "failure";
399 message = std::string("Differences detected in " + file + ".");
400
401 // ask the shinytest package where the tests live (this location varies between versions of
402 // the shinytest package
403 std::string testsDir;
404 r::exec::RFunction findTests(".rs.findShinyTestsDir",
405 basePathResolved.getAbsolutePath());
406 error = findTests.call(&testsDir);
407 if (error)
408 LOG_ERROR(error);
409
410 SourceMarker err(module_context::sourceMarkerTypeFromString(type),
411 FilePath(testsDir).completePath(file + ".R"),
412 core::safe_convert::stringTo<int>(line, 1),
413 core::safe_convert::stringTo<int>(column, 1),
414 core::html_utils::HTML(message),
415 true);
416 errors.push_back(err);
417 }
418 }
419 CATCH_UNEXPECTED_EXCEPTION;
420
421 return errors;
422 }
423
424 } // anonymous namespace
425
gccErrorParser(const FilePath & basePath)426 CompileErrorParser gccErrorParser(const FilePath& basePath)
427 {
428 return boost::bind(parseGccErrors, basePath, _1);
429 }
430
rErrorParser(const FilePath & basePath)431 CompileErrorParser rErrorParser(const FilePath& basePath)
432 {
433 return boost::bind(parseRErrors, basePath, _1);
434 }
435
testthatErrorParser(const FilePath & basePath,const core::Version & testthatVersion)436 CompileErrorParser testthatErrorParser(const FilePath& basePath,
437 const core::Version& testthatVersion)
438 {
439 return boost::bind(parseTestThatErrors, basePath, _1, testthatVersion);
440 }
441
shinytestErrorParser(const FilePath & basePath,const FilePath & rdsPath)442 CompileErrorParser shinytestErrorParser(const FilePath& basePath, const FilePath& rdsPath)
443 {
444 return boost::bind(parseShinyTestErrors, basePath, rdsPath, _1);
445 }
446
447 } // namespace build
448 } // namespace modules
449 } // namespace session
450 } // namespace rstudio
451