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