Add a DOM pinned properties dataset generator
Signed-off-by: Victor Porof <victorporof@chromium.org>
Bug: 1325812
Change-Id: I292d49d273d74d33a82e25179478405395159537
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3695372
Reviewed-by: Mathias Bynens <mathias@chromium.org>
diff --git a/scripts/webidl-properties/get-props.js b/scripts/webidl-properties/get-props.js
new file mode 100644
index 0000000..75d8796
--- /dev/null
+++ b/scripts/webidl-properties/get-props.js
@@ -0,0 +1,139 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {GLOBAL_ATTRIBUTES, SPECS, VALID_MEMBERS} from './config.js';
+import {merge} from './util.js';
+
+/**
+ * All the members relevant for generating the DOM pinned properties dataset
+ * from WebIDL interfaces, mixins and dictionaries.
+ */
+const ACCEPTED_MEMBER_TYPES = new Set(['attribute', 'field']);
+
+/**
+ * Generates the DOM pinned properties dataset.
+ *
+ * @param {array} specs A list of specs. Each spec specifies its name and
+ * all the idl definitions it contains.
+ * @returns {object} output An object with WebIDL type names as keys and their
+ * WebIDL properties and inheritance/include chains as values.
+ */
+export function getIDLProps(specs, output = {}) {
+ for (const spec of specs) {
+ transform(spec, output);
+ }
+ return output;
+}
+
+function transform({name, idls}, output = {}) {
+ const makeEntry = () => ({
+ inheritance: null,
+ includes: [],
+ props: {},
+ });
+
+ for (const idl of idls) {
+ switch (idl.type) {
+ case 'interface':
+ case 'interface mixin':
+ case 'dictionary': {
+ output[idl.name] = output[idl.name] ?? makeEntry();
+ let props = idl.members?.filter(member => ACCEPTED_MEMBER_TYPES.has(member.type));
+ props = props?.map(member => [member.name, {global: GLOBAL_ATTRIBUTES.has(member.name), specs: [name]}, ]);
+ merge(output[idl.name], {
+ inheritance: idl.inheritance,
+ props: Object.fromEntries(props),
+ });
+ break;
+ }
+ case 'includes': {
+ output[idl.target] = output[idl.target] ?? makeEntry();
+ merge(output[idl.target], {
+ includes: [idl.includes],
+ });
+ break;
+ }
+ case 'callback':
+ case 'callback interface':
+ case 'enum':
+ case 'typedef':
+ case 'namespace': {
+ break;
+ }
+ default: {
+ console.warn('Skipping unknown WebIDL type', idl.type);
+ }
+ }
+ }
+}
+
+/**
+ * Adds additional metadata to the DOM pinned properties dataset.
+ *
+ * Currently only adds information about which properties are valid based on
+ * some state, such as for the HTMLInputElement. See `VALID_MEMBERS`.
+ *
+ * @param {*} output
+ */
+export function addMetadata(output) {
+ for (const [key, value] of Object.entries(output)) {
+ const rule = VALID_MEMBERS[key];
+ if (!rule) {
+ continue;
+ }
+ const states = Object.entries(rule).map(([selector, allowlist]) => {
+ const valid = Object.entries(value.props).filter(([prop]) => allowlist.has(prop.toLowerCase()));
+ return [selector, Object.fromEntries(valid)];
+ });
+ value.states = Object.fromEntries(states);
+ }
+ return output;
+}
+
+/**
+ * Minimizes the DOM pinned properties dataset to remove the bits of data that
+ * don't contain information. For example, empty inheritance/includes chains.
+ *
+ * This should be done right at the end, before writing into the output file, to
+ * allow for certain diagnostics (such as finding "missing types").
+ *
+ * @param {*} output
+ * @returns {object}
+ */
+export function minimize(output) {
+ for (const [key, value] of Object.entries(output)) {
+ if (!value.inheritance) {
+ // Remove empty inheritance chains.
+ delete value.inheritance;
+ }
+ if (!value.includes.length) {
+ // Remove empty include chains.
+ delete value.includes;
+ }
+ const props = Object.entries(value.props);
+ if (!props.length) {
+ // Remove empty 'prop' lists.
+ delete value.props;
+ } else {
+ for (const [, value] of props) {
+ if (!value.global) {
+ // Remove the 'global' flag if it's false.
+ delete value.global;
+ }
+ if (value.specs.length === 1 && value.specs[0] === 'html') {
+ // Remove the 'specs' list if it's just "html".
+ delete value.specs;
+ } else {
+ // Combine multiple spec names into a single value.
+ value.specs = value.specs.reduce((acc, name) => acc | SPECS[name], 0);
+ }
+ }
+ }
+ // Remove the entire entry if there's nothing left after the cleanup above.
+ if (!Object.entries(value).length) {
+ delete output[key];
+ }
+ }
+ return output;
+}