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

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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