qtdeclarative/tools/qmlformat/qmlformat.cpp

347 lines
13 KiB
C++
Raw Normal View History

// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QCoreApplication>
#include <QFile>
#include <QTextStream>
#include <QtQml/private/qqmljslexer_p.h>
#include <QtQml/private/qqmljsparser_p.h>
#include <QtQml/private/qqmljsengine_p.h>
#include <QtQml/private/qqmljsastvisitor_p.h>
#include <QtQml/private/qqmljsast_p.h>
#include <QtQmlDom/private/qqmldomitem_p.h>
#include <QtQmlDom/private/qqmldomexternalitems_p.h>
#include <QtQmlDom/private/qqmldomtop_p.h>
#include <QtQmlDom/private/qqmldomoutwriter_p.h>
#if QT_CONFIG(commandlineparser)
# include <QCommandLineParser>
#endif
qmlls: move into own private static library Qmlls was completely implemented in the ./tools directory, which made its code complicated to test and try out. Also, it required some "dirty" hacks in the actual tests (including files from others targets to be able to use them) and made testing new features for qmlls more complicated. To remedy this, the qmlls code was split into a tool (qmlls) and a static library (QmlLSPrivate). The tool only contains tools/qmlls/qmllanguageservertool.cpp (which has the qmlls main method) and links to QmlLSPrivate, that contains all the other qmlls-related code. This way, the tests can also link to QmlLSPrivate and test out individual functions there without needing to include files from other targets. Also rename all the files to make syncqt happy (adding "_p" to headers and prepending "q" to headers and files and includeguards), and use QString::fromUtf8() to silence the QString()-constructor deprecation warnings. On the way, move tools/shared/qqmltoolingsettings.* into its own private static library, instead of recompiling it for each tool that requires it. Move the qqmltoolingsettings stuff into the qt namespace to be usable. Also, add qmlls as a dependency to the qmlls tests to avoid testing an outdated qmlls-binary. This commit prepares qmlls's code to implement the go-to and find-usages features. Task-number: QTBUG-109006 Change-Id: I91eed689c68a0c53fb88006de335b0f852cc1a83 Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
2023-01-24 09:45:21 +00:00
#include <QtQmlToolingSettings/private/qqmltoolingsettings_p.h>
using namespace QQmlJS::Dom;
struct Options
{
bool verbose = false;
bool inplace = false;
bool force = false;
bool tabs = false;
bool valid = false;
bool normalize = false;
bool ignoreSettings = false;
bool writeDefaultSettings = false;
bool objectsSpacing = false;
bool functionsSpacing = false;
int indentWidth = 4;
bool indentWidthSet = false;
QString newline = "native";
QStringList files;
QStringList arguments;
QStringList errors;
};
bool parseFile(const QString &filename, const Options &options)
{
auto envPtr =
DomEnvironment::create(QStringList(),
QQmlJS::Dom::DomEnvironment::Option::SingleThreaded
| QQmlJS::Dom::DomEnvironment::Option::NoDependencies);
DomItem tFile; // place where to store the loaded file
envPtr->loadFile(
FileToLoad::fromFileSystem(envPtr, filename),
[&tFile](Path, const DomItem &, const DomItem &newIt) {
tFile = newIt; // callback called when everything is loaded that receives the loaded
// external file pair (path, oldValue, newValue)
},
LoadOption::DefaultLoad);
envPtr->loadPendingDependencies();
DomItem qmlFile = tFile.fileObject();
std::shared_ptr<QmlFile> qmlFilePtr = qmlFile.ownerAs<QmlFile>();
if (!qmlFilePtr || !qmlFilePtr->isValid()) {
qmlFile.iterateErrors(
[](const DomItem &, const ErrorMessage &msg) {
errorToQDebug(msg);
return true;
},
true);
qWarning().noquote() << "Failed to parse" << filename;
return false;
}
// Turn AST back into source code
if (options.verbose)
qWarning().noquote() << "Dumping" << filename;
LineWriterOptions lwOptions;
lwOptions.formatOptions.indentSize = options.indentWidth;
lwOptions.formatOptions.useTabs = options.tabs;
lwOptions.updateOptions = LineWriterOptions::Update::None;
if (options.newline == "native") {
// find out current line endings...
QStringView code = qmlFilePtr->code();
int newlineIndex = code.indexOf(QChar(u'\n'));
int crIndex = code.indexOf(QChar(u'\r'));
if (newlineIndex >= 0) {
if (crIndex >= 0) {
if (crIndex + 1 == newlineIndex)
lwOptions.lineEndings = LineWriterOptions::LineEndings::Windows;
else
qWarning().noquote() << "Invalid line ending in file, using default";
} else {
lwOptions.lineEndings = LineWriterOptions::LineEndings::Unix;
}
} else if (crIndex >= 0) {
lwOptions.lineEndings = LineWriterOptions::LineEndings::OldMacOs;
} else {
qWarning().noquote() << "Unknown line ending in file, using default";
}
} else if (options.newline == "macos") {
lwOptions.lineEndings = LineWriterOptions::LineEndings::OldMacOs;
} else if (options.newline == "windows") {
lwOptions.lineEndings = LineWriterOptions::LineEndings::Windows;
} else if (options.newline == "unix") {
lwOptions.lineEndings = LineWriterOptions::LineEndings::Unix;
} else {
qWarning().noquote() << "Unknown line ending type" << options.newline;
return false;
}
if (options.normalize)
lwOptions.attributesSequence = LineWriterOptions::AttributesSequence::Normalize;
else
lwOptions.attributesSequence = LineWriterOptions::AttributesSequence::Preserve;
WriteOutChecks checks = WriteOutCheck::Default;
if (options.force || qmlFilePtr->code().size() > 32000)
checks = WriteOutCheck::None;
lwOptions.objectsSpacing = options.objectsSpacing;
lwOptions.functionsSpacing = options.functionsSpacing;
MutableDomItem res;
if (options.inplace) {
if (options.verbose)
qWarning().noquote() << "Writing to file" << filename;
FileWriter fw;
const unsigned numberOfBackupFiles = 0;
res = qmlFile.writeOut(filename, numberOfBackupFiles, lwOptions, &fw, checks);
} else {
QFile out;
out.open(stdout, QIODevice::WriteOnly);
LineWriter lw([&out](QStringView s) { out.write(s.toUtf8()); }, filename, lwOptions);
OutWriter ow(lw);
res = qmlFile.writeOutForFile(ow, checks);
ow.flush();
}
return bool(res);
}
Options buildCommandLineOptions(const QCoreApplication &app)
{
#if QT_CONFIG(commandlineparser)
QCommandLineParser parser;
parser.setApplicationDescription("Formats QML files according to the QML Coding Conventions.");
parser.addHelpOption();
parser.addVersionOption();
parser.addOption(
QCommandLineOption({ "V", "verbose" },
QStringLiteral("Verbose mode. Outputs more detailed information.")));
QCommandLineOption writeDefaultsOption(
QStringList() << "write-defaults",
QLatin1String("Writes defaults settings to .qmlformat.ini and exits (Warning: This "
"will overwrite any existing settings and comments!)"));
parser.addOption(writeDefaultsOption);
QCommandLineOption ignoreSettings(QStringList() << "ignore-settings",
QLatin1String("Ignores all settings files and only takes "
"command line options into consideration"));
parser.addOption(ignoreSettings);
parser.addOption(QCommandLineOption(
{ "i", "inplace" },
QStringLiteral("Edit file in-place instead of outputting to stdout.")));
parser.addOption(QCommandLineOption({ "f", "force" },
QStringLiteral("Continue even if an error has occurred.")));
parser.addOption(
QCommandLineOption({ "t", "tabs" }, QStringLiteral("Use tabs instead of spaces.")));
parser.addOption(QCommandLineOption({ "w", "indent-width" },
QStringLiteral("How many spaces are used when indenting."),
"width", "4"));
parser.addOption(QCommandLineOption({ "n", "normalize" },
QStringLiteral("Reorders the attributes of the objects "
"according to the QML Coding Guidelines.")));
parser.addOption(QCommandLineOption(
{ "F", "files" }, QStringLiteral("Format all files listed in file, in-place"), "file"));
parser.addOption(QCommandLineOption(
{ "l", "newline" },
QStringLiteral("Override the new line format to use (native macos unix windows)."),
"newline", "native"));
parser.addOption(QCommandLineOption(QStringList() << "objects-spacing", QStringLiteral("Ensure spaces between objects (only works with normalize option).")));
parser.addOption(QCommandLineOption(QStringList() << "functions-spacing", QStringLiteral("Ensure spaces between functions (only works with normalize option).")));
parser.addPositionalArgument("filenames", "files to be processed by qmlformat");
parser.process(app);
if (parser.isSet(writeDefaultsOption)) {
Options options;
options.writeDefaultSettings = true;
options.valid = true;
return options;
}
bool indentWidthOkay = false;
const int indentWidth = parser.value("indent-width").toInt(&indentWidthOkay);
if (!indentWidthOkay) {
Options options;
options.errors.push_back("Error: Invalid value passed to -w");
return options;
}
QStringList files;
if (!parser.value("files").isEmpty()) {
QFile file(parser.value("files"));
file.open(QIODevice::Text | QIODevice::ReadOnly);
if (file.isOpen()) {
QTextStream in(&file);
while (!in.atEnd()) {
QString file = in.readLine();
if (file.isEmpty())
continue;
files.push_back(file);
}
}
}
Options options;
options.verbose = parser.isSet("verbose");
options.inplace = parser.isSet("inplace");
options.force = parser.isSet("force");
options.tabs = parser.isSet("tabs");
options.normalize = parser.isSet("normalize");
options.ignoreSettings = parser.isSet("ignore-settings");
options.objectsSpacing = parser.isSet("objects-spacing");
options.functionsSpacing = parser.isSet("functions-spacing");
options.valid = true;
options.indentWidth = indentWidth;
options.indentWidthSet = parser.isSet("indent-width");
options.newline = parser.value("newline");
options.files = files;
options.arguments = parser.positionalArguments();
return options;
#else
return Options {};
#endif
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QCoreApplication::setApplicationName("qmlformat");
QCoreApplication::setApplicationVersion(QT_VERSION_STR);
QQmlToolingSettings settings(QLatin1String("qmlformat"));
const QString &useTabsSetting = QStringLiteral("UseTabs");
settings.addOption(useTabsSetting);
const QString &indentWidthSetting = QStringLiteral("IndentWidth");
settings.addOption(indentWidthSetting, 4);
const QString &normalizeSetting = QStringLiteral("NormalizeOrder");
settings.addOption(normalizeSetting);
const QString &newlineSetting = QStringLiteral("NewlineType");
settings.addOption(newlineSetting, QStringLiteral("native"));
const QString &objectsSpacingSetting = QStringLiteral("ObjectsSpacing");
settings.addOption(objectsSpacingSetting);
const QString &functionsSpacingSetting = QStringLiteral("FunctionsSpacing");
settings.addOption(functionsSpacingSetting);
const auto options = buildCommandLineOptions(app);
if (!options.valid) {
for (const auto &error : options.errors) {
qWarning().noquote() << error;
}
return -1;
}
if (options.writeDefaultSettings)
return settings.writeDefaults() ? 0 : -1;
auto getSettings = [&](const QString &file, Options options) {
// Perform formatting inplace if --files option is set.
if (!options.files.isEmpty())
options.inplace = true;
if (options.ignoreSettings || !settings.search(file))
return options;
Options perFileOptions = options;
// Allow for tab settings to be overwritten by the command line
if (!options.indentWidthSet) {
if (settings.isSet(indentWidthSetting))
perFileOptions.indentWidth = settings.value(indentWidthSetting).toInt();
if (settings.isSet(useTabsSetting))
perFileOptions.tabs = settings.value(useTabsSetting).toBool();
}
if (settings.isSet(normalizeSetting))
perFileOptions.normalize = settings.value(normalizeSetting).toBool();
if (settings.isSet(newlineSetting))
perFileOptions.newline = settings.value(newlineSetting).toString();
if (settings.isSet(objectsSpacingSetting))
perFileOptions.objectsSpacing = settings.value(objectsSpacingSetting).toBool();
if (settings.isSet(functionsSpacingSetting))
perFileOptions.functionsSpacing = settings.value(functionsSpacingSetting).toBool();
return perFileOptions;
};
bool success = true;
if (!options.files.isEmpty()) {
if (!options.arguments.isEmpty())
qWarning() << "Warning: Positional arguments are ignored when -F is used";
for (const QString &file : options.files) {
Q_ASSERT(!file.isEmpty());
if (!parseFile(file, getSettings(file, options)))
success = false;
}
} else {
for (const QString &file : options.arguments) {
if (!parseFile(file, getSettings(file, options)))
success = false;
}
}
return success ? 0 : 1;
}