1 /*
2  * CodeCompletion.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 "CodeCompletion.hpp"
17 
18 #include <iostream>
19 
20 #include <core/Debug.hpp>
21 #include <shared_core/Error.hpp>
22 #include <core/FileSerializer.hpp>
23 #include <core/system/Process.hpp>
24 #include <core/RegexUtils.hpp>
25 
26 #include <r/RExec.hpp>
27 
28 #include <session/projects/SessionProjects.hpp>
29 #include <session/SessionModuleContext.hpp>
30 
31 #include "RCompilationDatabase.hpp"
32 #include "RSourceIndex.hpp"
33 #include "Diagnostics.hpp"
34 
35 using namespace rstudio::core;
36 using namespace rstudio::core::libclang;
37 
38 #ifndef _WIN32
39 # define kDevNull "/dev/null"
40 #else
41 # define kDevNull "NUL"
42 #endif
43 
44 namespace rstudio {
45 namespace session {
46 namespace modules {
47 namespace clang {
48 
49 namespace {
50 
51 // cache our system include paths
52 std::vector<std::string> s_systemIncludePaths;
53 
friendlyCompletionText(const CodeCompleteResult & result)54 json::Object friendlyCompletionText(const CodeCompleteResult& result)
55 {
56    // transform text
57    std::string text = result.getText();
58    boost::algorithm::replace_all(
59      text,
60      "std::basic_string<char, std::char_traits<char>, std::allocator<char> >",
61      "std::string");
62 
63    // creat text object
64    json::Object textJson;
65    textJson["text"] = text;
66    textJson["comment"] = result.getComment();
67 
68    return textJson;
69 }
70 
71 const int kCompletionUnknown = 0;
72 const int kCompletionVariable = 1;
73 const int kCompletionFunction = 2;
74 const int kCompletionConstructor = 3;
75 const int kCompletionDestructor = 4;
76 const int kCompletionClass = 5;
77 const int kCompletionStruct = 6;
78 const int kCompletionNamespace = 7;
79 const int kCompletionEnum = 8;
80 const int kCompletionEnumValue = 9;
81 const int kCompletionKeyword = 10;
82 const int kCompletionMacro = 11;
83 const int kCompletionFile = 12;
84 const int kCompletionDirectory = 13;
85 
completionType(CXCursorKind kind)86 int completionType(CXCursorKind kind)
87 {
88    switch(kind)
89    {
90    case CXCursor_UnexposedDecl:
91       return kCompletionVariable;
92    case CXCursor_StructDecl:
93    case CXCursor_UnionDecl:
94       return kCompletionStruct;
95    case CXCursor_ClassDecl:
96       return kCompletionClass;
97    case CXCursor_EnumDecl:
98       return kCompletionEnum;
99    case CXCursor_FieldDecl:
100       return kCompletionVariable;
101    case CXCursor_EnumConstantDecl:
102       return kCompletionEnumValue;
103    case CXCursor_FunctionDecl:
104       return kCompletionFunction;
105    case CXCursor_VarDecl:
106    case CXCursor_ParmDecl:
107       return kCompletionVariable;
108    case CXCursor_ObjCInterfaceDecl:
109    case CXCursor_ObjCCategoryDecl:
110    case CXCursor_ObjCProtocolDecl:
111       return kCompletionClass;
112    case CXCursor_ObjCPropertyDecl:
113    case CXCursor_ObjCIvarDecl:
114       return kCompletionVariable;
115    case CXCursor_ObjCInstanceMethodDecl:
116    case CXCursor_ObjCClassMethodDecl:
117       return kCompletionFunction;
118    case CXCursor_ObjCImplementationDecl:
119    case CXCursor_ObjCCategoryImplDecl:
120       return kCompletionClass;
121    case CXCursor_TypedefDecl: // while these are typically classes, we don't
122                               // have access to the underlying cursor for the
123                               // completion (just a CXCursorKind) so there is
124                               // no way to know for sure
125 
126       return kCompletionClass;
127    case CXCursor_CXXMethod:
128       return kCompletionFunction;
129    case CXCursor_Namespace:
130       return kCompletionNamespace;
131    case CXCursor_LinkageSpec:
132       return kCompletionKeyword;
133    case CXCursor_Constructor:
134       return kCompletionConstructor;
135    case CXCursor_Destructor:
136       return kCompletionDestructor;
137    case CXCursor_ConversionFunction:
138       return kCompletionFunction;
139    case CXCursor_TemplateTypeParameter:
140    case CXCursor_NonTypeTemplateParameter:
141       return kCompletionVariable;
142    case CXCursor_FunctionTemplate:
143       return kCompletionFunction;
144    case CXCursor_ClassTemplate:
145    case CXCursor_ClassTemplatePartialSpecialization:
146       return kCompletionClass;
147    case CXCursor_NamespaceAlias:
148    case CXCursor_UsingDirective:
149    case CXCursor_UsingDeclaration:
150    case CXCursor_TypeAliasDecl:
151       return kCompletionVariable;
152    case CXCursor_ObjCSynthesizeDecl:
153    case CXCursor_ObjCDynamicDecl:
154    case CXCursor_CXXAccessSpecifier:
155       return kCompletionKeyword;
156    case CXCursor_MacroDefinition:
157       return kCompletionMacro;
158    default:
159       return kCompletionUnknown;
160    }
161 }
162 
toJson(const CodeCompleteResult & result)163 core::json::Object toJson(const CodeCompleteResult& result)
164 {
165    json::Object resultJson;
166    resultJson["type"] = completionType(result.getKind());
167    resultJson["typed_text"] = result.getTypedText();
168    json::Array textJson;
169    textJson.push_back(friendlyCompletionText(result));
170    resultJson["text"] = textJson;
171    return resultJson;
172 }
173 
discoverTranslationUnitIncludePaths(const FilePath & filePath,std::vector<std::string> * pIncludePaths)174 void discoverTranslationUnitIncludePaths(const FilePath& filePath,
175                                          std::vector<std::string>* pIncludePaths)
176 {
177    std::vector<std::string> args =
178          rCompilationDatabase().compileArgsForTranslationUnit(
179             filePath.getAbsolutePathNative(), false);
180 
181    for (const std::string& arg : args)
182    {
183       if (boost::algorithm::starts_with(arg, "-I"))
184       {
185          // skip RStudio's libclang builtin header paths
186          if (arg.find("libclang/builtin-headers") != std::string::npos)
187             continue;
188 
189          pIncludePaths->push_back(
190                   string_utils::strippedOfQuotes(arg.substr(2)));
191       }
192    }
193 }
194 
195 } // end anonymous namespace
196 
discoverSystemIncludePaths(std::vector<std::string> * pIncludePaths)197 void discoverSystemIncludePaths(std::vector<std::string>* pIncludePaths)
198 {
199    // if we've cached results already, just use that
200    if (!s_systemIncludePaths.empty())
201    {
202       pIncludePaths->insert(
203                pIncludePaths->end(),
204                s_systemIncludePaths.begin(),
205                s_systemIncludePaths.end());
206       return;
207    }
208 
209    core::system::ProcessOptions processOptions;
210    processOptions.redirectStdErrToStdOut = true;
211 
212    // add Rtools to PATH if necessary
213    core::system::Options environment;
214    core::system::environment(&environment);
215 
216    std::string warning;
217    module_context::addRtoolsToPathIfNecessary(&environment, &warning);
218    processOptions.environment = environment;
219 
220    // get the CXX compiler by asking R
221    std::string compilerPath;
222 
223 #ifdef _WIN32
224    {
225       core::system::ProcessResult result;
226       Error error = core::system::runCommand("where.exe gcc.exe", processOptions, &result);
227       if (error)
228          LOG_ERROR(error);
229       else if (result.exitStatus != EXIT_SUCCESS)
230          LOG_ERROR_MESSAGE("Error querying CXX compiler: " + result.stdOut);
231       else
232          compilerPath = string_utils::trimWhitespace(result.stdOut);
233    }
234 #else
235    {
236       Error error;
237 
238       // resolve R CMD location for shell command
239       FilePath rBinDir;
240       error = module_context::rBinDir(&rBinDir);
241       if (error)
242       {
243          LOG_ERROR(error);
244          return;
245       }
246 
247       shell_utils::ShellCommand rCmd = module_context::rCmd(rBinDir);
248 
249       core::system::ProcessResult result;
250       rCmd << "config";
251       rCmd << "CXX";
252       error = core::system::runCommand(rCmd, processOptions, &result);
253       if (error)
254          LOG_ERROR(error);
255       else if (result.exitStatus != EXIT_SUCCESS)
256          LOG_ERROR_MESSAGE("Error querying CXX compiler: " + result.stdOut);
257       else
258          compilerPath = string_utils::trimWhitespace(result.stdOut);
259    }
260 #endif
261 
262    if (compilerPath.empty())
263       return;
264 
265    // it is likely that R is configured to use a compiler that exists
266    // on the PATH; however, when invoked through 'runCommand()' this
267    // can fail unless we explicitly find and resolve that path. also
268    // note that one can configure CXX with multiple arguments
269    // (e.g. as 'g++ -std=c++17') so we must also tear off only the
270    // compiler name. we hence assume that the supplemental arguments
271    // to CXX do not influence the compiler include paths.
272 #ifndef _WIN32
273    {
274        std::string compilerName = compilerPath;
275        std::size_t index = compilerPath.find(' ');
276        if (index != std::string::npos)
277        {
278            compilerName = compilerPath.substr(0, index);
279        }
280        else
281        {
282            compilerName = compilerPath;
283        }
284 
285        core::system::ProcessResult result;
286        std::vector<std::string> args = { compilerName };
287        Error error = core::system::runProgram("/usr/bin/which", args, "", processOptions, &result);
288        if (error)
289           LOG_ERROR(error);
290        else if (result.exitStatus != EXIT_SUCCESS)
291           LOG_ERROR_MESSAGE("Error qualifying CXX compiler path: " + result.stdOut);
292        else
293           compilerPath = string_utils::trimWhitespace(result.stdOut);
294    }
295 #endif
296 
297    // ask the compiler what the system include paths are (note that both
298    // gcc and clang accept the same command)
299    std::string includePathOutput;
300    {
301       core::system::ProcessResult result;
302       std::string cmd = compilerPath + " -E -x c++ - -v < " kDevNull;
303 
304       Error error = core::system::runCommand(cmd, processOptions, &result);
305       if (error)
306          LOG_ERROR(error);
307       else if (result.exitStatus != EXIT_SUCCESS)
308          LOG_ERROR_MESSAGE("Error retrieving system include paths: " + result.stdOut);
309       else
310          includePathOutput = string_utils::trimWhitespace(result.stdOut);
311    }
312    if (includePathOutput.empty())
313       return;
314 
315    // strip out the include paths from the output
316    std::string startString = "#include <...> search starts here:";
317    std::string endString   = "End of search list.";
318 
319    std::string::size_type startPos = includePathOutput.find(startString);
320    if (startPos == std::string::npos)
321       return;
322 
323    std::string::size_type endPos = includePathOutput.find(endString, startPos);
324    if (endPos == std::string::npos)
325       return;
326 
327    std::string includePathsString = string_utils::trimWhitespace(
328             string_utils::substring(includePathOutput,
329                                     startPos + startString.size(),
330                                     endPos));
331 
332    // split on newlines
333    std::vector<std::string> includePaths = core::algorithm::split(includePathsString, "\n");
334 
335    // remove framework directories
336    core::algorithm::expel_if(includePaths, string_utils::Contains("(framework directory)"));
337 
338    // trim whitespace
339    for (std::size_t i = 0, n = includePaths.size(); i < n; ++i)
340       includePaths[i] = string_utils::trimWhitespace(includePaths[i]);
341 
342    // update cache
343    s_systemIncludePaths = includePaths;
344 
345    // fill result vector
346    pIncludePaths->insert(
347             pIncludePaths->end(),
348             includePaths.begin(),
349             includePaths.end());
350 }
351 
352 namespace {
353 
discoverRelativeIncludePaths(const FilePath & filePath,const std::string & parentDir,std::vector<std::string> * pIncludePaths)354 void discoverRelativeIncludePaths(const FilePath& filePath,
355                                   const std::string& parentDir,
356                                   std::vector<std::string>* pIncludePaths)
357 {
358    // Construct the directory in which to search for includes
359    FilePath targetPath = filePath.getParent().completePath(parentDir);
360    if (!targetPath.exists())
361       return;
362 
363    pIncludePaths->push_back(targetPath.getAbsolutePath());
364 }
365 
jsonHeaderCompletionResult(const std::string & name,const std::string & source,int completionType)366 json::Object jsonHeaderCompletionResult(const std::string& name,
367                                         const std::string& source,
368                                         int completionType)
369 {
370    json::Object completionJson;
371    completionJson["type"]       = completionType;
372    completionJson["typed_text"] = name;
373 
374    json::Array textJson;
375    json::Object objectJson;
376    objectJson["text"]    = name;
377    objectJson["comment"] = source;
378    textJson.push_back(objectJson);
379    completionJson["text"] = textJson;
380 
381    return completionJson;
382 }
383 
getHeaderCompletionsImpl(const std::string & token,const std::string & parentDir,const FilePath & filePath,const std::string & docId,bool systemHeadersOnly,const core::json::JsonRpcRequest & request,core::json::JsonRpcResponse * pResponse)384 Error getHeaderCompletionsImpl(const std::string& token,
385                                const std::string& parentDir,
386                                const FilePath& filePath,
387                                const std::string& docId,
388                                bool systemHeadersOnly,
389                                const core::json::JsonRpcRequest& request,
390                                core::json::JsonRpcResponse* pResponse)
391 {
392    std::vector<std::string> includePaths;
393 
394    // discover the system headers
395    discoverSystemIncludePaths(&includePaths);
396 
397    // discover TU-related include paths
398    discoverTranslationUnitIncludePaths(filePath, &includePaths);
399 
400    // discover local include paths
401    if (!systemHeadersOnly)
402       discoverRelativeIncludePaths(filePath, parentDir, &includePaths);
403 
404    // remove dupes
405    std::sort(includePaths.begin(), includePaths.end());
406    includePaths.erase(std::unique(includePaths.begin(), includePaths.end()),
407                       includePaths.end());
408 
409    // loop through header include paths and return paths that match token
410    std::set<std::string> discoveredEntries;
411    json::Array completionsJson;
412 
413    for (const std::string& path : includePaths)
414    {
415       FilePath includePath(path);
416       if (!includePath.exists())
417          continue;
418 
419       FilePath targetPath = includePath.completePath(parentDir);
420       if (!targetPath.exists())
421          continue;
422 
423       std::vector<FilePath> children;
424       Error error = targetPath.getChildren(children);
425       if (error)
426          LOG_ERROR(error);
427 
428       for (const FilePath& childPath : children)
429       {
430          std::string name = childPath.getFilename();
431          if (discoveredEntries.count(name))
432             continue;
433 
434          std::string extension = childPath.getExtensionLowerCase();
435          if (!(extension == ".h" || extension == ".hpp" || extension == ""))
436             continue;
437 
438          if (string_utils::isSubsequence(name, token, true))
439          {
440             int type = childPath.isDirectory() ? kCompletionDirectory : kCompletionFile;
441             completionsJson.push_back(jsonHeaderCompletionResult(name,
442                                                                  childPath.getAbsolutePath(),
443                                                                  type));
444          }
445 
446          discoveredEntries.insert(name);
447       }
448    }
449 
450    // construct completion result
451    json::Object resultJson;
452    resultJson["completions"] = completionsJson;
453    pResponse->setResult(resultJson);
454    return Success();
455 }
456 
getHeaderCompletions(std::string line,const FilePath & filePath,const std::string & docId,const core::json::JsonRpcRequest & request,core::json::JsonRpcResponse * pResponse)457 Error getHeaderCompletions(std::string line,
458                            const FilePath& filePath,
459                            const std::string& docId,
460                            const core::json::JsonRpcRequest& request,
461                            core::json::JsonRpcResponse* pResponse)
462 {
463    // extract portion of the path that the user has produced
464    std::string::size_type idx = line.find_first_of("<\"");
465    if (idx == std::string::npos)
466       return Success();
467 
468    bool isSystemHeader = line[idx] == '<';
469    std::string pathString = line.substr(idx + 1);
470 
471    // split path into 'parentDir' + 'token', e.g.
472    //
473    //    #include <foo/bar/ba
474    //              ^^^^^^^|^^
475    std::string parentDir, token;
476    std::string::size_type pathDelimIdx = pathString.find_last_of("/");
477    if (pathDelimIdx == std::string::npos)
478    {
479       token = pathString;
480    }
481    else
482    {
483       parentDir = pathString.substr(0, pathDelimIdx);
484       token     = pathString.substr(pathDelimIdx + 1);
485    }
486 
487    return getHeaderCompletionsImpl(token,
488                                    parentDir,
489                                    filePath,
490                                    docId,
491                                    isSystemHeader,
492                                    request,
493                                    pResponse);
494 }
495 
496 
497 } // anonymous namespace
498 
499 
getCppCompletions(const core::json::JsonRpcRequest & request,core::json::JsonRpcResponse * pResponse)500 Error getCppCompletions(const core::json::JsonRpcRequest& request,
501                         core::json::JsonRpcResponse* pResponse)
502 {
503    // empty response by default
504    pResponse->setResult(json::Value());
505 
506    // get params
507    std::string line, docPath, docId, userText;
508    int row, column;
509    Error error = json::readParams(request.params,
510                                   &line,
511                                   &docPath,
512                                   &docId,
513                                   &row,
514                                   &column,
515                                   &userText);
516    if (error)
517       return error;
518 
519    // resolve the docPath if it's aliased
520    FilePath filePath = module_context::resolveAliasedPath(docPath);
521 
522    // if it looks like we're requesting autocompletions for an '#include'
523    // statement, take a separate completion path
524    static const boost::regex reInclude("^\\s*#+\\s*include");
525    if (regex_utils::textMatches(line, reInclude, true, true))
526       return getHeaderCompletions(line, filePath, docId, request, pResponse);
527 
528    // get the translation unit and do the code completion
529    std::string filename = filePath.getAbsolutePath();
530    TranslationUnit tu = rSourceIndex().getTranslationUnit(filename);
531 
532    if (!tu.empty())
533    {
534       std::string lastTypedText;
535       json::Array completionsJson;
536       boost::shared_ptr<CodeCompleteResults> pResults =
537                               tu.codeCompleteAt(filename, row, column);
538       if (!pResults->empty())
539       {
540          // get results
541          for (unsigned i = 0; i<pResults->getNumResults(); i++)
542          {
543             CodeCompleteResult result = pResults->getResult(i);
544 
545             // filter on user text if we have it
546             if (!userText.empty() &&
547                 !boost::algorithm::starts_with(result.getTypedText(), userText))
548             {
549                continue;
550             }
551 
552             // check whether this completion is valid and bail if not
553             if (result.getAvailability() != CXAvailability_Available)
554             {
555                continue;
556             }
557 
558             std::string typedText = result.getTypedText();
559 
560             // if we have the same typed text then just ammend previous result
561             if ((typedText == lastTypedText) && !completionsJson.isEmpty())
562             {
563                json::Object res = completionsJson.getBack().getObject();
564                json::Array text = res["text"].getArray();
565                text.push_back(friendlyCompletionText(result));
566             }
567             else
568             {
569                completionsJson.push_back(toJson(result));
570             }
571 
572             lastTypedText = typedText;
573          }
574       }
575 
576       json::Object resultJson;
577       resultJson["completions"] = completionsJson.clone();
578       pResponse->setResult(resultJson);
579    }
580 
581    return Success();
582 }
583 
584 } // namespace clang
585 } // namespace modules
586 } // namespace session
587 } // namespace rstudio
588 
589