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