From 97153206ad07c98e6f8f4f0a62b1f52eecbdf847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Mon, 19 Feb 2024 13:09:15 +0900 Subject: [PATCH] fix(es/minifier): Abort property hoister on `this` usage (#8647) **Related issue:** - Closes #8643 --- .../src/compress/optimize/props.rs | 6 +- .../tests/fixture/issues/8643/input.js | 391 ++++++++++++++++++ .../tests/fixture/issues/8643/output.js | 145 +++++++ .../hoist_props/contains_this_2/output.js | 10 +- .../compress/hoist_props/new_this/output.js | 12 +- 5 files changed, 554 insertions(+), 10 deletions(-) create mode 100644 crates/swc_ecma_minifier/tests/fixture/issues/8643/input.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/issues/8643/output.js diff --git a/crates/swc_ecma_minifier/src/compress/optimize/props.rs b/crates/swc_ecma_minifier/src/compress/optimize/props.rs index 9e9c12a38a66..2d81b9d11c03 100644 --- a/crates/swc_ecma_minifier/src/compress/optimize/props.rs +++ b/crates/swc_ecma_minifier/src/compress/optimize/props.rs @@ -1,6 +1,6 @@ use swc_common::{util::take::Take, DUMMY_SP}; use swc_ecma_ast::*; -use swc_ecma_utils::{private_ident, prop_name_eq, ExprExt}; +use swc_ecma_utils::{contains_this_expr, private_ident, prop_name_eq, ExprExt}; use super::{unused::PropertyAccessOpts, Optimizer}; use crate::util::deeply_contains_this_expr; @@ -222,7 +222,9 @@ impl Optimizer<'_> { fn is_expr_fine_for_hoist_props(value: &Expr) -> bool { match value { - Expr::Ident(..) | Expr::Lit(..) | Expr::Arrow(..) | Expr::Fn(..) | Expr::Class(..) => true, + Expr::Ident(..) | Expr::Lit(..) | Expr::Arrow(..) | Expr::Class(..) => true, + + Expr::Fn(f) => !contains_this_expr(&f.function.body), Expr::Unary(u) => match u.op { op!("void") | op!("typeof") | op!("!") => is_expr_fine_for_hoist_props(&u.arg), diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/8643/input.js b/crates/swc_ecma_minifier/tests/fixture/issues/8643/input.js new file mode 100644 index 000000000000..3539a2b12c7c --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/issues/8643/input.js @@ -0,0 +1,391 @@ + +const Cache = { + + enabled: false, + + files: {}, + + add: function (key, file) { + + if (this.enabled === false) return; + + // console.log( 'THREE.Cache', 'Adding key:', key ); + + this.files[key] = file; + + }, + + get: function (key) { + + if (this.enabled === false) return; + + // console.log( 'THREE.Cache', 'Checking key:', key ); + + return this.files[key]; + + }, + + remove: function (key) { + + delete this.files[key]; + + }, + + clear: function () { + + this.files = {}; + + } + +}; + + + + +class Loader { + + constructor(manager) { + + this.manager = (manager !== undefined) ? manager : DefaultLoadingManager; + + this.crossOrigin = 'anonymous'; + this.withCredentials = false; + this.path = ''; + this.resourcePath = ''; + this.requestHeader = {}; + + } + + load( /* url, onLoad, onProgress, onError */) { } + + loadAsync(url, onProgress) { + + const scope = this; + + return new Promise(function (resolve, reject) { + + scope.load(url, resolve, onProgress, reject); + + }); + + } + + parse( /* data */) { } + + setCrossOrigin(crossOrigin) { + + this.crossOrigin = crossOrigin; + return this; + + } + + setWithCredentials(value) { + + this.withCredentials = value; + return this; + + } + + setPath(path) { + + this.path = path; + return this; + + } + + setResourcePath(resourcePath) { + + this.resourcePath = resourcePath; + return this; + + } + + setRequestHeader(requestHeader) { + + this.requestHeader = requestHeader; + return this; + + } + +} + +Loader.DEFAULT_MATERIAL_NAME = '__DEFAULT'; + +const loading = {}; + +class HttpError extends Error { + + constructor(message, response) { + + super(message); + this.response = response; + + } + +} + +export class FileLoader extends Loader { + + constructor(manager) { + + super(manager); + + } + + load(url, onLoad, onProgress, onError) { + + if (url === undefined) url = ''; + + if (this.path !== undefined) url = this.path + url; + + url = this.manager.resolveURL(url); + + const cached = Cache.get(url); + + if (cached !== undefined) { + + this.manager.itemStart(url); + + setTimeout(() => { + + if (onLoad) onLoad(cached); + + this.manager.itemEnd(url); + + }, 0); + + return cached; + + } + + // Check if request is duplicate + + if (loading[url] !== undefined) { + + loading[url].push({ + + onLoad: onLoad, + onProgress: onProgress, + onError: onError + + }); + + return; + + } + + // Initialise array for duplicate requests + loading[url] = []; + + loading[url].push({ + onLoad: onLoad, + onProgress: onProgress, + onError: onError, + }); + + // create request + const req = new Request(url, { + headers: new Headers(this.requestHeader), + credentials: this.withCredentials ? 'include' : 'same-origin', + // An abort controller could be added within a future PR + }); + + // record states ( avoid data race ) + const mimeType = this.mimeType; + const responseType = this.responseType; + + // start the fetch + fetch(req) + .then(response => { + + if (response.status === 200 || response.status === 0) { + + // Some browsers return HTTP Status 0 when using non-http protocol + // e.g. 'file://' or 'data://'. Handle as success. + + if (response.status === 0) { + + console.warn('THREE.FileLoader: HTTP Status 0 received.'); + + } + + // Workaround: Checking if response.body === undefined for Alipay browser #23548 + + if (typeof ReadableStream === 'undefined' || response.body === undefined || response.body.getReader === undefined) { + + return response; + + } + + const callbacks = loading[url]; + const reader = response.body.getReader(); + + // Nginx needs X-File-Size check + // https://serverfault.com/questions/482875/why-does-nginx-remove-content-length-header-for-chunked-content + const contentLength = response.headers.get('Content-Length') || response.headers.get('X-File-Size'); + const total = contentLength ? parseInt(contentLength) : 0; + const lengthComputable = total !== 0; + let loaded = 0; + + // periodically read data into the new stream tracking while download progress + const stream = new ReadableStream({ + start(controller) { + + readData(); + + function readData() { + + reader.read().then(({ done, value }) => { + + if (done) { + + controller.close(); + + } else { + + loaded += value.byteLength; + + const event = new ProgressEvent('progress', { lengthComputable, loaded, total }); + for (let i = 0, il = callbacks.length; i < il; i++) { + + const callback = callbacks[i]; + if (callback.onProgress) callback.onProgress(event); + + } + + controller.enqueue(value); + readData(); + + } + + }); + + } + + } + + }); + + return new Response(stream); + + } else { + + throw new HttpError(`fetch for "${response.url}" responded with ${response.status}: ${response.statusText}`, response); + + } + + }) + .then(response => { + + switch (responseType) { + + case 'arraybuffer': + + return response.arrayBuffer(); + + case 'blob': + + return response.blob(); + + case 'document': + + return response.text() + .then(text => { + + const parser = new DOMParser(); + return parser.parseFromString(text, mimeType); + + }); + + case 'json': + + return response.json(); + + default: + + if (mimeType === undefined) { + + return response.text(); + + } else { + + // sniff encoding + const re = /charset="?([^;"\s]*)"?/i; + const exec = re.exec(mimeType); + const label = exec && exec[1] ? exec[1].toLowerCase() : undefined; + const decoder = new TextDecoder(label); + return response.arrayBuffer().then(ab => decoder.decode(ab)); + + } + + } + + }) + .then(data => { + + // Add to cache only on HTTP success, so that we do not cache + // error response bodies as proper responses to requests. + Cache.add(url, data); + + const callbacks = loading[url]; + delete loading[url]; + + for (let i = 0, il = callbacks.length; i < il; i++) { + + const callback = callbacks[i]; + if (callback.onLoad) callback.onLoad(data); + + } + + }) + .catch(err => { + + // Abort errors and other errors are handled the same + + const callbacks = loading[url]; + + if (callbacks === undefined) { + + // When onLoad was called and url was deleted in `loading` + this.manager.itemError(url); + throw err; + + } + + delete loading[url]; + + for (let i = 0, il = callbacks.length; i < il; i++) { + + const callback = callbacks[i]; + if (callback.onError) callback.onError(err); + + } + + this.manager.itemError(url); + + }) + .finally(() => { + + this.manager.itemEnd(url); + + }); + + this.manager.itemStart(url); + + } + + setResponseType(value) { + + this.responseType = value; + return this; + + } + + setMimeType(value) { + + this.mimeType = value; + return this; + + } + +} diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/8643/output.js b/crates/swc_ecma_minifier/tests/fixture/issues/8643/output.js new file mode 100644 index 000000000000..4f161566e9ba --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/issues/8643/output.js @@ -0,0 +1,145 @@ +const Cache = { + enabled: !1, + files: {}, + add: function(key, file) { + !1 !== this.enabled && (this.files[key] = file); + }, + get: function(key) { + if (!1 !== this.enabled) return this.files[key]; + } +}; +class Loader { + constructor(manager){ + this.manager = void 0 !== manager ? manager : DefaultLoadingManager, this.crossOrigin = 'anonymous', this.withCredentials = !1, this.path = '', this.resourcePath = '', this.requestHeader = {}; + } + load() {} + loadAsync(url, onProgress) { + const scope = this; + return new Promise(function(resolve, reject) { + scope.load(url, resolve, onProgress, reject); + }); + } + parse() {} + setCrossOrigin(crossOrigin) { + return this.crossOrigin = crossOrigin, this; + } + setWithCredentials(value) { + return this.withCredentials = value, this; + } + setPath(path) { + return this.path = path, this; + } + setResourcePath(resourcePath) { + return this.resourcePath = resourcePath, this; + } + setRequestHeader(requestHeader) { + return this.requestHeader = requestHeader, this; + } +} +Loader.DEFAULT_MATERIAL_NAME = '__DEFAULT'; +const loading = {}; +class HttpError extends Error { + constructor(message, response){ + super(message), this.response = response; + } +} +export class FileLoader extends Loader { + constructor(manager){ + super(manager); + } + load(url, onLoad, onProgress, onError) { + void 0 === url && (url = ''), void 0 !== this.path && (url = this.path + url), url = this.manager.resolveURL(url); + const cached = Cache.get(url); + if (void 0 !== cached) return this.manager.itemStart(url), setTimeout(()=>{ + onLoad && onLoad(cached), this.manager.itemEnd(url); + }, 0), cached; + if (void 0 !== loading[url]) { + loading[url].push({ + onLoad: onLoad, + onProgress: onProgress, + onError: onError + }); + return; + } + loading[url] = [], loading[url].push({ + onLoad: onLoad, + onProgress: onProgress, + onError: onError + }); + const req = new Request(url, { + headers: new Headers(this.requestHeader), + credentials: this.withCredentials ? 'include' : 'same-origin' + }), mimeType = this.mimeType, responseType = this.responseType; + fetch(req).then((response)=>{ + if (200 === response.status || 0 === response.status) { + if (0 === response.status && console.warn('THREE.FileLoader: HTTP Status 0 received.'), 'undefined' == typeof ReadableStream || void 0 === response.body || void 0 === response.body.getReader) return response; + const callbacks = loading[url], reader = response.body.getReader(), contentLength = response.headers.get('Content-Length') || response.headers.get('X-File-Size'), total = contentLength ? parseInt(contentLength) : 0, lengthComputable = 0 !== total; + let loaded = 0; + return new Response(new ReadableStream({ + start (controller) { + (function readData() { + reader.read().then(({ done, value })=>{ + if (done) controller.close(); + else { + const event = new ProgressEvent('progress', { + lengthComputable, + loaded: loaded += value.byteLength, + total + }); + for(let i = 0, il = callbacks.length; i < il; i++){ + const callback = callbacks[i]; + callback.onProgress && callback.onProgress(event); + } + controller.enqueue(value), readData(); + } + }); + })(); + } + })); + } + throw new HttpError(`fetch for "${response.url}" responded with ${response.status}: ${response.statusText}`, response); + }).then((response)=>{ + switch(responseType){ + case 'arraybuffer': + return response.arrayBuffer(); + case 'blob': + return response.blob(); + case 'document': + return response.text().then((text)=>new DOMParser().parseFromString(text, mimeType)); + case 'json': + return response.json(); + default: + if (void 0 === mimeType) return response.text(); + { + const exec = /charset="?([^;"\s]*)"?/i.exec(mimeType), decoder = new TextDecoder(exec && exec[1] ? exec[1].toLowerCase() : void 0); + return response.arrayBuffer().then((ab)=>decoder.decode(ab)); + } + } + }).then((data)=>{ + Cache.add(url, data); + const callbacks = loading[url]; + delete loading[url]; + for(let i = 0, il = callbacks.length; i < il; i++){ + const callback = callbacks[i]; + callback.onLoad && callback.onLoad(data); + } + }).catch((err)=>{ + const callbacks = loading[url]; + if (void 0 === callbacks) throw this.manager.itemError(url), err; + delete loading[url]; + for(let i = 0, il = callbacks.length; i < il; i++){ + const callback = callbacks[i]; + callback.onError && callback.onError(err); + } + this.manager.itemError(url); + }).finally(()=>{ + this.manager.itemEnd(url); + }), this.manager.itemStart(url); + } + setResponseType(value) { + return this.responseType = value, this; + } + setMimeType(value) { + return this.mimeType = value, this; + } +} diff --git a/crates/swc_ecma_minifier/tests/terser/compress/hoist_props/contains_this_2/output.js b/crates/swc_ecma_minifier/tests/terser/compress/hoist_props/contains_this_2/output.js index e4bc28cfa337..5266580b38ac 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/hoist_props/contains_this_2/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/hoist_props/contains_this_2/output.js @@ -1,3 +1,7 @@ -console.log(1, 1, function () { - return this === this; -}); +var o = { + u: function() { + return this === this; + }, + p: 1 +}; +console.log(o.p, o.p, o.u); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/hoist_props/new_this/output.js b/crates/swc_ecma_minifier/tests/terser/compress/hoist_props/new_this/output.js index f9ce8e6b20ad..d3901b79cf2b 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/hoist_props/new_this/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/hoist_props/new_this/output.js @@ -1,6 +1,8 @@ -console.log( - new (function (a) { +var o = { + a: 1, + b: 2, + f: function(a) { this.b = a; - })(1).b, - 2 -); + } +}; +console.log(new o.f(o.a).b, o.b);