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 | |
18 | struct PropertyInfo |
19 | { |
20 | QString name; |
21 | bool writable; |
22 | }; |
23 | |
24 | static 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 | |
31 | static 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 | |
37 | QList<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 | |
63 | struct State { |
64 | QMap<QString, QJSValue> constructors; |
65 | QMap<QString, QJSValue> prototypes; |
66 | QSet<QString> primitives; |
67 | }; |
68 | |
69 | static QString buildConstructor(const QJSManagedValue &constructor, QJsonArray *classes, |
70 | State *seen, const QString &name, QJSManagedValue *constructed); |
71 | |
72 | static 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 | |
92 | static 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 | |
268 | static 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 | |
324 | int 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 | |