qmltoolings add --dry-run option for qmlformat and qmllint

Add reportConfigForFiles to generic tooling class. It will perform the
search for the given files. Use stdout to report. qmllint had already an
option --dry-run for printing the fixed codes, but we can still allow this to
use that option name.

For qmlls, report the path if verbose option is set.

Fixes: QTBUG-137874
Change-Id: I6bd43866073b3df832b6fd89d477bced869d74c0
Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
This commit is contained in:
Semih Yavuz 2025-08-11 22:28:47 +02:00
parent 141993c0a5
commit e3891d7459
12 changed files with 146 additions and 14 deletions

View File

@ -188,6 +188,13 @@ QQmlFormatOptions QQmlFormatOptions::buildCommandLineOptions(const QStringList &
"rule"_L1, "always"_L1);
parser.addOption(semicolonRuleOption);
QCommandLineOption dryrunOption(
QStringList() << "dry-run"_L1,
QStringLiteral("Prints the settings file that would be used for this instance."
"This is useful to see what settings would be used "
"without actually performing anything."));
parser.addOption(dryrunOption);
parser.addPositionalArgument("filenames"_L1, "files to be processed by qmlformat"_L1);
parser.process(args);
@ -250,6 +257,7 @@ QQmlFormatOptions QQmlFormatOptions::buildCommandLineOptions(const QStringList &
}
}
options.setDryRun(parser.isSet(dryrunOption));
options.setIsVerbose(parser.isSet("verbose"_L1));
options.setIsInplace(parser.isSet("inplace"_L1));
options.setForceEnabled(parser.isSet("force"_L1));
@ -322,7 +330,7 @@ QQmlFormatOptions QQmlFormatOptions::optionsForFile(const QString &fileName,
if (hasFiles)
perFileOptions.setIsInplace(true);
if (!ignoreSettingsEnabled() && settings->search(fileName).isValid())
if (!ignoreSettingsEnabled() && settings->search(fileName, { m_verbose }).isValid())
perFileOptions.applySettings(*settings);
return perFileOptions;

View File

@ -126,6 +126,8 @@ public:
bool indentWidthSet() const { return m_indentWidthSet; }
void setIndentWidthSet(bool newIndentWidthSet) { m_indentWidthSet = newIndentWidthSet; }
bool dryRun() const { return m_dryRun; }
void setDryRun(bool newDryRun) { m_dryRun = newDryRun; }
QStringList errors() const { return m_errors; }
void addError(const QString &newError) { m_errors.append(newError); };
@ -171,6 +173,7 @@ private:
bool m_writeDefaultSettings = false;
bool m_indentWidthSet = false;
std::bitset<SettingsCount> m_settingBits;
bool m_dryRun = false;
};
QT_END_NAMESPACE

View File

@ -301,6 +301,7 @@ int qmllsMain(int argv, char *argc[])
if (parser.isSet(writeDefaultsOption)) {
return settings.writeDefaults() ? 0 : 1;
}
if (parser.isSet(logFileOption)) {
QString fileName = parser.value(logFileOption);
qInfo() << "will log to" << fileName;
@ -319,8 +320,6 @@ int qmllsMain(int argv, char *argc[])
logFile->flush();
});
}
if (parser.isSet(verboseOption))
QLoggingCategory::setFilterRules("qt.languageserver*.debug=true\n"_L1);
if (parser.isSet(waitOption)) {
int waitSeconds = parser.value(waitOption).toInt();
if (waitSeconds > 0)
@ -337,6 +336,10 @@ int qmllsMain(int argv, char *argc[])
},
(parser.isSet(ignoreSettings) ? nullptr : &settings));
if (parser.isSet(verboseOption)) {
QLoggingCategory::setFilterRules("qt.languageserver*.debug=true\n"_L1);
qmlServer.codeModelManager()->setVerbose(true);
}
if (parser.isSet(docDir))
qmlServer.codeModelManager()->setDocumentationRootPath(
QString::fromUtf8(parser.value(docDir).toUtf8()));

View File

@ -277,7 +277,7 @@ void QQmlCodeModel::initializeCMakeStatus(const QString &pathForSettings)
{
if (m_settings) {
const QString cmakeCalls = u"no-cmake-calls"_s;
m_settings->search(pathForSettings);
m_settings->search(pathForSettings, { m_verbose });
if (m_settings->isSet(cmakeCalls) && m_settings->value(cmakeCalls).toBool()) {
qWarning() << "Disabling CMake calls via .qmlls.ini setting.";
m_cmakeStatus = DoesNotHaveCMake;
@ -553,7 +553,8 @@ QStringList QQmlCodeModel::importPathsForUrl(const QByteArray &url)
QStringList result = importPaths();
const QString importPaths = u"importPaths"_s;
if (m_settings && m_settings->search(fileName).isValid() && m_settings->isSet(importPaths)) {
if (m_settings && m_settings->search(fileName, { m_verbose }).isValid()
&& m_settings->isSet(importPaths)) {
result.append(m_settings->value(importPaths).toString().split(QDir::listSeparator()));
}
@ -628,7 +629,7 @@ QStringList QQmlCodeModel::buildPathsForFileUrl(const QByteArray &url)
// look in the settings.
// This is the one that is passed via the .qmlls.ini file.
if (buildPaths.isEmpty() && m_settings) {
m_settings->search(path);
m_settings->search(path, { m_verbose });
QString buildDir = QStringLiteral(u"buildDir");
if (m_settings->isSet(buildDir))
buildPaths += m_settings->value(buildDir).toString().split(QDir::listSeparator(),

View File

@ -135,6 +135,9 @@ public:
QSet<QString> ignoreForWatching() const { return m_ignoreForWatching; }
HelpManager *helpManager() { return &m_helpManager; }
void setVerbose(bool verbose) { m_verbose = verbose; }
bool verbose() const { return m_verbose; }
Q_SIGNALS:
void updatedSnapshot(const QByteArray &url);
void documentationRootPathChanged(const QString &path);
@ -174,6 +177,7 @@ private:
QString m_documentationRootPath;
QSet<QString> m_ignoreForWatching;
HelpManager m_helpManager;
bool m_verbose = false;
private slots:
void onCppFileChanged(const QString &);
};

View File

@ -242,6 +242,13 @@ void QQmlCodeModelManager::setDocumentationRootPath(const QString &path)
ws.codeModel->setDocumentationRootPath(path);
}
void QQmlCodeModelManager::setVerbose(bool verbose)
{
m_verbose = verbose;
for (const auto &ws : m_workspaces)
ws.codeModel->setVerbose(verbose);
}
void QQmlCodeModelManager::setBuildPathsForRootUrl(const QByteArray &url, const QStringList &paths)
{
m_buildInformation.loadSettingsFrom(paths);

View File

@ -71,6 +71,8 @@ public:
void setDocumentationRootPath(const QString &path);
HelpManager *helpManagerForUrl(const QByteArray &);
void setVerbose(bool verbose);
protected:
using Workspaces = std::vector<QQmlWorkspace>;
using WorkspaceIterator = Workspaces::const_iterator;
@ -96,6 +98,7 @@ protected:
QStringList m_defaultImportPaths;
bool m_defaultDisableCMakeCalls = false;
QString m_defaultDocumentationRootPath;
bool m_verbose = false;
Q_SIGNALS:
void updatedSnapshot(const QByteArray &url);

View File

@ -7,6 +7,7 @@
#include <QtCore/qdir.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qset.h>
#include <QtCore/qtextstream.h>
#if QT_CONFIG(settings)
#include <QtCore/qsettings.h>
#endif
@ -161,8 +162,13 @@ QQmlToolingSettings::SearchResult QQmlToolingSettings::Searcher::search(const QS
return SearchResult();
}
QQmlToolingSettings::SearchResult QQmlToolingSettings::search(const QString &path)
QQmlToolingSettings::SearchResult QQmlToolingSettings::search(const QString &path, SearchOptions options)
{
const auto maybeReport = qScopeGuard([&]() {
if (options.verbose)
reportConfigForFiles({ path });
});
if (const SearchResult result = m_searcher.search(path); result.isValid())
return read(result.iniFilePath);
@ -184,3 +190,48 @@ bool QQmlToolingSettings::isSet(const QString &name) const
// Unset is encoded as an empty string
return !(variant.canConvert(QMetaType(QMetaType::QString)) && variant.toString().isEmpty());
}
bool QQmlToolingSettings::reportConfigForFiles(const QStringList &files)
{
constexpr int maxAllowedFileLength = 255;
constexpr int minAllowedFileLength = 40;
bool headerPrinted = false;
auto lengthForFile = [maxAllowedFileLength](const QString &file) {
return std::min(int(file.length()), maxAllowedFileLength);
};
int maxFileLength =
std::accumulate(files.begin(), files.end(), 0, [&](int acc, const QString &file) {
return std::max(acc, lengthForFile(file));
});
if (maxFileLength < minAllowedFileLength)
maxFileLength = minAllowedFileLength;
for (const auto &file : files) {
if (file.isEmpty()) {
qWarning().noquote() << "Error: Could not find file" << file;
return false;
}
QString displayFile = file;
if (displayFile.length() > maxAllowedFileLength) {
displayFile = "..." + displayFile.right(maxAllowedFileLength - 3);
}
const auto result = search(file);
if (!headerPrinted) {
QString header =
QStringLiteral("%1 | %2").arg("File", -maxFileLength).arg("Settings File");
qWarning().noquote() << header;
qWarning().noquote() << QString(header.length(), u'-');
headerPrinted = true;
}
QString line =
QStringLiteral("%1 | %2").arg(displayFile, -maxFileLength).arg(result.iniFilePath);
qWarning().noquote() << line;
}
return true;
}

View File

@ -27,6 +27,11 @@ QT_BEGIN_NAMESPACE
class QQmlToolingSettings
{
public:
QQmlToolingSettings(const QString &toolName);
struct SearchOptions
{
bool verbose;
};
struct SearchResult
{
enum class ResultType { Found, NotFound };
@ -56,16 +61,15 @@ public:
QHash<QString, QString> m_seenDirectories;
};
QQmlToolingSettings(const QString &toolName);
void addOption(const QString &name, const QVariant defaultValue = QVariant());
SearchResult search(const QString &path, SearchOptions options = {});
bool writeDefaults() const;
SearchResult search(const QString &path);
QVariant value(const QString &name) const;
bool isSet(const QString &name) const;
bool reportConfigForFiles(const QStringList &files);
private:
QString m_currentSettingsPath;
QVariantHash m_values;
@ -91,10 +95,10 @@ public:
return QQmlToolingSettings::writeDefaults();
}
SearchResult search(const QString &path)
SearchResult search(const QString &path, SearchOptions options = {})
{
QMutexLocker lock(&m_mutex);
return QQmlToolingSettings::search(path);
return QQmlToolingSettings::search(path, options);
}
QVariant value(const QString &name) const

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtTest/QTest>
#include <QtLogging>
#include <QtQmlToolingSettings/private/qqmltoolingsettings_p.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>
@ -16,6 +17,9 @@ public:
private Q_SLOTS:
void searchConfig_data();
void searchConfig();
void reportConfigForFiles_data();
void reportConfigForFiles();
};
tst_qmltoolingsettings::tst_qmltoolingsettings() : QQmlDataTest(QT_QMLTEST_DATADIR) { }
@ -62,5 +66,40 @@ void tst_qmltoolingsettings::searchConfig()
QCOMPARE(actualResult.iniFilePath, expectedResult.iniFilePath);
}
void tst_qmltoolingsettings::reportConfigForFiles_data()
{
QTest::addColumn<QStringList>("files");
QTest::addColumn<QString>("expectedResultCapture"); // string captured from output
QStringList files = { testFile("B/B1/test.qml"), testFile("B/B2/test.qml") };
QTest::newRow("validFiles") << files << "B/B2/test.qml";
}
void tst_qmltoolingsettings::reportConfigForFiles()
{
QFETCH(QStringList, files);
QFETCH(QString, expectedResultCapture);
static QString out;
QtMessageHandler handler([](QtMsgType type, const QMessageLogContext &, const QString &msg) {
if (type == QtWarningMsg) {
QTextStream stream(&out);
stream << msg << Qt::endl;
}
});
const auto oldMessageHandler = qInstallMessageHandler(handler);
const auto guard =
qScopeGuard([&oldMessageHandler]() { qInstallMessageHandler(oldMessageHandler); });
QQmlToolingSettings settings("qmlformat");
settings.reportConfigForFiles(files);
QVERIFY(out.contains("File"));
QVERIFY(out.contains("Settings File"));
QVERIFY(out.contains(expectedResultCapture));
}
QTEST_MAIN(tst_qmltoolingsettings)
#include "tst_qmltoolingsettings.moc"

View File

@ -136,6 +136,11 @@ int main(int argc, char *argv[])
return -1;
}
if (options.dryRun()) {
settings.reportConfigForFiles(options.arguments());
return 0;
}
if (options.writeDefaultSettingsEnabled())
return settings.writeDefaults() ? 0 : -1;

View File

@ -165,7 +165,8 @@ All warnings can be set to three levels:
QCommandLineOption dryRun(QStringList() << "dry-run",
QLatin1String("Only print out the contents of the file after fix "
"suggestions without applying them"));
"suggestions without applying them. Also prints the "
"settings file that would be used for this instance."));
parser.addOption(dryRun);
QCommandLineOption listPluginsOption(QStringList() << "list-plugins",
@ -333,6 +334,9 @@ All warnings can be set to three levels:
parser.showHelp(-1);
}
if (parser.isSet(dryRun))
settings.reportConfigForFiles(positionalArguments);
QJsonArray jsonFiles;
for (const QString &filename : positionalArguments) {