Skip to content

Commit 2fff492

Browse files
fix(javascript): keep any unread class whose heritage may be no constructor
The heritage check moves into the side-effects rules, through a `heritageConstructs` hook only the minifier sets, so an unread class expression `class extends f {}` keeps the TypeError it throws as a declaration does. A heritage constructs where it is `null`, an unset built-in constructor, or a class a declaration or a variable a class expression sets made before it and never set since. Claude-Session: https://claude.ai/code/session_01Tgxgyb4N35ndP6cC4KcrXC
1 parent 9a4527c commit 2fff492

3 files changed

Lines changed: 107 additions & 47 deletions

File tree

‎lib/javascript/side-effects.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const { Field, Flag, NodeType, Part } = require("./syntax-parser");
2424
* @property {boolean=} comparesConvert whether a comparison or an object's computed key converting what may be an object runs code, as the compressor's `pure_conversions: false` has it
2525
* @property {((node: number) => boolean | undefined)=} decides what the caller answers for a node before the rules read it, undefined to leave it to them
2626
* @property {((node: number) => number)=} readValue the value a name prints as where it is read, 0 for none
27+
* @property {((node: number) => boolean)=} heritageConstructs whether a class's heritage, past parentheses, is known to be a constructor or `null`, where a class extending anything else may throw; unset, any heritage making nothing happen is taken as one
2728
*/
2829

2930
/**
@@ -699,6 +700,8 @@ const ruledSideEffects = (tree, node, context) => {
699700
if (
700701
superClass !== 0 &&
701702
(extendsNoConstructor(tree, superClass) ||
703+
(context.heritageConstructs !== undefined &&
704+
!context.heritageConstructs(superClass)) ||
702705
hasSideEffects(tree, superClass, context))
703706
) {
704707
return true;

‎lib/javascript/syntax-printer.js‎

Lines changed: 87 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8215,35 +8215,43 @@ const createNodePrinter = (modules) => {
82158215
let constructedWhenCalled;
82168216

82178217
/**
8218-
* Whether a `new` of a built-in nothing assigns constructs as a call of it
8219-
* does, as webpack's improvements have it; a `RegExp` call hands back an
8220-
* expression it is given, so only one of a primitive or nothing does.
8221-
* @param {number} node the `new`
8222-
* @returns {boolean} true where it prints as the call
8218+
* @param {number} node a name
8219+
* @returns {boolean} whether it reads a built-in constructor nothing assigns, one constructing the same called without `new`
82238220
*/
8224-
const constructsWhenCalled = (node) => {
8225-
const callee = childOf(Field.callee, node);
8226-
if (typeOf(callee) !== NodeType.Identifier || mangledNames === null) {
8221+
const readsBuiltInConstructor = (node) => {
8222+
if (typeOf(node) !== NodeType.Identifier || mangledNames === null) {
82278223
return false;
82288224
}
8229-
const name = tree.name(callee);
82308225
if (constructedWhenCalled === undefined) {
82318226
constructedWhenCalled = new Set(
82328227
require("./syntax-printer-data").foldedBuiltIns().constructedWhenCalled
82338228
);
8234-
constructedWhenCalled.delete("RegExp");
82358229
}
8236-
const definition = mangledNames[callee];
8230+
const definition = mangledNames[node];
82378231
return (
8238-
(constructedWhenCalled.has(name) ||
8239-
(name === "RegExp" && patternIsPrimitive(node))) &&
8232+
constructedWhenCalled.has(tree.name(node)) &&
82408233
isMinifierVariable(definition) &&
82418234
definition.undeclared &&
82428235
!definition.written &&
82438236
!(/** @type {MinifierScope} */ (definition.scope).isPinned())
82448237
);
82458238
};
82468239

8240+
/**
8241+
* Whether a `new` of a built-in nothing assigns constructs as a call of it
8242+
* does, as webpack's improvements have it; a `RegExp` call hands back an
8243+
* expression it is given, so only one of a primitive or nothing does.
8244+
* @param {number} node the `new`
8245+
* @returns {boolean} true where it prints as the call
8246+
*/
8247+
const constructsWhenCalled = (node) => {
8248+
const callee = childOf(Field.callee, node);
8249+
return (
8250+
readsBuiltInConstructor(callee) &&
8251+
(tree.name(callee) !== "RegExp" || patternIsPrimitive(node))
8252+
);
8253+
};
8254+
82478255
/**
82488256
* Whether a name reads the global `undefined`, read where a unary operator
82498257
* may stand for it.
@@ -8345,7 +8353,8 @@ const createNodePrinter = (modules) => {
83458353
nameKind,
83468354
isPureCall,
83478355
readValue: collapsedValue,
8348-
comparesConvert
8356+
comparesConvert,
8357+
heritageConstructs
83498358
};
83508359
}
83518360
sideEffectsContext.decides =
@@ -8572,28 +8581,28 @@ const createNodePrinter = (modules) => {
85728581
keepsEffect[declarator] === 1 && aliasesRead[declarator] !== 3;
85738582

85748583
/**
8575-
* Whether defining a class makes nothing happen: what it runs is pure and
8576-
* its heritage is a constructor, which only a class made before it is known
8577-
* to be, so extending an arrow or a generator still throws where it was.
8578-
* @param {number} node a class declaration
8579-
* @returns {boolean} whether it may go unread
8584+
* Whether a class's heritage is a constructor, so defining the class does
8585+
* not throw: `null`, a built-in one, or a class made before it and never
8586+
* set since, which an arrow or a generator a name holds is not.
8587+
* @param {number} heritage the heritage, past parentheses
8588+
* @returns {boolean} true where it is known to construct
85808589
*/
8581-
const definesNothing = (node) => {
8582-
const heritage = childOf(Field.superClass, node);
8583-
if (heritage !== 0 && !(madeReads.size !== 0 && madeReads.has(heritage))) {
8584-
const left =
8585-
typeOf(heritage) === NodeType.LogicalExpression &&
8586-
tree.textOf(Part.operator, heritage) === "&&"
8587-
? childOf(Field.left, heritage)
8588-
: heritage;
8589-
if (
8590-
typeOf(left) !== NodeType.Literal ||
8591-
tree.textOf(Part.raw, left) !== "null"
8592-
) {
8593-
return false;
8594-
}
8590+
const heritageConstructs = (heritage) => {
8591+
if (
8592+
(madeReads.size !== 0 && madeReads.has(heritage)) ||
8593+
readsBuiltInConstructor(heritage)
8594+
) {
8595+
return true;
85958596
}
8596-
return isPureValue(node);
8597+
const left =
8598+
typeOf(heritage) === NodeType.LogicalExpression &&
8599+
tree.textOf(Part.operator, heritage) === "&&"
8600+
? childOf(Field.left, heritage)
8601+
: heritage;
8602+
return (
8603+
typeOf(left) === NodeType.Literal &&
8604+
tree.textOf(Part.raw, left) === "null"
8605+
);
85978606
};
85988607

85998608
/**
@@ -8608,7 +8617,7 @@ const createNodePrinter = (modules) => {
86088617
if (type === NodeType.ClassDeclaration) {
86098618
return (
86108619
isUnusedBinding(tree.field(0, Field.id, candidate)) &&
8611-
definesNothing(candidate)
8620+
isPureValue(candidate)
86128621
);
86138622
}
86148623
if (!isUnusedBinding(childOf(Field.id, candidate))) return false;
@@ -10297,27 +10306,55 @@ const createNodePrinter = (modules) => {
1029710306
};
1029810307

1029910308
/**
10300-
* Notes a class extending a class declared before it in the same list,
10301-
* and never set again, whose read there is made and so cannot throw.
10302-
* @param {number} declaration a class declaration of a list
10309+
* @param {number} statement a statement of a list
10310+
* @param {TreeDefinition} definition a name's definition
10311+
* @param {number=} count how many of a declaration's first declarators to read, all where not given
10312+
* @returns {boolean} whether it makes that name a class: declares the class, or a variable a class expression sets
10313+
*/
10314+
const makesClass = (statement, definition, count = -1) => {
10315+
const names = /** @type {TreeDefinition[]} */ (mangledNames);
10316+
if (typeOf(statement) === NodeType.ExportNamedDeclaration) {
10317+
statement = tree.field(0, Field.declaration, statement);
10318+
}
10319+
const type = statement === 0 ? 0 : typeOf(statement);
10320+
if (type === NodeType.ClassDeclaration) {
10321+
return names[tree.field(0, Field.id, statement)] === definition;
10322+
}
10323+
if (type !== NodeType.VariableDeclaration) return false;
10324+
if (count === -1) count = tree.fieldCount(Field.declarations, statement);
10325+
for (let i = 0; i < count; i++) {
10326+
const declarator = tree.field(i, Field.declarations, statement);
10327+
if (names[tree.field(0, Field.id, declarator)] === definition) {
10328+
return (
10329+
typeOf(childOf(Field.init, declarator)) === NodeType.ClassExpression
10330+
);
10331+
}
10332+
}
10333+
return false;
10334+
};
10335+
10336+
/**
10337+
* Notes a class extending a class made before it in the same list, and
10338+
* never set again, whose read there is made and so cannot throw.
10339+
* @param {number} node a class declaration of a list, or a class expression one of its declarators sets
1030310340
* @param {number} list the list
10304-
* @param {number} index its place there
10341+
* @param {number} index the place there of the statement holding it
10342+
* @param {number=} declarator where it is a declarator's value, that declarator's place in the statement, whose declarators before it are read too
1030510343
* @returns {void}
1030610344
*/
10307-
const noteExtendsDeclared = (declaration, list, index) => {
10308-
const superClass = childOf(Field.superClass, declaration);
10345+
const noteExtendsDeclared = (node, list, index, declarator = 0) => {
10346+
const superClass = childOf(Field.superClass, node);
1030910347
if (typeOf(superClass) !== NodeType.Identifier) return;
1031010348
const definition = /** @type {TreeDefinition[]} */ (mangledNames)[
1031110349
superClass
1031210350
];
1031310351
if (!isMinifierVariable(definition) || definition.written) return;
10314-
for (let i = 0; i < index; i++) {
10352+
for (let i = 0; i <= index; i++) {
1031510353
const statement = tree.field(i, Field.body, list);
1031610354
if (
10317-
typeOf(statement) === NodeType.ClassDeclaration &&
10318-
/** @type {TreeDefinition[]} */ (mangledNames)[
10319-
tree.field(0, Field.id, statement)
10320-
] === definition
10355+
i < index
10356+
? makesClass(statement, definition)
10357+
: declarator > 0 && makesClass(statement, definition, declarator)
1032110358
) {
1032210359
madeReads.add(superClass);
1032310360
return;
@@ -10388,6 +10425,9 @@ const createNodePrinter = (modules) => {
1038810425
keepsEffect[declarator] = 2;
1038910426
}
1039010427
const init = childOf(Field.init, declarator);
10428+
if (typeOf(init) === NodeType.ClassExpression) {
10429+
noteExtendsDeclared(init, node, i, j);
10430+
}
1039110431
if (quiet && init !== 0 && findNode(init, findRun)) quiet = false;
1039210432
}
1039310433
} else if (
@@ -11931,7 +11971,7 @@ const createNodePrinter = (modules) => {
1193111971
if (!mayGoUnread(id)) return 0;
1193211972
if (type === NodeType.FunctionDeclaration) return id;
1193311973
if (type === NodeType.ClassDeclaration) {
11934-
return definesNothing(candidate) ? id : 0;
11974+
return isPureValue(candidate) ? id : 0;
1193511975
}
1193611976
const init = childOf(Field.init, candidate);
1193711977
return init !== 0 &&

‎test/unitCases/javascript/printer.unittest.js‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,23 @@ describe("JavaScript minifier printing a SyntaxTree", () => {
13981398
"(function(){var f = () => {}; class C extends f{} g()})()",
13991399
"!function(){var f=()=>{};class C extends f{}g()}();"
14001400
],
1401+
[
1402+
"(function(){var f = () => {}; var C = class extends f{}; g()})()",
1403+
"!function(){var f=()=>{};(class extends f{});g()}();"
1404+
],
1405+
// A class a variable is set to before, and never again, constructs.
1406+
[
1407+
"(function(){var B = class {}; var C = class extends B{}; g()})()",
1408+
"g();"
1409+
],
1410+
[
1411+
"(function(){var B = class {}; B = h(); var C = class extends B{}; g()})()",
1412+
"!function(){var B=class{};B=h();(class extends B{});g()}();"
1413+
],
1414+
[
1415+
"(function(){var C = class extends B{}; var B = class {}; g()})()",
1416+
"!function(){(class extends B{});var B=class{};g()}();"
1417+
],
14011418
[
14021419
"(function(){class C extends K{} class K{} g()})()",
14031420
"!function(){class C extends K{}class K{}g()}();"

0 commit comments

Comments
 (0)