1// Copyright (C) 2016 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Sergio Martins <sergio.martins@kdab.com>
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include <QtQmlToolingSettings/private/qqmltoolingsettings_p.h>
5#include <QtQmlToolingSettings/private/qqmltoolingutils_p.h>
6
7#include <QtQmlCompiler/private/qqmljscompiler_p.h>
8#include <QtQmlCompiler/private/qqmljslinter_p.h>
9#include <QtQmlCompiler/private/qqmljsloggingutils_p.h>
10#include <QtQmlCompiler/private/qqmljsresourcefilemapper_p.h>
11#include <QtQmlCompiler/private/qqmljsutils_p.h>
12
13#include <QtCore/qdebug.h>
14#include <QtCore/qfile.h>
15#include <QtCore/qfileinfo.h>
16#include <QtCore/qcoreapplication.h>
17#include <QtCore/qdiriterator.h>
18#include <QtCore/qjsonobject.h>
19#include <QtCore/qjsonarray.h>
20#include <QtCore/qjsondocument.h>
21#include <QtCore/qscopeguard.h>
22
23#if QT_CONFIG(commandlineparser)
24#include <QtCore/qcommandlineparser.h>
25#endif
26
27#include <QtCore/qlibraryinfo.h>
28
29#include <cstdio>
30
31using namespace Qt::StringLiterals;
32
33constexpr int JSON_LOGGING_FORMAT_REVISION = 4;
34
35bool argumentsFromCommandLineAndFile(QStringList& allArguments, const QStringList &arguments)
36{
37 allArguments.reserve(asize: arguments.size());
38 for (const QString &argument : arguments) {
39 // "@file" doesn't start with a '-' so we can't use QCommandLineParser for it
40 if (argument.startsWith(c: u'@')) {
41 QString optionsFile = argument;
42 optionsFile.remove(i: 0, len: 1);
43 if (optionsFile.isEmpty()) {
44 qWarning().nospace() << "The @ option requires an input file";
45 return false;
46 }
47 QFile f(optionsFile);
48 if (!f.open(flags: QIODevice::ReadOnly | QIODevice::Text)) {
49 qWarning().nospace() << "Cannot open options file specified with @";
50 return false;
51 }
52 while (!f.atEnd()) {
53 QString line = QString::fromLocal8Bit(ba: f.readLine().trimmed());
54 if (!line.isEmpty())
55 allArguments << line;
56 }
57 } else {
58 allArguments << argument;
59 }
60 }
61 return true;
62}
63
64int main(int argc, char *argv[])
65{
66 QHashSeed::setDeterministicGlobalSeed();
67 QList<QQmlJS::LoggerCategory> categories;
68
69 QCoreApplication app(argc, argv);
70 QCoreApplication::setApplicationName("qmllint");
71 QCoreApplication::setApplicationVersion(QT_VERSION_STR);
72 QCommandLineParser parser;
73 QQmlToolingSettings settings(QLatin1String("qmllint"));
74 parser.setApplicationDescription(QLatin1String(R"(QML syntax verifier and analyzer
75
76All warnings can be set to three levels:
77 disable - Fully disables the warning.
78 info - Displays the warning but does not influence the return code.
79 warning - Displays the warning and leads to a non-zero exit code if more warnings than max-warnings occur.
80 error - Displays the warning as error and leads to a non-zero exit code if encountered.
81)"));
82
83 parser.addHelpOption();
84 parser.addVersionOption();
85
86 QCommandLineOption silentOption(QStringList() << "s" << "silent",
87 QLatin1String("Don't output syntax errors"));
88 parser.addOption(commandLineOption: silentOption);
89
90 QCommandLineOption jsonOption(QStringList() << "json",
91 QLatin1String("Write output as JSON to file (or use the special "
92 "filename '-' to write to stdout)"),
93 QLatin1String("file"), QString());
94 parser.addOption(commandLineOption: jsonOption);
95
96 QCommandLineOption writeDefaultsOption(
97 QStringList() << "write-defaults",
98 QLatin1String("Writes defaults settings to .qmllint.ini and exits (Warning: This "
99 "will overwrite any existing settings and comments!)"));
100 parser.addOption(commandLineOption: writeDefaultsOption);
101
102 QCommandLineOption ignoreSettings(QStringList() << "ignore-settings",
103 QLatin1String("Ignores all settings files and only takes "
104 "command line options into consideration"));
105 parser.addOption(commandLineOption: ignoreSettings);
106
107 QCommandLineOption moduleOption({ QStringLiteral("M"), QStringLiteral("module") },
108 QStringLiteral("Lint modules instead of files"));
109 parser.addOption(commandLineOption: moduleOption);
110
111 QCommandLineOption resourceOption(
112 { QStringLiteral("resource") },
113 QStringLiteral("Look for related files in the given resource file"),
114 QStringLiteral("resource"));
115 parser.addOption(commandLineOption: resourceOption);
116 const QString &resourceSetting = QLatin1String("ResourcePath");
117 settings.addOption(name: resourceSetting);
118
119 QCommandLineOption qmlImportPathsOption(
120 QStringList() << "I"
121 << "qmldirs",
122 QLatin1String("Look for QML modules in specified directory"),
123 QLatin1String("directory"));
124 parser.addOption(commandLineOption: qmlImportPathsOption);
125 const QString qmlImportPathsSetting = QLatin1String("AdditionalQmlImportPaths");
126 settings.addOption(name: qmlImportPathsSetting);
127
128 QCommandLineOption environmentOption(
129 QStringList() << "E",
130 QLatin1String("Use the QML_IMPORT_PATH environment variable to look for QML Modules"));
131 parser.addOption(commandLineOption: environmentOption);
132
133 QCommandLineOption qmlImportNoDefault(
134 QStringList() << "bare",
135 QLatin1String("Do not include default import directories or the current directory. "
136 "This may be used to run qmllint on a project using a different Qt version."));
137 parser.addOption(commandLineOption: qmlImportNoDefault);
138 const QString qmlImportNoDefaultSetting = QLatin1String("DisableDefaultImports");
139 settings.addOption(name: qmlImportNoDefaultSetting, defaultValue: false);
140
141 QCommandLineOption qmldirFilesOption(
142 QStringList() << "i"
143 << "qmltypes",
144 QLatin1String("Import the specified qmldir files. By default, the qmldir file found "
145 "in the current directory is used if present. If no qmldir file is found,"
146 "but qmltypes files are, those are imported instead. When this option is "
147 "set, you have to explicitly add the qmldir or any qmltypes files in the "
148 "current directory if you want it to be used. Importing qmltypes files "
149 "without their corresponding qmldir file is inadvisable."),
150 QLatin1String("qmldirs"));
151 parser.addOption(commandLineOption: qmldirFilesOption);
152 const QString qmldirFilesSetting = QLatin1String("OverwriteImportTypes");
153 settings.addOption(name: qmldirFilesSetting);
154
155 QCommandLineOption absolutePath(
156 QStringList() << "absolute-path",
157 QLatin1String("Use absolute paths for logging instead of relative ones."));
158 absolutePath.setFlags(QCommandLineOption::HiddenFromHelp);
159 parser.addOption(commandLineOption: absolutePath);
160
161 QCommandLineOption fixFile(QStringList() << "f"
162 << "fix",
163 QLatin1String("Automatically apply fix suggestions"));
164 parser.addOption(commandLineOption: fixFile);
165
166 QCommandLineOption dryRun(QStringList() << "dry-run",
167 QLatin1String("Only print out the contents of the file after fix "
168 "suggestions without applying them"));
169 parser.addOption(commandLineOption: dryRun);
170
171 QCommandLineOption listPluginsOption(QStringList() << "list-plugins",
172 QLatin1String("List all available plugins"));
173 parser.addOption(commandLineOption: listPluginsOption);
174
175 QCommandLineOption pluginsDisable(
176 QStringList() << "D"
177 << "disable-plugins",
178 QLatin1String("List of qmllint plugins to disable (all to disable all plugins)"),
179 QLatin1String("plugins"));
180 parser.addOption(commandLineOption: pluginsDisable);
181 const QString pluginsDisableSetting = QLatin1String("DisablePlugins");
182 settings.addOption(name: pluginsDisableSetting);
183
184 QCommandLineOption pluginPathsOption(
185 QStringList() << "P"
186 << "plugin-paths",
187 QLatin1String("Look for qmllint plugins in specified directory"),
188 QLatin1String("directory"));
189 parser.addOption(commandLineOption: pluginPathsOption);
190
191 QCommandLineOption maxWarnings(
192 QStringList() << "W"
193 << "max-warnings",
194 QLatin1String("Exit with an error code if more than \"count\" many"
195 "warnings are found by qmllint. By default or if \"count\" "
196 "is -1, warnings do not cause qmllint "
197 "to return with an error exit code."),
198 "count"
199 );
200 parser.addOption(commandLineOption: maxWarnings);
201 const QString maxWarningsSetting = QLatin1String("MaxWarnings");
202 settings.addOption(name: maxWarningsSetting, defaultValue: -1);
203
204 // QTBUG-135020: don't break existing user configs and still accept PropertyAliasCycles
205 settings.addOption(name: "PropertyAliasCycles"_L1);
206
207 auto addCategory = [&](const QQmlJS::LoggerCategory &category) {
208 categories.push_back(t: category);
209 if (category.isDefault())
210 return;
211 QCommandLineOption option(
212 category.id().name().toString(),
213 category.description()
214 + QStringLiteral(" (default: %1)")
215 .arg(a: QQmlJS::LoggingUtils::levelToString(category)),
216 QStringLiteral("level"), QQmlJS::LoggingUtils::levelToString(category));
217 if (category.isIgnored())
218 option.setFlags(QCommandLineOption::HiddenFromHelp);
219 parser.addOption(commandLineOption: option);
220 settings.addOption(QStringLiteral("Warnings/") + category.settingsName(),
221 defaultValue: QQmlJS::LoggingUtils::levelToString(category));
222 };
223
224 for (const auto &category : QQmlJSLogger::defaultCategories()) {
225 addCategory(category);
226 }
227
228 parser.addPositionalArgument(name: QLatin1String("files"),
229 description: QLatin1String("list of qml or js files to verify"));
230
231 QStringList arguments;
232 if (!argumentsFromCommandLineAndFile(allArguments&: arguments, arguments: app.arguments())) {
233 // argumentsFromCommandLine already printed any necessary warnings.
234 return 1;
235 }
236
237 parser.parse(arguments); // parse but ignore unknown options temporarily: plugins might add some
238 // later
239
240 // Since we can't use QCommandLineParser::process(), we need to handle version and help manually
241 if (parser.isSet(name: "version"))
242 parser.showVersion();
243
244 auto updateLogLevels = [&]() {
245 QQmlJS::LoggingUtils::updateLogLevels(categories, settings, parser: &parser);
246 };
247
248 bool silent = parser.isSet(option: silentOption);
249 bool useAbsolutePath = parser.isSet(option: absolutePath);
250 bool useJson = parser.isSet(option: jsonOption);
251
252 // use host qml import path as a sane default if not explicitly disabled
253 QStringList defaultImportPaths = { QDir::currentPath() };
254
255 if (parser.isSet(option: resourceOption)) {
256 defaultImportPaths.append(t: QLatin1String(":/qt-project.org/imports"));
257 defaultImportPaths.append(t: QLatin1String(":/qt/qml"));
258 };
259
260 defaultImportPaths.append(t: QLibraryInfo::path(p: QLibraryInfo::QmlImportsPath));
261
262 QStringList qmlImportPaths =
263 parser.isSet(option: qmlImportNoDefault) ? QStringList {} : defaultImportPaths;
264
265 QStringList defaultQmldirFiles;
266 if (parser.isSet(option: qmldirFilesOption)) {
267 defaultQmldirFiles = QQmlJSUtils::cleanPaths(paths: parser.values(option: qmldirFilesOption));
268 } else if (!parser.isSet(option: qmlImportNoDefault)){
269 // If nothing given explicitly, use the qmldir file from the current directory.
270 QFileInfo qmldirFile(QStringLiteral("qmldir"));
271 if (qmldirFile.isFile()) {
272 defaultQmldirFiles.append(t: qmldirFile.absoluteFilePath());
273 } else {
274 // If no qmldir file is found, use the qmltypes files
275 // from the current directory for backwards compatibility.
276 QDirIterator it(".", {"*.qmltypes"}, QDir::Files);
277 while (it.hasNext()) {
278 it.next();
279 defaultQmldirFiles.append(t: it.fileInfo().absoluteFilePath());
280 }
281 }
282 }
283 QStringList qmldirFiles = defaultQmldirFiles;
284
285 const QStringList defaultResourceFiles =
286 parser.isSet(option: resourceOption) ? parser.values(option: resourceOption) : QStringList {};
287 QStringList resourceFiles = defaultResourceFiles;
288
289 bool success = true;
290
291 QStringList pluginPaths;
292
293 if (parser.isSet(option: pluginPathsOption))
294 pluginPaths << parser.values(option: pluginPathsOption);
295
296 QQmlJSLinter linter(qmlImportPaths, pluginPaths, useAbsolutePath);
297
298 for (const QQmlJSLinter::Plugin &plugin : linter.plugins()) {
299 for (const QQmlJS::LoggerCategory &category : plugin.categories())
300 addCategory(category);
301 }
302
303 if (parser.isSet(option: writeDefaultsOption)) {
304 return settings.writeDefaults() ? 0 : 1;
305 }
306
307 if (parser.isSet(name: "help") || parser.isSet(name: "help-all"))
308 parser.showHelp(exitCode: 0);
309
310 if (!parser.unknownOptionNames().isEmpty())
311 parser.process(app);
312
313 updateLogLevels();
314
315 if (parser.isSet(option: listPluginsOption)) {
316 const std::vector<QQmlJSLinter::Plugin> &plugins = linter.plugins();
317 if (!plugins.empty()) {
318 qInfo().nospace().noquote() << "Plugin\t\t\tBuilt-in?\tVersion\tAuthor\t\tDescription";
319 for (const QQmlJSLinter::Plugin &plugin : plugins) {
320 qInfo().nospace().noquote()
321 << plugin.name() << "\t\t\t" << (plugin.isBuiltin() ? "Yes" : "No")
322 << "\t\t" << plugin.version() << "\t" << plugin.author() << "\t\t"
323 << plugin.description();
324 }
325 } else {
326 qWarning() << "No plugins installed.";
327 }
328 return 0;
329 }
330
331 const auto positionalArguments = parser.positionalArguments();
332 if (positionalArguments.isEmpty()) {
333 parser.showHelp(exitCode: -1);
334 }
335
336 QJsonArray jsonFiles;
337
338 for (const QString &filename : positionalArguments) {
339 if (!parser.isSet(option: ignoreSettings))
340 settings.search(path: filename);
341 updateLogLevels();
342
343 const QDir fileDir = QFileInfo(filename).absoluteDir();
344 auto addAbsolutePaths = [&](QStringList &list, const QStringList &entries) {
345 for (const QString &file : entries)
346 list << (QFileInfo(file).isAbsolute() ? file : fileDir.filePath(fileName: file));
347 };
348
349 resourceFiles = defaultResourceFiles;
350
351 addAbsolutePaths(resourceFiles, settings.value(name: resourceSetting).toStringList());
352
353 qmldirFiles = defaultQmldirFiles;
354 if (settings.isSet(name: qmldirFilesSetting)
355 && !settings.value(name: qmldirFilesSetting).toStringList().isEmpty()) {
356 qmldirFiles = {};
357 addAbsolutePaths(qmldirFiles, settings.value(name: qmldirFilesSetting).toStringList());
358 }
359
360 if (parser.isSet(option: qmlImportNoDefault)
361 || (settings.isSet(name: qmlImportNoDefaultSetting)
362 && settings.value(name: qmlImportNoDefaultSetting).toBool())) {
363 qmlImportPaths = {};
364 } else {
365 qmlImportPaths = defaultImportPaths;
366 }
367
368 if (parser.isSet(option: qmlImportPathsOption))
369 qmlImportPaths << parser.values(option: qmlImportPathsOption);
370 if (parser.isSet(option: environmentOption)) {
371 if (silent) {
372 qmlImportPaths << qEnvironmentVariable(varName: "QML_IMPORT_PATH")
373 .split(sep: QDir::separator(), behavior: Qt::SkipEmptyParts)
374 << qEnvironmentVariable(varName: "QML2_IMPORT_PATH")
375 .split(sep: QDir::separator(), behavior: Qt::SkipEmptyParts);
376 } else {
377 if (const QStringList dirsFromEnv =
378 QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(environmentVariableName: u"QML_IMPORT_PATH"_s);
379 !dirsFromEnv.isEmpty()) {
380 qInfo().nospace().noquote()
381 << "Using import directories passed from environment variable "
382 "\"QML_IMPORT_PATH\": \""
383 << dirsFromEnv.join(sep: u"\", \""_s) << "\".";
384 qmlImportPaths << dirsFromEnv;
385 }
386 if (const QStringList dirsFromEnv =
387 QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(
388 environmentVariableName: u"QML2_IMPORT_PATH"_s);
389 !dirsFromEnv.isEmpty()) {
390 qInfo().nospace().noquote() << "Using import directories passed from the "
391 "deprecated environment variable "
392 "\"QML2_IMPORT_PATH\": \""
393 << dirsFromEnv.join(sep: u"\", \""_s) << "\".";
394 qmlImportPaths << dirsFromEnv;
395 }
396 }
397 }
398
399 addAbsolutePaths(qmlImportPaths, settings.value(name: qmlImportPathsSetting).toStringList());
400
401 QSet<QString> disabledPlugins;
402
403 if (parser.isSet(option: pluginsDisable)) {
404 for (const QString &plugin : parser.values(option: pluginsDisable))
405 disabledPlugins << plugin.toLower();
406 }
407
408 if (settings.isSet(name: pluginsDisableSetting)) {
409 for (const QString &plugin : settings.value(name: pluginsDisableSetting).toStringList())
410 disabledPlugins << plugin.toLower();
411 }
412
413 linter.setPluginsEnabled(!disabledPlugins.contains(value: "all"));
414
415 if (!linter.pluginsEnabled())
416 continue;
417
418 auto &plugins = linter.plugins();
419
420 for (auto &plugin : plugins)
421 plugin.setEnabled(!disabledPlugins.contains(value: plugin.name().toLower()));
422
423 const bool isFixing = parser.isSet(option: fixFile);
424
425 QQmlJSLinter::LintResult lintResult;
426
427 if (parser.isSet(option: moduleOption)) {
428 lintResult = linter.lintModule(uri: filename, silent, json: useJson ? &jsonFiles : nullptr,
429 qmlImportPaths, resourceFiles);
430 } else {
431 // TODO: collect root urls here
432 const QQmlJS::ContextProperties contextProperties;
433 lintResult = linter.lintFile(filename, fileContents: nullptr, silent: silent || isFixing,
434 json: useJson ? &jsonFiles : nullptr, qmlImportPaths,
435 qmldirFiles, resourceFiles, categories, contextProperties);
436 }
437 success &= (lintResult == QQmlJSLinter::LintSuccess || lintResult == QQmlJSLinter::HasWarnings);
438 if (success) {
439 const qsizetype value = parser.isSet(option: maxWarnings)
440 ? parser.value(option: maxWarnings).toInt()
441 : settings.value(name: maxWarningsSetting).toInt();
442 if (value != -1 && value < linter.logger()->numWarnings())
443 success = false;
444 }
445
446 if (isFixing) {
447 if (lintResult != QQmlJSLinter::LintSuccess && lintResult != QQmlJSLinter::HasWarnings)
448 continue;
449
450 QString fixedCode;
451 const QQmlJSLinter::FixResult result = linter.applyFixes(fixedCode: &fixedCode, silent);
452
453 if (result != QQmlJSLinter::NothingToFix && result != QQmlJSLinter::FixSuccess) {
454 success = false;
455 continue;
456 }
457
458 if (parser.isSet(option: dryRun)) {
459 QTextStream(stdout) << fixedCode;
460 } else {
461 if (result == QQmlJSLinter::NothingToFix) {
462 if (!silent)
463 qWarning().nospace() << "Nothing to fix in " << filename;
464 continue;
465 }
466
467 const QString backupFile = filename + u".bak"_s;
468 if (QFile::exists(fileName: backupFile) && !QFile::remove(fileName: backupFile)) {
469 if (!silent) {
470 qWarning().nospace() << "Failed to remove old backup file " << backupFile
471 << ", aborting";
472 }
473 success = false;
474 continue;
475 }
476 if (!QFile::copy(fileName: filename, newName: backupFile)) {
477 if (!silent) {
478 qWarning().nospace()
479 << "Failed to create backup file " << backupFile << ", aborting";
480 }
481 success = false;
482 continue;
483 }
484
485 QFile file(filename);
486 if (!file.open(flags: QIODevice::WriteOnly)) {
487 if (!silent) {
488 qWarning().nospace() << "Failed to open " << filename
489 << " for writing:" << file.errorString();
490 }
491 success = false;
492 continue;
493 }
494
495 const QByteArray data = fixedCode.toUtf8();
496 if (file.write(data) != data.size()) {
497 if (!silent) {
498 qWarning().nospace() << "Failed to write new contents to " << filename
499 << ": " << file.errorString();
500 }
501 success = false;
502 continue;
503 }
504 if (!silent) {
505 qDebug().nospace() << "Applied fixes to " << filename << ". Backup created at "
506 << backupFile;
507 }
508 }
509 }
510 }
511
512 if (useJson) {
513 QJsonObject result;
514
515 result[u"revision"_s] = JSON_LOGGING_FORMAT_REVISION;
516 result[u"files"_s] = jsonFiles;
517
518 QString fileName = parser.value(option: jsonOption);
519
520 const QByteArray json = QJsonDocument(result).toJson(format: QJsonDocument::Compact);
521
522 if (fileName == u"-") {
523 QTextStream(stdout) << QString::fromUtf8(ba: json);
524 } else {
525 QFile file(fileName);
526 if (file.open(flags: QFile::WriteOnly))
527 file.write(data: json);
528 else
529 success = false;
530 }
531 }
532
533 return success ? 0 : -1;
534}
535

source code of qtdeclarative/tools/qmllint/main.cpp