Skip to content

Explore using TypeScript AST/type info for more accurate and efficient usage-based polyfilling #7924

@mqudsi

Description

@mqudsi

Describe the feature

Currently, swc trips up on a lot of code that shares property names with ES library code; leading it to incorrectly deduce that a certain up-versioned ES feature is used by the code when it isn't, which then leads to unnecessary core-js polyfills being injected.

For example, the following TypeScript when compiled w/ swc targeting ie6 (swc 1.3.74, jsc.parser.syntax = "typescript", module.type = "es6", env.targets.ie = "6", env.mode = "usage", env.coreJS = "3"), results in the output with a number of unnecessarily applied polyfills to the output:

Input:

class Foo {
  value: string;

  trim(): Foo {
    const trimmed = this.value.replace(/^\s+|\s+$/g, "");
    return new Foo(trimmed);
  }

  constructor(s: string) {
    this.value = s;
  }

  toString(): string {
      return this. value;
  }
}

const hello = new Foo("  hello   ");
console.log(`hello: "${hello}"`);
console.log(`hello.trim(): "${hello.trim()}"`);
console.assert(hello.trim().toString() == "hello");

Output:

function _class_call_check(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}
function _defineProperties(target, props) {
    for(var i = 0; i < props.length; i++){
        var descriptor = props[i];
        descriptor.enumerable = descriptor.enumerable || false;
        descriptor.configurable = true;
        if ("value" in descriptor) descriptor.writable = true;
        Object.defineProperty(target, descriptor.key, descriptor);
    }
}
function _create_class(Constructor, protoProps, staticProps) {
    if (protoProps) _defineProperties(Constructor.prototype, protoProps);
    if (staticProps) _defineProperties(Constructor, staticProps);
    return Constructor;
}
function _define_property(obj, key, value) {
    if (key in obj) {
        Object.defineProperty(obj, key, {
            value: value,
            enumerable: true,
            configurable: true,
            writable: true
        });
    } else {
        obj[key] = value;
    }
    return obj;
}
require("core-js/modules/es.string.replace.js");
require("core-js/modules/es.regexp.exec.js");
require("core-js/modules/es.array.concat.js");
require("core-js/modules/es.string.trim.js");
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.date.to-string.js");
require("core-js/modules/es.regexp.to-string.js");
var Foo = function() {
    "use strict";
    function Foo(s) {
        _class_call_check(this, Foo);
        _define_property(this, "value", void 0);
        this.value = s;
    }
    _create_class(Foo, [
        {
            key: "trim",
            value: function trim() {
                var trimmed = this.value.replace(/^\s+|\s+$/g, "");
                return new Foo(trimmed);
            }
        },
        {
            key: "toString",
            value: function toString() {
                return this.value;
            }
        }
    ]);
    return Foo;
}();
var hello = new Foo("  hello   ");
console.log('hello: "'.concat(hello, '"'));
console.log('hello.trim(): "'.concat(hello.trim(), '"'));
console.assert(hello.trim().toString() == "hello");

The polyfills injected:

  • es.string.replace.js,
  • es.regexp.exec.js,
  • es.array.concat.js,
  • es.string.trim.js,
  • es.object.to-string.js,
  • es.date.to-string.js,
  • es.regexp.to-string.js

I could be mistaken, but so far as I can see, the presence of es.array.concat is because of the "hello: ".concat(...) call, es.string.trim is because of the call to hello.trim(), es.date.to-string is because of hello.trim().toString(), and I'm not sure about es.regexp.to-string but it seems to also be erring on the side of caution from the same.

It seems to me that these false positives for polyfill-required properties could be avoided if the type information from the source TypeScript code were used. In particular, es.string.trim should only be used if the AST reveals that in the case of obj.trim(), obj extends string, es.date.to-string should only be injected in the case of obj.toString() where obj extends Date, es.regexp.to-string should only be injected in the case of obj.toString() where obj extends RegExp, and es.array.concat should only be required in the case of obj.concat() where obj extends Array.

(I'm not sure why es.string.replace and es.regexp.exec are injected since ie6 has full support for the both of them, and I think the same is also true of es.object.to-string but I can't be sure!)

Babel plugin or link to the feature description

No response

Additional context

I am aware that type inference would probably be out-of-scope for the swc compiler itself and I am not suggesting that this approach necessarily change.

This is mainly a brainstorming issue/feature request that I'm humbly opening to just bring in some perspective of a user that sees a wonderful tool possibly not availing itself of all the info it has access to, and wondering if there is something that could be done about it.

I envision one such way this could play out is an swc plugin that adds a new env.mode that goes a step further than env.mode = "usage"; perhaps an env.mode = "type-usage". This plugin would in turn depend on typescript and lean on that to produce the type-enhanced ast which could then either be fed back to swc core by the plugin for purposes of eliminating unnecessary polyfills or otherwise the plugin could take over the usage-based detection of polyfills using that type-enhanced ast and generate a more efficient list of polyfills to be used (but in the case of the plugin not feeding that info back to core, that would necessarily involve a great level of duplication between the polyfill detection and filtering that happens in core and that happens in the plugin).

Thank you for considering this unusual issue, thanks for making this amazing tool, and thanks for being awesome ♥.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions