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