Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert generation script to TS, and update the source webidl #383

Merged
merged 94 commits into from
Mar 7, 2018

Conversation

mhegazy
Copy link
Contributor

@mhegazy mhegazy commented Mar 1, 2018

This change includes two parts:

  1. A complete rewrite of the generator script:
  • Moving from F# to TS to simplify contribution and code review
  • Using JSON files for all inputs instead of xml
  1. An update to the Edge webidl file
  • The new file is more inline with the standard webidl format, which will allow us to reuse the script to generate directly from webidl
  • The new file comes in JSON format with no need for preprocessing
  • The new file has multiple changes that bring us closer to the spec, with removals of MS-specific types and adding new definitions

A note about methodology:
I have converted the original F# script to TS first, maintain the input file source, and testing as I went to ensure the output was the same. when that was complete, i then plugged in the new file definition file and continued from there. Unfortunately the new input file is different enough from the new one that it made splitting this change into two more expensive.

A note for reviewers:
There are two things to look out for, first and foremost the changes in the generated files, and second the generation script.

Testing:

  • I have run the local TS tests, as well as the RWC tests. and addressed the issues that came up,
  • I still need to run the user tests and DefinitelyTyped on the new generated files.

package.json Outdated
"main": "index.js",
"scripts": {
"build": "tsc --p ./tsconfig.json && node ./lib/index.js",
"baseline-accept": "copy /Y generated\\* baselines\\",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy won't work on linux, could you use a node script for this instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

src/helpers.ts Outdated
export function filter(obj: any, fn: (o: any, n: string | undefined) => boolean) {
if (typeof obj === "object") {
if (Array.isArray(obj)) {
const newArray: any[] = [];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return mapDefined(obj, e => fn(e, undefined) ? filter(e, fn) : undefined);

return src;
}

function mergeNamedArrays<T extends { name: string }>(srcProp: T[], targetProp: T[]) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only ever called on an any[], so might as well just make the args { name: string }[]...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i do not think there is harm from the extra polymorphism

src/index.ts Outdated
import * as path from "path";
import { filter, merge, filterProperties, mapToArray, distinct, map, toNameMap } from "./helpers";

enum Flavor {
Copy link

@ghost ghost Mar 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: const enum

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

src/index.ts Outdated
function getEventHandler(i: Browser.Interface) {
const ownEventHandler =
i.properties
? mapToArray(i.properties.property).filter(p => p["event-handler"]).map(p => ({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: map<EventHandler> or there is no contextual type.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be:

return !i.properties ? [] : mapDefined<Browser.Property, EventHandler>(mapToArray(i.properties.property), p => {
    const eventName = p["event-handler"]!;
    if (eventName === undefined) return undefined;
    const eType = eNameToEType[eventName] || defaultEventType;
    const eventType = eType === "Event" || dependsOn(eType, "Event") ? eType : defaultEventType;
    return { name: p.name, eventName, eventType };
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

src/index.ts Outdated
// Some params have the type of "(DOMString or DOMString [] or Number)"
// we need to transform it into [“DOMString", "DOMString []", "Number"]
function decomposeTypes(t: string) {
return t.replace(/[\(\)]/g, "").split(" or ");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe to be a bit safer:

if (t[0] !== "(") return [t];
assert(t[t.length - 1] === ")");
return t.slice(0, t.length - 1).split(" or ");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove this code path altogether anyways, just leaving it for now until we consume other webidl files.

src/index.ts Outdated
return t.replace(/[\(\)]/g, "").split(" or ");
}

function createTextWriter(newLine: string) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to print to nodes eventually?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not think so. so far our transformations are not that complicated..

src/index.ts Outdated
}

function makeArrayType(elementType: string): string {
return elementType.indexOf("|") > -1 ? `(${elementType})[]` : `${elementType}[]`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.includes("|")

src/index.ts Outdated
}

function convertDomTypeToTsTypeWorker(obj: Browser.Typed): { name: string; nullable: boolean } {
let type;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like obj["type-original"] is never used?

This function seems to be equivalent to:

if (typeof obj.type === "string") {
    const name = covertDomTypeToTsTypeSimple(obj.type);
    const typeArg = obj.subtype && convertDomTypeToTsType(obj.subtype);
    return {
        name: typeArg ? name === "Array" ? makeArrayType(typeArg) : `${name}<${typeArg}>` : name,
        nullable: !!obj.nullable,
    };
}
else {
    assert(!obj.subtype);
    const types = obj.type.map(convertDomTypeToTsTypeWorker);
    return {
        name: types.map(t => t.name).join(" | "),
        nullable: types.some(t => t.nullable),
    };
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. i wanted to leave it for now to see what other idl files have

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could still apply the above refactoring though?

src/index.ts Outdated
}

function covertDomTypeToTsTypeSimple(objDomType: string): string {
if (typeof objDomType !== "string") {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this debugging code?

@mhegazy
Copy link
Contributor Author

mhegazy commented Mar 5, 2018

@weswigham and @andy-ms any more comments?

@kenchris
Copy link

kenchris commented Mar 6, 2018

Btw, just wanted to point out (not sure if relevant) that WebIDL is seeing many changes lately after being abandoned for quite some years. This happens on the editors draft though and it not in stable yet:

https://heycam.github.io/webidl/

ghost referenced this pull request in DefinitelyTyped/DefinitelyTyped Mar 6, 2018
mhegazy added a commit to DefinitelyTyped/DefinitelyTyped that referenced this pull request Mar 6, 2018
* Fixes in preparation of lib changes

See microsoft/TypeScript-DOM-lib-generator#383

* Fix breakes

* revert changes to knuddels-userapps-api

* Fix compressedTexSubImage2D overload

* Fix event emitter methods

* Remove duplicate declaration

* Fix ScreenOrientation.type
src/index.ts Outdated
import { Flavor, emitWebIDl } from "./emitter";

function emitDomWorker(webidl: Browser.WebIdl, knownWorkerTypes: Set<string>, tsWorkerOutput: string) {
const worker: Browser.WebIdl = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an odd name, would call it converted. Also, this initializer is identical to const result: Browser.WebIdl later, so could factor those out to a getEmptyWebIdl() function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is the filtered webidl file to represent the webworker APIs, simiar to the other one, browser.


const result = emitWebIDl(worker, Flavor.Worker);
fs.writeFileSync(tsWorkerOutput, result);
return;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unnecessary return

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is intentinal to get an error if i have any code mixed with the helpers.


const result = emitWebIDl(browser, Flavor.Web);
fs.writeFileSync(tsWebOutput, result);
return;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unnecessary return

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

src/index.ts Outdated

if (obj["callback-functions"]) result["callback-functions"]!["callback-function"] = filterProperties(obj["callback-functions"]!["callback-function"], (cb) => !(template["callback-functions"] && template["callback-functions"]!["callback-function"][cb.name]));
if (obj["callback-interfaces"]) result["callback-interfaces"]!.interface = filterInterface(obj["callback-interfaces"]!.interface, template["callback-interfaces"] && template["callback-interfaces"]!.interface);
if (obj.dictionaries) result.dictionaries!.dictionary = filterDictinary(obj.dictionaries.dictionary, template.dictionaries && template.dictionaries.dictionary);
Copy link

@ghost ghost Mar 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: dictionary (in filterDictinary)
Also, this could be if (obj.dictionaries && template.dictionaries) instead of having the null check at the start of filterDictinary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to assign it something, so having them in the functions seems easier to read.

src/emitter.ts Outdated
"AudioContext": { extendType: ["OfflineContext"], memberNames: new Set(["suspend"]) },
"HTMLCollection": { extendType: ["HTMLFormControlsCollection"], memberNames: new Set(["namedItem"]) },
};
const evetTypeMap: Record<string, string> = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: event

src/emitter.ts Outdated
function emitProperties(prefix: string, emitScope: EmitScope, i: Browser.Interface, conflictedMembers: Set<string>) {
// Note: the schema file shows the property doesn't have "static" attribute,
// therefore all properties are emited for the instance type.
if (emitScope !== EmitScope.StaticOnly) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: double if could be replaced with &&

src/emitter.ts Outdated
function emitMethods(prefix: string, emitScope: EmitScope, i: Browser.Interface, conflictedMembers: Set<string>) {
// If prefix is not empty, then this is the global declare function addEventListener, we want to override this
// Otherwise, this is EventTarget.addEventListener, we want to keep that.
function mFilter(m: Browser.Method) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would inline this fn

}

/// Emit the properties and methods of a given interface
function emitMembers(prefix: string, emitScope: EmitScope, i: Browser.Interface) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would move prefix to the end and make it optional.
Also, the only prefix value currently (besides "") is "declare var", so maybe the parameter should be just inNamespace = false.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah. i need to do this for all prefixes.. but will do this separately. no need to pass a string in.

}
}

function emitEventHandlers(prefix: string, i: Browser.Interface) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function emitEventHandlers(i: Browser.Interface, inNamespace = false) {
    for (const addOrRemove of ["add", "remove"]) {
        const optionsType = addOrRemove === "add" ? "AddEventListenerOptions" : "EventListenerOptions";
        const fPrefix = inNamespace ? "declare function " : "";
        if (tryEmitTypedEventHandlerForInterface()) {
            // only emit the string event handler if we just emited a typed handler
            printer.printLine(`${fPrefix}${addOrRemove}EventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | ${optionsType}): void;`);
        }

        function emitTypedEventHandler(iParent: Browser.Interface) {
            printer.printLine(`${fPrefix}${addOrRemove}EventListener<K extends keyof ${iParent.name}EventMap>(type: K, listener: (this: ${i.name}, ev: ${iParent.name}EventMap[K]) => any, options?: boolean | ${optionsType}): void;`);
        }

        function tryEmitTypedEventHandlerForInterface() {
            if (iNameToEhList[i.name] && iNameToEhList[i.name].length) {
                emitTypedEventHandler(i);
                return true;
            }
            if (iNameToEhParents[i.name] && iNameToEhParents[i.name].length) {
                iNameToEhParents[i.name]
                    .sort(compareName)
                    .forEach(emitTypedEventHandler);
                return true;
            }
            return false;
        }
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure how i feel about functions declared in a for loop. will refactor though.

src/emitter.ts Outdated
}

function emitConstructorSignature(i: Browser.Interface) {
const constructor = typeof i.constructor === "object" ? i.constructor : undefined;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there some bad input this is avoiding?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no this is cause all objects have a constructor property on them :D

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah 🤦‍♂️

@timocov
Copy link
Contributor

timocov commented Mar 26, 2018

@mhegazy seems that this PR change the type of touch* events (touchstart for example) from TouchEvent to just Event.

See microsoft/TypeScript#22565

@mhegazy
Copy link
Contributor Author

mhegazy commented Mar 26, 2018

PRs welcomed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants