diff --git a/packages/babel-helper-define-polyfill-provider/src/utils.ts b/packages/babel-helper-define-polyfill-provider/src/utils.ts index 0637dbcd..2874111e 100644 --- a/packages/babel-helper-define-polyfill-provider/src/utils.ts +++ b/packages/babel-helper-define-polyfill-provider/src/utils.ts @@ -81,6 +81,11 @@ export function resolveKey( } } +export function resolveInstance(obj: NodePath): string | null { + const source = resolveSource(obj); + return source.placement === "prototype" ? source.id : null; +} + export function resolveSource(obj: NodePath): { id: string | null; placement: "prototype" | "static" | null; @@ -103,21 +108,176 @@ export function resolveSource(obj: NodePath): { } const path = resolve(obj); + switch (path?.type) { + case "NullLiteral": + return { id: null, placement: null }; case "RegExpLiteral": return { id: "RegExp", placement: "prototype" }; - case "FunctionExpression": - return { id: "Function", placement: "prototype" }; case "StringLiteral": + case "TemplateLiteral": return { id: "String", placement: "prototype" }; - case "NumberLiteral": + case "NumericLiteral": return { id: "Number", placement: "prototype" }; case "BooleanLiteral": return { id: "Boolean", placement: "prototype" }; + case "BigIntLiteral": + return { id: "BigInt", placement: "prototype" }; case "ObjectExpression": return { id: "Object", placement: "prototype" }; case "ArrayExpression": return { id: "Array", placement: "prototype" }; + case "FunctionExpression": + case "ArrowFunctionExpression": + case "ClassExpression": + return { id: "Function", placement: "prototype" }; + // new Constructor() -> resolve the constructor name + case "NewExpression": { + const calleeId = resolveId( + (path as NodePath).get("callee"), + ); + if (calleeId) return { id: calleeId, placement: "prototype" }; + return { id: null, placement: null }; + } + // Unary expressions -> result type depends on operator + case "UnaryExpression": { + const { operator } = path.node as t.UnaryExpression; + if (operator === "typeof") + return { id: "String", placement: "prototype" }; + if (operator === "!" || operator === "delete") + return { id: "Boolean", placement: "prototype" }; + // Unary + always produces Number (throws on BigInt) + if (operator === "+") return { id: "Number", placement: "prototype" }; + // Unary - and ~ can produce Number or BigInt depending on operand + if (operator === "-" || operator === "~") { + const arg = resolveInstance( + (path as NodePath).get("argument"), + ); + if (arg === "BigInt") return { id: "BigInt", placement: "prototype" }; + if (arg !== null) return { id: "Number", placement: "prototype" }; + return { id: null, placement: null }; + } + return { id: null, placement: null }; + } + // ++i, i++ produce Number or BigInt depending on the argument + case "UpdateExpression": { + const arg = resolveInstance( + (path as NodePath).get("argument"), + ); + if (arg === "BigInt") return { id: "BigInt", placement: "prototype" }; + if (arg !== null) return { id: "Number", placement: "prototype" }; + return { id: null, placement: null }; + } + // Binary expressions -> result type depends on operator + case "BinaryExpression": { + const { operator } = path.node as t.BinaryExpression; + if ( + operator === "==" || + operator === "!=" || + operator === "===" || + operator === "!==" || + operator === "<" || + operator === ">" || + operator === "<=" || + operator === ">=" || + operator === "instanceof" || + operator === "in" + ) { + return { id: "Boolean", placement: "prototype" }; + } + // >>> always produces Number + if (operator === ">>>") { + return { id: "Number", placement: "prototype" }; + } + // Arithmetic and bitwise operators can produce Number or BigInt + if ( + operator === "-" || + operator === "*" || + operator === "/" || + operator === "%" || + operator === "**" || + operator === "&" || + operator === "|" || + operator === "^" || + operator === "<<" || + operator === ">>" + ) { + const left = resolveInstance( + (path as NodePath).get("left"), + ); + const right = resolveInstance( + (path as NodePath).get("right"), + ); + if (left === "BigInt" && right === "BigInt") { + return { id: "BigInt", placement: "prototype" }; + } + if (left !== null && right !== null) { + return { id: "Number", placement: "prototype" }; + } + return { id: null, placement: null }; + } + // + depends on operand types: string wins, otherwise number or bigint + if (operator === "+") { + const left = resolveInstance( + (path as NodePath).get("left"), + ); + const right = resolveInstance( + (path as NodePath).get("right"), + ); + if (left === "String" || right === "String") { + return { id: "String", placement: "prototype" }; + } + if (left === "Number" && right === "Number") { + return { id: "Number", placement: "prototype" }; + } + if (left === "BigInt" && right === "BigInt") { + return { id: "BigInt", placement: "prototype" }; + } + } + return { id: null, placement: null }; + } + // (a, b, c) -> the result is the last expression + case "SequenceExpression": { + const expressions = (path as NodePath).get( + "expressions", + ); + return resolveSource(expressions[expressions.length - 1]); + } + // a = b -> the result is the right side + case "AssignmentExpression": { + if ((path.node as t.AssignmentExpression).operator === "=") { + return resolveSource( + (path as NodePath).get("right"), + ); + } + return { id: null, placement: null }; + } + // a ? b : c -> if both branches resolve to the same type, use it + case "ConditionalExpression": { + const consequent = resolveSource( + (path as NodePath).get("consequent"), + ); + const alternate = resolveSource( + (path as NodePath).get("alternate"), + ); + if (consequent.id && consequent.id === alternate.id) { + return consequent; + } + return { id: null, placement: null }; + } + // (expr) -> unwrap parenthesized expressions + case "ParenthesizedExpression": + return resolveSource( + (path as NodePath).get("expression"), + ); + // TypeScript / Flow type wrappers -> unwrap to the inner expression + case "TSAsExpression": + case "TSSatisfiesExpression": + case "TSNonNullExpression": + case "TSInstantiationExpression": + case "TSTypeAssertion": + case "TypeCastExpression": + return resolveSource(path.get("expression") as NodePath); } return { id: null, placement: null }; diff --git a/packages/babel-helper-define-polyfill-provider/test/descriptors.js b/packages/babel-helper-define-polyfill-provider/test/descriptors.js index 809e932a..67a06517 100644 --- a/packages/babel-helper-define-polyfill-provider/test/descriptors.js +++ b/packages/babel-helper-define-polyfill-provider/test/descriptors.js @@ -165,4 +165,537 @@ describe("descriptors", () => { expect(path.type).toBe("OptionalMemberExpression"); expect(path.toString()).toBe("a?.includes"); }); + + it("instance property - numeric literal", () => { + const [desc] = getDescriptor("(1).toFixed;"); + + expect(desc).toEqual({ + kind: "property", + object: "Number", + key: "toFixed", + placement: "prototype", + }); + }); + + it("instance property - string literal", () => { + const [desc] = getDescriptor("'hello'.includes;"); + + expect(desc).toEqual({ + kind: "property", + object: "String", + key: "includes", + placement: "prototype", + }); + }); + + it("instance property - template literal", () => { + const [desc] = getDescriptor("`hello`.includes;"); + + expect(desc).toEqual({ + kind: "property", + object: "String", + key: "includes", + placement: "prototype", + }); + }); + + it("instance property - boolean literal", () => { + const [desc] = getDescriptor("true.toString;"); + + expect(desc).toEqual({ + kind: "property", + object: "Boolean", + key: "toString", + placement: "prototype", + }); + }); + + it("instance property - bigint literal", () => { + const [desc] = getDescriptor("(1n).toString;"); + + expect(desc).toEqual({ + kind: "property", + object: "BigInt", + key: "toString", + placement: "prototype", + }); + }); + + it("instance property - regexp literal", () => { + const [desc] = getDescriptor("/foo/.test;"); + + expect(desc).toEqual({ + kind: "property", + object: "RegExp", + key: "test", + placement: "prototype", + }); + }); + + it("instance property - object expression", () => { + const [desc] = getDescriptor("({}).hasOwnProperty;"); + + expect(desc).toEqual({ + kind: "property", + object: "Object", + key: "hasOwnProperty", + placement: "prototype", + }); + }); + + it("instance property - function expression", () => { + const [desc] = getDescriptor("(function(){}).bind;"); + + expect(desc).toEqual({ + kind: "property", + object: "Function", + key: "bind", + placement: "prototype", + }); + }); + + it("instance property - arrow function", () => { + const [desc] = getDescriptor("(() => {}).bind;"); + + expect(desc).toEqual({ + kind: "property", + object: "Function", + key: "bind", + placement: "prototype", + }); + }); + + it("instance property - class expression", () => { + const [desc] = getDescriptor("(class {}).name;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: "Function", + key: "name", + placement: "prototype", + }); + }); + + it("instance property - new expression with known constructor", () => { + const [desc] = getDescriptor("new Map().get;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: "Map", + key: "get", + placement: "prototype", + }); + }); + + it("instance property - new expression with unknown constructor", () => { + const [desc] = getDescriptor( + "var Foo = class {}; new Foo().bar;", + "property", + ); + + expect(desc).toEqual({ + kind: "property", + object: null, + key: "bar", + placement: null, + }); + }); + + it("instance property - typeof produces string", () => { + const [desc] = getDescriptor("(typeof x).includes;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: "String", + key: "includes", + placement: "prototype", + }); + }); + + it("instance property - logical not produces boolean", () => { + const [desc] = getDescriptor("(!0).toString;"); + + expect(desc).toEqual({ + kind: "property", + object: "Boolean", + key: "toString", + placement: "prototype", + }); + }); + + it("instance property - unary plus produces number", () => { + const [desc] = getDescriptor('(+"5").toFixed;'); + + expect(desc).toEqual({ + kind: "property", + object: "Number", + key: "toFixed", + placement: "prototype", + }); + }); + + it("instance property - unary minus on number literal produces number", () => { + const [desc] = getDescriptor("(-1).toFixed;"); + + expect(desc).toEqual({ + kind: "property", + object: "Number", + key: "toFixed", + placement: "prototype", + }); + }); + + it("instance property - unary minus on unknown is ambiguous", () => { + const [desc] = getDescriptor("(-x).foo;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: null, + key: "foo", + placement: null, + }); + }); + + it("instance property - unary minus on bigint produces bigint", () => { + const [desc] = getDescriptor("(-1n).toString;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: "BigInt", + key: "toString", + placement: "prototype", + }); + }); + + it("instance property - unary minus on const bigint produces bigint", () => { + const [desc] = getDescriptor("const n = 1n; (-n).toString;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: "BigInt", + key: "toString", + placement: "prototype", + }); + }); + + it("instance property - bitwise not on number literal produces number", () => { + const [desc] = getDescriptor("(~1).toFixed;"); + + expect(desc).toEqual({ + kind: "property", + object: "Number", + key: "toFixed", + placement: "prototype", + }); + }); + + it("instance property - bitwise not on unknown is ambiguous", () => { + const [desc] = getDescriptor("(~x).foo;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: null, + key: "foo", + placement: null, + }); + }); + + it("instance property - bitwise not on bigint produces bigint", () => { + const [desc] = getDescriptor("(~1n).toString;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: "BigInt", + key: "toString", + placement: "prototype", + }); + }); + + it("instance property - postfix increment on non-constant is ambiguous", () => { + const [desc] = getDescriptor("var i = 0; (i++).toFixed;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: null, + key: "toFixed", + placement: null, + }); + }); + + it("instance property - subtraction of numbers produces number", () => { + const [desc] = getDescriptor( + "var a = 1, b = 2; (a - b).toFixed;", + "property", + ); + + expect(desc).toEqual({ + kind: "property", + object: "Number", + key: "toFixed", + placement: "prototype", + }); + }); + + it("instance property - subtraction of bigints produces bigint", () => { + const [desc] = getDescriptor("(1n - 2n).toString;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: "BigInt", + key: "toString", + placement: "prototype", + }); + }); + + it("instance property - subtraction of bigint variables produces bigint", () => { + const [desc] = getDescriptor( + "const a = 1n, b = 2n; (a - b).toString;", + "property", + ); + + expect(desc).toEqual({ + kind: "property", + object: "BigInt", + key: "toString", + placement: "prototype", + }); + }); + + it("instance property - subtraction with unknown operands is ambiguous", () => { + const [desc] = getDescriptor("var a; var b; (a - b).foo;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: null, + key: "foo", + placement: null, + }); + }); + + it("instance property - multiplication produces number", () => { + const [desc] = getDescriptor( + "var a = 1, b = 2; (a * b).toFixed;", + "property", + ); + + expect(desc).toEqual({ + kind: "property", + object: "Number", + key: "toFixed", + placement: "prototype", + }); + }); + + it("instance property - bitwise and produces number", () => { + const [desc] = getDescriptor( + "var a = 1, b = 2; (a & b).toFixed;", + "property", + ); + + expect(desc).toEqual({ + kind: "property", + object: "Number", + key: "toFixed", + placement: "prototype", + }); + }); + + it("instance property - unsigned right shift always produces number", () => { + const [desc] = getDescriptor("(x >>> 1).toFixed;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: "Number", + key: "toFixed", + placement: "prototype", + }); + }); + + it("instance property - strict equality produces boolean", () => { + const [desc] = getDescriptor( + "var a = 1, b = 2; (a === b).toString;", + "property", + ); + + expect(desc).toEqual({ + kind: "property", + object: "Boolean", + key: "toString", + placement: "prototype", + }); + }); + + it("instance property - instanceof produces boolean", () => { + const [desc] = getDescriptor( + "var a = []; (a instanceof Array).toString;", + "property", + ); + + expect(desc).toEqual({ + kind: "property", + object: "Boolean", + key: "toString", + placement: "prototype", + }); + }); + + it("instance property - addition of two numbers produces number", () => { + const [desc] = getDescriptor("(1 + 2).toFixed;"); + + expect(desc).toEqual({ + kind: "property", + object: "Number", + key: "toFixed", + placement: "prototype", + }); + }); + + it("instance property - addition with string produces string", () => { + const [desc] = getDescriptor('("a" + 1).includes;'); + + expect(desc).toEqual({ + kind: "property", + object: "String", + key: "includes", + placement: "prototype", + }); + }); + + it("instance property - addition string on right produces string", () => { + const [desc] = getDescriptor('(1 + "b").includes;'); + + expect(desc).toEqual({ + kind: "property", + object: "String", + key: "includes", + placement: "prototype", + }); + }); + + it("instance property - addition of two strings produces string", () => { + const [desc] = getDescriptor('("a" + "b").includes;'); + + expect(desc).toEqual({ + kind: "property", + object: "String", + key: "includes", + placement: "prototype", + }); + }); + + it("instance property - addition of template literal and number produces string", () => { + const [desc] = getDescriptor("(`a` + 1).includes;"); + + expect(desc).toEqual({ + kind: "property", + object: "String", + key: "includes", + placement: "prototype", + }); + }); + + it("instance property - addition of two bigints produces bigint", () => { + const [desc] = getDescriptor("(1n + 2n).toString;"); + + expect(desc).toEqual({ + kind: "property", + object: "BigInt", + key: "toString", + placement: "prototype", + }); + }); + + it("instance property - addition of number variables produces number", () => { + const [desc] = getDescriptor( + "const a = 1, b = 2; (a + b).toFixed;", + "property", + ); + + expect(desc).toEqual({ + kind: "property", + object: "Number", + key: "toFixed", + placement: "prototype", + }); + }); + + it("instance property - addition with unknown operands is ambiguous", () => { + const [desc] = getDescriptor("var a; var b; (a + b).foo;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: null, + key: "foo", + placement: null, + }); + }); + + it("instance property - sequence expression resolves last element", () => { + const [desc] = getDescriptor("var a; (a, []).includes;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: "Array", + key: "includes", + placement: "prototype", + }); + }); + + it("instance property - assignment expression resolves right side", () => { + const [desc] = getDescriptor("var a; (a = []).includes;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: "Array", + key: "includes", + placement: "prototype", + }); + }); + + it("instance property - conditional same type on both branches", () => { + const [desc] = getDescriptor("var a; (a ? [] : []).includes;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: "Array", + key: "includes", + placement: "prototype", + }); + }); + + it("instance property - conditional different types on branches", () => { + const [desc] = getDescriptor("var a; (a ? [] : {}).foo;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: null, + key: "foo", + placement: null, + }); + }); + + it("instance property - resolved through variable", () => { + const [desc] = getDescriptor("const x = []; x.includes;", "property"); + + expect(desc).toEqual({ + kind: "property", + object: "Array", + key: "includes", + placement: "prototype", + }); + }); + + it("instance property - resolved through multiple variables", () => { + const [desc] = getDescriptor( + "const x = []; const y = x; y.includes;", + "property", + ); + + expect(desc).toEqual({ + kind: "property", + object: "Array", + key: "includes", + placement: "prototype", + }); + }); }); diff --git a/packages/babel-plugin-polyfill-corejs3/test/fixtures/usage-global/all-corejs-3.21/output.mjs b/packages/babel-plugin-polyfill-corejs3/test/fixtures/usage-global/all-corejs-3.21/output.mjs index b833e0ca..db251beb 100644 --- a/packages/babel-plugin-polyfill-corejs3/test/fixtures/usage-global/all-corejs-3.21/output.mjs +++ b/packages/babel-plugin-polyfill-corejs3/test/fixtures/usage-global/all-corejs-3.21/output.mjs @@ -16,7 +16,6 @@ import "core-js/modules/es.set.js"; import "core-js/modules/es.string.iterator.js"; import "core-js/modules/es.string.match.js"; import "core-js/modules/esnext.array.with.js"; -import "core-js/modules/esnext.iterator.constructor.js"; import "core-js/modules/web.btoa.js"; import "core-js/modules/web.dom-collections.iterator.js"; import "core-js/modules/web.dom-exception.constructor.js"; diff --git a/packages/babel-plugin-polyfill-corejs3/test/fixtures/usage-global/all-proposals-corejs-3.20/output.mjs b/packages/babel-plugin-polyfill-corejs3/test/fixtures/usage-global/all-proposals-corejs-3.20/output.mjs index a63a297b..2cbd08de 100644 --- a/packages/babel-plugin-polyfill-corejs3/test/fixtures/usage-global/all-proposals-corejs-3.20/output.mjs +++ b/packages/babel-plugin-polyfill-corejs3/test/fixtures/usage-global/all-proposals-corejs-3.20/output.mjs @@ -17,8 +17,6 @@ import "core-js/modules/es.string.iterator.js"; import "core-js/modules/es.string.match.js"; import "core-js/modules/esnext.array.group-by-to-map.js"; import "core-js/modules/esnext.array.with.js"; -import "core-js/modules/esnext.async-iterator.constructor.js"; -import "core-js/modules/esnext.iterator.constructor.js"; import "core-js/modules/esnext.map.delete-all.js"; import "core-js/modules/esnext.map.emplace.js"; import "core-js/modules/esnext.map.every.js"; diff --git a/packages/babel-plugin-polyfill-corejs3/test/fixtures/usage-global/typed-array-at-proposals-corejs-3.8/output.mjs b/packages/babel-plugin-polyfill-corejs3/test/fixtures/usage-global/typed-array-at-proposals-corejs-3.8/output.mjs index 311f2fec..95eca298 100644 --- a/packages/babel-plugin-polyfill-corejs3/test/fixtures/usage-global/typed-array-at-proposals-corejs-3.8/output.mjs +++ b/packages/babel-plugin-polyfill-corejs3/test/fixtures/usage-global/typed-array-at-proposals-corejs-3.8/output.mjs @@ -27,7 +27,5 @@ import "core-js/modules/es.typed-array.sort.js"; import "core-js/modules/es.typed-array.subarray.js"; import "core-js/modules/es.typed-array.to-locale-string.js"; import "core-js/modules/es.typed-array.to-string.js"; -import "core-js/modules/esnext.array.at.js"; -import "core-js/modules/esnext.string.at.js"; import "core-js/modules/esnext.typed-array.at.js"; new Int8Array(1).at(-1);