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 | |
25 | using namespace QQmlJS::Dom; |
26 | |
27 | struct 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 | |
49 | bool 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 | |
145 | Options 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 | |
256 | int 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 | |