1// Copyright (C) 2021 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 <QtQml/private/qjsvalue_p.h>
5#include <QtQml/private/qv4propertykey_p.h>
6#include <QtQml/private/qv4global_p.h>
7#include <QtQml/private/qv4functionobject_p.h>
8#include <QtQml/qjsengine.h>
9#include <QtQml/qjsmanagedvalue.h>
10
11#include <QtCore/qcoreapplication.h>
12#include <QtCore/qfile.h>
13
14#include <QtCore/qjsondocument.h>
15#include <QtCore/qjsonarray.h>
16#include <QtCore/qjsonobject.h>
17
18struct PropertyInfo
19{
20 QString name;
21 bool writable;
22};
23
24static QV4::ReturnedValue asManaged(const QJSManagedValue &value)
25{
26 const QJSValue jsVal = value.toJSValue();
27 const QV4::Managed *managed = QJSValuePrivate::asManagedType<QV4::Managed>(jsval: &jsVal);
28 return managed ? managed->asReturnedValue() : QV4::Encode::undefined();
29}
30
31static QJSManagedValue checkedProperty(const QJSManagedValue &value, const QString &name)
32{
33 return value.hasProperty(name) ? QJSManagedValue(value.property(name), value.engine())
34 : QJSManagedValue(QJSPrimitiveUndefined(), value.engine());
35}
36
37QList<PropertyInfo> getPropertyInfos(const QJSManagedValue &value)
38{
39 QV4::Scope scope(value.engine()->handle());
40 QV4::ScopedObject scoped(scope, asManaged(value));
41 if (!scoped)
42 return {};
43
44 QList<PropertyInfo> infos;
45
46 QScopedPointer<QV4::OwnPropertyKeyIterator> iterator(scoped->ownPropertyKeys(target: scoped));
47 QV4::Scoped<QV4::InternalClass> internalClass(scope, scoped->internalClass());
48
49 for (auto key = iterator->next(o: scoped); key.isValid(); key = iterator->next(o: scoped)) {
50 if (key.isSymbol())
51 continue;
52
53 const auto *entry = internalClass->d()->propertyTable.lookup(identifier: key);
54 infos.append(t: {
55 .name: key.toQString(),
56 .writable: !entry || internalClass->d()->propertyData.at(i: entry->index).isWritable()
57 });
58 };
59
60 return infos;
61}
62
63struct State {
64 QMap<QString, QJSValue> constructors;
65 QMap<QString, QJSValue> prototypes;
66 QSet<QString> primitives;
67};
68
69static QString buildConstructor(const QJSManagedValue &constructor, QJsonArray *classes,
70 State *seen, const QString &name, QJSManagedValue *constructed);
71
72static QString findClassName(const QJSManagedValue &value)
73{
74 if (value.isUndefined())
75 return QStringLiteral("undefined");
76 if (value.isBoolean())
77 return QStringLiteral("boolean");
78 if (value.isNumber())
79 return QStringLiteral("number");
80 if (value.isString())
81 return QStringLiteral("string");
82 if (value.isSymbol())
83 return QStringLiteral("symbol");
84
85 QV4::Scope scope(value.engine()->handle());
86 if (QV4::ScopedValue scoped(scope, asManaged(value)); scoped->isManaged())
87 return scoped->managed()->vtable()->className;
88
89 Q_UNREACHABLE_RETURN(QString());
90}
91
92static QString buildClass(const QJSManagedValue &value, QJsonArray *classes,
93 State *seen, const QString &name)
94{
95 if (value.isNull())
96 return QString();
97
98 if (seen->primitives.contains(value: name))
99 return name;
100 else if (name.at(i: 0).isLower())
101 seen->primitives.insert(value: name);
102
103 QJsonObject classObject;
104 QV4::Scope scope(value.engine()->handle());
105
106 classObject[QStringLiteral("className")] = name;
107 classObject[QStringLiteral("qualifiedClassName")] = name;
108
109 classObject[QStringLiteral("classInfos")] = QJsonArray({
110 QJsonObject({
111 { QStringLiteral("name"), QStringLiteral("QML.Element") },
112 { QStringLiteral("value"), QStringLiteral("anonymous") }
113 })
114 });
115
116 if (value.isObject() || value.isFunction())
117 classObject[QStringLiteral("object")] = true;
118 else
119 classObject[QStringLiteral("gadget")] = true;
120
121 const QJSManagedValue prototype = value.prototype();
122
123 if (!prototype.isNull()) {
124 QString protoName;
125 for (auto it = seen->prototypes.begin(), end = seen->prototypes.end(); it != end; ++it) {
126 if (prototype.strictlyEquals(other: QJSManagedValue(*it, value.engine()))) {
127 protoName = it.key();
128 break;
129 }
130 }
131
132 if (protoName.isEmpty()) {
133 if (name.endsWith(QStringLiteral("ErrorPrototype"))
134 && name != QStringLiteral("ErrorPrototype")) {
135 protoName = QStringLiteral("ErrorPrototype");
136 } else if (name.endsWith(QStringLiteral("Prototype"))) {
137 protoName = findClassName(value: prototype);
138 if (!protoName.endsWith(QStringLiteral("Prototype")))
139 protoName += QStringLiteral("Prototype");
140 } else {
141 protoName = name.at(i: 0).toUpper() + name.mid(position: 1) + QStringLiteral("Prototype");
142 }
143
144 auto it = seen->prototypes.find(key: protoName);
145 if (it == seen->prototypes.end()) {
146 seen->prototypes.insert(key: protoName, value: prototype.toJSValue());
147 buildClass(value: prototype, classes, seen, name: protoName);
148 } else if (!it->strictlyEquals(other: prototype.toJSValue())) {
149 qWarning() << "Cannot find a distinct name for the prototype of" << name;
150 qWarning() << protoName << "is already in use.";
151 }
152 }
153
154 classObject[QStringLiteral("superClasses")] = QJsonArray {
155 QJsonObject ({
156 { QStringLiteral("access"), QStringLiteral("public") },
157 { QStringLiteral("name"), protoName }
158 })};
159 }
160
161 QJsonArray properties, methods;
162
163 auto defineProperty = [&](const QJSManagedValue &prop, const PropertyInfo &info) {
164 QJsonObject propertyObject;
165 propertyObject.insert(QStringLiteral("name"), value: info.name);
166
167 // Insert faux member entry if we're allowed to write to this
168 if (info.writable)
169 propertyObject.insert(QStringLiteral("member"), QStringLiteral("fakeMember"));
170
171 if (!prop.isUndefined() && !prop.isNull()) {
172 QString propClassName = findClassName(value: prop);
173 if (!propClassName.at(i: 0).isLower() && info.name != QStringLiteral("prototype")) {
174 propClassName = (name == QStringLiteral("GlobalObject"))
175 ? QString()
176 : name.at(i: 0).toUpper() + name.mid(position: 1);
177
178 propClassName += info.name.at(i: 0).toUpper() + info.name.mid(position: 1);
179 propertyObject.insert(QStringLiteral("type"),
180 value: buildClass(value: prop, classes, seen, name: propClassName));
181 } else {
182 // If it's the "prototype" property we just refer to generic "Object",
183 // and if it's a value type, we handle it separately.
184 propertyObject.insert(QStringLiteral("type"), value: propClassName);
185 }
186 }
187 return propertyObject;
188 };
189
190 QList<PropertyInfo> unRetrievedProperties;
191 QJSManagedValue constructed;
192 for (const PropertyInfo &info : getPropertyInfos(value)) {
193 QJSManagedValue prop = checkedProperty(value, name: info.name);
194 if (prop.engine()->hasError()) {
195 unRetrievedProperties.append(t: info);
196 prop.engine()->catchError();
197 continue;
198 }
199
200 // Method or constructor
201 if (prop.isFunction()) {
202 QV4::Scoped<QV4::FunctionObject> propFunction(scope, asManaged(value: prop));
203
204 QJsonObject methodObject;
205
206 methodObject.insert(QStringLiteral("access"), QStringLiteral("public"));
207 methodObject.insert(QStringLiteral("name"), value: info.name);
208 methodObject.insert(QStringLiteral("isJavaScriptFunction"), value: true);
209
210 const int formalParams = propFunction->getLength();
211 if (propFunction->isConstructor()) {
212 methodObject.insert(QStringLiteral("isConstructor"), value: true);
213
214 QString ctorName;
215 if (info.name.at(i: 0).isUpper()) {
216 ctorName = info.name;
217 } else if (info.name == QStringLiteral("constructor")) {
218 if (name.endsWith(QStringLiteral("Prototype")))
219 ctorName = name.chopped(n: strlen(s: "Prototype"));
220 else if (name.endsWith(QStringLiteral("PrototypeMember")))
221 ctorName = name.chopped(n: strlen(s: "PrototypeMember"));
222 else
223 ctorName = name;
224
225 if (!ctorName.endsWith(QStringLiteral("Constructor")))
226 ctorName += QStringLiteral("Constructor");
227 }
228
229 methodObject.insert(
230 QStringLiteral("returnType"),
231 value: buildConstructor(constructor: prop, classes, seen, name: ctorName, constructed: &constructed));
232 }
233
234 QJsonArray arguments;
235 for (int i = 0; i < formalParams; i++)
236 arguments.append(value: QJsonObject {});
237
238 methodObject.insert(QStringLiteral("arguments"), value: arguments);
239
240 methods.append(value: methodObject);
241
242 continue;
243 }
244
245 // ...else it's just a property
246 properties.append(value: defineProperty(prop, info));
247 }
248
249 for (const PropertyInfo &info : unRetrievedProperties) {
250 QJSManagedValue prop = checkedProperty(
251 value: constructed.isUndefined() ? value : constructed, name: info.name);
252 if (prop.engine()->hasError()) {
253 qWarning() << "Cannot retrieve property " << info.name << "of" << name << constructed.toString();
254 qWarning().noquote() << " " << prop.engine()->catchError().toString();
255 }
256
257 properties.append(value: defineProperty(prop, info));
258 }
259
260 classObject[QStringLiteral("properties")] = properties;
261 classObject[QStringLiteral("methods")] = methods;
262
263 classes->append(value: classObject);
264
265 return name;
266}
267
268static QString buildConstructor(const QJSManagedValue &constructor, QJsonArray *classes,
269 State *seen, const QString &name, QJSManagedValue *constructed)
270{
271 QJSEngine *engine = constructor.engine();
272
273 // If the constructor appears in the global object, use the name from there.
274 const QJSManagedValue globalObject(engine->globalObject(), engine);
275 const auto infos = getPropertyInfos(value: globalObject);
276 for (const auto &info : infos) {
277 const QJSManagedValue member(globalObject.property(name: info.name), engine);
278 if (member.strictlyEquals(other: constructor) && info.name != name)
279 return buildConstructor(constructor, classes, seen, name: info.name, constructed);
280 }
281
282 if (name == QStringLiteral("Symbol"))
283 return QStringLiteral("undefined"); // Cannot construct symbols with "new";
284
285 if (name == QStringLiteral("URL")) {
286 *constructed = QJSManagedValue(
287 constructor.callAsConstructor(arguments: { QJSValue(QStringLiteral("http://a.bc")) }),
288 engine);
289 } else if (name == QStringLiteral("Promise")) {
290 *constructed = QJSManagedValue(
291 constructor.callAsConstructor(
292 arguments: { engine->evaluate(QStringLiteral("(function() {})")) }),
293 engine);
294 } else if (name == QStringLiteral("DataView")) {
295 *constructed = QJSManagedValue(
296 constructor.callAsConstructor(
297 arguments: { engine->evaluate(QStringLiteral("new ArrayBuffer()")) }),
298 engine);
299 } else if (name == QStringLiteral("Proxy")) {
300 *constructed = QJSManagedValue(constructor.callAsConstructor(
301 arguments: { engine->newObject(), engine->newObject() }), engine);
302 } else {
303 *constructed = QJSManagedValue(constructor.callAsConstructor(), engine);
304 }
305
306 if (engine->hasError()) {
307 qWarning() << "Calling constructor" << name << "failed";
308 qWarning().noquote() << " " << engine->catchError().toString();
309 return QString();
310 } else if (name.isEmpty()) {
311 Q_UNREACHABLE();
312 }
313
314 auto it = seen->constructors.find(key: name);
315 if (it == seen->constructors.end()) {
316 seen->constructors.insert(key: name, value: constructor.toJSValue());
317 return buildClass(value: *constructed, classes, seen, name);
318 } else if (!constructor.strictlyEquals(other: QJSManagedValue(*it, constructor.engine()))) {
319 qWarning() << "Two constructors of the same name seen:" << name;
320 }
321 return name;
322}
323
324int main(int argc, char *argv[])
325{
326 QCoreApplication app(argc, argv);
327 QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR));
328
329 QStringList args = app.arguments();
330
331 if (args.size() != 2) {
332 qWarning().noquote() << app.applicationName() << "[output json path]";
333 return 1;
334 }
335
336 QString fileName = args.at(i: 1);
337
338 QJSEngine engine;
339 engine.installExtensions(extensions: QJSEngine::AllExtensions);
340
341 QJsonArray classesArray;
342 State seen;
343
344 // object. Do this first to claim the "Object" name for the prototype.
345 buildClass(value: QJSManagedValue(engine.newObject(), &engine), classes: &classesArray, seen: &seen,
346 QStringLiteral("object"));
347
348
349 buildClass(value: QJSManagedValue(engine.globalObject(), &engine), classes: &classesArray, seen: &seen,
350 QStringLiteral("GlobalObject"));
351
352 // Add JS types, in case they aren't used anywhere.
353
354
355 // function
356 buildClass(value: QJSManagedValue(engine.evaluate(QStringLiteral("(function() {})")), &engine),
357 classes: &classesArray, seen: &seen, QStringLiteral("function"));
358
359 // string
360 buildClass(value: QJSManagedValue(QStringLiteral("s"), &engine), classes: &classesArray, seen: &seen,
361 QStringLiteral("string"));
362
363 // undefined
364 buildClass(value: QJSManagedValue(QJSPrimitiveUndefined(), &engine), classes: &classesArray, seen: &seen,
365 QStringLiteral("undefined"));
366
367 // number
368 buildClass(value: QJSManagedValue(QJSPrimitiveValue(1.1), &engine), classes: &classesArray, seen: &seen,
369 QStringLiteral("number"));
370
371 // boolean
372 buildClass(value: QJSManagedValue(QJSPrimitiveValue(true), &engine), classes: &classesArray, seen: &seen,
373 QStringLiteral("boolean"));
374
375 // symbol
376 buildClass(value: QJSManagedValue(engine.newSymbol(QStringLiteral("s")), &engine),
377 classes: &classesArray, seen: &seen, QStringLiteral("symbol"));
378
379 // Generate the fake metatypes json structure
380 QJsonDocument metatypesJson = QJsonDocument(
381 QJsonArray({
382 QJsonObject({
383 {QStringLiteral("classes"), classesArray}
384 })
385 })
386 );
387
388 QFile file(fileName);
389 if (!file.open(flags: QFile::WriteOnly)) {
390 qWarning() << "Failed to write metatypes json to" << fileName;
391 return 1;
392 }
393
394 file.write(data: metatypesJson.toJson());
395 file.close();
396
397 return 0;
398}
399

source code of qtdeclarative/tools/qmljsrootgen/main.cpp