1// Copyright (C) 2016 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 "qqmldirparser_p.h"
5
6#include <QtCore/QtDebug>
7
8QT_BEGIN_NAMESPACE
9
10static int parseInt(QStringView str, bool *ok)
11{
12 int pos = 0;
13 int number = 0;
14 while (pos < str.size() && str.at(n: pos).isDigit()) {
15 if (pos != 0)
16 number *= 10;
17 number += str.at(n: pos).unicode() - '0';
18 ++pos;
19 }
20 if (pos != str.size())
21 *ok = false;
22 else
23 *ok = true;
24 return number;
25}
26
27static QTypeRevision parseVersion(const QString &str)
28{
29 const int dotIndex = str.indexOf(ch: QLatin1Char('.'));
30 if (dotIndex != -1 && str.indexOf(ch: QLatin1Char('.'), from: dotIndex + 1) == -1) {
31 bool ok = false;
32 const int major = parseInt(str: QStringView(str).left(n: dotIndex), ok: &ok);
33 if (!ok) return QTypeRevision();
34 const int minor = parseInt(str: QStringView(str).mid(pos: dotIndex + 1, n: str.size() - dotIndex - 1), ok: &ok);
35 return ok ? QTypeRevision::fromVersion(majorVersion: major, minorVersion: minor) : QTypeRevision();
36 }
37 return QTypeRevision();
38}
39
40void QQmlDirParser::clear()
41{
42 _errors.clear();
43 _typeNamespace.clear();
44 _components.clear();
45 _dependencies.clear();
46 _imports.clear();
47 _scripts.clear();
48 _plugins.clear();
49 _designerSupported = false;
50 _typeInfos.clear();
51 _classNames.clear();
52 _linkTarget.clear();
53}
54
55inline static void scanSpace(const QChar *&ch) {
56 while (ch->isSpace() && !ch->isNull() && *ch != QLatin1Char('\n'))
57 ++ch;
58}
59
60inline static void scanToEnd(const QChar *&ch) {
61 while (*ch != QLatin1Char('\n') && !ch->isNull())
62 ++ch;
63}
64
65inline static void scanWord(const QChar *&ch) {
66 while (!ch->isSpace() && !ch->isNull())
67 ++ch;
68}
69
70/*!
71\a url is used for generating errors.
72*/
73bool QQmlDirParser::parse(const QString &source)
74{
75 quint16 lineNumber = 0;
76 bool firstLine = true;
77
78 auto readImport = [&](const QString *sections, int sectionCount, Import::Flags flags) {
79 Import import;
80 if (sectionCount == 2) {
81 import = Import(sections[1], QTypeRevision(), flags);
82 } else if (sectionCount == 3) {
83 if (sections[2] == QLatin1String("auto")) {
84 import = Import(sections[1], QTypeRevision(), flags | Import::Auto);
85 } else {
86 const auto version = parseVersion(str: sections[2]);
87 if (version.isValid()) {
88 import = Import(sections[1], version, flags);
89 } else {
90 reportError(line: lineNumber, column: 0,
91 QStringLiteral("invalid version %1, expected <major>.<minor>")
92 .arg(a: sections[2]));
93 return false;
94 }
95 }
96 } else {
97 reportError(line: lineNumber, column: 0,
98 QStringLiteral("%1 requires 1 or 2 arguments, but %2 were provided")
99 .arg(a: sections[0]).arg(a: sectionCount - 1));
100 return false;
101 }
102 if (sections[0] == QStringLiteral("import"))
103 _imports.append(t: import);
104 else
105 _dependencies.append(t: import);
106 return true;
107 };
108
109 auto readPlugin = [&](const QString *sections, int sectionCount, bool isOptional) {
110 if (sectionCount < 2 || sectionCount > 3) {
111 reportError(line: lineNumber, column: 0, QStringLiteral("plugin directive requires one or two "
112 "arguments, but %1 were provided")
113 .arg(a: sectionCount - 1));
114 return false;
115 }
116
117 const Plugin entry(sections[1], sections[2], isOptional);
118 _plugins.append(t: entry);
119 return true;
120 };
121
122 const QChar *ch = source.constData();
123 while (!ch->isNull()) {
124 ++lineNumber;
125
126 bool invalidLine = false;
127 const QChar *lineStart = ch;
128
129 scanSpace(ch);
130 if (*ch == QLatin1Char('\n')) {
131 ++ch;
132 continue;
133 }
134 if (ch->isNull())
135 break;
136
137 QString sections[4];
138 int sectionCount = 0;
139
140 do {
141 if (*ch == QLatin1Char('#')) {
142 scanToEnd(ch);
143 break;
144 }
145 const QChar *start = ch;
146 scanWord(ch);
147 if (sectionCount < 4) {
148 sections[sectionCount++] = source.mid(position: start-source.constData(), n: ch-start);
149 } else {
150 reportError(line: lineNumber, column: start-lineStart, message: QLatin1String("unexpected token"));
151 scanToEnd(ch);
152 invalidLine = true;
153 break;
154 }
155 scanSpace(ch);
156 } while (*ch != QLatin1Char('\n') && !ch->isNull());
157
158 if (!ch->isNull())
159 ++ch;
160
161 if (invalidLine) {
162 reportError(line: lineNumber, column: 0,
163 QStringLiteral("invalid qmldir directive contains too many tokens"));
164 continue;
165 } else if (sectionCount == 0) {
166 continue; // no sections, no party.
167
168 } else if (sections[0] == QLatin1String("module")) {
169 if (sectionCount != 2) {
170 reportError(line: lineNumber, column: 0,
171 QStringLiteral("module identifier directive requires one argument, but %1 were provided").arg(a: sectionCount - 1));
172 continue;
173 }
174 if (!_typeNamespace.isEmpty()) {
175 reportError(line: lineNumber, column: 0,
176 QStringLiteral("only one module identifier directive may be defined in a qmldir file"));
177 continue;
178 }
179 if (!firstLine) {
180 reportError(line: lineNumber, column: 0,
181 QStringLiteral("module identifier directive must be the first directive in a qmldir file"));
182 continue;
183 }
184
185 _typeNamespace = sections[1];
186
187 } else if (sections[0] == QLatin1String("plugin")) {
188 if (!readPlugin(sections, sectionCount, false))
189 continue;
190 } else if (sections[0] == QLatin1String("optional")) {
191 if (sectionCount < 2) {
192 reportError(line: lineNumber, column: 0, QStringLiteral("optional directive requires further "
193 "arguments, but none were provided."));
194 continue;
195 }
196
197 if (sections[1] == QStringLiteral("plugin")) {
198 if (!readPlugin(sections + 1, sectionCount - 1, true))
199 continue;
200 } else if (sections[1] == QLatin1String("import")) {
201 if (!readImport(sections + 1, sectionCount - 1, Import::Optional))
202 continue;
203 } else {
204 reportError(line: lineNumber, column: 0, QStringLiteral("only import and plugin can be optional, "
205 "not %1.").arg(a: sections[1]));
206 continue;
207 }
208 } else if (sections[0] == QLatin1String("default")) {
209 if (sectionCount < 2) {
210 reportError(line: lineNumber, column: 0,
211 QStringLiteral("default directive requires further "
212 "arguments, but none were provided."));
213 continue;
214 }
215 if (sections[1] == QLatin1String("import")) {
216 if (!readImport(sections + 1, sectionCount - 1,
217 Import::Flags({ Import::Optional, Import::OptionalDefault })))
218 continue;
219 } else {
220 reportError(line: lineNumber, column: 0,
221 QStringLiteral("only optional imports can have a default, "
222 "not %1.")
223 .arg(a: sections[1]));
224 }
225 } else if (sections[0] == QLatin1String("classname")) {
226 if (sectionCount < 2) {
227 reportError(line: lineNumber, column: 0,
228 QStringLiteral("classname directive requires an argument, but %1 were provided").arg(a: sectionCount - 1));
229
230 continue;
231 }
232
233 _classNames.append(t: sections[1]);
234
235 } else if (sections[0] == QLatin1String("internal")) {
236 if (sectionCount == 3) {
237 Component entry(sections[1], sections[2], QTypeRevision());
238 entry.internal = true;
239 _components.insert(key: entry.typeName, value: entry);
240 } else if (sectionCount == 4) {
241 const QTypeRevision version = parseVersion(str: sections[2]);
242 if (version.isValid()) {
243 Component entry(sections[1], sections[3], version);
244 entry.internal = true;
245 _components.insert(key: entry.typeName, value: entry);
246 } else {
247 reportError(line: lineNumber, column: 0,
248 QStringLiteral("invalid version %1, expected <major>.<minor>")
249 .arg(a: sections[2]));
250 continue;
251 }
252 } else {
253 reportError(line: lineNumber, column: 0,
254 QStringLiteral("internal types require 2 or 3 arguments, "
255 "but %1 were provided").arg(a: sectionCount - 1));
256 continue;
257 }
258 } else if (sections[0] == QLatin1String("singleton")) {
259 if (sectionCount < 3 || sectionCount > 4) {
260 reportError(line: lineNumber, column: 0,
261 QStringLiteral("singleton types require 2 or 3 arguments, but %1 were provided").arg(a: sectionCount - 1));
262 continue;
263 } else if (sectionCount == 3) {
264 // handle qmldir directory listing case where singleton is defined in the following pattern:
265 // singleton TestSingletonType TestSingletonType.qml
266 Component entry(sections[1], sections[2], QTypeRevision());
267 entry.singleton = true;
268 _components.insert(key: entry.typeName, value: entry);
269 } else {
270 // handle qmldir module listing case where singleton is defined in the following pattern:
271 // singleton TestSingletonType 2.0 TestSingletonType20.qml
272 const QTypeRevision version = parseVersion(str: sections[2]);
273 if (version.isValid()) {
274 const QString &fileName = sections[3];
275 Component entry(sections[1], fileName, version);
276 entry.singleton = true;
277 _components.insert(key: entry.typeName, value: entry);
278 } else {
279 reportError(line: lineNumber, column: 0, QStringLiteral("invalid version %1, expected <major>.<minor>").arg(a: sections[2]));
280 }
281 }
282 } else if (sections[0] == QLatin1String("typeinfo")) {
283 if (sectionCount != 2) {
284 reportError(line: lineNumber, column: 0,
285 QStringLiteral("typeinfo requires 1 argument, but %1 were provided").arg(a: sectionCount - 1));
286 continue;
287 }
288 _typeInfos.append(t: sections[1]);
289 } else if (sections[0] == QLatin1String("designersupported")) {
290 if (sectionCount != 1)
291 reportError(line: lineNumber, column: 0, QStringLiteral("designersupported does not expect any argument"));
292 else
293 _designerSupported = true;
294 } else if (sections[0] == QLatin1String("static")) {
295 if (sectionCount != 1)
296 reportError(line: lineNumber, column: 0, QStringLiteral("static does not expect any argument"));
297 else
298 _isStaticModule = true;
299 } else if (sections[0] == QLatin1String("system")) {
300 if (sectionCount != 1)
301 reportError(line: lineNumber, column: 0, QStringLiteral("system does not expect any argument"));
302 else
303 _isSystemModule = true;
304 } else if (sections[0] == QLatin1String("import")
305 || sections[0] == QLatin1String("depends")) {
306 if (!readImport(sections, sectionCount, Import::Default))
307 continue;
308 } else if (sections[0] == QLatin1String("prefer")) {
309 if (sectionCount < 2) {
310 reportError(line: lineNumber, column: 0,
311 QStringLiteral("prefer directive requires one argument, "
312 "but %1 were provided").arg(a: sectionCount - 1));
313 continue;
314 }
315
316 if (!_preferredPath.isEmpty()) {
317 reportError(line: lineNumber, column: 0, QStringLiteral(
318 "only one prefer directive may be defined in a qmldir file"));
319 continue;
320 }
321
322 if (!sections[1].endsWith(c: u'/')) {
323 // Yes. People should realize it's a directory.
324 reportError(line: lineNumber, column: 0, QStringLiteral(
325 "the preferred directory has to end with a '/'"));
326 continue;
327 }
328
329 _preferredPath = sections[1];
330 } else if (sections[0] == QLatin1String("linktarget")) {
331 if (sectionCount < 2) {
332 reportError(line: lineNumber, column: 0,
333 QStringLiteral("linktarget directive requires an argument, "
334 "but %1 were provided")
335 .arg(a: sectionCount - 1));
336 continue;
337 }
338
339 if (!_linkTarget.isEmpty()) {
340 reportError(
341 line: lineNumber, column: 0,
342 QStringLiteral(
343 "only one linktarget directive may be defined in a qmldir file"));
344 continue;
345 }
346
347 _linkTarget = sections[1];
348 } else if (sectionCount == 2) {
349 // No version specified (should only be used for relative qmldir files)
350 insertComponentOrScript(name: sections[0], fileName: sections[1], version: QTypeRevision());
351 } else if (sectionCount == 3) {
352 const QTypeRevision version = parseVersion(str: sections[1]);
353 if (version.isValid()) {
354 insertComponentOrScript(name: sections[0], fileName: sections[2], version);
355 } else {
356 reportError(
357 line: lineNumber, column: 0,
358 QStringLiteral("invalid version %1, expected <major>.<minor>")
359 .arg(a: sections[1]));
360 }
361 } else {
362 reportError(line: lineNumber, column: 0,
363 QStringLiteral("a component declaration requires two or three arguments, but %1 were provided").arg(a: sectionCount));
364 }
365
366 firstLine = false;
367 }
368
369 return hasError();
370}
371
372/* removes all file selector occurrences in path
373 firstPlus is the position of the initial '+' in the path
374 which we always have as we check for '+' to decide whether
375 we need to do some work at all
376*/
377static QString pathWithoutFileSelectors(QString path, // we want a copy of path
378 qsizetype firstPlus)
379{
380 do {
381 Q_ASSERT(path.at(firstPlus) == u'+');
382 const auto eos = path.size();
383 qsizetype terminatingSlashPos = firstPlus + 1;
384 while (terminatingSlashPos != eos && path.at(i: terminatingSlashPos) != u'/')
385 ++terminatingSlashPos;
386 path.remove(i: firstPlus, len: terminatingSlashPos - firstPlus + 1);
387 firstPlus = path.indexOf(ch: u'+', from: firstPlus);
388 } while (firstPlus != -1);
389 return path;
390}
391
392static bool canDisambiguate(
393 const QString &fileName1, const QString &fileName2, QString *correctedFileName)
394{
395 // If the entries are exactly the same we can delete one without losing anything.
396 if (fileName1 == fileName2)
397 return true;
398
399 // If we detect conflicting paths, we check if they agree when we remove anything
400 // looking like a file selector.
401
402 // ugly heuristic to deal with file selectors
403 const qsizetype file2PotentialFileSelectorPos = fileName2.indexOf(ch: u'+');
404 const bool file2MightHaveFileSelector = file2PotentialFileSelectorPos != -1;
405
406 if (const qsizetype fileSelectorPos1 = fileName1.indexOf(ch: u'+'); fileSelectorPos1 != -1) {
407 // existing entry was file selector entry, fix it up
408 // it could also be the case that _both_ are using file selectors
409 const QString baseName = file2MightHaveFileSelector
410 ? pathWithoutFileSelectors(path: fileName2, firstPlus: file2PotentialFileSelectorPos)
411 : fileName2;
412
413 if (pathWithoutFileSelectors(path: fileName1, firstPlus: fileSelectorPos1) != baseName)
414 return false;
415
416 *correctedFileName = baseName;
417 return true;
418 }
419
420 // new entry contains file selector (and we know that fileName1 did not)
421 if (file2MightHaveFileSelector
422 && pathWithoutFileSelectors(path: fileName2, firstPlus: file2PotentialFileSelectorPos) == fileName1) {
423 *correctedFileName = fileName1;
424 return true;
425 }
426
427 return false;
428}
429
430static void disambiguateFileSelectedComponents(QQmlDirComponents *components)
431{
432 using ConstIterator = QQmlDirComponents::const_iterator;
433
434 // end iterator may get invalidated by the erasing below.
435 // Therefore, refetch it on each iteration.
436 for (ConstIterator cit = components->constBegin(); cit != components->constEnd();) {
437
438 // We can erase and re-assign cit if we immediately forget cit2.
439 // But we cannot erase cit2 without potentially invalidating cit.
440
441 bool doErase = false;
442 const ConstIterator cend = components->constEnd();
443 for (ConstIterator cit2 = ++ConstIterator(cit); cit2 != cend; ++cit2) {
444 if (cit2.key() != cit.key())
445 break;
446
447 Q_ASSERT(cit2->typeName == cit->typeName);
448
449 if (cit2->version != cit->version
450 || cit2->internal != cit->internal
451 || cit2->singleton != cit->singleton) {
452 continue;
453 }
454
455 // The two components may differ only by fileName now.
456
457 if (canDisambiguate(fileName1: cit->fileName, fileName2: cit2->fileName, correctedFileName: &(cit2->fileName))) {
458 doErase = true;
459 break;
460 }
461 }
462
463 if (doErase)
464 cit = components->erase(it: cit);
465 else
466 ++cit;
467 }
468}
469
470static void disambiguateFileSelectedScripts(QQmlDirScripts *scripts)
471{
472 using Iterator = QQmlDirScripts::iterator;
473
474 Iterator send = scripts->end();
475
476 for (Iterator sit = scripts->begin(); sit != send; ++sit) {
477 send = std::remove_if(first: ++Iterator(sit), last: send, pred: [sit](const QQmlDirParser::Script &script2) {
478 if (sit->nameSpace != script2.nameSpace || sit->version != script2.version)
479 return false;
480
481 // The two scripts may differ only by fileName now.
482 return canDisambiguate(fileName1: sit->fileName, fileName2: script2.fileName, correctedFileName: &(sit->fileName));
483 });
484 }
485
486 scripts->erase(abegin: send, aend: scripts->end());
487}
488
489void QQmlDirParser::disambiguateFileSelectors()
490{
491 disambiguateFileSelectedComponents(components: &_components);
492 disambiguateFileSelectedScripts(scripts: &_scripts);
493}
494
495void QQmlDirParser::reportError(quint16 line, quint16 column, const QString &description)
496{
497 QQmlJS::DiagnosticMessage error;
498 error.loc.startLine = line;
499 error.loc.startColumn = column;
500 error.message = description;
501 _errors.append(t: error);
502}
503
504void QQmlDirParser::insertComponentOrScript(
505 const QString &name, const QString &fileName, QTypeRevision version)
506{
507 // A 'js' extension indicates a namespaced script import
508 if (fileName.endsWith(s: QLatin1String(".js")) || fileName.endsWith(s: QLatin1String(".mjs")))
509 _scripts.append(t: Script(name, fileName, version));
510 else
511 _components.insert(key: name, value: Component(name, fileName, version));
512}
513
514void QQmlDirParser::setError(const QQmlJS::DiagnosticMessage &e)
515{
516 _errors.clear();
517 reportError(line: e.loc.startLine, column: e.loc.startColumn, description: e.message);
518}
519
520QList<QQmlJS::DiagnosticMessage> QQmlDirParser::errors(const QString &uri) const
521{
522 QList<QQmlJS::DiagnosticMessage> errors;
523 const int numErrors = _errors.size();
524 errors.reserve(asize: numErrors);
525 for (int i = 0; i < numErrors; ++i) {
526 QQmlJS::DiagnosticMessage e = _errors.at(i);
527 e.message.replace(before: QLatin1String("$$URI$$"), after: uri);
528 errors << e;
529 }
530 return errors;
531}
532
533QDebug &operator<< (QDebug &debug, const QQmlDirParser::Component &component)
534{
535 const QString output = QStringLiteral("{%1 %2.%3}")
536 .arg(a: component.typeName).arg(a: component.version.majorVersion())
537 .arg(a: component.version.minorVersion());
538 return debug << qPrintable(output);
539}
540
541QDebug &operator<< (QDebug &debug, const QQmlDirParser::Script &script)
542{
543 const QString output = QStringLiteral("{%1 %2.%3}")
544 .arg(a: script.nameSpace).arg(a: script.version.majorVersion())
545 .arg(a: script.version.minorVersion());
546 return debug << qPrintable(output);
547}
548
549QT_END_NAMESPACE
550

source code of qtdeclarative/src/qml/qmldirparser/qqmldirparser.cpp