1 /*
2     SPDX-FileCopyrightText: 2012 Milian Wolff <mail@milianw.de>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "parsesession.h"
8 #include "debugvisitor.h"
9 #include "cache.h"
10 
11 #include <qmljs/parser/qmljsast_p.h>
12 #include <qmljs/parser/qmljsengine_p.h>
13 
14 #include <language/duchain/stringhelpers.h>
15 #include <language/duchain/duchain.h>
16 #include <language/duchain/duchainlock.h>
17 #include <language/duchain/declaration.h>
18 #include <language/backgroundparser/backgroundparser.h>
19 #include <language/editor/documentrange.h>
20 #include <interfaces/ilanguagecontroller.h>
21 #include <interfaces/icore.h>
22 
23 using namespace KDevelop;
24 
languageString()25 IndexedString ParseSession::languageString()
26 {
27     static const IndexedString langString("QML/JS");
28     return langString;
29 }
30 
isSorted(const QList<QmlJS::AST::SourceLocation> & locations)31 bool isSorted(const QList<QmlJS::AST::SourceLocation>& locations)
32 {
33     if (locations.size() <= 1) {
34         return true;
35     }
36     for(int i = 1; i < locations.size(); ++i) {
37         if (locations.at(i).begin() <= locations.at(i-1).begin()) {
38             return false;
39         }
40     }
41     return true;
42 }
43 
guessLanguageFromSuffix(const QString & path)44 QmlJS::Dialect ParseSession::guessLanguageFromSuffix(const QString& path)
45 {
46     if (path.endsWith(QLatin1String(".js"))) {
47         return QmlJS::Dialect::JavaScript;
48     } else if (path.endsWith(QLatin1String(".json"))) {
49         return QmlJS::Dialect::Json;
50     } else {
51         return QmlJS::Dialect::Qml;
52     }
53 }
54 
ParseSession(const IndexedString & url,const QString & contents,int priority)55 ParseSession::ParseSession(const IndexedString& url, const QString& contents, int priority)
56 : m_url(url),
57   m_ownPriority(priority),
58   m_allDependenciesSatisfied(true)
59 {
60     const QString path = m_url.str();
61     m_doc = QmlJS::Document::create(path, guessLanguageFromSuffix(path));
62     m_doc->setSource(contents);
63     m_doc->parse();
64     Q_ASSERT(isSorted(m_doc->engine()->comments()));
65 
66     // Parse the module name and the version of url (this is used only when the file
67     // is a QML module, but doesn't break for JavaScript files)
68     m_baseName = QString::fromUtf8(m_url.byteArray())
69         .section(QLatin1Char('/'), -1, -1)                   // Base name
70         .section(QLatin1Char('.'), 0, -2);                   // Without extension
71 }
72 
isParsedCorrectly() const73 bool ParseSession::isParsedCorrectly() const
74 {
75     return m_doc->isParsedCorrectly();
76 }
77 
ast() const78 QmlJS::AST::Node* ParseSession::ast() const
79 {
80     return m_doc->ast();
81 }
82 
url() const83 IndexedString ParseSession::url() const
84 {
85     return m_url;
86 }
87 
moduleName() const88 QString ParseSession::moduleName() const
89 {
90     return m_baseName;
91 }
92 
addProblem(QmlJS::AST::Node * node,const QString & message,IProblem::Severity severity)93 void ParseSession::addProblem(QmlJS::AST::Node* node,
94                               const QString& message,
95                               IProblem::Severity severity)
96 {
97     ProblemPointer p(new Problem);
98 
99     p->setDescription(message);
100     p->setSeverity(severity);
101     p->setSource(IProblem::SemanticAnalysis);
102     p->setFinalLocation(DocumentRange(m_url, editorFindRange(node, node).castToSimpleRange()));
103 
104     m_problems << p;
105 }
106 
problems() const107 QList<ProblemPointer> ParseSession::problems() const
108 {
109     QList<ProblemPointer> problems = m_problems;
110 
111     const auto diagnosticMessages = m_doc->diagnosticMessages();
112     problems.reserve(problems.size() + diagnosticMessages.size());
113     for (const auto& msg : diagnosticMessages) {
114         ProblemPointer p(new Problem);
115         p->setDescription(msg.message);
116         p->setSeverity(IProblem::Error);
117         p->setSource(IProblem::Parser);
118         p->setFinalLocation(DocumentRange(m_url, locationToRange(msg.loc).castToSimpleRange()));
119         problems << p;
120     }
121 
122     return problems;
123 }
124 
symbolAt(const QmlJS::AST::SourceLocation & location) const125 QString ParseSession::symbolAt(const QmlJS::AST::SourceLocation& location) const
126 {
127     return m_doc->source().mid(location.offset, location.length);
128 }
129 
language() const130 QmlJS::Dialect ParseSession::language() const
131 {
132     return m_doc->language();
133 }
134 
compareSourceLocation(const QmlJS::AST::SourceLocation & l,const QmlJS::AST::SourceLocation & r)135 bool compareSourceLocation(const QmlJS::AST::SourceLocation& l,
136                            const QmlJS::AST::SourceLocation& r)
137 {
138     return l.begin() < r.begin();
139 }
140 
commentForLocation(const QmlJS::AST::SourceLocation & location) const141 QString ParseSession::commentForLocation(const QmlJS::AST::SourceLocation& location) const
142 {
143     // find most recent comment in sorted list of comments
144     const QList< QmlJS::AST::SourceLocation >& comments = m_doc->engine()->comments();
145     auto it = std::lower_bound(
146         comments.constBegin(),
147         comments.constEnd(),
148         location, compareSourceLocation
149     );
150 
151     if (it == comments.constBegin()) {
152         return QString();
153     }
154 
155     // lower bound returns the place of insertion,
156     // we want the comment before that
157     it--;
158     RangeInRevision input = locationToRange(location);
159     RangeInRevision match = locationToRange(*it);
160     if (match.end.line != input.start.line - 1 && match.end.line != input.start.line) {
161         return QString();
162     }
163 
164     ///TODO: merge consecutive //-style comments?
165     return formatComment(symbolAt(*it));
166 }
167 
locationToRange(const QmlJS::AST::SourceLocation & location) const168 RangeInRevision ParseSession::locationToRange(const QmlJS::AST::SourceLocation& location) const
169 {
170     const int linesInLocation = m_doc->source().midRef(location.offset, location.length).count(QLatin1Char('\n'));
171     return RangeInRevision(location.startLine - 1, location.startColumn - 1,
172                            location.startLine - 1 + linesInLocation, location.startColumn - 1 + location.length);
173 }
174 
locationsToRange(const QmlJS::AST::SourceLocation & locationFrom,const QmlJS::AST::SourceLocation & locationTo) const175 RangeInRevision ParseSession::locationsToRange(const QmlJS::AST::SourceLocation& locationFrom,
176                                                const QmlJS::AST::SourceLocation& locationTo) const
177 {
178     return RangeInRevision(locationToRange(locationFrom).start,
179                            locationToRange(locationTo).end);
180 }
181 
locationsToInnerRange(const QmlJS::AST::SourceLocation & locationFrom,const QmlJS::AST::SourceLocation & locationTo) const182 RangeInRevision ParseSession::locationsToInnerRange(const QmlJS::AST::SourceLocation& locationFrom,
183                                                     const QmlJS::AST::SourceLocation& locationTo) const
184 {
185     return RangeInRevision(locationToRange(locationFrom).end,
186                            locationToRange(locationTo).start);
187 }
188 
editorFindRange(QmlJS::AST::Node * fromNode,QmlJS::AST::Node * toNode) const189 RangeInRevision ParseSession::editorFindRange(QmlJS::AST::Node* fromNode, QmlJS::AST::Node* toNode) const
190 {
191     return locationsToRange(fromNode->firstSourceLocation(), toNode->lastSourceLocation());
192 }
193 
setContextOnNode(QmlJS::AST::Node * node,DUContext * context)194 void ParseSession::setContextOnNode(QmlJS::AST::Node* node, DUContext* context)
195 {
196     m_astToContext.insert(node, DUContextPointer(context));
197 }
198 
contextFromNode(QmlJS::AST::Node * node) const199 DUContext* ParseSession::contextFromNode(QmlJS::AST::Node* node) const
200 {
201     return m_astToContext.value(node, DUContextPointer()).data();
202 }
203 
allDependenciesSatisfied() const204 bool ParseSession::allDependenciesSatisfied() const
205 {
206     return m_allDependenciesSatisfied;
207 }
208 
contextOfFile(const QString & fileName)209 ReferencedTopDUContext ParseSession::contextOfFile(const QString& fileName)
210 {
211     ReferencedTopDUContext res = contextOfFile(fileName, m_url, m_ownPriority);
212 
213     if (!res) {
214         // The file was not yet present in the DUChain, store this information.
215         // This will prevent the second parsing pass from running (it would be
216         // useless as the file will be re-parsed when res will become available)
217         m_allDependenciesSatisfied = false;
218     }
219 
220     return res;
221 }
222 
contextOfFile(const QString & fileName,const KDevelop::IndexedString & url,int ownPriority)223 ReferencedTopDUContext ParseSession::contextOfFile(const QString& fileName,
224                                                    const KDevelop::IndexedString& url,
225                                                    int ownPriority)
226 {
227     if (fileName.isEmpty()) {
228         return ReferencedTopDUContext();
229     }
230 
231     // Get the top context of this module file
232     DUChainReadLocker lock;
233     IndexedString moduleFileString(fileName);
234     ReferencedTopDUContext moduleContext = DUChain::self()->chainForDocument(moduleFileString);
235 
236     lock.unlock();
237     QmlJS::Cache::instance().addDependency(url, moduleFileString);
238 
239     if (!moduleContext) {
240         // Queue the file on which we depend with a lower priority than the one of this file
241         scheduleForParsing(moduleFileString, ownPriority - 1);
242 
243         // Register a dependency between this file and the imported one
244         return ReferencedTopDUContext();
245     } else {
246         return moduleContext;
247     }
248 }
249 
reparseImporters()250 void ParseSession::reparseImporters()
251 {
252     const auto& files = QmlJS::Cache::instance().filesThatDependOn(m_url);
253     for (const KDevelop::IndexedString& file : files) {
254         scheduleForParsing(file, m_ownPriority);
255     }
256 }
257 
scheduleForParsing(const IndexedString & url,int priority)258 void ParseSession::scheduleForParsing(const IndexedString& url, int priority)
259 {
260     BackgroundParser* bgparser = KDevelop::ICore::self()->languageController()->backgroundParser();
261     const auto features = TopDUContext::ForceUpdate | TopDUContext::AllDeclarationsContextsAndUses;
262 
263     if (bgparser->isQueued(url)) {
264         bgparser->removeDocument(url);
265     }
266 
267     bgparser->addDocument(url, features, priority, nullptr, ParseJob::FullSequentialProcessing);
268 }
269 
dumpNode(QmlJS::AST::Node * node) const270 void ParseSession::dumpNode(QmlJS::AST::Node* node) const
271 {
272     DebugVisitor v(this);
273     v.startVisiting(node);
274 }
275