1// Copyright (C) 2020 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 <QtCore/qdebug.h>
5#include <QtCore/qfile.h>
6#include <QtCore/qfileinfo.h>
7#include <QtCore/qcoreapplication.h>
8#include <QtCore/qdir.h>
9#include <QtCore/QTextStream>
10#include <QtCore/QThread>
11
12#include <QtQmlDom/private/qqmldomtop_p.h>
13#include <QtQmlDom/private/qqmldomfilewriter_p.h>
14#include <QtQmlDom/private/qqmldomoutwriter_p.h>
15#include <QtQmlDom/private/qqmldomelements_p.h>
16#include <QtQmlDom/private/qqmldomfieldfilter_p.h>
17#include <QtQmlDom/private/qqmldomastdumper_p.h>
18
19#include <cstdio>
20#include <optional>
21
22#if QT_CONFIG(commandlineparser)
23# include <QtCore/qcommandlineparser.h>
24#endif
25
26#include <QtCore/qlibraryinfo.h>
27using namespace QQmlJS::Dom;
28
29namespace tt {
30Q_NAMESPACE
31
32enum class Dependencies { None, Required };
33Q_ENUM_NS(Dependencies);
34
35};
36using namespace tt;
37
38int main(int argc, char *argv[])
39{
40 FieldFilter filter = FieldFilter::defaultFilter();
41 QCoreApplication a(argc, argv);
42 QCoreApplication::setApplicationName("qmldom");
43 QCoreApplication::setApplicationVersion("1.0");
44#if QT_CONFIG(commandlineparser)
45 QCommandLineParser parser;
46 parser.setApplicationDescription(QLatin1String("QML dom tool"));
47 parser.addHelpOption();
48 parser.addVersionOption();
49
50 QCommandLineOption dumpOption(QStringList() << "d"
51 << "dump",
52 QLatin1String("Dumps the code model"));
53 parser.addOption(commandLineOption: dumpOption);
54 QCommandLineOption reformatOption(QStringList() << "r"
55 << "reformat",
56 QLatin1String("reformats the files explicitly passed in"));
57 parser.addOption(commandLineOption: reformatOption);
58
59 QCommandLineOption filterOption(
60 QStringList() << "f"
61 << "filter-fields",
62 QLatin1String("commas separated list of fields to filter out. Prepending a field with "
63 "'-' skips the field, with '+' it adds it. The field might be prepended "
64 "by '<type>:' to apply only to elements of that type"
65 "The default filters are ")
66 + filter.describeFieldsFilter(),
67 QLatin1String("fields"));
68 parser.addOption(commandLineOption: filterOption);
69
70 QCommandLineOption qmltypesDirsOption(
71 QStringList() << "I"
72 << "qmldirs",
73 QLatin1String("Look for qmltypes files in specified directory"),
74 QLatin1String("directory"));
75 parser.addOption(commandLineOption: qmltypesDirsOption);
76
77 QCommandLineOption qmltypesFilesOption(QStringList() << "i"
78 << "qmltypes",
79 QLatin1String("Include the specified qmltypes files"),
80 QLatin1String("qmltypes"));
81 parser.addOption(commandLineOption: qmltypesFilesOption);
82
83 QCommandLineOption pathToDumpOption(
84 QStringList() << "path-to-dump",
85 QLatin1String("adds a path to dump. By default the base path of each file is dumped. "
86 "If any path starts with $ ($env for example) then the environment (and "
87 "not the loaded files) is used as basis."),
88 QLatin1String("pathToDump"));
89 parser.addOption(commandLineOption: pathToDumpOption);
90
91 QCommandLineOption dependenciesOption(
92 QStringList() << "D"
93 << "dependencies",
94 QLatin1String("Dependencies to load: none, required, reachable"),
95 QLatin1String("dependenciesToLoad"), QLatin1String("none"));
96 parser.addOption(commandLineOption: dependenciesOption);
97
98 QCommandLineOption reformatDirOption(
99 QStringList() << "reformat-dir",
100 QLatin1String(
101 "Target directory for the reformatted files, "
102 "if not given the files are reformatted in place (but backup files are kept)"),
103 QLatin1String("reformatDir"));
104 parser.addOption(commandLineOption: reformatDirOption);
105
106 QCommandLineOption nBackupsOption(
107 QStringList() << "backups",
108 QLatin1String("Number of backup files to generate (default is 2, the oldest, "
109 "and the last version are kept), "),
110 QLatin1String("nBackups"));
111 parser.addOption(commandLineOption: nBackupsOption);
112
113 QCommandLineOption dumpAstOption(QStringList() << "dump-ast",
114 QLatin1String("Dumps the AST of the given QML file."));
115 parser.addOption(commandLineOption: dumpAstOption);
116
117 parser.addPositionalArgument(name: QLatin1String("files"),
118 description: QLatin1String("list of qml or js files to verify"));
119
120 parser.process(app: a);
121
122 const auto positionalArguments = parser.positionalArguments();
123 if (positionalArguments.isEmpty()) {
124 parser.showHelp(exitCode: -1);
125 }
126
127 if (parser.isSet(option: filterOption)) {
128 qDebug() << "filters: " << parser.values(option: filterOption);
129 for (const QString &fFields : parser.values(option: filterOption)) {
130 if (!filter.addFilter(f: fFields)) {
131 return 1;
132 }
133 }
134 filter.setFiltred();
135 }
136
137 std::optional<DomType> fileType;
138 if (parser.isSet(option: reformatOption))
139 fileType = DomType::QmlFile;
140
141 Dependencies dep = Dependencies::None;
142 for (const QString &depName : parser.values(option: dependenciesOption)) {
143 QMetaEnum metaEnum = QMetaEnum::fromType<Dependencies>();
144 bool found = false;
145 for (int i = 0; i < metaEnum.keyCount(); ++i) {
146 if (QLatin1String(metaEnum.key(index: i)).compare(other: depName, cs: Qt::CaseInsensitive) == 0) {
147 found = true;
148 dep = Dependencies(metaEnum.value(index: i));
149 }
150 }
151 if (!found) {
152 QStringList values;
153 for (int i = 0; i < metaEnum.keyCount(); ++i)
154 values.append(t: QString::fromUtf8(utf8: metaEnum.key(index: i)).toLower());
155 qDebug().noquote() << "Invalid dependencies argument, expected one of "
156 << values.join(sep: QLatin1Char(','));
157 return 1;
158 }
159 }
160
161 int nBackups = 2;
162 if (parser.isSet(option: nBackupsOption)) {
163 bool intOk;
164 nBackups = parser.value(option: nBackupsOption).toInt(ok: &intOk);
165 if (!intOk) {
166 qDebug() << "expected an integer giving the number of backups after --backups, not "
167 << parser.value(option: nBackupsOption);
168 }
169 }
170
171 QList<Path> pathsToDump;
172 for (const QString &pStr : parser.values(option: pathToDumpOption)) {
173 pathsToDump.append(t: Path::fromString(s: pStr));
174 }
175 if (pathsToDump.isEmpty())
176 pathsToDump.append(t: Path());
177
178 // use host qml import path as a sane default if nothing else has been provided
179 QStringList qmltypeDirs = parser.isSet(option: qmltypesDirsOption)
180 ? parser.values(option: qmltypesDirsOption)
181 : QStringList { QLibraryInfo::path(p: QLibraryInfo::Qml2ImportsPath) };
182
183 if (!parser.isSet(option: qmltypesFilesOption))
184 qmltypeDirs << ".";
185
186 QStringList qmltypeFiles =
187 parser.isSet(option: qmltypesFilesOption) ? parser.values(option: qmltypesFilesOption) : QStringList {};
188#else
189 QStringList qmltypeDirs {};
190 QStringList qmltypeFiles {};
191#endif
192
193 {
194 QDebug dbg = qDebug();
195 dbg << "dirs:\n";
196 for (const QString &d : std::as_const(t&: qmltypeDirs))
197 dbg << " '" << d << "'\n";
198 dbg << "files:\n";
199 for (const QString &f : std::as_const(t: positionalArguments))
200 dbg << " '" << f << "'\n";
201 dbg << "fieldFilter: " << filter.describeFieldsFilter();
202 dbg << "\n";
203 }
204 DomEnvironment::Options options = DomEnvironment::Option::SingleThreaded;
205 if (dep == Dependencies::None)
206 options = options | DomEnvironment::Option::NoDependencies;
207 std::shared_ptr<DomEnvironment> envPtr(new DomEnvironment(qmltypeDirs, options));
208 DomItem env(envPtr);
209 qDebug() << "will load\n";
210 if (dep != Dependencies::None)
211 env.loadBuiltins();
212 QList<DomItem> loadedFiles(positionalArguments.size());
213 qsizetype iPos = 0;
214 for (const QString &s : std::as_const(t: positionalArguments)) {
215 env.loadFile(
216 file: FileToLoad::fromFileSystem(environment: env.ownerAs<DomEnvironment>(), canonicalPath: s),
217 callback: [&loadedFiles, iPos](Path, const DomItem &, const DomItem &newIt) {
218 loadedFiles[iPos] = newIt;
219 },
220 loadOptions: LoadOption::DefaultLoad, fileType);
221 }
222 envPtr->loadPendingDependencies(self&: env);
223 bool hadFailures = false;
224 const qsizetype largestFileSizeToCheck = 32000;
225
226 if (parser.isSet(option: reformatOption)) {
227 for (auto &qmlFile : loadedFiles) {
228 QString qmlFilePath = qmlFile.canonicalFilePath();
229 if (qmlFile.internalKind() != DomType::QmlFile) {
230 qWarning() << "cannot reformat" << qmlFile.internalKindStr() << "(" << qmlFilePath
231 << ")";
232 continue;
233 }
234 qDebug() << "reformatting" << qmlFilePath;
235 FileWriter fw;
236 LineWriterOptions lwOptions;
237 WriteOutChecks checks = WriteOutCheck::Default;
238 if (std::shared_ptr<QmlFile> qmlFilePtr = qmlFile.ownerAs<QmlFile>())
239 if (qmlFilePtr->code().size() > largestFileSizeToCheck)
240 checks = WriteOutCheck::None;
241 QString target = qmlFilePath;
242 QString rDir = parser.value(option: reformatDirOption);
243 if (!rDir.isEmpty()) {
244 QFileInfo f(qmlFilePath);
245 QDir d(rDir);
246 target = d.filePath(fileName: f.fileName());
247 }
248 MutableDomItem res = qmlFile.writeOut(path: target, nBackups, opt: lwOptions, fw: &fw, extraChecks: checks);
249 switch (fw.status) {
250 case FileWriter::Status::ShouldWrite:
251 case FileWriter::Status::SkippedDueToFailure:
252 qWarning() << "failure reformatting " << qmlFilePath;
253 break;
254 case FileWriter::Status::DidWrite:
255 qDebug() << "success";
256 break;
257 case FileWriter::Status::SkippedEqual:
258 qDebug() << "no change";
259 }
260 hadFailures = hadFailures || !bool(res);
261 }
262 } else if (parser.isSet(option: dumpAstOption)) {
263 if (pathsToDump.size() > 1) {
264 qWarning() << "--dump-ast can only be used with a single file";
265 return 1;
266 }
267 for (auto &fileItem : loadedFiles) {
268 const auto file = fileItem.fileObject().ownerAs<QmlFile>();
269 if (!file) {
270 qWarning() << "cannot dump AST for" << fileItem.canonicalPath();
271 qWarning() << "is it a valid QML file?";
272 continue;
273 }
274 const QString ast =
275 QQmlJS::Dom::astNodeDump(n: file->ast(), opt: AstDumperOption::DumpNode, indent: 1, baseIndent: 0);
276 QTextStream ts(stdout);
277 ts << ast << Qt::flush;
278 }
279 } else if (parser.isSet(option: dumpOption) || !parser.isSet(option: reformatOption)
280 || !parser.isSet(option: dumpAstOption)) {
281 qDebug() << "will dump\n";
282 QTextStream ts(stdout);
283 auto sink = [&ts](QStringView v) {
284 ts << v; /* ts.flush(); */
285 };
286 qsizetype iPathToDump = 0;
287 bool globalPaths = false;
288 for (auto p : pathsToDump)
289 if (p.headKind() == Path::Kind::Root)
290 globalPaths = true;
291 if (globalPaths)
292 loadedFiles = QList<DomItem>({ env });
293 bool dumpDict = pathsToDump.size() > 1 || loadedFiles.size() > 1;
294 if (dumpDict)
295 sink(u"{\n");
296 while (iPathToDump < pathsToDump.size()) {
297 for (auto &fileItem : loadedFiles) {
298 Path p = pathsToDump.at(i: iPathToDump++ % pathsToDump.size());
299 if (dumpDict) {
300 if (iPathToDump > 1)
301 sink(u",\n");
302 sink(u"\"");
303 if (fileItem.internalKind() != DomType::DomEnvironment) {
304 sinkEscaped(sink, s: fileItem.canonicalFilePath(),
305 options: EscapeOptions::NoOuterQuotes);
306 sink(u"/");
307 }
308 sinkEscaped(sink, s: p.toString(), options: EscapeOptions::NoOuterQuotes);
309 sink(u"\":\n");
310 }
311 fileItem.path(p).dump(sink, indent: 0, filter);
312 }
313 }
314 if (dumpDict)
315 sink(u"}\n");
316 Qt::endl(s&: ts).flush();
317 }
318 for (int i = 0; i < 100; ++i)
319 QThread::yieldCurrentThread(); // let buggy integrations catch up with the output
320 // return a.exec();
321 return 0;
322}
323
324#include "qmldomtool.moc"
325

source code of qtdeclarative/tools/qmldom/qmldomtool.cpp