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