1 | // Copyright (C) 2021 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | #include "qmltccommandlineutils.h" |
5 | #include "qmltcvisitor.h" |
6 | #include "qmltctyperesolver.h" |
7 | |
8 | #include "qmltccompiler.h" |
9 | |
10 | #include <private/qqmljscompiler_p.h> |
11 | #include <private/qqmljsresourcefilemapper_p.h> |
12 | |
13 | #include <QtCore/qcoreapplication.h> |
14 | #include <QtCore/qurl.h> |
15 | #include <QtCore/qhashfunctions.h> |
16 | #include <QtCore/qfileinfo.h> |
17 | #include <QtCore/qlibraryinfo.h> |
18 | #include <QtCore/qcommandlineparser.h> |
19 | #include <QtCore/qregularexpression.h> |
20 | |
21 | #include <QtQml/private/qqmljslexer_p.h> |
22 | #include <QtQml/private/qqmljsparser_p.h> |
23 | #include <QtQml/private/qqmljsengine_p.h> |
24 | #include <QtQml/private/qqmljsastvisitor_p.h> |
25 | #include <QtQml/private/qqmljsast_p.h> |
26 | #include <QtQml/private/qqmljsdiagnosticmessage_p.h> |
27 | |
28 | #include <cstdlib> // EXIT_SUCCESS, EXIT_FAILURE |
29 | |
30 | using namespace Qt::StringLiterals; |
31 | |
32 | void setupLogger(QQmlJSLogger &logger) // prepare logger to work with compiler |
33 | { |
34 | for (const QQmlJS::LoggerCategory &category : logger.categories()) { |
35 | if (category.id() == qmlUnusedImports) |
36 | continue; |
37 | logger.setCategoryLevel(id: category.id(), level: QtCriticalMsg); |
38 | logger.setCategoryIgnored(id: category.id(), error: false); |
39 | } |
40 | } |
41 | |
42 | int main(int argc, char **argv) |
43 | { |
44 | // Produce reliably the same output for the same input by disabling QHash's |
45 | // random seeding. |
46 | QHashSeed::setDeterministicGlobalSeed(); |
47 | QCoreApplication app(argc, argv); |
48 | QCoreApplication::setApplicationName(u"qmltc"_s ); |
49 | QCoreApplication::setApplicationVersion(QStringLiteral(QT_VERSION_STR)); |
50 | |
51 | // command-line parsing: |
52 | QCommandLineParser parser; |
53 | parser.addHelpOption(); |
54 | parser.addVersionOption(); |
55 | |
56 | QCommandLineOption bareOption { |
57 | u"bare"_s , |
58 | QCoreApplication::translate( |
59 | context: "main" , key: "Do not include default import directories. This may be used to run " |
60 | "qmltc on a project using a different Qt version." ) |
61 | }; |
62 | parser.addOption(commandLineOption: bareOption); |
63 | |
64 | QCommandLineOption importPathOption { |
65 | u"I"_s , QCoreApplication::translate(context: "main" , key: "Look for QML modules in specified directory" ), |
66 | QCoreApplication::translate(context: "main" , key: "import directory" ) |
67 | }; |
68 | parser.addOption(commandLineOption: importPathOption); |
69 | QCommandLineOption qmldirOption { |
70 | u"i"_s , QCoreApplication::translate(context: "main" , key: "Include extra qmldir files" ), |
71 | QCoreApplication::translate(context: "main" , key: "qmldir file" ) |
72 | }; |
73 | parser.addOption(commandLineOption: qmldirOption); |
74 | QCommandLineOption outputCppOption { |
75 | u"impl"_s , QCoreApplication::translate(context: "main" , key: "Generated C++ source file path" ), |
76 | QCoreApplication::translate(context: "main" , key: "cpp path" ) |
77 | }; |
78 | parser.addOption(commandLineOption: outputCppOption); |
79 | QCommandLineOption outputHOption { |
80 | u"header"_s , QCoreApplication::translate(context: "main" , key: "Generated C++ header file path" ), |
81 | QCoreApplication::translate(context: "main" , key: "h path" ) |
82 | }; |
83 | parser.addOption(commandLineOption: outputHOption); |
84 | QCommandLineOption resourceOption { |
85 | u"resource"_s , |
86 | QCoreApplication::translate( |
87 | context: "main" , key: "Qt resource file that might later contain one of the compiled files" ), |
88 | QCoreApplication::translate(context: "main" , key: "resource file name" ) |
89 | }; |
90 | parser.addOption(commandLineOption: resourceOption); |
91 | QCommandLineOption metaResourceOption { |
92 | u"meta-resource"_s , |
93 | QCoreApplication::translate(context: "main" , key: "Qt meta information file (in .qrc format)" ), |
94 | QCoreApplication::translate(context: "main" , key: "meta file name" ) |
95 | }; |
96 | parser.addOption(commandLineOption: metaResourceOption); |
97 | QCommandLineOption namespaceOption { |
98 | u"namespace"_s , QCoreApplication::translate(context: "main" , key: "Namespace of the generated C++ code" ), |
99 | QCoreApplication::translate(context: "main" , key: "namespace" ) |
100 | }; |
101 | parser.addOption(commandLineOption: namespaceOption); |
102 | QCommandLineOption exportOption{ u"export"_s , |
103 | QCoreApplication::translate( |
104 | context: "main" , key: "Export macro used in the generated C++ code" ), |
105 | QCoreApplication::translate(context: "main" , key: "export" ) }; |
106 | parser.addOption(commandLineOption: exportOption); |
107 | QCommandLineOption exportIncludeOption{ |
108 | u"exportInclude"_s , |
109 | QCoreApplication::translate( |
110 | context: "main" , key: "Header defining the export macro to be used in the generated C++ code" ), |
111 | QCoreApplication::translate(context: "main" , key: "exportInclude" ) |
112 | }; |
113 | parser.addOption(commandLineOption: exportIncludeOption); |
114 | |
115 | parser.process(app); |
116 | |
117 | const QStringList sources = parser.positionalArguments(); |
118 | if (sources.size() != 1) { |
119 | if (sources.isEmpty()) { |
120 | parser.showHelp(); |
121 | } else { |
122 | fprintf(stderr, format: "%s\n" , |
123 | qPrintable(u"Too many input files specified: '"_s + sources.join(u"' '"_s ) |
124 | + u'\'')); |
125 | } |
126 | return EXIT_FAILURE; |
127 | } |
128 | const QString inputFile = sources.first(); |
129 | |
130 | QString url = parseUrlArgument(arg: inputFile); |
131 | if (url.isNull()) |
132 | return EXIT_FAILURE; |
133 | if (!url.endsWith(s: u".qml" )) { |
134 | fprintf(stderr, format: "Non-QML file passed as input\n" ); |
135 | return EXIT_FAILURE; |
136 | } |
137 | |
138 | static QRegularExpression nameChecker(u"^[a-zA-Z_][a-zA-Z0-9_]*\\.qml$"_s ); |
139 | if (auto match = nameChecker.match(subject: QUrl(url).fileName()); !match.hasMatch()) { |
140 | fprintf(stderr, |
141 | format: "The given QML filename is unsuited for type compilation: the name must consist of " |
142 | "letters, digits and underscores, starting with " |
143 | "a letter or an underscore and ending in '.qml'!\n" ); |
144 | return EXIT_FAILURE; |
145 | } |
146 | |
147 | QString sourceCode = loadUrl(url); |
148 | if (sourceCode.isEmpty()) |
149 | return EXIT_FAILURE; |
150 | |
151 | QString implicitImportDirectory = getImplicitImportDirectory(url); |
152 | if (implicitImportDirectory.isEmpty()) |
153 | return EXIT_FAILURE; |
154 | |
155 | QStringList importPaths; |
156 | |
157 | if (parser.isSet(option: resourceOption)) { |
158 | importPaths.append(t: QLatin1String(":/qt-project.org/imports" )); |
159 | importPaths.append(t: QLatin1String(":/qt/qml" )); |
160 | }; |
161 | |
162 | if (parser.isSet(option: importPathOption)) |
163 | importPaths.append(other: parser.values(option: importPathOption)); |
164 | |
165 | if (!parser.isSet(option: bareOption)) |
166 | importPaths.append(t: QLibraryInfo::path(p: QLibraryInfo::QmlImportsPath)); |
167 | |
168 | QStringList qmldirFiles = parser.values(option: qmldirOption); |
169 | |
170 | QString outputCppFile; |
171 | if (!parser.isSet(option: outputCppOption)) { |
172 | outputCppFile = url.first(n: url.size() - 3) + u"cpp"_s ; |
173 | } else { |
174 | outputCppFile = parser.value(option: outputCppOption); |
175 | } |
176 | |
177 | QString outputHFile; |
178 | if (!parser.isSet(option: outputHOption)) { |
179 | outputHFile = url.first(n: url.size() - 3) + u"h"_s ; |
180 | } else { |
181 | outputHFile = parser.value(option: outputHOption); |
182 | } |
183 | |
184 | if (!parser.isSet(option: resourceOption)) { |
185 | fprintf(stderr, format: "No resource paths for file: %s\n" , qPrintable(inputFile)); |
186 | return EXIT_FAILURE; |
187 | } |
188 | |
189 | // main logic: |
190 | QQmlJS::Engine engine; |
191 | QQmlJS::Lexer lexer(&engine); |
192 | lexer.setCode(code: sourceCode, /*lineno = */ 1); |
193 | QQmlJS::Parser qmlParser(&engine); |
194 | if (!qmlParser.parse()) { |
195 | const auto diagnosticMessages = qmlParser.diagnosticMessages(); |
196 | for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages) { |
197 | fprintf(stderr, "%s\n" , |
198 | qPrintable(QStringLiteral("%1:%2:%3: %4" ) |
199 | .arg(inputFile) |
200 | .arg(m.loc.startLine) |
201 | .arg(m.loc.startColumn) |
202 | .arg(m.message))); |
203 | } |
204 | return EXIT_FAILURE; |
205 | } |
206 | |
207 | const QStringList resourceFiles = parser.values(option: resourceOption); |
208 | QQmlJSResourceFileMapper mapper(resourceFiles); |
209 | const QStringList metaResourceFiles = parser.values(option: metaResourceOption); |
210 | QQmlJSResourceFileMapper metaDataMapper(metaResourceFiles); |
211 | |
212 | const auto firstQml = [](const QStringList &paths) { |
213 | auto it = std::find_if(first: paths.cbegin(), last: paths.cend(), |
214 | pred: [](const QString &x) { return x.endsWith(s: u".qml"_s ); }); |
215 | if (it == paths.cend()) |
216 | return QString(); |
217 | return *it; |
218 | }; |
219 | // verify that we can map current file to qrc (then use the qrc path later) |
220 | const QStringList paths = mapper.resourcePaths(filter: QQmlJSResourceFileMapper::localFileFilter(file: url)); |
221 | if (paths.isEmpty()) { |
222 | fprintf(stderr, format: "Failed to find a resource path for file: %s\n" , qPrintable(inputFile)); |
223 | return EXIT_FAILURE; |
224 | } else if (paths.size() > 1) { |
225 | bool good = !firstQml(paths).isEmpty(); |
226 | good &= std::any_of(first: paths.cbegin(), last: paths.cend(), |
227 | pred: [](const QString &x) { return x.endsWith(s: u".h"_s ); }); |
228 | if (!good || paths.size() > 2) { |
229 | fprintf(stderr, format: "Unexpected resource paths for file: %s\n" , qPrintable(inputFile)); |
230 | return EXIT_FAILURE; |
231 | } |
232 | } |
233 | |
234 | QmltcCompilerInfo info; |
235 | info.outputCppFile = parser.value(option: outputCppOption); |
236 | info.outputHFile = parser.value(option: outputHOption); |
237 | info.resourcePath = firstQml(paths); |
238 | info.outputNamespace = parser.value(option: namespaceOption); |
239 | info.exportMacro = parser.value(option: exportOption); |
240 | info.exportInclude = parser.value(option: exportIncludeOption); |
241 | |
242 | if (info.outputCppFile.isEmpty()) { |
243 | fprintf(stderr, format: "An output C++ file is required. Pass one using --impl" ); |
244 | return EXIT_FAILURE; |
245 | } |
246 | if (info.outputHFile.isEmpty()) { |
247 | fprintf(stderr, format: "An output C++ header file is required. Pass one using --header" ); |
248 | return EXIT_FAILURE; |
249 | } |
250 | |
251 | QQmlJSImporter importer { importPaths, &mapper }; |
252 | importer.setMetaDataMapper(&metaDataMapper); |
253 | auto createQmltcVisitor = [](const QQmlJSScope::Ptr &root, QQmlJSImporter *importer, |
254 | QQmlJSLogger *logger, const QString &implicitImportDirectory, |
255 | const QStringList &qmldirFiles) -> QQmlJSImportVisitor * { |
256 | return new QmltcVisitor(root, importer, logger, implicitImportDirectory, qmldirFiles); |
257 | }; |
258 | importer.setImportVisitorCreator(createQmltcVisitor); |
259 | |
260 | QQmlJSLogger logger; |
261 | logger.setFileName(url); |
262 | logger.setCode(sourceCode); |
263 | setupLogger(logger); |
264 | |
265 | QmltcVisitor visitor(QQmlJSScope::create(), &importer, &logger, |
266 | QQmlJSImportVisitor::implicitImportDirectory(localFile: url, mapper: &mapper), qmldirFiles); |
267 | visitor.setMode(QmltcVisitor::Compile); |
268 | QmltcTypeResolver typeResolver { &importer }; |
269 | typeResolver.init(visitor: &visitor, program: qmlParser.rootNode()); |
270 | |
271 | if (logger.hasErrors()) |
272 | return EXIT_FAILURE; |
273 | |
274 | QList<QQmlJS::DiagnosticMessage> warnings = importer.takeGlobalWarnings(); |
275 | if (!warnings.isEmpty()) { |
276 | logger.log(QStringLiteral("Type warnings occurred while compiling file:" ), id: qmlImport, |
277 | srcLocation: QQmlJS::SourceLocation()); |
278 | logger.processMessages(messages: warnings, id: qmlImport); |
279 | // Log_Import is critical for the compiler |
280 | return EXIT_FAILURE; |
281 | } |
282 | |
283 | QmltcCompiler compiler(url, &typeResolver, &visitor, &logger); |
284 | compiler.compile(info); |
285 | |
286 | if (logger.hasErrors()) |
287 | return EXIT_FAILURE; |
288 | |
289 | return EXIT_SUCCESS; |
290 | } |
291 | |