1// Copyright (C) 2020 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qqmldomtop_p.h"
5#include "qqmldomoutwriter_p.h"
6#include "qqmldomcomments_p.h"
7#include "qqmldommock_p.h"
8#include "qqmldomelements_p.h"
9
10#include <QtQml/private/qqmljslexer_p.h>
11#include <QtQml/private/qqmljsparser_p.h>
12#include <QtQml/private/qqmljsengine_p.h>
13#include <QtQml/private/qqmljsastvisitor_p.h>
14#include <QtQml/private/qqmljsast_p.h>
15#include <QtCore/QDir>
16#include <QtCore/QScopeGuard>
17#include <QtCore/QFileInfo>
18#include <QtCore/QRegularExpression>
19#include <QtCore/QRegularExpressionMatch>
20
21#include <algorithm>
22
23QT_BEGIN_NAMESPACE
24
25using namespace Qt::StringLiterals;
26
27namespace QQmlJS {
28namespace Dom {
29
30ExternalOwningItem::ExternalOwningItem(QString filePath, QDateTime lastDataUpdateAt, Path path,
31 int derivedFrom, QString code)
32 : OwningItem(derivedFrom, lastDataUpdateAt),
33 m_canonicalFilePath(filePath),
34 m_code(code),
35 m_path(path)
36{}
37
38QString ExternalOwningItem::canonicalFilePath(DomItem &) const
39{
40 return m_canonicalFilePath;
41}
42
43QString ExternalOwningItem::canonicalFilePath() const
44{
45 return m_canonicalFilePath;
46}
47
48Path ExternalOwningItem::canonicalPath(DomItem &) const
49{
50 return m_path;
51}
52
53Path ExternalOwningItem::canonicalPath() const
54{
55 return m_path;
56}
57
58ErrorGroups QmldirFile::myParsingErrors()
59{
60 static ErrorGroups res = { .groups: { DomItem::domErrorGroup, NewErrorGroup("Qmldir"),
61 NewErrorGroup("Parsing") } };
62 return res;
63}
64
65std::shared_ptr<QmldirFile> QmldirFile::fromPathAndCode(QString path, QString code)
66{
67 QString canonicalFilePath = QFileInfo(path).canonicalFilePath();
68
69 QDateTime dataUpdate = QDateTime::currentDateTimeUtc();
70 auto res = std::make_shared<QmldirFile>(args&: canonicalFilePath, args&: code, args&: dataUpdate);
71
72 if (canonicalFilePath.isEmpty() && !path.isEmpty())
73 res->addErrorLocal(
74 msg: myParsingErrors().error(message: tr(sourceText: "QmldirFile started from invalid path '%1'").arg(a: path)));
75 res->parse();
76 return res;
77}
78
79void QmldirFile::parse()
80{
81 if (canonicalFilePath().isEmpty()) {
82 addErrorLocal(msg: myParsingErrors().error(message: tr(sourceText: "canonicalFilePath is empty")));
83 setIsValid(false);
84 } else {
85 m_qmldir.parse(source: m_code);
86 setFromQmldir();
87 }
88}
89
90void QmldirFile::setFromQmldir()
91{
92 m_uri = QmlUri::fromUriString(importStr: m_qmldir.typeNamespace());
93 if (m_uri.isValid())
94 m_uri = QmlUri::fromDirectoryString(importStr: canonicalFilePath());
95 Path exportsPath = Path::Field(s: Fields::exports);
96 QDir baseDir = QFileInfo(canonicalFilePath()).dir();
97 int majorVersion = Version::Undefined;
98 bool ok;
99 int vNr = QFileInfo(baseDir.dirName()).suffix().toInt(ok: &ok);
100 if (ok && vNr > 0) // accept 0?
101 majorVersion = vNr;
102 Path exportSource = canonicalPath();
103 for (auto const &el : m_qmldir.components()) {
104 QString exportFilePath = baseDir.filePath(fileName: el.fileName);
105 QString canonicalExportFilePath = QFileInfo(exportFilePath).canonicalFilePath();
106 if (canonicalExportFilePath.isEmpty()) // file does not exist (yet? assuming it might be
107 // created where we expect it)
108 canonicalExportFilePath = exportFilePath;
109 Export exp;
110 exp.exportSourcePath = exportSource;
111 exp.isSingleton = el.singleton;
112 exp.isInternal = el.internal;
113 exp.version =
114 Version((el.version.hasMajorVersion() ? el.version.majorVersion() : majorVersion),
115 el.version.hasMinorVersion() ? el.version.minorVersion() : 0);
116 exp.typeName = el.typeName;
117 exp.typePath = Paths::qmlFileObjectPath(canonicalFilePath: canonicalExportFilePath);
118 exp.uri = uri().toString();
119 m_exports.insert(key: exp.typeName, value: exp);
120 if (exp.version.majorVersion > 0)
121 m_majorVersions.insert(value: exp.version.majorVersion);
122 }
123 for (auto const &el : m_qmldir.scripts()) {
124 QString exportFilePath = baseDir.filePath(fileName: el.fileName);
125 QString canonicalExportFilePath = QFileInfo(exportFilePath).canonicalFilePath();
126 if (canonicalExportFilePath.isEmpty()) // file does not exist (yet? assuming it might be
127 // created where we expect it)
128 canonicalExportFilePath = exportFilePath;
129 Export exp;
130 exp.exportSourcePath = exportSource;
131 exp.isSingleton = true;
132 exp.isInternal = false;
133 exp.version =
134 Version((el.version.hasMajorVersion() ? el.version.majorVersion() : majorVersion),
135 el.version.hasMinorVersion() ? el.version.minorVersion() : 0);
136 exp.typePath = Paths::jsFilePath(path: canonicalExportFilePath).field(name: Fields::rootComponent);
137 exp.uri = uri().toString();
138 exp.typeName = el.nameSpace;
139 m_exports.insert(key: exp.typeName, value: exp);
140 if (exp.version.majorVersion > 0)
141 m_majorVersions.insert(value: exp.version.majorVersion);
142 }
143 for (QQmlDirParser::Import const &imp : m_qmldir.imports()) {
144 QString uri = imp.module;
145 bool isAutoImport = imp.flags & QQmlDirParser::Import::Auto;
146 Version v;
147 if (isAutoImport)
148 v = Version(majorVersion, int(Version::Latest));
149 else {
150 v = Version((imp.version.hasMajorVersion() ? imp.version.majorVersion()
151 : int(Version::Latest)),
152 (imp.version.hasMinorVersion() ? imp.version.minorVersion()
153 : int(Version::Latest)));
154 }
155 m_imports.append(t: Import(QmlUri::fromUriString(importStr: uri), v));
156 m_autoExports.append(
157 t: ModuleAutoExport { .import: Import(QmlUri::fromUriString(importStr: uri), v), .inheritVersion: isAutoImport });
158 }
159 for (QQmlDirParser::Import const &imp : m_qmldir.dependencies()) {
160 QString uri = imp.module;
161 if (imp.flags & QQmlDirParser::Import::Auto)
162 qWarning() << "qmldir contains dependency with auto keyword";
163 Version v = Version(
164 (imp.version.hasMajorVersion() ? imp.version.majorVersion() : int(Version::Latest)),
165 (imp.version.hasMinorVersion() ? imp.version.minorVersion()
166 : int(Version::Latest)));
167 m_imports.append(t: Import(QmlUri::fromUriString(importStr: uri), v));
168 }
169 bool hasInvalidTypeinfo = false;
170 for (auto const &el : m_qmldir.typeInfos()) {
171 QString elStr = el;
172 QFileInfo elPath(elStr);
173 if (elPath.isRelative())
174 elPath = QFileInfo(baseDir.filePath(fileName: elStr));
175 QString typeInfoPath = elPath.canonicalFilePath();
176 if (typeInfoPath.isEmpty()) {
177 hasInvalidTypeinfo = true;
178 typeInfoPath = elPath.absoluteFilePath();
179 }
180 m_qmltypesFilePaths.append(t: Paths::qmltypesFilePath(path: typeInfoPath));
181 }
182 if (m_qmltypesFilePaths.isEmpty() || hasInvalidTypeinfo) {
183 // add all type info files in the directory...
184 for (QFileInfo const &entry :
185 baseDir.entryInfoList(nameFilters: QStringList({ QLatin1String("*.qmltypes") }),
186 filters: QDir::Filter::Readable | QDir::Filter::Files)) {
187 Path p = Paths::qmltypesFilePath(path: entry.canonicalFilePath());
188 if (!m_qmltypesFilePaths.contains(t: p))
189 m_qmltypesFilePaths.append(t: p);
190 }
191 }
192 bool hasErrors = false;
193 for (auto const &el : m_qmldir.errors(uri: uri().toString())) {
194 ErrorMessage msg = myParsingErrors().errorMessage(msg: el);
195 addErrorLocal(msg);
196 if (msg.level == ErrorLevel::Error || msg.level == ErrorLevel::Fatal)
197 hasErrors = true;
198 }
199 setIsValid(!hasErrors); // consider it valid also with errors?
200 m_plugins = m_qmldir.plugins();
201}
202
203QList<ModuleAutoExport> QmldirFile::autoExports() const
204{
205 return m_autoExports;
206}
207
208void QmldirFile::setAutoExports(const QList<ModuleAutoExport> &autoExport)
209{
210 m_autoExports = autoExport;
211}
212
213void QmldirFile::ensureInModuleIndex(DomItem &self, QString uri)
214{
215 // ModuleIndex keeps the various sources of types from a given module uri import
216 // this method ensures that all major versions that are contained in this qmldir
217 // file actually have a ModuleIndex. This is required so that when importing the
218 // latest version the correct "lastest major version" is found, for example for
219 // qml only modules (qmltypes files also register their versions)
220 DomItem env = self.environment();
221 if (std::shared_ptr<DomEnvironment> envPtr = env.ownerAs<DomEnvironment>()) {
222 for (int majorV : m_majorVersions) {
223 auto mIndex = envPtr->moduleIndexWithUri(self&: env, uri, majorVersion: majorV, lookup: EnvLookup::Normal,
224 changeable: Changeable::Writable);
225 }
226 }
227}
228
229QCborValue pluginData(QQmlDirParser::Plugin &pl, QStringList cNames)
230{
231 QCborArray names;
232 for (QString n : cNames)
233 names.append(value: n);
234 return QCborMap({ { QCborValue(QStringView(Fields::name)), pl.name },
235 { QStringView(Fields::path), pl.path },
236 { QStringView(Fields::classNames), names } });
237}
238
239bool QmldirFile::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
240{
241 bool cont = ExternalOwningItem::iterateDirectSubpaths(self, visitor);
242 cont = cont && self.dvValueField(visitor, f: Fields::uri, value: uri().toString());
243 cont = cont && self.dvValueField(visitor, f: Fields::designerSupported, value: designerSupported());
244 cont = cont && self.dvReferencesField(visitor, f: Fields::qmltypesFiles, paths: m_qmltypesFilePaths);
245 cont = cont && self.dvWrapField(visitor, f: Fields::exports, obj&: m_exports);
246 cont = cont && self.dvWrapField(visitor, f: Fields::imports, obj&: m_imports);
247 cont = cont && self.dvItemField(visitor, f: Fields::plugins, it: [this, &self]() {
248 QStringList cNames = classNames();
249 return self.subListItem(list: List::fromQListRef<QQmlDirParser::Plugin>(
250 pathFromOwner: self.pathFromOwner().field(name: Fields::plugins), list&: m_plugins,
251 elWrapper: [cNames](DomItem &list, const PathEls::PathComponent &p,
252 QQmlDirParser::Plugin &plugin) {
253 return list.subDataItem(c: p, value: pluginData(pl&: plugin, cNames));
254 }));
255 });
256 // add qmlfiles as map because this way they are presented the same way as
257 // the qmlfiles in a directory
258 cont = cont && self.dvItemField(visitor, f: Fields::qmlFiles, it: [this, &self]() {
259 const QMap<QString, QString> typeFileMap = qmlFiles();
260 return self.subMapItem(map: Map(
261 self.pathFromOwner().field(name: Fields::qmlFiles),
262 [typeFileMap](DomItem &map, QString typeV) {
263 QString path = typeFileMap.value(key: typeV);
264 if (path.isEmpty())
265 return DomItem();
266 else
267 return map.subReferencesItem(
268 c: PathEls::Key(typeV),
269 paths: QList<Path>({ Paths::qmlFileObjectPath(canonicalFilePath: path) }));
270 },
271 [typeFileMap](DomItem &) {
272 return QSet<QString>(typeFileMap.keyBegin(), typeFileMap.keyEnd());
273 },
274 QStringLiteral(u"QList<Reference>")));
275 });
276 cont = cont && self.dvWrapField(visitor, f: Fields::autoExports, obj&: m_autoExports);
277 return cont;
278}
279
280QMap<QString, QString> QmldirFile::qmlFiles() const
281{
282 // add qmlfiles as map because this way they are presented the same way as
283 // the qmlfiles in a directory which gives them as fileName->list of references to files
284 // this is done only to ensure that they are loaded as dependencies
285 QMap<QString, QString> res;
286 for (const auto &e : m_exports)
287 res.insert(key: e.typeName + QStringLiteral(u"-") + e.version.stringValue(),
288 value: e.typePath[2].headName());
289 return res;
290}
291
292std::shared_ptr<OwningItem> QmlFile::doCopy(DomItem &) const
293{
294 auto res = std::make_shared<QmlFile>(args: *this);
295 return res;
296}
297
298QmlFile::QmlFile(const QmlFile &o)
299 : ExternalOwningItem(o),
300 m_engine(o.m_engine),
301 m_ast(o.m_ast),
302 m_astComments(o.m_astComments),
303 m_comments(o.m_comments),
304 m_fileLocationsTree(o.m_fileLocationsTree),
305 m_components(o.m_components),
306 m_pragmas(o.m_pragmas),
307 m_imports(o.m_imports),
308 m_importScope(o.m_importScope)
309{
310 if (m_astComments)
311 m_astComments = std::make_shared<AstComments>(args&: *m_astComments);
312}
313
314QmlFile::QmlFile(QString filePath, QString code, QDateTime lastDataUpdateAt, int derivedFrom)
315 : ExternalOwningItem(filePath, lastDataUpdateAt, Paths::qmlFilePath(canonicalFilePath: filePath), derivedFrom,
316 code),
317 m_engine(new QQmlJS::Engine),
318 m_astComments(new AstComments(m_engine)),
319 m_fileLocationsTree(FileLocations::createTree(basePath: canonicalPath()))
320{
321 QQmlJS::Lexer lexer(m_engine.get());
322 lexer.setCode(code, /*lineno = */ 1, /*qmlMode=*/true);
323 QQmlJS::Parser parser(m_engine.get());
324 m_isValid = parser.parse();
325 for (DiagnosticMessage msg : parser.diagnosticMessages())
326 addErrorLocal(myParsingErrors().errorMessage(msg).withFile(filePath).withPath(m_path));
327 m_ast = parser.ast();
328}
329
330ErrorGroups QmlFile::myParsingErrors()
331{
332 static ErrorGroups res = { .groups: { DomItem::domErrorGroup, NewErrorGroup("QmlFile"),
333 NewErrorGroup("Parsing") } };
334 return res;
335}
336
337bool QmlFile::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
338{
339 bool cont = ExternalOwningItem::iterateDirectSubpaths(self, visitor);
340 cont = cont && self.dvWrapField(visitor, f: Fields::components, obj&: m_components);
341 cont = cont && self.dvWrapField(visitor, f: Fields::pragmas, obj&: m_pragmas);
342 cont = cont && self.dvWrapField(visitor, f: Fields::imports, obj&: m_imports);
343 cont = cont && self.dvWrapField(visitor, f: Fields::importScope, obj&: m_importScope);
344 cont = cont && self.dvWrapField(visitor, f: Fields::fileLocationsTree, obj&: m_fileLocationsTree);
345 cont = cont && self.dvWrapField(visitor, f: Fields::comments, obj&: m_comments);
346 cont = cont && self.dvWrapField(visitor, f: Fields::astComments, obj&: m_astComments);
347 return cont;
348}
349
350DomItem QmlFile::field(DomItem &self, QStringView name)
351{
352 if (name == Fields::components)
353 return self.wrapField(f: Fields::components, obj&: m_components);
354 return DomBase::field(self, name);
355}
356
357void QmlFile::addError(DomItem &self, ErrorMessage msg)
358{
359 self.containingObject().addError(msg);
360}
361
362void QmlFile::writeOut(DomItem &self, OutWriter &ow) const
363{
364 for (DomItem &p : self.field(name: Fields::pragmas).values()) {
365 p.writeOut(lw&: ow);
366 }
367 for (auto i : self.field(name: Fields::imports).values()) {
368 i.writeOut(lw&: ow);
369 }
370 ow.ensureNewline(nNewlines: 2);
371 DomItem mainC = self.field(name: Fields::components).key(name: QString()).index(0);
372 mainC.writeOut(lw&: ow);
373}
374
375std::shared_ptr<OwningItem> GlobalScope::doCopy(DomItem &self) const
376{
377 auto res = std::make_shared<GlobalScope>(
378 args: canonicalFilePath(self), args: lastDataUpdateAt(), args: revision());
379 return res;
380}
381
382bool GlobalScope::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
383{
384 bool cont = ExternalOwningItem::iterateDirectSubpaths(self, visitor);
385 return cont;
386}
387
388void QmltypesFile::ensureInModuleIndex(DomItem &self)
389{
390 auto it = m_uris.begin();
391 auto end = m_uris.end();
392 DomItem env = self.environment();
393 if (std::shared_ptr<DomEnvironment> envPtr = env.ownerAs<DomEnvironment>()) {
394 while (it != end) {
395 QString uri = it.key();
396 for (int majorV : it.value()) {
397 auto mIndex = envPtr->moduleIndexWithUri(self&: env, uri, majorVersion: majorV, lookup: EnvLookup::Normal,
398 changeable: Changeable::Writable);
399 mIndex->addQmltypeFilePath(p: self.canonicalPath());
400 }
401 ++it;
402 }
403 }
404}
405
406bool QmltypesFile::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
407{
408 bool cont = ExternalOwningItem::iterateDirectSubpaths(self, visitor);
409 cont = cont && self.dvWrapField(visitor, f: Fields::components, obj&: m_components);
410 cont = cont && self.dvWrapField(visitor, f: Fields::exports, obj&: m_exports);
411 cont = cont && self.dvItemField(visitor, f: Fields::uris, it: [this, &self]() {
412 return self.subMapItem(map: Map::fromMapRef<QSet<int>>(
413 pathFromOwner: self.pathFromOwner().field(name: Fields::uris), map&: m_uris,
414 elWrapper: [](DomItem &map, const PathEls::PathComponent &p, QSet<int> &el) {
415 QList<int> l(el.cbegin(), el.cend());
416 std::sort(first: l.begin(), last: l.end());
417 return map.subListItem(
418 list: List::fromQList<int>(pathFromOwner: map.pathFromOwner().appendComponent(c: p), list: l,
419 elWrapper: [](DomItem &list, const PathEls::PathComponent &p,
420 int &el) { return list.subDataItem(c: p, value: el); }));
421 }));
422 });
423 cont = cont && self.dvWrapField(visitor, f: Fields::imports, obj&: m_imports);
424 return cont;
425}
426
427QmlDirectory::QmlDirectory(QString filePath, QStringList dirList, QDateTime lastDataUpdateAt,
428 int derivedFrom)
429 : ExternalOwningItem(filePath, lastDataUpdateAt, Paths::qmlDirectoryPath(path: filePath), derivedFrom,
430 dirList.join(sep: QLatin1Char('\n')))
431{
432 for (QString f : dirList) {
433 addQmlFilePath(relativePath: f);
434 }
435}
436
437bool QmlDirectory::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
438{
439 bool cont = ExternalOwningItem::iterateDirectSubpaths(self, visitor);
440 cont = cont && self.dvWrapField(visitor, f: Fields::exports, obj&: m_exports);
441 cont = cont && self.dvItemField(visitor, f: Fields::qmlFiles, it: [this, &self]() -> DomItem {
442 QDir baseDir(canonicalFilePath());
443 return self.subMapItem(map: Map(
444 self.pathFromOwner().field(name: Fields::qmlFiles),
445 [this, baseDir](DomItem &map, QString key) -> DomItem {
446 QList<Path> res;
447 auto it = m_qmlFiles.find(key);
448 while (it != m_qmlFiles.end() && it.key() == key) {
449 res.append(t: Paths::qmlFilePath(
450 canonicalFilePath: QFileInfo(baseDir.filePath(fileName: it.value())).canonicalFilePath()));
451 ++it;
452 }
453 return map.subReferencesItem(c: PathEls::Key(key), paths: res);
454 },
455 [this](DomItem &) {
456 auto keys = m_qmlFiles.keys();
457 return QSet<QString>(keys.begin(), keys.end());
458 },
459 u"List<Reference>"_s));
460 });
461 return cont;
462}
463
464bool QmlDirectory::addQmlFilePath(QString relativePath)
465{
466 QRegularExpression qmlFileRe(QRegularExpression::anchoredPattern(
467 expression: uR"((?<compName>[a-zA-z0-9_]+)\.(?:qml|qmlannotation))"));
468 QRegularExpressionMatch m = qmlFileRe.match(subject: relativePath);
469 if (m.hasMatch() && !m_qmlFiles.values(key: m.captured(name: u"compName")).contains(str: relativePath)) {
470 m_qmlFiles.insert(key: m.captured(name: u"compName"), value: relativePath);
471 Export e;
472 QDir dir(canonicalFilePath());
473 QFileInfo fInfo(dir.filePath(fileName: relativePath));
474 e.exportSourcePath = canonicalPath();
475 e.typeName = m.captured(name: u"compName");
476 e.typePath = Paths::qmlFileObjectPath(canonicalFilePath: fInfo.canonicalFilePath());
477 e.uri = QLatin1String("file://") + canonicalFilePath();
478 m_exports.insert(key: e.typeName, value: e);
479 return true;
480 }
481 return false;
482}
483
484} // end namespace Dom
485} // end namespace QQmlJS
486
487QT_END_NAMESPACE
488

source code of qtdeclarative/src/qmldom/qqmldomexternalitems.cpp