| # Copyright 2023 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Generator that produces a definition file for typescript. |
| |
| Note: This is a work in progress, and generated definitions may need tweaking. |
| See bug: crbug.com/1203307 |
| This script is currently run manually. |
| """ |
| |
| import datetime |
| import os |
| import subprocess |
| import tempfile |
| |
| from code_util import Code |
| from js_util import JsUtil |
| from model import * |
| from schema_util import * |
| |
| CHROMIUM_SRC = os.path.abspath( |
| os.path.join(os.path.dirname(__file__), "..", "..")) |
| |
| |
| class TsDefinitionGenerator(object): |
| |
| def Generate(self, namespace): |
| return _Generator(namespace).Generate() |
| |
| |
| class _Generator(object): |
| |
| def __init__(self, namespace: Namespace): |
| self._namespace = namespace |
| self._events_required = False |
| self._js_util = JsUtil() |
| |
| def Generate(self): |
| main_code = Code() |
| body_code = Code() |
| # Generate the definition first to determine if an import is required. |
| self._AppendDefinitionBody(body_code) |
| # Create copyright header. |
| self._AppendChromiumHeader(main_code) |
| # Create file overview. |
| self._AppendFileOverview(main_code) |
| # Create import area. |
| self._AppendImportArea(main_code) |
| # Create namespaces. |
| namespaces_to_close = self._OpenNamespaces(main_code) |
| # Append definitions. |
| main_code.Concat(body_code) |
| # Close namespaces. |
| self._CloseNamespaces(main_code, namespaces_to_close) |
| # Cleanup a little. |
| main_code.TrimTrailingNewlines() |
| # If events are needed, add the import. |
| if self._events_required: |
| main_code.Substitute( |
| {"imports": "import {ChromeEvent} from './chrome_event.js';"}) |
| else: |
| main_code.Substitute({"imports": ""}) |
| main_code = self._ClangFormat(main_code) |
| # Final new line. |
| main_code.Append() |
| return main_code |
| |
| def _AppendChromiumHeader(self, c: Code): |
| c.Append(f"""// Copyright {datetime.date.today().year} The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file.""") |
| c.Append() |
| |
| def _AppendFileOverview(self, c: Code): |
| c.Append("""/** |
| * @fileoverview Definitions for chrome.{name} API |
| * Generated from: {file} |
| * run `tools/json_schema_compiler/compiler.py {file} -g ts_definitions` to |
| * regenerate. |
| */""".format(name=self._namespace.name, file=self._namespace.source_file)) |
| c.Append() |
| |
| def _AppendImportArea(self, c: Code): |
| # Assume these declarations will be placed in tools/typescript/definitions. |
| c.Append("%(imports)s") |
| c.Append() |
| |
| def _OpenNamespaces(self, c: Code): |
| namespaces_opened = 2 |
| declare_or_export = "declare" |
| # If adding an import the definition file becomes a module. |
| # If that happens we must declare something global specifically. |
| # Otherwise the definition file is considered global by default. |
| if self._events_required: |
| c.Sblock("declare global {") |
| namespaces_opened += 1 |
| c.Append() |
| declare_or_export = "export" |
| c.Sblock(f"{declare_or_export} namespace chrome {{") |
| c.Append() |
| c.Sblock(f"export namespace {self._namespace.name} {{") |
| c.Append() |
| return namespaces_opened |
| |
| def _AppendDefinitionBody(self, c: Code): |
| # Add namespace level properties. |
| for prop in self._namespace.properties.values(): |
| type_name = self._ExtractType(prop.type_) |
| # If the ref type has additional properties, do a namespace merge. |
| prop_type: Type = prop.type_ |
| if (len(prop_type.properties) > 0 |
| and prop_type.property_type == PropertyType.REF): |
| type_name = self._AppendInterfaceForProperty(c, prop, type_name) |
| c.Append(f"export const {prop.name}: {type_name};") |
| c.Append() |
| # Add types. |
| for type in self._namespace.types.values(): |
| self._AppendType(c, type) |
| # Add namespace level functions. |
| for func in self._namespace.functions.values(): |
| self._AppendFunction(c, func) |
| # Add Events. |
| for event in self._namespace.events.values(): |
| event_type = self._ExtractFunctionType(event) |
| c.Append(f"export const {event.name}: ChromeEvent<{event_type}>;") |
| c.Append() |
| self._events_required = True |
| |
| def _CloseNamespaces(self, c: Code, to_close: int): |
| for i in range(to_close): |
| c.Eblock("}") |
| |
| def _AppendFunction(self, c: Code, func): |
| params = self._ExtractFunctionParams(func) |
| ret_type = self._ExtractFunctionReturnType(func) |
| c.Append(f"export function {func.name}({params}): {ret_type};") |
| c.Append() |
| |
| # This appends an local only interface to allow for additional |
| # properties on an already defined type. |
| def _AppendInterfaceForProperty(self, c: Code, prop: Property, |
| prop_type_name): |
| if prop.deprecated: |
| return |
| prop_type = prop.type_ |
| interface_name = f"{prop.name}_{prop_type_name}" |
| # The names of these interfaces are not in pascal case. |
| # They are unexported though which results in the correct behavior. |
| c.Append("// eslint-disable-next-line @typescript-eslint/naming-convention") |
| c.Sblock(f"interface {interface_name} extends {prop_type_name}{{") |
| for prop in prop_type.properties.values(): |
| type_name = self._ExtractType(prop.type_) |
| c.Append(f"readonly {prop.name}: {type_name};") |
| # Add interface functions. |
| for func in prop_type.functions.values(): |
| self._AppendFunction(c, func) |
| # Add Events. |
| for event in prop_type.events.values(): |
| event_type = self._ExtractFunctionType(event) |
| c.Append(f"readonly {event.name}: ChromeEvent<{event_type}>;") |
| self._events_required = True |
| c.Eblock("}") |
| return interface_name |
| |
| def _AppendType(self, c: Code, type: Type): |
| if type.property_type is PropertyType.ENUM: |
| self._AppendEnum(c, type) |
| elif type.property_type is PropertyType.OBJECT: |
| self._AppendInterface(c, type) |
| elif type.property_type.is_fundamental: |
| # Type alias |
| c.Append(f"export type {type.name} = {type.property_type.name};") |
| c.Append() |
| elif (type.property_type is PropertyType.ARRAY |
| or type.property_type is PropertyType.CHOICES): |
| ts_type = self._ExtractType(type) |
| c.Append(f"export type {type.name} = {ts_type};") |
| c.Append() |
| else: |
| # Adding this for things we may not have accounted for here. |
| c.Append( |
| f"// TODO({os.getlogin()}) -- {type.name}: {type.property_type.name}") |
| |
| def _AppendInterface(self, c: Code, interface: Type): |
| c.Sblock(f"export interface {interface.name} {{") |
| # Add interface properties. |
| for property in interface.properties.values(): |
| c.Append(self._ExtractPropertyDefinition(property)) |
| # Add interface functions. |
| func: Function |
| for func in interface.functions.values(): |
| c.Append(f"{func.name}{self._ExtractFunctionType(func, ':')};") |
| # Add interface events. |
| for evnt in interface.events.values(): |
| event_type = self._ExtractFunctionType(evnt) |
| c.Append(f"{evnt.name}: ChromeEvent<{event_type}>;") |
| self._events_required = True |
| c.Eblock("}") |
| c.Append() |
| |
| def _AppendEnum(self, c: Code, enum): |
| c.Sblock(f"export enum {enum.name} {{") |
| for v in enum.enum_values: |
| c.Append(f"{self._js_util.GetPropertyName(v.name)} = '{v.name}',") |
| c.Eblock("}") |
| c.Append() |
| |
| def _AppendClass(self, c: Code, class_type: Type): |
| c.Sblock(f"export class {class_type.name} {{") |
| for property in class_type.properties.values(): |
| c.Append(self._ExtractPropertyDefinition(property)) |
| # Add class functions. |
| func: Function |
| for func in class_type.functions.values(): |
| c.Append(f"{func.name}{self._ExtractFunctionType(func, ':')};") |
| # Add class events. |
| for evnt in class_type.events.values(): |
| event_type = self._ExtractFunctionType(evnt) |
| c.Append(f"{evnt.name}: ChromeEvent<{event_type}>;") |
| self._events_required = True |
| c.Eblock("}") |
| |
| def _ExtractFunctionReturnType(self, func: Function): |
| ret_type = "void" |
| if func.returns is not None: |
| ret_type = self._ExtractType(func.returns) |
| elif (func.returns_async is not None |
| and func.returns_async.can_return_promise): |
| ret_type = f"Promise<{self._ExtractPromiseType(func.returns_async)}>" |
| return ret_type |
| |
| # Extracts the code required to define a type. |
| # Uses recursion to get types within types. |
| def _ExtractType(self, type: Type): |
| if type is None: |
| return "void" |
| if type.property_type in (PropertyType.INTEGER, PropertyType.DOUBLE): |
| return "number" |
| elif type.property_type is PropertyType.OBJECT: |
| return self._ExtractObjectDefinition(type) |
| elif type.property_type is PropertyType.REF: |
| return type.ref_type |
| elif type.property_type is PropertyType.CHOICES: |
| type_list = "" |
| for i, choice in enumerate(type.choices): |
| if i != 0: |
| type_list += "|" |
| type_list += self._ExtractType(choice) |
| return type_list |
| elif type.property_type is PropertyType.ARRAY: |
| if type.item_type.property_type is PropertyType.OBJECT: |
| element_type = self._ExtractType(type.item_type) |
| # Trying to idenfity non-simple elements to use the syntax: |
| # Array<string | number> |
| # Array<{prop: string}> |
| # Array<() => void> |
| if '|' in element_type or '(' in element_type or '{' in element_type: |
| return f"Array<{element_type}>" |
| |
| # For simple type use like the syntax: string[] |
| return f"{element_type}[]" |
| elif type.item_type.property_type is PropertyType.CHOICES: |
| return f"({self._ExtractType(type.item_type)})[]" |
| else: |
| return f"{self._ExtractType(type.item_type)}[]" |
| elif type.property_type.is_fundamental: |
| return type.property_type.name |
| elif type.property_type is PropertyType.FUNCTION: |
| return self._ExtractFunctionType(type.function) |
| elif type.property_type is PropertyType.ANY: |
| return "any" |
| elif type.property_type is PropertyType.BINARY: |
| return "ArrayBuffer" |
| else: |
| # Added for accounting for unknown objects. |
| return f"unknown /*TODO({os.getlogin()})*/" |
| |
| def _ExtractPropertyDefinition(self, prop: Property, terminator=";"): |
| q_mark = "?" if prop.optional else "" |
| type_name = self._ExtractType(prop.type_) |
| return f"{prop.name}{q_mark}: {type_name}{terminator}" |
| |
| # Extracts the function type as an arrow function. |
| # The delimiter can be changed so this can be used for interface / object |
| # members. |
| def _ExtractFunctionType(self, func: Function, return_delim=" =>"): |
| params = self._ExtractFunctionParams(func) |
| ret_type = self._ExtractFunctionReturnType(func) |
| return f"({params}){return_delim} {ret_type}" |
| |
| # Extracts an object definition. |
| def _ExtractObjectDefinition(self, obj: Type): |
| if obj.instance_of: |
| return obj.instance_of |
| |
| # If there are no specific properties on the object then we should expect |
| # and object of random keys with specific values. |
| if len(obj.properties) == 0: |
| value_type = self._ExtractType(obj.additional_properties) |
| return "{[key:string]: %s,}" % value_type |
| |
| ## Otherwise we will build a definition similar to an interface |
| obj_code = Code() |
| obj_code.Append("{") |
| for property in obj.properties.values(): |
| obj_code.Append(self._ExtractPropertyDefinition(property, ",")) |
| func: Function |
| for func in obj.functions.values(): |
| obj_code.Append(f"{func.name}{self._ExtractFunctionType(func, ':')};") |
| obj_code.Append("}") |
| for evnt in obj.events.values(): |
| event_type = self._ExtractFunctionType(evnt) |
| obj_code.Append(f"{evnt.name}: ChromeEvent<{event_type}>;") |
| self._events_required = True |
| return obj_code.Render() |
| |
| # Extracts parameters from a function as a string representation. |
| # Example = "p1: string, p2: number, p3: any". |
| def _ExtractFunctionParams(self, func: Function): |
| param_str = self._ExtractParams(func.params) |
| |
| # When the return async isn't a promise, we append it as a return callback |
| # at the end of the parameters. |
| use_callback = (func.returns_async |
| and not func.returns_async.can_return_promise) |
| if use_callback: |
| callback_params = self._ExtractParams(func.returns_async.params) |
| if param_str: |
| param_str += ", " |
| |
| param_str += f"{func.returns_async.name} " |
| if func.returns_async.optional: |
| param_str += "?" |
| param_str += f": ({callback_params}) => void" |
| |
| return param_str |
| |
| def _ExtractParams(self, params: list): |
| param_str = "" |
| required_index = -1 |
| for i, param in reversed(list(enumerate(params))): |
| if not param.optional: |
| required_index = i |
| break |
| for i, param in enumerate(params): |
| q_mark = "?" if param.optional and not i < required_index else "" |
| type_name = self._ExtractType(param.type_) |
| # Typescript doesn't allow an optional before a required param. |
| # In this case append | undefined to the parameter. |
| if i < required_index and param.optional: |
| type_name += "|undefined" |
| param_str += f"{param.name}{q_mark}: {type_name}" |
| if i < len(params) - 1: |
| param_str += ", " |
| return param_str |
| |
| # Extracts the type from a promise. |
| def _ExtractPromiseType(self, async_return: ReturnsAsync): |
| retval = "void" |
| # Assume that there is at most only one param since functions can only |
| # return one thing. This includes those that are async and use a promise to |
| # return a value. It could also be 0 for void return type. |
| assert len(async_return.params) <= 1 |
| for ret in async_return.params: |
| retval = self._ExtractType(ret.type_) |
| if ret.optional: |
| retval += "|undefined" |
| return retval |
| |
| def _ClangFormat(self, c: Code, level=0): |
| # temp = tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js") |
| # f_name = temp.name |
| with tempfile.NamedTemporaryFile("w", |
| encoding="utf-8", |
| suffix=".js", |
| delete=False) as f: |
| f.write(c.Render()) |
| f_name = f.name |
| script_path = self._GetChromiumClangFormatScriptPath() |
| style_path = self._GetChromiumClangFormatStylePath() |
| cmd = (f'python3 {script_path} --fallback-style=none ' |
| f'--style=file:{style_path} "{f_name}"') |
| p = subprocess.Popen(cmd, |
| cwd=CHROMIUM_SRC, |
| encoding="utf-8", |
| shell=True, |
| stdout=subprocess.PIPE) |
| out = p.communicate()[0] |
| out_code = Code() |
| out_code.Append(out) |
| os.remove(f_name) |
| return out_code |
| |
| def _GetChromiumClangFormatScriptPath(self): |
| return os.path.join(CHROMIUM_SRC, "third_party", "depot_tools", |
| "clang_format.py") |
| |
| def _GetChromiumClangFormatStylePath(self): |
| return os.path.join(CHROMIUM_SRC, ".clang-format") |