1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the QtQml module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #include "qqmldirparser_p.h" |
41 | |
42 | #include <QtCore/QtDebug> |
43 | |
44 | QT_BEGIN_NAMESPACE |
45 | |
46 | static int parseInt(const QStringRef &str, bool *ok) |
47 | { |
48 | int pos = 0; |
49 | int number = 0; |
50 | while (pos < str.length() && str.at(i: pos).isDigit()) { |
51 | if (pos != 0) |
52 | number *= 10; |
53 | number += str.at(i: pos).unicode() - '0'; |
54 | ++pos; |
55 | } |
56 | if (pos != str.length()) |
57 | *ok = false; |
58 | else |
59 | *ok = true; |
60 | return number; |
61 | } |
62 | |
63 | static bool parseVersion(const QString &str, int *major, int *minor) |
64 | { |
65 | const int dotIndex = str.indexOf(c: QLatin1Char('.')); |
66 | if (dotIndex != -1 && str.indexOf(c: QLatin1Char('.'), from: dotIndex + 1) == -1) { |
67 | bool ok = false; |
68 | *major = parseInt(str: QStringRef(&str, 0, dotIndex), ok: &ok); |
69 | if (ok) |
70 | *minor = parseInt(str: QStringRef(&str, dotIndex + 1, str.length() - dotIndex - 1), ok: &ok); |
71 | return ok; |
72 | } |
73 | return false; |
74 | } |
75 | |
76 | void QQmlDirParser::clear() |
77 | { |
78 | _errors.clear(); |
79 | _typeNamespace.clear(); |
80 | _components.clear(); |
81 | _dependencies.clear(); |
82 | _imports.clear(); |
83 | _scripts.clear(); |
84 | _plugins.clear(); |
85 | _designerSupported = false; |
86 | _typeInfos.clear(); |
87 | _className.clear(); |
88 | } |
89 | |
90 | inline static void scanSpace(const QChar *&ch) { |
91 | while (ch->isSpace() && !ch->isNull() && *ch != QLatin1Char('\n')) |
92 | ++ch; |
93 | } |
94 | |
95 | inline static void scanToEnd(const QChar *&ch) { |
96 | while (*ch != QLatin1Char('\n') && !ch->isNull()) |
97 | ++ch; |
98 | } |
99 | |
100 | inline static void scanWord(const QChar *&ch) { |
101 | while (!ch->isSpace() && !ch->isNull()) |
102 | ++ch; |
103 | } |
104 | |
105 | /*! |
106 | \a url is used for generating errors. |
107 | */ |
108 | bool QQmlDirParser::parse(const QString &source) |
109 | { |
110 | quint16 lineNumber = 0; |
111 | bool firstLine = true; |
112 | |
113 | const QChar *ch = source.constData(); |
114 | while (!ch->isNull()) { |
115 | ++lineNumber; |
116 | |
117 | bool invalidLine = false; |
118 | const QChar *lineStart = ch; |
119 | |
120 | scanSpace(ch); |
121 | if (*ch == QLatin1Char('\n')) { |
122 | ++ch; |
123 | continue; |
124 | } |
125 | if (ch->isNull()) |
126 | break; |
127 | |
128 | QString sections[4]; |
129 | int sectionCount = 0; |
130 | |
131 | do { |
132 | if (*ch == QLatin1Char('#')) { |
133 | scanToEnd(ch); |
134 | break; |
135 | } |
136 | const QChar *start = ch; |
137 | scanWord(ch); |
138 | if (sectionCount < 4) { |
139 | sections[sectionCount++] = source.mid(position: start-source.constData(), n: ch-start); |
140 | } else { |
141 | reportError(line: lineNumber, column: start-lineStart, message: QLatin1String("unexpected token" )); |
142 | scanToEnd(ch); |
143 | invalidLine = true; |
144 | break; |
145 | } |
146 | scanSpace(ch); |
147 | } while (*ch != QLatin1Char('\n') && !ch->isNull()); |
148 | |
149 | if (!ch->isNull()) |
150 | ++ch; |
151 | |
152 | if (invalidLine) { |
153 | reportError(line: lineNumber, column: 0, |
154 | QStringLiteral("invalid qmldir directive contains too many tokens" )); |
155 | continue; |
156 | } else if (sectionCount == 0) { |
157 | continue; // no sections, no party. |
158 | |
159 | } else if (sections[0] == QLatin1String("module" )) { |
160 | if (sectionCount != 2) { |
161 | reportError(line: lineNumber, column: 0, |
162 | QStringLiteral("module identifier directive requires one argument, but %1 were provided" ).arg(a: sectionCount - 1)); |
163 | continue; |
164 | } |
165 | if (!_typeNamespace.isEmpty()) { |
166 | reportError(line: lineNumber, column: 0, |
167 | QStringLiteral("only one module identifier directive may be defined in a qmldir file" )); |
168 | continue; |
169 | } |
170 | if (!firstLine) { |
171 | reportError(line: lineNumber, column: 0, |
172 | QStringLiteral("module identifier directive must be the first directive in a qmldir file" )); |
173 | continue; |
174 | } |
175 | |
176 | _typeNamespace = sections[1]; |
177 | |
178 | } else if (sections[0] == QLatin1String("plugin" )) { |
179 | if (sectionCount < 2 || sectionCount > 3) { |
180 | reportError(line: lineNumber, column: 0, |
181 | QStringLiteral("plugin directive requires one or two arguments, but %1 were provided" ).arg(a: sectionCount - 1)); |
182 | |
183 | continue; |
184 | } |
185 | |
186 | const Plugin entry(sections[1], sections[2]); |
187 | |
188 | _plugins.append(t: entry); |
189 | |
190 | } else if (sections[0] == QLatin1String("classname" )) { |
191 | if (sectionCount < 2) { |
192 | reportError(line: lineNumber, column: 0, |
193 | QStringLiteral("classname directive requires an argument, but %1 were provided" ).arg(a: sectionCount - 1)); |
194 | |
195 | continue; |
196 | } |
197 | |
198 | _className = sections[1]; |
199 | |
200 | } else if (sections[0] == QLatin1String("internal" )) { |
201 | if (sectionCount != 3) { |
202 | reportError(line: lineNumber, column: 0, |
203 | QStringLiteral("internal types require 2 arguments, but %1 were provided" ).arg(a: sectionCount - 1)); |
204 | continue; |
205 | } |
206 | Component entry(sections[1], sections[2], -1, -1); |
207 | entry.internal = true; |
208 | _components.insert(akey: entry.typeName, avalue: entry); |
209 | } else if (sections[0] == QLatin1String("singleton" )) { |
210 | if (sectionCount < 3 || sectionCount > 4) { |
211 | reportError(line: lineNumber, column: 0, |
212 | QStringLiteral("singleton types require 2 or 3 arguments, but %1 were provided" ).arg(a: sectionCount - 1)); |
213 | continue; |
214 | } else if (sectionCount == 3) { |
215 | // handle qmldir directory listing case where singleton is defined in the following pattern: |
216 | // singleton TestSingletonType TestSingletonType.qml |
217 | Component entry(sections[1], sections[2], -1, -1); |
218 | entry.singleton = true; |
219 | _components.insert(akey: entry.typeName, avalue: entry); |
220 | } else { |
221 | // handle qmldir module listing case where singleton is defined in the following pattern: |
222 | // singleton TestSingletonType 2.0 TestSingletonType20.qml |
223 | int major, minor; |
224 | if (parseVersion(str: sections[2], major: &major, minor: &minor)) { |
225 | const QString &fileName = sections[3]; |
226 | Component entry(sections[1], fileName, major, minor); |
227 | entry.singleton = true; |
228 | _components.insert(akey: entry.typeName, avalue: entry); |
229 | } else { |
230 | reportError(line: lineNumber, column: 0, QStringLiteral("invalid version %1, expected <major>.<minor>" ).arg(a: sections[2])); |
231 | } |
232 | } |
233 | } else if (sections[0] == QLatin1String("typeinfo" )) { |
234 | if (sectionCount != 2) { |
235 | reportError(line: lineNumber, column: 0, |
236 | QStringLiteral("typeinfo requires 1 argument, but %1 were provided" ).arg(a: sectionCount - 1)); |
237 | continue; |
238 | } |
239 | #ifdef QT_CREATOR |
240 | TypeInfo typeInfo(sections[1]); |
241 | _typeInfos.append(typeInfo); |
242 | #endif |
243 | |
244 | } else if (sections[0] == QLatin1String("designersupported" )) { |
245 | if (sectionCount != 1) |
246 | reportError(line: lineNumber, column: 0, QStringLiteral("designersupported does not expect any argument" )); |
247 | else |
248 | _designerSupported = true; |
249 | } else if (sections[0] == QLatin1String("depends" )) { |
250 | if (sectionCount != 3) { |
251 | reportError(line: lineNumber, column: 0, |
252 | QStringLiteral("depends requires 2 arguments, but %1 were provided" ).arg(a: sectionCount - 1)); |
253 | continue; |
254 | } |
255 | |
256 | int major, minor; |
257 | if (parseVersion(str: sections[2], major: &major, minor: &minor)) { |
258 | Component entry(sections[1], QString(), major, minor); |
259 | entry.internal = true; |
260 | _dependencies.insert(akey: entry.typeName, avalue: entry); |
261 | } else { |
262 | reportError(line: lineNumber, column: 0, QStringLiteral("invalid version %1, expected <major>.<minor>" ).arg(a: sections[2])); |
263 | } |
264 | } else if (sections[0] == QLatin1String("import" )) { |
265 | if (sectionCount != 2) { |
266 | reportError(line: lineNumber, column: 0, |
267 | QStringLiteral("import requires 2 arguments, but %1 were provided" ).arg(a: sectionCount - 1)); |
268 | continue; |
269 | } |
270 | _imports << sections[1]; |
271 | } else if (sectionCount == 2) { |
272 | // No version specified (should only be used for relative qmldir files) |
273 | const Component entry(sections[0], sections[1], -1, -1); |
274 | _components.insert(akey: entry.typeName, avalue: entry); |
275 | } else if (sectionCount == 3) { |
276 | int major, minor; |
277 | if (parseVersion(str: sections[1], major: &major, minor: &minor)) { |
278 | const QString &fileName = sections[2]; |
279 | |
280 | if (fileName.endsWith(s: QLatin1String(".js" )) || fileName.endsWith(s: QLatin1String(".mjs" ))) { |
281 | // A 'js' extension indicates a namespaced script import |
282 | const Script entry(sections[0], fileName, major, minor); |
283 | _scripts.append(t: entry); |
284 | } else { |
285 | const Component entry(sections[0], fileName, major, minor); |
286 | _components.insert(akey: entry.typeName, avalue: entry); |
287 | } |
288 | } else { |
289 | reportError(line: lineNumber, column: 0, QStringLiteral("invalid version %1, expected <major>.<minor>" ).arg(a: sections[1])); |
290 | } |
291 | } else { |
292 | reportError(line: lineNumber, column: 0, |
293 | QStringLiteral("a component declaration requires two or three arguments, but %1 were provided" ).arg(a: sectionCount)); |
294 | } |
295 | |
296 | firstLine = false; |
297 | } |
298 | |
299 | return hasError(); |
300 | } |
301 | |
302 | void QQmlDirParser::reportError(quint16 line, quint16 column, const QString &description) |
303 | { |
304 | QQmlJS::DiagnosticMessage error; |
305 | error.loc.startLine = line; |
306 | error.loc.startColumn = column; |
307 | error.message = description; |
308 | _errors.append(t: error); |
309 | } |
310 | |
311 | bool QQmlDirParser::hasError() const |
312 | { |
313 | if (! _errors.isEmpty()) |
314 | return true; |
315 | |
316 | return false; |
317 | } |
318 | |
319 | void QQmlDirParser::setError(const QQmlJS::DiagnosticMessage &e) |
320 | { |
321 | _errors.clear(); |
322 | reportError(line: e.loc.startLine, column: e.loc.startColumn, description: e.message); |
323 | } |
324 | |
325 | QList<QQmlJS::DiagnosticMessage> QQmlDirParser::errors(const QString &uri) const |
326 | { |
327 | QList<QQmlJS::DiagnosticMessage> errors; |
328 | const int numErrors = _errors.size(); |
329 | errors.reserve(alloc: numErrors); |
330 | for (int i = 0; i < numErrors; ++i) { |
331 | QQmlJS::DiagnosticMessage e = _errors.at(i); |
332 | e.message.replace(before: QLatin1String("$$URI$$" ), after: uri); |
333 | errors << e; |
334 | } |
335 | return errors; |
336 | } |
337 | |
338 | QString QQmlDirParser::typeNamespace() const |
339 | { |
340 | return _typeNamespace; |
341 | } |
342 | |
343 | void QQmlDirParser::setTypeNamespace(const QString &s) |
344 | { |
345 | _typeNamespace = s; |
346 | } |
347 | |
348 | QList<QQmlDirParser::Plugin> QQmlDirParser::plugins() const |
349 | { |
350 | return _plugins; |
351 | } |
352 | |
353 | QMultiHash<QString, QQmlDirParser::Component> QQmlDirParser::components() const |
354 | { |
355 | return _components; |
356 | } |
357 | |
358 | QHash<QString, QQmlDirParser::Component> QQmlDirParser::dependencies() const |
359 | { |
360 | return _dependencies; |
361 | } |
362 | |
363 | QStringList QQmlDirParser::imports() const |
364 | { |
365 | return _imports; |
366 | } |
367 | |
368 | QList<QQmlDirParser::Script> QQmlDirParser::scripts() const |
369 | { |
370 | return _scripts; |
371 | } |
372 | |
373 | QList<QQmlDirParser::TypeInfo> QQmlDirParser::typeInfos() const |
374 | { |
375 | return _typeInfos; |
376 | } |
377 | |
378 | bool QQmlDirParser::designerSupported() const |
379 | { |
380 | return _designerSupported; |
381 | } |
382 | |
383 | QString QQmlDirParser::className() const |
384 | { |
385 | return _className; |
386 | } |
387 | |
388 | QDebug &operator<< (QDebug &debug, const QQmlDirParser::Component &component) |
389 | { |
390 | const QString output = QStringLiteral("{%1 %2.%3}" ). |
391 | arg(a: component.typeName).arg(a: component.majorVersion).arg(a: component.minorVersion); |
392 | return debug << qPrintable(output); |
393 | } |
394 | |
395 | QDebug &operator<< (QDebug &debug, const QQmlDirParser::Script &script) |
396 | { |
397 | const QString output = QStringLiteral("{%1 %2.%3}" ). |
398 | arg(a: script.nameSpace).arg(a: script.majorVersion).arg(a: script.minorVersion); |
399 | return debug << qPrintable(output); |
400 | } |
401 | |
402 | QT_END_NAMESPACE |
403 | |