1// Copyright (C) 2019 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include <QCoreApplication>
5#include <QFile>
6#include <QTextStream>
7
8#include <QtQml/private/qqmljslexer_p.h>
9#include <QtQml/private/qqmljsparser_p.h>
10#include <QtQml/private/qqmljsengine_p.h>
11#include <QtQml/private/qqmljsastvisitor_p.h>
12#include <QtQml/private/qqmljsast_p.h>
13#include <QtQmlDom/private/qqmldomitem_p.h>
14#include <QtQmlDom/private/qqmldomexternalitems_p.h>
15#include <QtQmlDom/private/qqmldomtop_p.h>
16#include <QtQmlDom/private/qqmldomoutwriter_p.h>
17
18#if QT_CONFIG(commandlineparser)
19# include <QCommandLineParser>
20#endif
21
22#include <QtQmlToolingSettings/private/qqmltoolingsettings_p.h>
23
24
25using namespace QQmlJS::Dom;
26
27struct Options
28{
29 bool verbose = false;
30 bool inplace = false;
31 bool force = false;
32 bool tabs = false;
33 bool valid = false;
34 bool normalize = false;
35 bool ignoreSettings = false;
36 bool writeDefaultSettings = false;
37 bool objectsSpacing = false;
38 bool functionsSpacing = false;
39
40 int indentWidth = 4;
41 bool indentWidthSet = false;
42 QString newline = "native";
43
44 QStringList files;
45 QStringList arguments;
46 QStringList errors;
47};
48
49bool parseFile(const QString &filename, const Options &options)
50{
51 DomItem env =
52 DomEnvironment::create(loadPaths: QStringList(),
53 options: QQmlJS::Dom::DomEnvironment::Option::SingleThreaded
54 | QQmlJS::Dom::DomEnvironment::Option::NoDependencies);
55 DomItem tFile; // place where to store the loaded file
56 env.loadFile(
57 file: FileToLoad::fromFileSystem(environment: env.ownerAs<DomEnvironment>(), canonicalPath: filename),
58 callback: [&tFile](Path, const DomItem &, const DomItem &newIt) {
59 tFile = newIt; // callback called when everything is loaded that receives the loaded
60 // external file pair (path, oldValue, newValue)
61 },
62 loadOptions: LoadOption::DefaultLoad);
63 env.loadPendingDependencies();
64 DomItem qmlFile = tFile.fileObject();
65 std::shared_ptr<QmlFile> qmlFilePtr = qmlFile.ownerAs<QmlFile>();
66 if (!qmlFilePtr || !qmlFilePtr->isValid()) {
67 qmlFile.iterateErrors(
68 visitor: [](DomItem, ErrorMessage msg) {
69 errorToQDebug(msg);
70 return true;
71 },
72 iterate: true);
73 qWarning().noquote() << "Failed to parse" << filename;
74 return false;
75 }
76
77 // Turn AST back into source code
78 if (options.verbose)
79 qWarning().noquote() << "Dumping" << filename;
80
81 LineWriterOptions lwOptions;
82 lwOptions.formatOptions.indentSize = options.indentWidth;
83 lwOptions.formatOptions.useTabs = options.tabs;
84 lwOptions.updateOptions = LineWriterOptions::Update::None;
85 if (options.newline == "native") {
86 // find out current line endings...
87 QStringView code = qmlFilePtr->code();
88 int newlineIndex = code.indexOf(c: QChar(u'\n'));
89 int crIndex = code.indexOf(c: QChar(u'\r'));
90 if (newlineIndex >= 0) {
91 if (crIndex >= 0) {
92 if (crIndex + 1 == newlineIndex)
93 lwOptions.lineEndings = LineWriterOptions::LineEndings::Windows;
94 else
95 qWarning().noquote() << "Invalid line ending in file, using default";
96
97 } else {
98 lwOptions.lineEndings = LineWriterOptions::LineEndings::Unix;
99 }
100 } else if (crIndex >= 0) {
101 lwOptions.lineEndings = LineWriterOptions::LineEndings::OldMacOs;
102 } else {
103 qWarning().noquote() << "Unknown line ending in file, using default";
104 }
105 } else if (options.newline == "macos") {
106 lwOptions.lineEndings = LineWriterOptions::LineEndings::OldMacOs;
107 } else if (options.newline == "windows") {
108 lwOptions.lineEndings = LineWriterOptions::LineEndings::Windows;
109 } else if (options.newline == "unix") {
110 lwOptions.lineEndings = LineWriterOptions::LineEndings::Unix;
111 } else {
112 qWarning().noquote() << "Unknown line ending type" << options.newline;
113 return false;
114 }
115
116 if (options.normalize)
117 lwOptions.attributesSequence = LineWriterOptions::AttributesSequence::Normalize;
118 else
119 lwOptions.attributesSequence = LineWriterOptions::AttributesSequence::Preserve;
120 WriteOutChecks checks = WriteOutCheck::Default;
121 if (options.force || qmlFilePtr->code().size() > 32000)
122 checks = WriteOutCheck::None;
123
124 lwOptions.objectsSpacing = options.objectsSpacing;
125 lwOptions.functionsSpacing = options.functionsSpacing;
126
127 MutableDomItem res;
128 if (options.inplace) {
129 if (options.verbose)
130 qWarning().noquote() << "Writing to file" << filename;
131 FileWriter fw;
132 const unsigned numberOfBackupFiles = 0;
133 res = qmlFile.writeOut(path: filename, nBackups: numberOfBackupFiles, opt: lwOptions, fw: &fw, extraChecks: checks);
134 } else {
135 QFile out;
136 out.open(stdout, ioFlags: QIODevice::WriteOnly);
137 LineWriter lw([&out](QStringView s) { out.write(data: s.toUtf8()); }, filename, lwOptions);
138 OutWriter ow(lw);
139 res = qmlFile.writeOutForFile(ow, extraChecks: checks);
140 ow.flush();
141 }
142 return bool(res);
143}
144
145Options buildCommandLineOptions(const QCoreApplication &app)
146{
147#if QT_CONFIG(commandlineparser)
148 QCommandLineParser parser;
149 parser.setApplicationDescription("Formats QML files according to the QML Coding Conventions.");
150 parser.addHelpOption();
151 parser.addVersionOption();
152
153 parser.addOption(
154 commandLineOption: QCommandLineOption({ "V", "verbose" },
155 QStringLiteral("Verbose mode. Outputs more detailed information.")));
156
157 QCommandLineOption writeDefaultsOption(
158 QStringList() << "write-defaults",
159 QLatin1String("Writes defaults settings to .qmlformat.ini and exits (Warning: This "
160 "will overwrite any existing settings and comments!)"));
161 parser.addOption(commandLineOption: writeDefaultsOption);
162
163 QCommandLineOption ignoreSettings(QStringList() << "ignore-settings",
164 QLatin1String("Ignores all settings files and only takes "
165 "command line options into consideration"));
166 parser.addOption(commandLineOption: ignoreSettings);
167
168 parser.addOption(commandLineOption: QCommandLineOption(
169 { "i", "inplace" },
170 QStringLiteral("Edit file in-place instead of outputting to stdout.")));
171
172 parser.addOption(commandLineOption: QCommandLineOption({ "f", "force" },
173 QStringLiteral("Continue even if an error has occurred.")));
174
175 parser.addOption(
176 commandLineOption: QCommandLineOption({ "t", "tabs" }, QStringLiteral("Use tabs instead of spaces.")));
177
178 parser.addOption(commandLineOption: QCommandLineOption({ "w", "indent-width" },
179 QStringLiteral("How many spaces are used when indenting."),
180 "width", "4"));
181
182 parser.addOption(commandLineOption: QCommandLineOption({ "n", "normalize" },
183 QStringLiteral("Reorders the attributes of the objects "
184 "according to the QML Coding Guidelines.")));
185
186 parser.addOption(commandLineOption: QCommandLineOption(
187 { "F", "files" }, QStringLiteral("Format all files listed in file, in-place"), "file"));
188
189 parser.addOption(commandLineOption: QCommandLineOption(
190 { "l", "newline" },
191 QStringLiteral("Override the new line format to use (native macos unix windows)."),
192 "newline", "native"));
193
194 parser.addOption(commandLineOption: QCommandLineOption(QStringList() << "objects-spacing", QStringLiteral("Ensure spaces between objects (only works with normalize option).")));
195
196 parser.addOption(commandLineOption: QCommandLineOption(QStringList() << "functions-spacing", QStringLiteral("Ensure spaces between functions (only works with normalize option).")));
197
198 parser.addPositionalArgument(name: "filenames", description: "files to be processed by qmlformat");
199
200 parser.process(app);
201
202 if (parser.isSet(option: writeDefaultsOption)) {
203 Options options;
204 options.writeDefaultSettings = true;
205 options.valid = true;
206 return options;
207 }
208
209 bool indentWidthOkay = false;
210 const int indentWidth = parser.value(name: "indent-width").toInt(ok: &indentWidthOkay);
211 if (!indentWidthOkay) {
212 Options options;
213 options.errors.push_back(t: "Error: Invalid value passed to -w");
214 return options;
215 }
216
217 QStringList files;
218 if (!parser.value(name: "files").isEmpty()) {
219 QFile file(parser.value(name: "files"));
220 file.open(flags: QIODevice::Text | QIODevice::ReadOnly);
221 if (file.isOpen()) {
222 QTextStream in(&file);
223 while (!in.atEnd()) {
224 QString file = in.readLine();
225
226 if (file.isEmpty())
227 continue;
228
229 files.push_back(t: file);
230 }
231 }
232 }
233
234 Options options;
235 options.verbose = parser.isSet(name: "verbose");
236 options.inplace = parser.isSet(name: "inplace");
237 options.force = parser.isSet(name: "force");
238 options.tabs = parser.isSet(name: "tabs");
239 options.normalize = parser.isSet(name: "normalize");
240 options.ignoreSettings = parser.isSet(name: "ignore-settings");
241 options.objectsSpacing = parser.isSet(name: "objects-spacing");
242 options.functionsSpacing = parser.isSet(name: "functions-spacing");
243 options.valid = true;
244
245 options.indentWidth = indentWidth;
246 options.indentWidthSet = parser.isSet(name: "indent-width");
247 options.newline = parser.value(name: "newline");
248 options.files = files;
249 options.arguments = parser.positionalArguments();
250 return options;
251#else
252 return Options {};
253#endif
254}
255
256int main(int argc, char *argv[])
257{
258 QCoreApplication app(argc, argv);
259 QCoreApplication::setApplicationName("qmlformat");
260 QCoreApplication::setApplicationVersion(QT_VERSION_STR);
261
262 QQmlToolingSettings settings(QLatin1String("qmlformat"));
263
264 const QString &useTabsSetting = QStringLiteral("UseTabs");
265 settings.addOption(name: useTabsSetting);
266
267 const QString &indentWidthSetting = QStringLiteral("IndentWidth");
268 settings.addOption(name: indentWidthSetting, defaultValue: 4);
269
270 const QString &normalizeSetting = QStringLiteral("NormalizeOrder");
271 settings.addOption(name: normalizeSetting);
272
273 const QString &newlineSetting = QStringLiteral("NewlineType");
274 settings.addOption(name: newlineSetting, QStringLiteral("native"));
275
276 const QString &objectsSpacingSetting = QStringLiteral("ObjectsSpacing");
277 settings.addOption(name: objectsSpacingSetting);
278
279 const QString &functionsSpacingSetting = QStringLiteral("FunctionsSpacing");
280 settings.addOption(name: functionsSpacingSetting);
281
282 const auto options = buildCommandLineOptions(app);
283 if (!options.valid) {
284 for (const auto &error : options.errors) {
285 qWarning().noquote() << error;
286 }
287
288 return -1;
289 }
290
291 if (options.writeDefaultSettings)
292 return settings.writeDefaults() ? 0 : -1;
293
294 auto getSettings = [&](const QString &file, Options options) {
295 if (options.ignoreSettings || !settings.search(path: file))
296 return options;
297
298 Options perFileOptions = options;
299
300 // Allow for tab settings to be overwritten by the command line
301 if (!options.indentWidthSet) {
302 if (settings.isSet(name: indentWidthSetting))
303 perFileOptions.indentWidth = settings.value(name: indentWidthSetting).toInt();
304 if (settings.isSet(name: useTabsSetting))
305 perFileOptions.tabs = settings.value(name: useTabsSetting).toBool();
306 }
307
308 if (settings.isSet(name: normalizeSetting))
309 perFileOptions.normalize = settings.value(name: normalizeSetting).toBool();
310
311 if (settings.isSet(name: newlineSetting))
312 perFileOptions.newline = settings.value(name: newlineSetting).toString();
313
314 if (settings.isSet(name: objectsSpacingSetting))
315 perFileOptions.objectsSpacing = settings.value(name: objectsSpacingSetting).toBool();
316
317 if (settings.isSet(name: functionsSpacingSetting))
318 perFileOptions.functionsSpacing = settings.value(name: functionsSpacingSetting).toBool();
319
320 return perFileOptions;
321 };
322
323 bool success = true;
324 if (!options.files.isEmpty()) {
325 if (!options.arguments.isEmpty())
326 qWarning() << "Warning: Positional arguments are ignored when -F is used";
327
328 for (const QString &file : options.files) {
329 Q_ASSERT(!file.isEmpty());
330
331 if (!parseFile(filename: file, options: getSettings(file, options)))
332 success = false;
333 }
334 } else {
335 for (const QString &file : options.arguments) {
336 if (!parseFile(filename: file, options: getSettings(file, options)))
337 success = false;
338 }
339 }
340
341 return success ? 0 : 1;
342}
343

source code of qtdeclarative/tools/qmlformat/qmlformat.cpp