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