From ec88340ddc8f8755cfd4aa144918e5f46d29fca4 Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Tue, 23 Mar 2021 00:38:47 +0000 Subject: [PATCH] Handle functions/classes with name reserved word [fix] Fixes #52. --- lib/serialize/parseFunction.js | 6 ++-- lib/serialize/utils.js | 16 ++++++++- lib/serialize/varNames.js | 20 ++--------- test/classes.test.js | 64 ++++++++++++++++++++++++++++++++++ test/functions.test.js | 36 +++++++++++++++++++ 5 files changed, 121 insertions(+), 21 deletions(-) diff --git a/lib/serialize/parseFunction.js b/lib/serialize/parseFunction.js index b917bb3d..b68ba454 100644 --- a/lib/serialize/parseFunction.js +++ b/lib/serialize/parseFunction.js @@ -22,7 +22,7 @@ const {identifierIsVariable, replaceWith} = require('../shared/functions.js'), EVAL_VAR_NAME_BODY, EVAL_COMMENT } = require('../shared/constants.js'), {createUnmangledVarNameTransform} = require('./varNames.js'), - {isJsIdentifier, isNumberKey} = require('./utils.js'), + {isJsIdentifier, isReservedWord, isNumberKey} = require('./utils.js'), {transpiledFiles} = require('../internal.js'), assertBug = require('../shared/assertBug.js'); @@ -627,7 +627,7 @@ module.exports = function parseFunction( const originalName = node.id.name; if (name !== originalName) { if ( - name && isJsIdentifier(name) && !containsEval + name && isJsIdentifier(name) && !isReservedWord(name) && !containsEval && !globalVarNames.has(name) && !functionNames.has(name) ) { // Rename all references to name @@ -644,7 +644,7 @@ module.exports = function parseFunction( } } } - } else if (name && (!isJsIdentifier(name) || globalVarNames.has(name))) { + } else if (name && (!isJsIdentifier(name) || isReservedWord(name) || globalVarNames.has(name))) { name = ''; } diff --git a/lib/serialize/utils.js b/lib/serialize/utils.js index 06c74298..b77fcb3c 100644 --- a/lib/serialize/utils.js +++ b/lib/serialize/utils.js @@ -6,7 +6,8 @@ 'use strict'; // Modules -const t = require('@babel/types'); +const checkReservedWord = require('reserved-words').check, + t = require('@babel/types'); // Imports const assertBug = require('../shared/assertBug.js'); @@ -15,6 +16,7 @@ const assertBug = require('../shared/assertBug.js'); module.exports = { isJsIdentifier, + isReservedWord, isNumberKey, isIntegerKey, toJsIdentifier, @@ -37,6 +39,18 @@ function isJsIdentifier(name) { const JS_ID_REGEX = /^[A-Za-z$_][A-Za-z0-9$_]*$/u; +/** + * Determine if var name is a JS reserved word e.g. 'break', 'class'. + * 'arguments' + 'eval' are additionally treated as a reserved words + * as they cannot be used as var names in strict mode. + * @param {string} name - Variable name + * @returns {boolean} - `true` if reserved word, `false` if not + */ +function isReservedWord(name) { + if (name === 'arguments' || name === 'eval') return true; + return checkReservedWord(name, 'es6', true); +} + /** * Determine if string is valid number object key. * e.g. `{0: 'a'}`, `{22: 'a'}` diff --git a/lib/serialize/varNames.js b/lib/serialize/varNames.js index 78010dff..a1a8e093 100644 --- a/lib/serialize/varNames.js +++ b/lib/serialize/varNames.js @@ -5,8 +5,8 @@ 'use strict'; -// Modules -const checkReservedWord = require('reserved-words').check; +// Imports +const {isReservedWord} = require('./utils.js'); // Exports @@ -29,12 +29,7 @@ class VarNameFactory { if (reservedNames && reservedNames.has(name)) return true; // Check if JS reserved word - if (isReservedWord(name)) return true; - - // Treat 'arguments' as a reserved word (NB 'this' is already caught by `checkReservedWord()`) - if (name === 'arguments') return true; - - return false; + return isReservedWord(name); } } @@ -131,12 +126,3 @@ module.exports = { MangledVarNameFactory, UnmangledVarNameFactory }; - -/** - * Determine if var name is a JS reserved word e.g. 'break', 'class'. - * @param {string} name - Variable name - * @returns {boolean} - `true` if reserved word, `false` if not - */ -function isReservedWord(name) { - return checkReservedWord(name, 'es6', true); -} diff --git a/test/classes.test.js b/test/classes.test.js index 19fce411..cd94ab61 100644 --- a/test/classes.test.js +++ b/test/classes.test.js @@ -2994,6 +2994,70 @@ describe('Classes', () => { }); }); + describe('name maintained where', () => { + itSerializes('unnamed class as object property', { + in: () => ({a: (0, class {})}), + out: '{a:(0,class{})}', + validate(obj) { + expect(obj).toBeObject(); + expect(obj).toContainAllKeys(['a']); + const Klass = obj.a; + expect(Klass).toBeFunction(); + expect(Klass.name).toBe(''); + } + }); + + itSerializes('not valid JS identifier', { + in: () => ({'0a': class {}}['0a']), + out: 'Object.defineProperties(class{},{name:{value:"0a",configurable:true}})', + validate(Klass) { + expect(Klass).toHaveOwnPropertyNames(['length', 'prototype', 'name']); + expect(Klass.name).toBe('0a'); + expect(Klass).toHaveDescriptorModifiersFor('name', false, false, true); + } + }); + + itSerializes('reserved word', { + in: () => ({export: class {}}.export), + out: 'Object.defineProperties(class{},{name:{value:"export",configurable:true}})', + validate(Klass) { + expect(Klass).toHaveOwnPropertyNames(['length', 'prototype', 'name']); + expect(Klass.name).toBe('export'); + expect(Klass).toHaveDescriptorModifiersFor('name', false, false, true); + } + }); + + itSerializes('arguments', { + in: () => ({arguments: class {}}.arguments), + out: 'Object.defineProperties(class{},{name:{value:"arguments",configurable:true}})', + validate(Klass) { + expect(Klass).toHaveOwnPropertyNames(['length', 'prototype', 'name']); + expect(Klass.name).toBe('arguments'); + expect(Klass).toHaveDescriptorModifiersFor('name', false, false, true); + } + }); + + itSerializes('eval', { + in: () => ({eval: class {}}.eval), + out: 'Object.defineProperties(class{},{name:{value:"eval",configurable:true}})', + validate(Klass) { + expect(Klass).toHaveOwnPropertyNames(['length', 'prototype', 'name']); + expect(Klass.name).toBe('eval'); + expect(Klass).toHaveDescriptorModifiersFor('name', false, false, true); + } + }); + + itSerializes('not valid JS identifier with static method', { + in: () => ({'0a': class {static foo() {}}}['0a']), + out: 'Object.defineProperties(class{},{foo:{value:{foo(){}}.foo,writable:true,configurable:true},name:{value:"0a",configurable:true}})', + validate(Klass) { + expectClassToHaveOwnPropertyNames(Klass, ['length', 'prototype', 'foo', 'name']); + expect(Klass.name).toBe('0a'); + expect(Klass).toHaveDescriptorModifiersFor('name', false, false, true); + } + }); + }); + describe('classes nested in functions left untouched', () => { describe('not extending another class', () => { itSerializes('empty', { diff --git a/test/functions.test.js b/test/functions.test.js index 781c57ba..737d909b 100644 --- a/test/functions.test.js +++ b/test/functions.test.js @@ -4695,6 +4695,42 @@ describe('Functions', () => { } }); + itSerializes('reserved word', { + in: () => ({export: function() {}}.export), // eslint-disable-line object-shorthand + out: 'Object.defineProperties(function(){},{name:{value:"export"}})', + validate(fn) { + expect( + Object.getOwnPropertyNames(fn).filter(key => key !== 'arguments' && key !== 'caller') + ).toEqual(['length', 'name', 'prototype']); + expect(fn.name).toBe('export'); + expect(fn).toHaveDescriptorModifiersFor('name', false, false, true); + } + }); + + itSerializes('arguments', { + in: () => ({arguments: function() {}}.arguments), // eslint-disable-line object-shorthand + out: 'Object.defineProperties(function(){},{name:{value:"arguments"}})', + validate(fn) { + expect( + Object.getOwnPropertyNames(fn).filter(key => key !== 'arguments' && key !== 'caller') + ).toEqual(['length', 'name', 'prototype']); + expect(fn.name).toBe('arguments'); + expect(fn).toHaveDescriptorModifiersFor('name', false, false, true); + } + }); + + itSerializes('eval', { + in: () => ({eval: function() {}}.eval), // eslint-disable-line object-shorthand + out: 'Object.defineProperties(function(){},{name:{value:"eval"}})', + validate(fn) { + expect( + Object.getOwnPropertyNames(fn).filter(key => key !== 'arguments' && key !== 'caller') + ).toEqual(['length', 'name', 'prototype']); + expect(fn.name).toBe('eval'); + expect(fn).toHaveDescriptorModifiersFor('name', false, false, true); + } + }); + describe('descriptor altered', () => { itSerializes('value altered', { in() {