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

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