1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "qmljsdocument.h"
27 #include "qmljsbind.h"
28 #include "qmljsconstants.h"
29 #include "qmljsimportdependencies.h"
30 #include <qmljs/parser/qmljslexer_p.h>
31 #include <qmljs/parser/qmljsparser_p.h>
32
33 #include <utils/qtcassert.h>
34
35 #include <QCryptographicHash>
36 #include <QDir>
37 #include <QFileInfo>
38 #include <QRegularExpression>
39
40 #include <algorithm>
41
42 static constexpr int sizeofQChar = int(sizeof(QChar));
43
44 using namespace QmlJS;
45 using namespace QmlJS::AST;
46
47 /*!
48 \class QmlJS::Document
49 \brief The Document class creates a QML or JavaScript document.
50 \sa Snapshot
51
52 Documents are usually created by the ModelManagerInterface
53 and stored in a Snapshot. They allow access to data such as
54 the file path, source code, abstract syntax tree and the Bind
55 instance for the document.
56
57 To make sure unused and outdated documents are removed correctly, Document
58 instances are usually accessed through a shared pointer, see Document::Ptr.
59
60 Documents in a Snapshot are immutable: They, or anything reachable through them,
61 must not be changed. This allows Documents to be shared freely among threads
62 without extra synchronization.
63 */
64
65 /*!
66 \class QmlJS::LibraryInfo
67 \brief The LibraryInfo class creates a QML library.
68 \sa Snapshot
69
70 A LibraryInfo is created when the ModelManagerInterface finds
71 a QML library and parses the qmldir file. The instance holds information about
72 which Components the library provides and which plugins to load.
73
74 The ModelManager will try to extract detailed information about the types
75 defined in the plugins this library loads. Once it is done, the data will
76 be available through the metaObjects() function.
77 */
78
79 /*!
80 \class QmlJS::Snapshot
81 \brief The Snapshot class holds and offers access to a set of
82 Document::Ptr and LibraryInfo instances.
83 \sa Document LibraryInfo
84
85 Usually Snapshots are copies of the snapshot maintained and updated by the
86 ModelManagerInterface that updates its instance as parsing
87 threads finish and new information becomes available.
88 */
89
Document(const QString & fileName,Dialect language)90 Document::Document(const QString &fileName, Dialect language)
91 : _engine(nullptr)
92 , _ast(nullptr)
93 , _bind(nullptr)
94 , _fileName(QDir::cleanPath(fileName))
95 , _editorRevision(0)
96 , _language(language)
97 , _parsedCorrectly(false)
98 {
99 QFileInfo fileInfo(fileName);
100 _path = QDir::cleanPath(fileInfo.absolutePath());
101
102 if (language.isQmlLikeLanguage()) {
103 _componentName = fileInfo.baseName();
104
105 if (! _componentName.isEmpty()) {
106 // ### TODO: check the component name.
107
108 if (! _componentName.at(0).isUpper())
109 _componentName.clear();
110 }
111 }
112 }
113
~Document()114 Document::~Document()
115 {
116 if (_bind)
117 delete _bind;
118
119 if (_engine)
120 delete _engine;
121 }
122
create(const QString & fileName,Dialect language)123 Document::MutablePtr Document::create(const QString &fileName, Dialect language)
124 {
125 Document::MutablePtr doc(new Document(fileName, language));
126 doc->_ptr = doc;
127 return doc;
128 }
129
ptr() const130 Document::Ptr Document::ptr() const
131 {
132 return _ptr.toStrongRef();
133 }
134
isQmlDocument() const135 bool Document::isQmlDocument() const
136 {
137 return _language.isQmlLikeLanguage();
138 }
139
language() const140 Dialect Document::language() const
141 {
142 return _language;
143 }
144
setLanguage(Dialect l)145 void Document::setLanguage(Dialect l)
146 {
147 _language = l;
148 }
149
importId() const150 QString Document::importId() const
151 {
152 return _fileName;
153 }
154
fingerprint() const155 QByteArray Document::fingerprint() const
156 {
157 return _fingerprint;
158 }
159
qmlProgram() const160 AST::UiProgram *Document::qmlProgram() const
161 {
162 return cast<UiProgram *>(_ast);
163 }
164
jsProgram() const165 AST::Program *Document::jsProgram() const
166 {
167 return cast<Program *>(_ast);
168 }
169
expression() const170 AST::ExpressionNode *Document::expression() const
171 {
172 if (_ast)
173 return _ast->expressionCast();
174
175 return nullptr;
176 }
177
ast() const178 AST::Node *Document::ast() const
179 {
180 return _ast;
181 }
182
engine() const183 const QmlJS::Engine *Document::engine() const
184 {
185 return _engine;
186 }
187
diagnosticMessages() const188 QList<DiagnosticMessage> Document::diagnosticMessages() const
189 {
190 return _diagnosticMessages;
191 }
192
source() const193 QString Document::source() const
194 {
195 return _source;
196 }
197
setSource(const QString & source)198 void Document::setSource(const QString &source)
199 {
200 _source = source;
201 QCryptographicHash sha(QCryptographicHash::Sha1);
202 sha.addData(source.toUtf8());
203 _fingerprint = sha.result();
204 }
205
editorRevision() const206 int Document::editorRevision() const
207 {
208 return _editorRevision;
209 }
210
setEditorRevision(int revision)211 void Document::setEditorRevision(int revision)
212 {
213 _editorRevision = revision;
214 }
215
fileName() const216 QString Document::fileName() const
217 {
218 return _fileName;
219
220 }
221
path() const222 QString Document::path() const
223 {
224 return _path;
225 }
226
componentName() const227 QString Document::componentName() const
228 {
229 return _componentName;
230 }
231
232 namespace {
233 class CollectDirectives : public Directives
234 {
addLocation(int line,int column)235 void addLocation(int line, int column) {
236 const SourceLocation loc = SourceLocation(
237 0, // placeholder
238 0, // placeholder
239 static_cast<quint32>(line),
240 static_cast<quint32>(column));
241 _locations += loc;
242 }
243
244 QList<SourceLocation> _locations;
245
246 public:
CollectDirectives(const QString & documentPath)247 CollectDirectives(const QString &documentPath)
248 : documentPath(documentPath)
249 , isLibrary(false)
250
251 {}
252
importFile(const QString & jsfile,const QString & module,int line,int column)253 void importFile(const QString &jsfile, const QString &module,
254 int line, int column) override
255 {
256 imports += ImportInfo::pathImport(
257 documentPath, jsfile, LanguageUtils::ComponentVersion(), module);
258 addLocation(line, column);
259 }
260
importModule(const QString & uri,const QString & version,const QString & module,int line,int column)261 void importModule(const QString &uri, const QString &version, const QString &module,
262 int line, int column) override
263 {
264 imports += ImportInfo::moduleImport(uri, LanguageUtils::ComponentVersion(version), module);
265 addLocation(line, column);
266 }
267
pragmaLibrary()268 void pragmaLibrary() override
269 {
270 isLibrary = true;
271 }
272
locations()273 virtual QList<SourceLocation> locations() { return _locations; }
274
275 const QString documentPath;
276 bool isLibrary;
277 QList<ImportInfo> imports;
278 };
279
280 } // anonymous namespace
281
282
jsDirectives() const283 QList<SourceLocation> Document::jsDirectives() const
284 {
285 return _jsdirectives;
286 }
287
parse_helper(int startToken)288 bool Document::parse_helper(int startToken)
289 {
290 Q_ASSERT(! _engine);
291 Q_ASSERT(! _ast);
292 Q_ASSERT(! _bind);
293
294 _engine = new Engine();
295
296 Lexer lexer(_engine);
297 Parser parser(_engine);
298
299 QString source = _source;
300 lexer.setCode(source, /*line = */ 1, /*qmlMode = */_language.isQmlLikeLanguage());
301
302 CollectDirectives directives = CollectDirectives(path());
303 _engine->setDirectives(&directives);
304
305 switch (startToken) {
306 case QmlJSGrammar::T_FEED_UI_PROGRAM:
307 _parsedCorrectly = parser.parse();
308 break;
309 case QmlJSGrammar::T_FEED_JS_SCRIPT:
310 case QmlJSGrammar::T_FEED_JS_MODULE: {
311 _parsedCorrectly = parser.parseProgram();
312 const QList<SourceLocation> locations = directives.locations();
313 for (const auto &d : locations) {
314 _jsdirectives << d;
315 }
316 } break;
317
318 case QmlJSGrammar::T_FEED_JS_EXPRESSION:
319 _parsedCorrectly = parser.parseExpression();
320 break;
321 default:
322 Q_ASSERT(0);
323 }
324
325 _ast = parser.rootNode();
326 _diagnosticMessages = parser.diagnosticMessages();
327
328 _bind = new Bind(this, &_diagnosticMessages, directives.isLibrary, directives.imports);
329
330 return _parsedCorrectly;
331 }
332
parse()333 bool Document::parse()
334 {
335 if (isQmlDocument())
336 return parseQml();
337
338 return parseJavaScript();
339 }
340
parseQml()341 bool Document::parseQml()
342 {
343 return parse_helper(QmlJSGrammar::T_FEED_UI_PROGRAM);
344 }
345
parseJavaScript()346 bool Document::parseJavaScript()
347 {
348 return parse_helper(QmlJSGrammar::T_FEED_JS_SCRIPT);
349 }
350
parseExpression()351 bool Document::parseExpression()
352 {
353 return parse_helper(QmlJSGrammar::T_FEED_JS_EXPRESSION);
354 }
355
bind() const356 Bind *Document::bind() const
357 {
358 return _bind;
359 }
360
LibraryInfo()361 LibraryInfo::LibraryInfo()
362 {
363 static const QByteArray emptyFingerprint = calculateFingerprint();
364 _fingerprint = emptyFingerprint;
365 }
366
LibraryInfo(Status status)367 LibraryInfo::LibraryInfo(Status status)
368 : _status(status)
369 {
370 updateFingerprint();
371 }
372
LibraryInfo(const QString & typeInfo)373 LibraryInfo::LibraryInfo(const QString &typeInfo)
374 : _status(Found)
375 {
376 _typeinfos.append(typeInfo);
377 updateFingerprint();
378 }
379
LibraryInfo(const QmlDirParser & parser,const QByteArray & fingerprint)380 LibraryInfo::LibraryInfo(const QmlDirParser &parser, const QByteArray &fingerprint)
381 : _status(Found)
382 , _components(parser.components().values())
383 , _plugins(parser.plugins())
384 , _typeinfos(parser.typeInfos())
385 , _imports(parser.imports())
386 , _fingerprint(fingerprint)
387 {
388 if (_fingerprint.isEmpty())
389 updateFingerprint();
390 }
391
calculateFingerprint() const392 QByteArray LibraryInfo::calculateFingerprint() const
393 {
394 QCryptographicHash hash(QCryptographicHash::Sha1);
395 hash.addData(reinterpret_cast<const char *>(&_status), sizeof(_status));
396 int len = _components.size();
397 hash.addData(reinterpret_cast<const char *>(&len), sizeof(len));
398 foreach (const QmlDirParser::Component &component, _components) {
399 len = component.fileName.size();
400 hash.addData(reinterpret_cast<const char *>(&len), sizeof(len));
401 hash.addData(reinterpret_cast<const char *>(component.fileName.constData()),
402 len * sizeofQChar);
403 hash.addData(reinterpret_cast<const char *>(&component.majorVersion), sizeof(component.majorVersion));
404 hash.addData(reinterpret_cast<const char *>(&component.minorVersion), sizeof(component.minorVersion));
405 len = component.typeName.size();
406 hash.addData(reinterpret_cast<const char *>(&len), sizeof(len));
407 hash.addData(reinterpret_cast<const char *>(component.typeName.constData()),
408 component.typeName.size() * sizeofQChar);
409 int flags = (component.singleton ? (1 << 0) : 0) + (component.internal ? (1 << 1) : 0);
410 hash.addData(reinterpret_cast<const char *>(&flags), sizeof(flags));
411 }
412 len = _plugins.size();
413 hash.addData(reinterpret_cast<const char *>(&len), sizeof(len));
414 foreach (const QmlDirParser::Plugin &plugin, _plugins) {
415 len = plugin.path.size();
416 hash.addData(reinterpret_cast<const char *>(&len), sizeof(len));
417 hash.addData(reinterpret_cast<const char *>(plugin.path.constData()), len * sizeofQChar);
418 len = plugin.name.size();
419 hash.addData(reinterpret_cast<const char *>(&len), sizeof(len));
420 hash.addData(reinterpret_cast<const char *>(plugin.name.constData()), len * sizeofQChar);
421 }
422 len = _typeinfos.size();
423 hash.addData(reinterpret_cast<const char *>(&len), sizeof(len));
424 foreach (const QString &typeinfo, _typeinfos) {
425 len = typeinfo.size();
426 hash.addData(reinterpret_cast<const char *>(&len), sizeof(len));
427 hash.addData(reinterpret_cast<const char *>(typeinfo.constData()),
428 len * sizeofQChar);
429 }
430 len = _metaObjects.size();
431 hash.addData(reinterpret_cast<const char *>(&len), sizeof(len));
432 QList<QByteArray> metaFingerprints;
433 foreach (const LanguageUtils::FakeMetaObject::ConstPtr &metaObject, _metaObjects)
434 metaFingerprints.append(metaObject->fingerprint());
435 std::sort(metaFingerprints.begin(), metaFingerprints.end());
436 foreach (const QByteArray &fp, metaFingerprints)
437 hash.addData(fp);
438 hash.addData(reinterpret_cast<const char *>(&_dumpStatus), sizeof(_dumpStatus));
439 len = _dumpError.size(); // localization dependent (avoid?)
440 hash.addData(reinterpret_cast<const char *>(&len), sizeof(len));
441 hash.addData(reinterpret_cast<const char *>(_dumpError.constData()), len * sizeofQChar);
442
443 len = _moduleApis.size();
444 hash.addData(reinterpret_cast<const char *>(&len), sizeof(len));
445 foreach (const ModuleApiInfo &moduleInfo, _moduleApis)
446 moduleInfo.addToHash(hash); // make it order independent?
447
448 len = _imports.size();
449 hash.addData(reinterpret_cast<const char *>(&len), sizeof(len));
450 foreach (const QmlDirParser::Import &import, _imports)
451 hash.addData(import.module.toUtf8()); // import order matters, keep order-dependent
452
453 QByteArray res(hash.result());
454 res.append('L');
455 return res;
456 }
457
updateFingerprint()458 void LibraryInfo::updateFingerprint()
459 {
460 _fingerprint = calculateFingerprint();
461 }
462
Snapshot()463 Snapshot::Snapshot()
464 {
465 }
466
~Snapshot()467 Snapshot::~Snapshot()
468 {
469 }
470
insert(const Document::Ptr & document,bool allowInvalid)471 void Snapshot::insert(const Document::Ptr &document, bool allowInvalid)
472 {
473 if (document && (allowInvalid || document->qmlProgram() || document->jsProgram())) {
474 const QString fileName = document->fileName();
475 const QString path = document->path();
476 remove(fileName);
477 _documentsByPath[path].append(document);
478 _documents.insert(fileName, document);
479 CoreImport cImport;
480 cImport.importId = document->importId();
481 cImport.language = document->language();
482 cImport.addPossibleExport(Export(ImportKey(ImportType::File, fileName),
483 {}, true, QFileInfo(fileName).baseName()));
484 cImport.fingerprint = document->fingerprint();
485 _dependencies.addCoreImport(cImport);
486 }
487 }
488
insertLibraryInfo(const QString & path,const LibraryInfo & info)489 void Snapshot::insertLibraryInfo(const QString &path, const LibraryInfo &info)
490 {
491 QTC_CHECK(!path.isEmpty());
492 QTC_CHECK(info.fingerprint() == info.calculateFingerprint());
493 _libraries.insert(QDir::cleanPath(path), info);
494 if (!info.wasFound()) return;
495 CoreImport cImport;
496 cImport.importId = path;
497 cImport.language = Dialect::AnyLanguage;
498 QSet<ImportKey> packages;
499 foreach (const ModuleApiInfo &moduleInfo, info.moduleApis()) {
500 ImportKey iKey(ImportType::Library, moduleInfo.uri, moduleInfo.version.majorVersion(),
501 moduleInfo.version.minorVersion());
502 packages.insert(iKey);
503 }
504 foreach (const LanguageUtils::FakeMetaObject::ConstPtr &metaO, info.metaObjects()) {
505 foreach (const LanguageUtils::FakeMetaObject::Export &e, metaO->exports()) {
506 ImportKey iKey(ImportType::Library, e.package, e.version.majorVersion(),
507 e.version.minorVersion());
508 packages.insert(iKey);
509 }
510 }
511
512 QStringList splitPath = path.split(QLatin1Char('/'));
513 const QRegularExpression vNr(QLatin1String("^(.+)\\.([0-9]+)(?:\\.([0-9]+))?$"));
514 const QRegularExpression safeName(QLatin1String("^[a-zA-Z_][[a-zA-Z0-9_]*$"));
515 foreach (const ImportKey &importKey, packages) {
516 if (importKey.splitPath.size() == 1 && importKey.splitPath.at(0).isEmpty() && splitPath.length() > 0) {
517 // relocatable
518 QStringList myPath = splitPath;
519 QRegularExpressionMatch match = vNr.match(myPath.last());
520 if (match.hasMatch())
521 myPath.last() = match.captured(1);
522 for (int iPath = myPath.size(); iPath != 1; ) {
523 --iPath;
524 match = safeName.match(myPath.at(iPath));
525 if (!match.hasMatch())
526 break;
527 ImportKey iKey(ImportType::Library, QStringList(myPath.mid(iPath)).join(QLatin1Char('.')),
528 importKey.majorVersion, importKey.minorVersion);
529 cImport.addPossibleExport(Export(iKey, (iPath == 1) ? QLatin1String("/") :
530 QStringList(myPath.mid(0, iPath)).join(QLatin1Char('/')), true));
531 }
532 } else {
533 QString requiredPath = QStringList(splitPath.mid(0, splitPath.size() - importKey.splitPath.size()))
534 .join(QLatin1String("/"));
535 cImport.addPossibleExport(Export(importKey, requiredPath, true));
536 }
537 }
538 if (cImport.possibleExports.isEmpty() && splitPath.size() > 0) {
539 const QRegularExpression vNr(QLatin1String("^(.+)\\.([0-9]+)(?:\\.([0-9]+))?$"));
540 const QRegularExpression safeName(QLatin1String("^[a-zA-Z_][[a-zA-Z0-9_]*$"));
541 int majorVersion = LanguageUtils::ComponentVersion::NoVersion;
542 int minorVersion = LanguageUtils::ComponentVersion::NoVersion;
543
544 foreach (const QmlDirParser::Component &component, info.components()) {
545 if (component.majorVersion > majorVersion)
546 majorVersion = component.majorVersion;
547 if (component.minorVersion > minorVersion)
548 minorVersion = component.minorVersion;
549 }
550
551 QRegularExpressionMatch match = vNr.match(splitPath.last());
552 if (match.hasMatch()) {
553 splitPath.last() = match.captured(1);
554 bool ok;
555 majorVersion = match.captured(2).toInt(&ok);
556 if (!ok)
557 majorVersion = LanguageUtils::ComponentVersion::NoVersion;
558 minorVersion = match.captured(3).toInt(&ok);
559 if (match.captured(3).isEmpty() || !ok)
560 minorVersion = LanguageUtils::ComponentVersion::NoVersion;
561 }
562
563 for (int iPath = splitPath.size(); iPath != 1; ) {
564 --iPath;
565 match = safeName.match(splitPath.at(iPath));
566 if (!match.hasMatch())
567 break;
568 ImportKey iKey(ImportType::Library, QStringList(splitPath.mid(iPath)).join(QLatin1Char('.')),
569 majorVersion, minorVersion);
570 cImport.addPossibleExport(Export(iKey, (iPath == 1) ? QLatin1String("/") :
571 QStringList(splitPath.mid(0, iPath)).join(QLatin1Char('/')), true));
572 }
573 }
574 foreach (const QmlDirParser::Component &component, info.components()) {
575 foreach (const Export &e, cImport.possibleExports)
576 _dependencies.addExport(component.fileName, e.exportName, e.pathRequired, e.typeName);
577 }
578
579 cImport.fingerprint = info.fingerprint();
580 _dependencies.addCoreImport(cImport);
581 }
582
remove(const QString & fileName)583 void Snapshot::remove(const QString &fileName)
584 {
585 Document::Ptr doc = _documents.value(fileName);
586 if (!doc.isNull()) {
587 const QString &path = doc->path();
588
589 QList<Document::Ptr> docs = _documentsByPath.value(path);
590 docs.removeAll(doc);
591 _documentsByPath[path] = docs;
592
593 _documents.remove(fileName);
594 }
595 }
596
importDependencies() const597 const QmlJS::ImportDependencies *Snapshot::importDependencies() const
598 {
599 return &_dependencies;
600 }
601
importDependencies()602 QmlJS::ImportDependencies *Snapshot::importDependencies()
603 {
604 return &_dependencies;
605 }
606
documentFromSource(const QString & code,const QString & fileName,Dialect language) const607 Document::MutablePtr Snapshot::documentFromSource(
608 const QString &code, const QString &fileName,
609 Dialect language) const
610 {
611 Document::MutablePtr newDoc = Document::create(fileName, language);
612
613 if (Document::Ptr thisDocument = document(fileName))
614 newDoc->_editorRevision = thisDocument->_editorRevision;
615
616 newDoc->setSource(code);
617 return newDoc;
618 }
619
document(const QString & fileName) const620 Document::Ptr Snapshot::document(const QString &fileName) const
621 {
622 return _documents.value(QDir::cleanPath(fileName));
623 }
624
documentsInDirectory(const QString & path) const625 QList<Document::Ptr> Snapshot::documentsInDirectory(const QString &path) const
626 {
627 return _documentsByPath.value(QDir::cleanPath(path));
628 }
629
libraryInfo(const QString & path) const630 LibraryInfo Snapshot::libraryInfo(const QString &path) const
631 {
632 return _libraries.value(QDir::cleanPath(path));
633 }
634
635
addToHash(QCryptographicHash & hash) const636 void ModuleApiInfo::addToHash(QCryptographicHash &hash) const
637 {
638 int len = uri.length();
639 hash.addData(reinterpret_cast<const char *>(&len), sizeof(len));
640 hash.addData(reinterpret_cast<const char *>(uri.constData()), len * sizeofQChar);
641 version.addToHash(hash);
642 len = cppName.length();
643 hash.addData(reinterpret_cast<const char *>(&len), sizeof(len));
644 hash.addData(reinterpret_cast<const char *>(cppName.constData()), len * sizeofQChar);
645 }
646