File indexing completed on 2024-05-19 15:46:42

0001 /*
0002     SPDX-FileCopyrightText: 2013 Sven Brauch <svenbrauch@googlemail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "context.h"
0008 
0009 #include "items/modulecompletionitem.h"
0010 #include "items/functioncalltipcompletionitem.h"
0011 
0012 #include <language/codecompletion/codecompletionitem.h>
0013 #include <language/codecompletion/normaldeclarationcompletionitem.h>
0014 #include <language/duchain/declaration.h>
0015 #include <language/duchain/duchainlock.h>
0016 #include <language/duchain/classdeclaration.h>
0017 #include <language/duchain/namespacealiasdeclaration.h>
0018 #include <language/duchain/codemodel.h>
0019 
0020 #include <qmljs/qmljsdocument.h>
0021 #include <qmljs/parser/qmljslexer_p.h>
0022 #include "../duchain/expressionvisitor.h"
0023 #include "../duchain/helper.h"
0024 #include "../duchain/cache.h"
0025 #include "../duchain/frameworks/nodejs.h"
0026 
0027 #include <QDir>
0028 #include <QRegExp>
0029 
0030 using namespace KDevelop;
0031 
0032 using DeclarationDepthPair = QPair<Declaration*, int>;
0033 
0034 namespace QmlJS {
0035 
0036 CodeCompletionContext::CodeCompletionContext(const DUContextPointer& context, const QString& text,
0037                                              const CursorInRevision& position, int depth)
0038 : KDevelop::CodeCompletionContext(context, extractLastLine(text), position, depth),
0039   m_completionKind(NormalCompletion)
0040 {
0041     // Detect "import ..." and provide import completions
0042     if (m_text.startsWith(QLatin1String("import "))) {
0043         m_completionKind = ImportCompletion;
0044     }
0045 
0046     // Node.js module completions
0047     if (m_text.endsWith(QLatin1String("require("))) {
0048         m_completionKind = NodeModulesCompletion;
0049     }
0050 
0051     // Detect whether the cursor is in a comment
0052     bool isLastLine = true;
0053     bool inString = false;
0054 
0055     for (int index = text.size()-1; index > 0; --index) {
0056         const QChar c = text.at(index);
0057         const QChar prev = text.at(index - 1);
0058 
0059         if (c == QLatin1Char('\n')) {
0060             isLastLine = false;
0061         } else if (isLastLine && prev == QLatin1Char('/') && c == QLatin1Char('/')) {
0062             // Single-line comment on the current line, we are in a comment
0063             m_completionKind = CommentCompletion;
0064             break;
0065         } else if (prev == QLatin1Char('/') && c == QLatin1Char('*')) {
0066             // Start of a multi-line comment encountered
0067             m_completionKind = CommentCompletion;
0068             break;
0069         } else if (prev == QLatin1Char('*') && c == QLatin1Char('/')) {
0070             // End of a multi-line comment. Because /* and */ cannot be nested,
0071             // encountering a */ is enough to know that the cursor is outside a
0072             // comment
0073             break;
0074         } else if (prev != QLatin1Char('\\') && (c == QLatin1Char('"') || c == QLatin1Char('\''))) {
0075             // Toggle whether we are in a string or not
0076             inString = !inString;
0077         }
0078     }
0079 
0080     if (inString) {
0081         m_completionKind = StringCompletion;
0082     }
0083 
0084     // Some specific constructs don't need any code-completion at all (mainly
0085     // because the user will declare new things, not use ones)
0086     if (m_text.contains(QRegExp(QLatin1String("(var|function)\\s+$"))) ||                   // "var |" or "function |"
0087         m_text.contains(QRegExp(QLatin1String("property\\s+[a-zA-Z0-9_]+\\s+$"))) ||        // "property <type> |"
0088         m_text.contains(QRegExp(QLatin1String("function(\\s+[a-zA-Z0-9_]+)?\\s*\\($"))) ||  // "function (|" or "function <name> (|"
0089         m_text.contains(QRegExp(QLatin1String("id:\\s*")))                                  // "id: |"
0090     ) {
0091         m_completionKind = NoCompletion;
0092     }
0093 }
0094 
0095 QList<CompletionTreeItemPointer> CodeCompletionContext::completionItems(bool& abort, bool fullCompletion)
0096 {
0097     Q_UNUSED (fullCompletion);
0098 
0099     if (abort) {
0100         return QList<CompletionTreeItemPointer>();
0101     }
0102 
0103     switch (m_completionKind) {
0104     case NormalCompletion:
0105         return normalCompletion();
0106     case CommentCompletion:
0107         return commentCompletion();
0108     case ImportCompletion:
0109         return importCompletion();
0110     case NodeModulesCompletion:
0111         return nodeModuleCompletions();
0112     case StringCompletion:
0113     case NoCompletion:
0114         break;
0115     }
0116 
0117     return QList<CompletionTreeItemPointer>();
0118 }
0119 
0120 AbstractType::Ptr CodeCompletionContext::typeToMatch() const
0121 {
0122     return m_typeToMatch;
0123 }
0124 
0125 QList<KDevelop::CompletionTreeItemPointer> CodeCompletionContext::normalCompletion()
0126 {
0127     QList<CompletionTreeItemPointer> items;
0128     QChar lastChar = m_text.size() > 0 ? m_text.at(m_text.size() - 1) : QLatin1Char('\0');
0129     bool inQmlObjectScope = (m_duContext->type() == DUContext::Class);
0130 
0131     // Start with the function call-tips, because functionCallTips is also responsible
0132     // for setting m_declarationForTypeMatch
0133     items << functionCallTips();
0134 
0135     if (lastChar == QLatin1Char('.') || lastChar == QLatin1Char('[')) {
0136         // Offer completions for object members and array subscripts
0137         items << fieldCompletions(
0138             m_text.left(m_text.size() - 1),
0139             lastChar == QLatin1Char('[') ? CompletionItem::QuotesAndBracket :
0140             CompletionItem::NoDecoration
0141         );
0142     }
0143 
0144     // "object." must only display the members of object, the declarations
0145     // available in the current context.
0146     if (lastChar != QLatin1Char('.')) {
0147         if (inQmlObjectScope) {
0148             DUChainReadLocker lock;
0149 
0150             // The cursor is in a QML object and there is nothing before it. Display
0151             // a list of properties and signals that can be used in a script binding.
0152             // Note that the properties/signals of parent QML objects are not displayed here
0153             items << completionsInContext(m_duContext,
0154                                           CompletionOnlyLocal | CompletionHideWrappers,
0155                                           CompletionItem::ColonOrBracket);
0156             items << completionsFromImports(CompletionHideWrappers);
0157             items << completionsInContext(DUContextPointer(m_duContext->topContext()),
0158                                           CompletionHideWrappers,
0159                                           CompletionItem::NoDecoration);
0160         } else {
0161             items << completionsInContext(m_duContext,
0162                                           CompletionInContextFlags(),
0163                                           CompletionItem::NoDecoration);
0164             items << completionsFromImports(CompletionInContextFlags());
0165             items << completionsFromNodeModule(CompletionInContextFlags(), QStringLiteral("__builtin_ecmascript"));
0166 
0167             if (!QmlJS::isQmlFile(m_duContext.data())) {
0168                 items << completionsFromNodeModule(CompletionInContextFlags(), QStringLiteral("__builtin_dom"));
0169             }
0170         }
0171     }
0172 
0173     return items;
0174 }
0175 
0176 QList<CompletionTreeItemPointer> CodeCompletionContext::commentCompletion()
0177 {
0178     return QList<CompletionTreeItemPointer>();
0179 }
0180 
0181 QList<CompletionTreeItemPointer> CodeCompletionContext::importCompletion()
0182 {
0183     QList<CompletionTreeItemPointer> items;
0184     const auto fragment = m_text.section(QLatin1Char(' '), -1, -1);
0185 
0186     auto addModules = [&items, &fragment](const QString& dataDir) {
0187         if (dataDir.isEmpty())
0188             return;
0189 
0190         QDir dir(dataDir);
0191         if (!dir.exists())
0192             return;
0193 
0194         const auto dirEntries = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
0195         items.reserve(dirEntries.size());
0196         for (const QString& entry : dirEntries) {
0197             items.append(CompletionTreeItemPointer(new ModuleCompletionItem(
0198                 fragment + entry.section(QLatin1Char('.'), 0, 0),
0199                 ModuleCompletionItem::Import
0200             )));
0201         }
0202     };
0203 
0204     // Use the cache to find the directory corresponding to the fragment
0205     // (org.kde is, for instance, /usr/lib64/kde4/imports/org/kde), and list
0206     // its subdirectories
0207     addModules(Cache::instance().modulePath(m_duContext->url(), fragment));
0208 
0209     if (items.isEmpty()) {
0210         // fallback to list all directories we can find
0211         const auto paths = Cache::instance().libraryPaths(m_duContext->url());
0212         auto fragmentPath = fragment;
0213         fragmentPath.replace(QLatin1Char('.'), QLatin1Char('/'));
0214         for (const auto& path : paths) {
0215             addModules(path.cd(fragmentPath).path());
0216         }
0217     }
0218 
0219     return items;
0220 }
0221 
0222 QList<CompletionTreeItemPointer> CodeCompletionContext::nodeModuleCompletions()
0223 {
0224     QList<CompletionTreeItemPointer> items;
0225     QDir dir;
0226 
0227     const auto& paths = NodeJS::instance().moduleDirectories(m_duContext->url().str());
0228     for (auto& path : paths) {
0229         dir.setPath(path.toLocalFile());
0230 
0231         const auto& entries = dir.entryList(QDir::Files, QDir::Name);
0232         for (QString entry : entries) {
0233             entry.remove(QLatin1String(".js"));
0234 
0235             if (entry.startsWith(QLatin1String("__"))) {
0236                 // Internal module, don't show
0237                 continue;
0238             }
0239 
0240             items.append(CompletionTreeItemPointer(
0241                 new ModuleCompletionItem(entry, ModuleCompletionItem::Quotes)
0242             ));
0243         }
0244     }
0245 
0246     return items;
0247 }
0248 
0249 QList<CompletionTreeItemPointer> CodeCompletionContext::functionCallTips()
0250 {
0251     Stack<ExpressionStackEntry> stack = expressionStack(m_text);
0252     QList<CompletionTreeItemPointer> items;
0253     int argumentHintDepth = 1;
0254     bool isTopOfStack = true;
0255     DUChainReadLocker lock;
0256 
0257     while (!stack.isEmpty()) {
0258         ExpressionStackEntry entry = stack.pop();
0259 
0260         if (isTopOfStack && entry.operatorStart > entry.startPosition) {
0261             // Deduce the declaration for type matching using operatorStart:
0262             //
0263             // table[document.base +
0264             //       [             ^
0265             //
0266             // ^ = operatorStart. Just before operatorStart is a code snippet that ends
0267             // with the declaration whose type should be used.
0268             DeclarationPointer decl = declarationAtEndOfString(m_text.left(entry.operatorStart));
0269 
0270             if (decl) {
0271                 m_typeToMatch = decl->abstractType();
0272             }
0273         }
0274 
0275         if (entry.startPosition > 0 && m_text.at(entry.startPosition - 1) == QLatin1Char('(')) {
0276             // The current entry represents a function call, create a call-tip for it
0277             DeclarationPointer functionDecl = declarationAtEndOfString(m_text.left(entry.startPosition - 1));
0278 
0279             if (functionDecl) {
0280                 auto  item = new FunctionCalltipCompletionItem(
0281                     functionDecl,
0282                     argumentHintDepth,
0283                     entry.commas
0284                 );
0285 
0286                 items << CompletionTreeItemPointer(item);
0287                 argumentHintDepth++;
0288 
0289                 if (isTopOfStack && !m_typeToMatch) {
0290                     m_typeToMatch = item->currentArgumentType();
0291                 }
0292             }
0293         }
0294 
0295         isTopOfStack = false;
0296     }
0297 
0298     return items;
0299 }
0300 
0301 QList<CompletionTreeItemPointer> CodeCompletionContext::completionsFromImports(CompletionInContextFlags flags)
0302 {
0303     QList<CompletionTreeItemPointer> items;
0304 
0305     // Iterate over all the imported namespaces and add their definitions
0306     DUChainReadLocker lock;
0307     const QList<Declaration*> imports = m_duContext->findDeclarations(globalImportIdentifier());
0308     QList<Declaration*> realImports;
0309 
0310     for (Declaration* import : imports) {
0311         if (import->kind() != Declaration::NamespaceAlias) {
0312             continue;
0313         }
0314 
0315         auto* decl = static_cast<NamespaceAliasDeclaration *>(import);
0316         realImports << m_duContext->findDeclarations(decl->importIdentifier());
0317     }
0318 
0319     items.reserve(realImports.size());
0320     foreach (Declaration* import, realImports) {
0321         items << completionsInContext(
0322             DUContextPointer(import->internalContext()),
0323             flags,
0324             CompletionItem::NoDecoration
0325         );
0326     }
0327 
0328     return items;
0329 }
0330 
0331 QList<CompletionTreeItemPointer> CodeCompletionContext::completionsFromNodeModule(CompletionInContextFlags flags,
0332                                                                                   const QString& module)
0333 {
0334     return completionsInContext(
0335         DUContextPointer(QmlJS::getInternalContext(
0336             QmlJS::NodeJS::instance().moduleExports(module, m_duContext->url())
0337         )),
0338         flags | CompletionOnlyLocal,
0339         CompletionItem::NoDecoration
0340     );
0341 }
0342 
0343 QList<CompletionTreeItemPointer> CodeCompletionContext::completionsInContext(const DUContextPointer& context,
0344                                                                              CompletionInContextFlags flags,
0345                                                                              CompletionItem::Decoration decoration)
0346 {
0347     QList<CompletionTreeItemPointer> items;
0348     DUChainReadLocker lock;
0349 
0350     if (context) {
0351         const auto declarations = context->allDeclarations(
0352             CursorInRevision::invalid(),
0353             context->topContext(),
0354             !flags.testFlag(CompletionOnlyLocal)
0355         );
0356 
0357         for (const DeclarationDepthPair& decl : declarations) {
0358             DeclarationPointer declaration(decl.first);
0359             CompletionItem::Decoration decorationOfThisItem = decoration;
0360 
0361             if (declaration->identifier() == globalImportIdentifier()) {
0362                 continue;
0363             } if (declaration->qualifiedIdentifier().isEmpty()) {
0364                 continue;
0365             } else if (context->owner() && (
0366                             context->owner()->kind() == Declaration::Namespace ||
0367                             context->owner()->kind() == Declaration::NamespaceAlias
0368                        ) && decl.second != 0 && decl.second != 1001) {
0369                 // Only show the local declarations of modules, or the declarations
0370                 // immediately in its imported parent contexts (that are global
0371                 // contexts, hence the distance of 1001). This prevents "String()",
0372                 // "QtQuick1.0" and "builtins" from being listed when the user
0373                 // types "PlasmaCore.".
0374                 continue;
0375             } else if (decorationOfThisItem == CompletionItem::NoDecoration &&
0376                        declaration->abstractType() &&
0377                        declaration->abstractType()->whichType() == AbstractType::TypeFunction) {
0378                 // Decorate function calls with brackets
0379                 decorationOfThisItem = CompletionItem::Brackets;
0380             } else if (flags.testFlag(CompletionHideWrappers)) {
0381                 auto* classDecl = dynamic_cast<ClassDeclaration*>(declaration.data());
0382 
0383                 if (classDecl && classDecl->classType() == ClassDeclarationData::Interface) {
0384                     continue;
0385                 }
0386             }
0387 
0388             items << CompletionTreeItemPointer(new CompletionItem(declaration, decl.second, decorationOfThisItem));
0389         }
0390     }
0391 
0392     return items;
0393 }
0394 
0395 QList<CompletionTreeItemPointer> CodeCompletionContext::fieldCompletions(const QString& expression,
0396                                                                          CompletionItem::Decoration decoration)
0397 {
0398     // The statement given to this method ends with an expression that may identify
0399     // a declaration ("foo" in "test(1, 2, foo"). List the declarations of this
0400     // inner context
0401     DUContext* context = getInternalContext(declarationAtEndOfString(expression));
0402 
0403     if (context) {
0404         return completionsInContext(DUContextPointer(context),
0405                                     CompletionOnlyLocal,
0406                                     decoration);
0407     } else {
0408         return QList<CompletionTreeItemPointer>();
0409     }
0410 }
0411 
0412 Stack<CodeCompletionContext::ExpressionStackEntry> CodeCompletionContext::expressionStack(const QString& expression)
0413 {
0414     Stack<CodeCompletionContext::ExpressionStackEntry> stack;
0415     ExpressionStackEntry entry;
0416     QmlJS::Lexer lexer(nullptr);
0417     bool atEnd = false;
0418 
0419     lexer.setCode(expression, 1, false);
0420 
0421     entry.startPosition = 0;
0422     entry.operatorStart = 0;
0423     entry.operatorEnd = 0;
0424     entry.commas = 0;
0425 
0426     stack.push(entry);
0427 
0428     // NOTE: KDevelop uses 0-indexed columns while QMLJS uses 1-indexed columns
0429     while (!atEnd) {
0430         switch (lexer.lex()) {
0431         case QmlJSGrammar::EOF_SYMBOL:
0432             atEnd = true;
0433             break;
0434         case QmlJSGrammar::T_LBRACE:
0435         case QmlJSGrammar::T_LBRACKET:
0436         case QmlJSGrammar::T_LPAREN:
0437             entry.startPosition = lexer.tokenEndColumn() - 1;
0438             entry.operatorStart = entry.startPosition;
0439             entry.operatorEnd = entry.startPosition;
0440             entry.commas = 0;
0441 
0442             stack.push(entry);
0443             break;
0444         case QmlJSGrammar::T_RBRACE:
0445         case QmlJSGrammar::T_RBRACKET:
0446         case QmlJSGrammar::T_RPAREN:
0447             if (stack.count() > 1) {
0448                 stack.pop();
0449             }
0450             break;
0451         case QmlJSGrammar::T_IDENTIFIER:
0452         case QmlJSGrammar::T_DOT:
0453         case QmlJSGrammar::T_THIS:
0454             break;
0455         case QmlJSGrammar::T_COMMA:
0456             stack.top().commas++;
0457             break;
0458         default:
0459             // The last operator of every sub-expression is stored on the stack
0460             // so that "A = foo." can know that attributes of foo having the same
0461             // type as A should be highlighted.
0462             stack.top().operatorStart = lexer.tokenStartColumn() - 1;
0463             stack.top().operatorEnd = lexer.tokenEndColumn() - 1;
0464             break;
0465         }
0466     }
0467 
0468     return stack;
0469 }
0470 
0471 DeclarationPointer CodeCompletionContext::declarationAtEndOfString(const QString& expression)
0472 {
0473     // Build the expression stack of expression and use the valid portion of the
0474     // top sub-expression to find the right-most declaration that can be found
0475     // in expression.
0476     QmlJS::Document::MutablePtr doc = QmlJS::Document::create(QStringLiteral("inline"), Dialect::JavaScript);
0477     ExpressionStackEntry topEntry = expressionStack(expression).top();
0478 
0479     doc->setSource(expression.mid(topEntry.operatorEnd + topEntry.commas));
0480     doc->parseExpression();
0481 
0482     if (!doc || !doc->isParsedCorrectly()) {
0483         return DeclarationPointer();
0484     }
0485 
0486     // Use ExpressionVisitor to find the type (and associated declaration) of
0487     // the snippet that has been parsed. The inner context of the declaration
0488     // can be used to get the list of completions
0489     ExpressionVisitor visitor(m_duContext.data());
0490     doc->ast()->accept(&visitor);
0491 
0492     return visitor.lastDeclaration();
0493 }
0494 }