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

0001 /*
0002     SPDX-FileCopyrightText: 2012 Milian Wolff <mail@milianw.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "parsesession.h"
0008 #include "debugvisitor.h"
0009 #include "cache.h"
0010 
0011 #include <qmljs/parser/qmljsast_p.h>
0012 #include <qmljs/parser/qmljsengine_p.h>
0013 
0014 #include <language/duchain/stringhelpers.h>
0015 #include <language/duchain/duchain.h>
0016 #include <language/duchain/duchainlock.h>
0017 #include <language/duchain/declaration.h>
0018 #include <language/backgroundparser/backgroundparser.h>
0019 #include <language/editor/documentrange.h>
0020 #include <interfaces/ilanguagecontroller.h>
0021 #include <interfaces/icore.h>
0022 
0023 using namespace KDevelop;
0024 
0025 IndexedString ParseSession::languageString()
0026 {
0027     static const IndexedString langString("QML/JS");
0028     return langString;
0029 }
0030 
0031 bool isSorted(const QList<QmlJS::AST::SourceLocation>& locations)
0032 {
0033     if (locations.size() <= 1) {
0034         return true;
0035     }
0036     for(int i = 1; i < locations.size(); ++i) {
0037         if (locations.at(i).begin() <= locations.at(i-1).begin()) {
0038             return false;
0039         }
0040     }
0041     return true;
0042 }
0043 
0044 QmlJS::Dialect ParseSession::guessLanguageFromSuffix(const QString& path)
0045 {
0046     if (path.endsWith(QLatin1String(".js"))) {
0047         return QmlJS::Dialect::JavaScript;
0048     } else if (path.endsWith(QLatin1String(".json"))) {
0049         return QmlJS::Dialect::Json;
0050     } else {
0051         return QmlJS::Dialect::Qml;
0052     }
0053 }
0054 
0055 ParseSession::ParseSession(const IndexedString& url, const QString& contents, int priority)
0056 : m_url(url),
0057   m_ownPriority(priority),
0058   m_allDependenciesSatisfied(true)
0059 {
0060     const QString path = m_url.str();
0061     m_doc = QmlJS::Document::create(path, guessLanguageFromSuffix(path));
0062     m_doc->setSource(contents);
0063     m_doc->parse();
0064     Q_ASSERT(isSorted(m_doc->engine()->comments()));
0065 
0066     // Parse the module name and the version of url (this is used only when the file
0067     // is a QML module, but doesn't break for JavaScript files)
0068     m_baseName = QString::fromUtf8(m_url.byteArray())
0069         .section(QLatin1Char('/'), -1, -1)                   // Base name
0070         .section(QLatin1Char('.'), 0, -2);                   // Without extension
0071 }
0072 
0073 bool ParseSession::isParsedCorrectly() const
0074 {
0075     return m_doc->isParsedCorrectly();
0076 }
0077 
0078 QmlJS::AST::Node* ParseSession::ast() const
0079 {
0080     return m_doc->ast();
0081 }
0082 
0083 IndexedString ParseSession::url() const
0084 {
0085     return m_url;
0086 }
0087 
0088 QString ParseSession::moduleName() const
0089 {
0090     return m_baseName;
0091 }
0092 
0093 void ParseSession::addProblem(QmlJS::AST::Node* node,
0094                               const QString& message,
0095                               IProblem::Severity severity)
0096 {
0097     ProblemPointer p(new Problem);
0098 
0099     p->setDescription(message);
0100     p->setSeverity(severity);
0101     p->setSource(IProblem::SemanticAnalysis);
0102     p->setFinalLocation(DocumentRange(m_url, editorFindRange(node, node).castToSimpleRange()));
0103 
0104     m_problems << p;
0105 }
0106 
0107 QList<ProblemPointer> ParseSession::problems() const
0108 {
0109     QList<ProblemPointer> problems = m_problems;
0110 
0111     const auto diagnosticMessages = m_doc->diagnosticMessages();
0112     problems.reserve(problems.size() + diagnosticMessages.size());
0113     for (const auto& msg : diagnosticMessages) {
0114         ProblemPointer p(new Problem);
0115         p->setDescription(msg.message);
0116         p->setSeverity(IProblem::Error);
0117         p->setSource(IProblem::Parser);
0118         p->setFinalLocation(DocumentRange(m_url, locationToRange(msg.loc).castToSimpleRange()));
0119         problems << p;
0120     }
0121 
0122     return problems;
0123 }
0124 
0125 QString ParseSession::symbolAt(const QmlJS::AST::SourceLocation& location) const
0126 {
0127     return m_doc->source().mid(location.offset, location.length);
0128 }
0129 
0130 QmlJS::Dialect ParseSession::language() const
0131 {
0132     return m_doc->language();
0133 }
0134 
0135 bool compareSourceLocation(const QmlJS::AST::SourceLocation& l,
0136                            const QmlJS::AST::SourceLocation& r)
0137 {
0138     return l.begin() < r.begin();
0139 }
0140 
0141 QString ParseSession::commentForLocation(const QmlJS::AST::SourceLocation& location) const
0142 {
0143     // find most recent comment in sorted list of comments
0144     const QList< QmlJS::AST::SourceLocation >& comments = m_doc->engine()->comments();
0145     auto it = std::lower_bound(
0146         comments.constBegin(),
0147         comments.constEnd(),
0148         location, compareSourceLocation
0149     );
0150 
0151     if (it == comments.constBegin()) {
0152         return QString();
0153     }
0154 
0155     // lower bound returns the place of insertion,
0156     // we want the comment before that
0157     it--;
0158     RangeInRevision input = locationToRange(location);
0159     RangeInRevision match = locationToRange(*it);
0160     if (match.end.line != input.start.line - 1 && match.end.line != input.start.line) {
0161         return QString();
0162     }
0163 
0164     ///TODO: merge consecutive //-style comments?
0165     return formatComment(symbolAt(*it));
0166 }
0167 
0168 RangeInRevision ParseSession::locationToRange(const QmlJS::AST::SourceLocation& location) const
0169 {
0170     const int linesInLocation = m_doc->source().midRef(location.offset, location.length).count(QLatin1Char('\n'));
0171     return RangeInRevision(location.startLine - 1, location.startColumn - 1,
0172                            location.startLine - 1 + linesInLocation, location.startColumn - 1 + location.length);
0173 }
0174 
0175 RangeInRevision ParseSession::locationsToRange(const QmlJS::AST::SourceLocation& locationFrom,
0176                                                const QmlJS::AST::SourceLocation& locationTo) const
0177 {
0178     return RangeInRevision(locationToRange(locationFrom).start,
0179                            locationToRange(locationTo).end);
0180 }
0181 
0182 RangeInRevision ParseSession::locationsToInnerRange(const QmlJS::AST::SourceLocation& locationFrom,
0183                                                     const QmlJS::AST::SourceLocation& locationTo) const
0184 {
0185     return RangeInRevision(locationToRange(locationFrom).end,
0186                            locationToRange(locationTo).start);
0187 }
0188 
0189 RangeInRevision ParseSession::editorFindRange(QmlJS::AST::Node* fromNode, QmlJS::AST::Node* toNode) const
0190 {
0191     return locationsToRange(fromNode->firstSourceLocation(), toNode->lastSourceLocation());
0192 }
0193 
0194 void ParseSession::setContextOnNode(QmlJS::AST::Node* node, DUContext* context)
0195 {
0196     m_astToContext.insert(node, DUContextPointer(context));
0197 }
0198 
0199 DUContext* ParseSession::contextFromNode(QmlJS::AST::Node* node) const
0200 {
0201     return m_astToContext.value(node, DUContextPointer()).data();
0202 }
0203 
0204 bool ParseSession::allDependenciesSatisfied() const
0205 {
0206     return m_allDependenciesSatisfied;
0207 }
0208 
0209 ReferencedTopDUContext ParseSession::contextOfFile(const QString& fileName)
0210 {
0211     ReferencedTopDUContext res = contextOfFile(fileName, m_url, m_ownPriority);
0212 
0213     if (!res) {
0214         // The file was not yet present in the DUChain, store this information.
0215         // This will prevent the second parsing pass from running (it would be
0216         // useless as the file will be re-parsed when res will become available)
0217         m_allDependenciesSatisfied = false;
0218     }
0219 
0220     return res;
0221 }
0222 
0223 ReferencedTopDUContext ParseSession::contextOfFile(const QString& fileName,
0224                                                    const KDevelop::IndexedString& url,
0225                                                    int ownPriority)
0226 {
0227     if (fileName.isEmpty()) {
0228         return ReferencedTopDUContext();
0229     }
0230 
0231     // Get the top context of this module file
0232     DUChainReadLocker lock;
0233     IndexedString moduleFileString(fileName);
0234     ReferencedTopDUContext moduleContext = DUChain::self()->chainForDocument(moduleFileString);
0235 
0236     lock.unlock();
0237     QmlJS::Cache::instance().addDependency(url, moduleFileString);
0238 
0239     if (!moduleContext) {
0240         // Queue the file on which we depend with a lower priority than the one of this file
0241         scheduleForParsing(moduleFileString, ownPriority - 1);
0242 
0243         // Register a dependency between this file and the imported one
0244         return ReferencedTopDUContext();
0245     } else {
0246         return moduleContext;
0247     }
0248 }
0249 
0250 void ParseSession::reparseImporters()
0251 {
0252     const auto& files = QmlJS::Cache::instance().filesThatDependOn(m_url);
0253     for (const KDevelop::IndexedString& file : files) {
0254         scheduleForParsing(file, m_ownPriority);
0255     }
0256 }
0257 
0258 void ParseSession::scheduleForParsing(const IndexedString& url, int priority)
0259 {
0260     BackgroundParser* bgparser = KDevelop::ICore::self()->languageController()->backgroundParser();
0261     const auto features = TopDUContext::ForceUpdate | TopDUContext::AllDeclarationsContextsAndUses;
0262 
0263     if (bgparser->isQueued(url)) {
0264         bgparser->removeDocument(url);
0265     }
0266 
0267     bgparser->addDocument(url, features, priority, nullptr, ParseJob::FullSequentialProcessing);
0268 }
0269 
0270 void ParseSession::dumpNode(QmlJS::AST::Node* node) const
0271 {
0272     DebugVisitor v(this);
0273     v.startVisiting(node);
0274 }