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