UNPKG

objj-transpiler

Version:

JavaScript (ECMAScript) and Objective-J compiler with preprocessor

1,320 lines (1,129 loc) 143 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('objj-parser'), require('source-map'), require('acorn-walk')) : typeof define === 'function' && define.amd ? define(['exports', 'objj-parser', 'source-map', 'acorn-walk'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ObjJCompiler = {}, global.objjParser, global.ObjectiveJ.sourceMap, global.acorn.walk)); })(this, (function (exports, objjParser, sourceMap, walk) { 'use strict'; function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var objjParser__namespace = /*#__PURE__*/_interopNamespace(objjParser); var walk__default = /*#__PURE__*/_interopDefaultLegacy(walk); class GlobalVariableMaybeWarning { constructor (/* String */ aMessage, /* SpiderMonkey AST node */ node, /* String */ code) { this.message = createMessage(aMessage, node, code); this.node = node; } checkIfWarning = function (/* Scope */ st) { const identifier = this.node.name; return !st.getLvar(identifier) && typeof global[identifier] === 'undefined' && (typeof window === 'undefined' || typeof window[identifier] === 'undefined') && !st.compiler.getClassDef(identifier) } isEqualTo = function (/* GlobalVariableMaybeWarning */ aWarning) { if (this.message.message !== aWarning.message.message) return false if (this.node.start !== aWarning.node.start) return false if (this.node.end !== aWarning.node.end) return false return true } } const warningUnusedButSetVariable = { name: 'unused-but-set-variable' }; const warningShadowIvar = { name: 'shadow-ivar' }; const warningCreateGlobalInsideFunctionOrMethod = { name: 'create-global-inside-function-or-method' }; const warningUnknownClassOrGlobal = { name: 'unknown-class-or-global' }; const warningUnknownIvarType = { name: 'unknown-ivar-type' }; const AllWarnings = [warningUnusedButSetVariable, warningShadowIvar, warningCreateGlobalInsideFunctionOrMethod, warningUnknownClassOrGlobal, warningUnknownIvarType]; function getLineOffsets (code, offset) { let lineEnd = offset; while (lineEnd < code.length) { if (objjParser__namespace.isNewLine(code.charCodeAt(lineEnd))) { break } lineEnd++; } let lineStart = offset; while (lineStart > 0) { if (objjParser__namespace.isNewLine(code.charCodeAt(lineStart))) { lineStart++; break } lineStart--; } return { lineStart, lineEnd } } function createMessage (/* String */ aMessage, /* SpiderMonkey AST node */ node, /* String */ code) { const message = {}; const { lineStart, lineEnd } = getLineOffsets(code, node.start); const loc = objjParser__namespace.getLineInfo(code, node.start); message.loc = loc; message.lineStart = lineStart; message.lineEnd = lineEnd; //message.line = line //message.column = column message.message = aMessage; // As a SyntaxError object can't change the property 'line' we also set the property 'messageOnLine' message.messageOnLine = loc.line; message.messageOnColumn = loc.column; message.messageForNode = node; message.messageType = 'WARNING'; message.messageForLine = code.substring(message.lineStart, message.lineEnd); return message } class Scope { constructor (prev, base) { this.vars = Object.create(null); if (base) for (const key in base) this[key] = base[key]; this.prev = prev; if (prev) { this.compiler = prev.compiler; this.nodeStack = prev.nodeStack.slice(0); this.nodePriorStack = prev.nodePriorStack.slice(0); this.nodeStackOverrideType = prev.nodeStackOverrideType.slice(0); } else { this.nodeStack = []; this.nodePriorStack = []; this.nodeStackOverrideType = []; } } toString () { return this.ivars ? 'ivars: ' + JSON.stringify(this.ivars) : '<No ivars>' } compiler () { return this.compiler } rootScope () { return this.prev ? this.prev.rootScope() : this } isRootScope () { return !this.prev } currentClassName () { return this.classDef ? this.classDef.name : this.prev ? this.prev.currentClassName() : null } currentProtocolName () { return this.protocolDef ? this.protocolDef.name : this.prev ? this.prev.currentProtocolName() : null } getIvarForCurrentClass (/* String */ ivarName) { if (this.ivars) { const ivar = this.ivars[ivarName]; if (ivar) { return ivar } } const prev = this.prev; // Stop at the class declaration if (prev && !this.classDef) { return prev.getIvarForCurrentClass(ivarName) } return null } getLvarScope (/* String */ lvarName, /* BOOL */ stopAtMethod) { if (this.vars) { const lvar = this.vars[lvarName]; if (lvar) { return this } } const prev = this.prev; // Stop at the method declaration if (prev && (!stopAtMethod || !this.methodType)) { return prev.getLvarScope(lvarName, stopAtMethod) } return this } getLvar (/* String */ lvarName, /* BOOL */ stopAtMethod) { if (this.vars) { const lvar = this.vars[lvarName]; if (lvar) { return lvar } } const prev = this.prev; // Stop at the method declaration if (prev && (!stopAtMethod || !this.methodType)) { return prev.getLvar(lvarName, stopAtMethod) } return null } getVarScope () { const prev = this.prev; return prev ? prev.getVarScope() : this } currentMethodType () { return this.methodType ? this.methodType : this.prev ? this.prev.currentMethodType() : null } copyAddedSelfToIvarsToParent () { if (this.prev && this.addedSelfToIvars) { for (const key in this.addedSelfToIvars) { const addedSelfToIvar = this.addedSelfToIvars[key]; const scopeAddedSelfToIvar = (this.prev.addedSelfToIvars || (this.prev.addedSelfToIvars = Object.create(null)))[key] || (this.prev.addedSelfToIvars[key] = []); scopeAddedSelfToIvar.push.apply(scopeAddedSelfToIvar, addedSelfToIvar); // Append at end in parent scope } } } addMaybeWarning (warning) { const rootScope = this.rootScope(); let maybeWarnings = rootScope._maybeWarnings; if (!maybeWarnings) { rootScope._maybeWarnings = maybeWarnings = [warning]; } else { const lastWarning = maybeWarnings[maybeWarnings.length - 1]; // MessageSendExpression (and maybe others) will walk some expressions multible times and // possible generate warnings multible times. Here we check if this warning is already added if (!lastWarning.isEqualTo(warning)) { maybeWarnings.push(warning); } } } variablesNotReadWarnings () { const compiler = this.compiler; // The warning option must be turned on. We can't be top scope. The scope must have some variables if (compiler.options.warnings.includes(warningUnusedButSetVariable) && this.prev && this.vars) { for (const key in this.vars) { const lvar = this.vars[key]; if (!lvar.isRead && (lvar.type === 'var' || lvar.type === 'let' || lvar.type === 'const')) { // print("Variable '" + key + "' is never read: " + lvar.type + ", line: " + lvar.node.start); compiler.addWarning(createMessage("Variable '" + key + "' is never read", lvar.node, compiler.source)); } } } } maybeWarnings () { return this.rootScope()._maybeWarnings } pushNode (node, overrideType) { // Here we push 3 things to a stack. The node, override type and an array that can keep track of prior nodes on this level. // The current node is also pushed to the last prior array. // Special case when node is the same as the parent node. This happends when using an override type when walking the AST // The same prior list is then used instead of a new empty one. const nodePriorStack = this.nodePriorStack; const length = nodePriorStack.length; const lastPriorList = length ? nodePriorStack[length - 1] : null; const lastNode = length ? this.nodeStack[length - 1] : null; // First add this node to parent list of nodes, if it has one if (lastPriorList) { if (lastNode !== node) { // If not the same node push the node lastPriorList.push(node); } } // Use the last prior list if it is the same node nodePriorStack.push(lastNode === node ? lastPriorList : []); this.nodeStack.push(node); this.nodeStackOverrideType.push(overrideType); } popNode () { this.nodeStackOverrideType.pop(); this.nodePriorStack.pop(); return this.nodeStack.pop() } currentNode () { const nodeStack = this.nodeStack; return nodeStack[nodeStack.length - 1] } currentOverrideType () { const nodeStackOverrideType = this.nodeStackOverrideType; return nodeStackOverrideType[nodeStackOverrideType.length - 1] } priorNode () { const nodePriorStack = this.nodePriorStack; const length = nodePriorStack.length; if (length > 1) { const parent = nodePriorStack[length - 2]; const l = parent.length; return parent[l - 2] || null } return null } formatDescription (index, formatDescription, useOverrideForNode) { const nodeStack = this.nodeStack; const length = nodeStack.length; index = index || 0; if (index >= length) { return null } // Get the nodes backwards from the stack const i = length - index - 1; const currentNode = nodeStack[i]; const currentFormatDescription = formatDescription || this.compiler.formatDescription; // Get the parent descriptions except if no formatDescription was provided, then it is the root description const parentFormatDescriptions = formatDescription ? formatDescription.parent : currentFormatDescription; let nextFormatDescription; if (parentFormatDescriptions) { const nodeType = useOverrideForNode === currentNode ? this.nodeStackOverrideType[i] : currentNode.type; // console.log("nodeType: " + nodeType + ", (useOverrideForNode === currentNode):" + + !!(useOverrideForNode === currentNode)); nextFormatDescription = parentFormatDescriptions[nodeType]; if (useOverrideForNode === currentNode && !nextFormatDescription) { // console.log("Stop"); return null } } // console.log("index: " + index + ", currentNode: " + JSON.stringify(currentNode) + ", currentFormatDescription: " + JSON.stringify(currentFormatDescription) + ", nextFormatDescription: " + JSON.stringify(nextFormatDescription)); if (nextFormatDescription) { // Check for more 'parent' attributes or return nextFormatDescription return this.formatDescription(index + 1, nextFormatDescription) } else { // Check for a virtual node one step up in the stack nextFormatDescription = this.formatDescription(index + 1, formatDescription, currentNode); if (nextFormatDescription) { return nextFormatDescription } else { // Ok, we have found a format description (currentFormatDescription). // Lets check if we have any other descriptions dependent on the prior node. const priorFormatDescriptions = currentFormatDescription.prior; if (priorFormatDescriptions) { const priorNode = this.priorNode(); const priorFormatDescription = priorFormatDescriptions[priorNode ? priorNode.type : 'None']; if (priorFormatDescription) { return priorFormatDescription } } return currentFormatDescription } } } } class BlockScope extends Scope { variablesNotReadWarnings () { Scope.prototype.variablesNotReadWarnings.call(this); const prev = this.prev; // Any possible hoisted variable in this scope has to be moved to the previous scope if it is not declared in the previsous scope // We can't be top scope. The scope must have some possible hoisted variables if (prev && this.possibleHoistedVariables) { for (const key in this.possibleHoistedVariables) { const possibleHoistedVariable = this.possibleHoistedVariables[key]; if (possibleHoistedVariable) { const varInPrevScope = prev.vars && prev.vars[key]; if (varInPrevScope != null) { const prevPossibleHoistedVariable = (prev.possibleHoistedVariables || (prev.possibleHoistedVariables = Object.create(null)))[key]; if (prevPossibleHoistedVariable == null) { prev.possibleHoistedVariables[key] = possibleHoistedVariable; } else { throw new Error("Internal inconsistency, previous scope should not have this possible hoisted variable '" + key + "'") } } } } } } } class FunctionScope extends BlockScope { getVarScope () { return this } } class StringBuffer { constructor (useSourceNode, file, sourceContent) { if (useSourceNode) { this.rootNode = new sourceMap.SourceNode(); this.concat = this.concatSourceNode; this.toString = this.toStringSourceNode; this.isEmpty = this.isEmptySourceNode; this.appendStringBuffer = this.appendStringBufferSourceNode; this.length = this.lengthSourceNode; this.removeAtIndex = this.removeAtIndexSourceNode; if (file) { const fileString = file.toString(); const filename = fileString.substr(fileString.lastIndexOf('/') + 1); const sourceRoot = fileString.substr(0, fileString.lastIndexOf('/') + 1); this.filename = filename; if (sourceRoot.length > 0) { this.sourceRoot = sourceRoot; } if (sourceContent != null) { this.rootNode.setSourceContent(filename, sourceContent); } } if (sourceContent != null) { this.sourceContent = sourceContent; } } else { this.atoms = []; this.concat = this.concatString; this.toString = this.toStringString; this.isEmpty = this.isEmptyString; this.appendStringBuffer = this.appendStringBufferString; this.length = this.lengthString; this.removeAtIndex = this.removeAtIndexString; } } toStringString () { return this.atoms.join('') } toStringSourceNode () { return this.rootNode.toStringWithSourceMap({ file: this.filename + 's', sourceRoot: this.sourceRoot }) } concatString (aString) { this.atoms.push(aString); } concatSourceNode (aString, node, originalName) { if (node) { // console.log("Snippet: " + aString + ", line: " + node.loc.start.line + ", column: " + node.loc.start.column + ", source: " + node.loc.source); this.rootNode.add(new sourceMap.SourceNode(node.loc.start.line, node.loc.start.column, node.loc.source, aString, originalName)); } else { this.rootNode.add(aString); } if (!this.notEmpty) { this.notEmpty = true; } } // '\n' will indent. '\n\0' will not indent. '\n\1' will indent one more then the current indent level. // '\n\-1' will indent one less then the current indent level. Numbers from 0-9 can me used. concatFormat (aString) { if (!aString) return const lines = aString.split('\n'); const size = lines.length; if (size > 1) { this.concat(lines[0]); for (let i = 1; i < size; i++) { let line = lines[i]; this.concat('\n'); if (line.slice(0, 1) === '\\') { let numberLength = 1; let indent = line.slice(1, 1 + numberLength); if (indent === '-') { numberLength = 2; indent = line.slice(1, 1 + numberLength); } const indentationNumber = parseInt(indent); if (indentationNumber) { this.concat(indentationNumber > 0 ? indentation + Array(indentationNumber * indentationSpaces + 1).join(indentType) : indentation.substring(indentationSize * -indentationNumber)); } line = line.slice(1 + numberLength); } else if (line || i === size - 1) { // Ident if there is something between line breaks or the last linebreak this.concat(this.indentation); } if (line) this.concat(line); } } else { this.concat(aString); } } isEmptyString () { return this.atoms.length !== 0 } isEmptySourceNode () { return this.notEmpty } appendStringBufferString (stringBuffer) { // We can't do 'this.atoms.push.apply(this.atoms, stringBuffer.atoms);' as JavaScriptCore (WebKit) has a limit on number of arguments at 65536. // Other browsers also have simular limits. const thisAtoms = this.atoms; const thisLength = thisAtoms.length; const stringBufferAtoms = stringBuffer.atoms; const stringBufferLength = stringBufferAtoms.length; thisAtoms.length = thisLength + stringBufferLength; for (let i = 0; i < stringBufferLength; i++) { thisAtoms[thisLength + i] = stringBufferAtoms[i]; } } appendStringBufferSourceNode (stringBuffer) { this.rootNode.add(stringBuffer.rootNode); } lengthString () { return this.atoms.length } lengthSourceNode () { return this.rootNode.children.length } removeAtIndexString (index) { this.atoms[index] = ''; } removeAtIndexSourceNode (index) { this.rootNode.children[index] = ''; } } // A optional argument can be given to further configure // the compiler. These options are recognized: const defaultOptions = { // Acorn options. For more information check objj-acorn. // We have a function here to create a new object every time we copy // the default options. acornOptions: function () { return Object.create(null) }, // Turn on `sourceMap` generate a source map for the compiler file. sourceMap: false, // Turn on `sourceMapIncludeSource` will include the source code in the source map. sourceMapIncludeSource: false, // The compiler can do different passes. // 1: Parse and walk AST tree to collect file dependencies. // 2: Parse and walk to generate code. // Pass one is only for the Objective-J load and runtime. pass: 2, // Pass in class definitions. New class definitions in source file will be added here when compiling. classDefs: function () { return Object.create(null) }, // Pass in protocol definitions. New protocol definitions in source file will be added here when compiling. protocolDefs: function () { return Object.create(null) }, // Pass in typeDef definitions. New typeDef definitions in source file will be added here when compiling. typeDefs: function () { return Object.create(null) }, // Turn off `generate` to make the compile copy the code from the source file (and replace needed parts) // instead of generate it from the AST tree. The preprocessor does not work if this is turn off as it alters // the AST tree and not the original source. We should deprecate this in the future. generate: true, // Turn on `generateObjJ` to generate Objecitve-J code instead of Javascript code. This can be used to beautify // the code. generateObjJ: false, // How many spaces for indentation when generation code. indentationSpaces: 4, // The type of indentation. Default is space. Can be changed to tab or any other string. indentationType: ' ', // There is a bug in Safari 2.0 that can't handle a named function declaration. See http://kangax.github.io/nfe/#safari-bug // Turn on `transformNamedFunctionDeclarationToAssignment` to make the compiler transform these. // We support this here as the old Objective-J compiler (Not a real compiler, Preprocessor.js) transformed // named function declarations to assignments. // Example: 'function f(x) { return x }' transforms to: 'f = function(x) { return x }' transformNamedFunctionDeclarationToAssignment: false, // Turn off `includeMethodFunctionNames` to remove function names on methods. includeMethodFunctionNames: true, // Turn off `includeMethodArgumentTypeSignatures` to remove type information on method arguments. includeMethodArgumentTypeSignatures: true, // Turn off `includeIvarTypeSignatures` to remove type information on ivars. includeIvarTypeSignatures: true, // Turn off `inlineMsgSendFunctions` to use message send functions. Needed to use message send decorators. inlineMsgSendFunctions: true, // `warning` includes the warnings that are turned on. It is just used for some warnings. warnings: [warningUnusedButSetVariable, warningShadowIvar, warningCreateGlobalInsideFunctionOrMethod, warningUnknownClassOrGlobal, warningUnknownIvarType], // An array of macro objects and/or text definitions may be passed in. // Definitions may be in one of two forms: // macro // macro=body macros: null }; // We copy the options to a new object as we don't want to mess up incoming options when we start compiling. function setupOptions (opts) { const options = Object.create(null); for (const opt in defaultOptions) { if (opts && Object.prototype.hasOwnProperty.call(opts, opt)) { const incomingOpt = opts[opt]; options[opt] = typeof incomingOpt === 'function' ? incomingOpt() : incomingOpt; } else if (Object.prototype.hasOwnProperty.call(defaultOptions, opt)) { const defaultOpt = defaultOptions[opt]; options[opt] = typeof defaultOpt === 'function' ? defaultOpt() : defaultOpt; } } return options } class TypeDef { constructor (name) { this.name = name; } } // methodDef = {"types": types, "name": selector} class MethodDef { constructor (name, types) { this.name = name; this.types = types; } } // Both the ClassDef and ProtocolDef conforms to a 'protocol' (That we can't declare in Javascript). // Both Objects have the attribute 'protocols': Array of ProtocolDef that they conform to // Both also have the functions: addInstanceMethod, addClassMethod, getInstanceMethod and getClassMethod // classDef = {"className": aClassName, "superClass": superClass , "ivars": myIvars, "instanceMethods": instanceMethodDefs, "classMethods": classMethodDefs, "protocols": myProtocols}; class ClassDef { constructor (isImplementationDeclaration, name, superClass, ivars, instanceMethods, classMethods, protocols) { this.name = name; if (superClass) { this.superClass = superClass; } if (ivars) { this.ivars = ivars; } if (isImplementationDeclaration) { this.instanceMethods = instanceMethods || Object.create(null); this.classMethods = classMethods || Object.create(null); } if (protocols) { this.protocols = protocols; } } addInstanceMethod (methodDef) { this.instanceMethods[methodDef.name] = methodDef; } addClassMethod (methodDef) { this.classMethods[methodDef.name] = methodDef; } listOfNotImplementedMethodsForProtocols (protocolDefs) { let resultList = []; const instanceMethods = this.getInstanceMethods(); const classMethods = this.getClassMethods(); for (let i = 0, size = protocolDefs.length; i < size; i++) { const protocolDef = protocolDefs[i]; const protocolInstanceMethods = protocolDef.requiredInstanceMethods; const protocolClassMethods = protocolDef.requiredClassMethods; const inheritFromProtocols = protocolDef.protocols; if (protocolInstanceMethods) { for (const methodName in protocolInstanceMethods) { const methodDef = protocolInstanceMethods[methodName]; if (!instanceMethods[methodName]) resultList.push({ methodDef, protocolDef }); } } if (protocolClassMethods) { for (const methodName in protocolClassMethods) { const methodDef = protocolClassMethods[methodName]; if (!classMethods[methodName]) resultList.push({ methodDef, protocolDef }); } } if (inheritFromProtocols) { resultList = resultList.concat(this.listOfNotImplementedMethodsForProtocols(inheritFromProtocols)); } } return resultList } getInstanceMethod (name) { const instanceMethods = this.instanceMethods; if (instanceMethods) { const method = instanceMethods[name]; if (method) { return method } } const superClass = this.superClass; if (superClass) { return superClass.getInstanceMethod(name) } return null } getClassMethod (name) { const classMethods = this.classMethods; if (classMethods) { const method = classMethods[name]; if (method) { return method } } const superClass = this.superClass; if (superClass) { return superClass.getClassMethod(name) } return null } // Return a new Array with all instance methods getInstanceMethods () { const instanceMethods = this.instanceMethods; if (instanceMethods) { const superClass = this.superClass; const returnObject = Object.create(null); if (superClass) { const superClassMethods = superClass.getInstanceMethods(); for (const methodName in superClassMethods) { returnObject[methodName] = superClassMethods[methodName]; } } for (const methodName in instanceMethods) { returnObject[methodName] = instanceMethods[methodName]; } return returnObject } return [] } // Return a new Array with all class methods getClassMethods () { const classMethods = this.classMethods; if (classMethods) { const superClass = this.superClass; const returnObject = Object.create(null); if (superClass) { const superClassMethods = superClass.getClassMethods(); for (const methodName in superClassMethods) { returnObject[methodName] = superClassMethods[methodName]; } } for (const methodName in classMethods) { returnObject[methodName] = classMethods[methodName]; } return returnObject } return [] } } // Both the ClassDef and ProtocolDef conforms to a 'protocol' (That we can't declare in Javascript). // Both Objects have the attribute 'protocols': Array of ProtocolDef that they conform to // Both also have the functions: addInstanceMethod, addClassMethod, getInstanceMethod and getClassMethod // protocolDef = {"name": aProtocolName, "protocols": inheritFromProtocols, "requiredInstanceMethods": requiredInstanceMethodDefs, "requiredClassMethods": requiredClassMethodDefs}; class ProtocolDef { constructor (name, protocols, requiredInstanceMethodDefs, requiredClassMethodDefs) { this.name = name; this.protocols = protocols; if (requiredInstanceMethodDefs) { this.requiredInstanceMethods = requiredInstanceMethodDefs; } if (requiredClassMethodDefs) { this.requiredClassMethods = requiredClassMethodDefs; } } addInstanceMethod = function (methodDef) { (this.requiredInstanceMethods || (this.requiredInstanceMethods = Object.create(null)))[methodDef.name] = methodDef; } addClassMethod = function (methodDef) { (this.requiredClassMethods || (this.requiredClassMethods = Object.create(null)))[methodDef.name] = methodDef; } getInstanceMethod = function (name) { const instanceMethods = this.requiredInstanceMethods; if (instanceMethods) { const method = instanceMethods[name]; if (method) { return method } } const protocols = this.protocols; for (let i = 0, size = protocols.length; i < size; i++) { const protocol = protocols[i]; const method = protocol.getInstanceMethod(name); if (method) { return method } } return null } getClassMethod = function (name) { const classMethods = this.requiredClassMethods; if (classMethods) { const method = classMethods[name]; if (method) { return method } } const protocols = this.protocols; for (let i = 0, size = protocols.length; i < size; i++) { const protocol = protocols[i]; const method = protocol.getClassMethod(name); if (method) { return method } } return null } } function wordsRegexp (words) { return new RegExp('^(?:' + words.replace(/ /g, '|') + ')$') } function isIdempotentExpression (node) { switch (node.type) { case 'Literal': case 'Identifier': return true case 'ArrayExpression': for (let i = 0; i < node.elements.length; ++i) { if (!isIdempotentExpression(node.elements[i])) { return false } } return true case 'DictionaryLiteral': for (let i = 0; i < node.keys.length; ++i) { if (!isIdempotentExpression(node.keys[i])) { return false } if (!isIdempotentExpression(node.values[i])) { return false } } return true case 'ObjectExpression': for (let i = 0; i < node.properties.length; ++i) { if (!isIdempotentExpression(node.properties[i].value)) { return false } } return true case 'FunctionExpression': for (let i = 0; i < node.params.length; ++i) { if (!isIdempotentExpression(node.params[i])) { return false } } return true case 'SequenceExpression': for (let i = 0; i < node.expressions.length; ++i) { if (!isIdempotentExpression(node.expressions[i])) { return false } } return true case 'UnaryExpression': return isIdempotentExpression(node.argument) case 'BinaryExpression': return isIdempotentExpression(node.left) && isIdempotentExpression(node.right) case 'ConditionalExpression': return isIdempotentExpression(node.test) && isIdempotentExpression(node.consequent) && isIdempotentExpression(node.alternate) case 'MemberExpression': return isIdempotentExpression(node.object) && (!node.computed || isIdempotentExpression(node.property)) case 'Dereference': return isIdempotentExpression(node.expr) case 'Reference': return isIdempotentExpression(node.element) default: return false } } // We do not allow dereferencing of expressions with side effects because we might need to evaluate the expression twice in certain uses of deref, which is not obvious when you look at the deref operator in plain code. function checkCanDereference (st, node) { if (!isIdempotentExpression(node)) { throw st.compiler.error_message('Dereference of expression with side effects', node) } } // Surround expression with parentheses function surroundExpression (c) { return function (node, st, override) { st.compiler.jsBuffer.concat('('); c(node, st, override); st.compiler.jsBuffer.concat(')'); } } const operatorPrecedence = { // MemberExpression // These two are never used as they are a MemberExpression with the attribute 'computed' which tells what operator it uses. // ".": 0, "[]": 0, // NewExpression // This is never used. // "new": 1, // All these are UnaryExpression or UpdateExpression and never used. // "!": 2, "~": 2, "-": 2, "+": 2, "++": 2, "--": 2, "typeof": 2, "void": 2, "delete": 2, // BinaryExpression '*': 3, '/': 3, '%': 3, '+': 4, '-': 4, '<<': 5, '>>': 5, '>>>': 5, '<': 6, '<=': 6, '>': 6, '>=': 6, in: 6, instanceof: 6, '==': 7, '!=': 7, '===': 7, '!==': 7, '&': 8, '^': 9, '|': 10, // LogicalExpression '&&': 11, '||': 12, '??': 13 // ConditionalExpression // AssignmentExpression }; const expressionTypePrecedence = { MemberExpression: 1, CallExpression: 1, NewExpression: 1, ChainExpression: 2, FunctionExpression: 3, ArrowFunctionExpression: 3, ImportExpression: 3, UnaryExpression: 4, UpdateExpression: 4, BinaryExpression: 5, LogicalExpression: 6, ConditionalExpression: 7, AssignmentExpression: 8 }; function ignore (_node, _st, _c) { } const pass1 = walk__default["default"].make({ ImportStatement: function (node, st, c) { const urlString = node.filename.value; st.compiler.dependencies.push({ url: urlString, isLocal: node.localfilepath }); // st.compiler.dependencies.push(typeof FileDependency !== 'undefined' ? new FileDependency(typeof CFURL !== 'undefined' ? new CFURL(urlString) : urlString, node.localfilepath) : urlString); }, TypeDefStatement: ignore, ClassStatement: ignore, ClassDeclarationStatement: ignore, MessageSendExpression: ignore, GlobalStatement: ignore, ProtocolDeclarationStatement: ignore, ArrayLiteral: ignore, Reference: ignore, DictionaryLiteral: ignore, Dereference: ignore, SelectorLiteralExpression: ignore }); // Returns true if subNode has higher precedence the the root node. // If the subNode is the right (as in left/right) subNode function nodePrecedence (node, subNode, right) { const nodeType = node.type; const nodePrecedence = expressionTypePrecedence[nodeType] || -1; const subNodePrecedence = expressionTypePrecedence[subNode.type] || -1; let nodeOperatorPrecedence; let subNodeOperatorPrecedence; return nodePrecedence < subNodePrecedence || (nodePrecedence === subNodePrecedence && isLogicalBinary.test(nodeType) && ((nodeOperatorPrecedence = operatorPrecedence[node.operator]) < (subNodeOperatorPrecedence = operatorPrecedence[subNode.operator]) || (right && nodeOperatorPrecedence === subNodeOperatorPrecedence))) } // Used for arrow functions. Checks if the parameter list needs parentheses. function mustHaveParentheses (paramList) { for (const param of paramList) { if (param.type !== 'Identifier') { return true } } return paramList.length > 1 || paramList.length === 0 } const reservedIdentifiers = wordsRegexp('self _cmd __filename undefined localStorage arguments'); const wordPrefixOperators = wordsRegexp('delete in instanceof new typeof void'); const isLogicalBinary = wordsRegexp('LogicalExpression BinaryExpression'); const pass2 = walk__default["default"].make({ Program: function (node, st, c) { for (let i = 0; i < node.body.length; ++i) { c(node.body[i], st, 'Statement'); } // Check maybe warnings const maybeWarnings = st.maybeWarnings(); if (maybeWarnings) { for (let i = 0; i < maybeWarnings.length; i++) { const maybeWarning = maybeWarnings[i]; if (maybeWarning.checkIfWarning(st)) { st.compiler.addWarning(maybeWarning.message); } } } }, BlockStatement: function (node, st, c) { const compiler = st.compiler; const buffer = compiler.jsBuffer; const isDecl = st.isDecl; if (isDecl != null) { delete st.isDecl; } const endOfScopeBody = st.endOfScopeBody; if (endOfScopeBody) { delete st.endOfScopeBody; } const skipIndentation = st.skipIndentation; if (skipIndentation) { delete st.skipIndentation; } else { buffer.concat(compiler.indentation.substring(compiler.indentationSize)); } buffer.concat('{\n', node); const inner = endOfScopeBody ? st : new BlockScope(st); for (let i = 0; i < node.body.length; ++i) { if (node.body[i].type === 'BlockStatement') { compiler.indentation += compiler.indentStep; c(node.body[i], inner, 'Statement'); compiler.indentation = compiler.indentation.substring(compiler.indentationSize); } else { c(node.body[i], inner, 'Statement'); } } !endOfScopeBody && inner.variablesNotReadWarnings(); const maxReceiverLevel = st.maxReceiverLevel; if (endOfScopeBody && maxReceiverLevel) { buffer.concat(compiler.indentation); buffer.concat('var '); for (let i = 0; i < maxReceiverLevel; i++) { if (i) buffer.concat(', '); buffer.concat('___r'); buffer.concat((i + 1) + ''); } buffer.concat(';\n'); } // Simulate a node for the last curly bracket // var endNode = node.loc && { loc: { start: { line : node.loc.end.line, column: node.loc.end.column}}, source: node.loc.source}; buffer.concat(compiler.indentation.substring(compiler.indentationSize)); buffer.concat('}', node); if (st.isDefaultExport) buffer.concat(';'); if (!skipIndentation && isDecl !== false) { buffer.concat('\n'); } st.indentBlockLevel--; }, ExpressionStatement: function (node, st, c) { const compiler = st.compiler; const buffer = compiler.jsBuffer; buffer.concat(st.compiler.indentation); if (node.expression.type === 'Reference') throw compiler.error_message("Can't have reference of expression as a statement", node.expression) if ((node.expression.type === 'AssignmentExpression' && node.expression.left.type === 'ObjectPattern') || node.expression.type === 'FunctionExpression' || node.expression.type === 'ObjectExpression' || (node.expression.type === 'BinaryExpression' && node.expression.left.type === 'FunctionExpression') || (node.expression.type === 'Literal' && node.expression.value === 'use strict' && !node.directive)) { surroundExpression(c)(node.expression, st, 'Expression'); } else { c(node.expression, st, 'Expression'); } buffer.concat(';\n', node); }, IfStatement: function (node, st, c) { const compiler = st.compiler; const buffer = compiler.jsBuffer; // Keep the 'else' and 'if' on the same line if it is an 'else if' if (!st.superNodeIsElse) { buffer.concat(st.compiler.indentation); } else { delete st.superNodeIsElse; } buffer.concat('if (', node); c(node.test, st, 'Expression'); // We don't want EmptyStatements to generate an extra parenthesis except when it is in a while, for, ... buffer.concat(')'); if (node.consequent.type !== 'EmptyStatement') buffer.concat('\n'); st.compiler.indentation += st.compiler.indentStep; c(node.consequent, st, 'Statement'); st.compiler.indentation = st.compiler.indentation.substring(st.compiler.indentationSize); const alternate = node.alternate; if (alternate) { const alternateNotIf = alternate.type !== 'IfStatement'; const emptyStatement = alternate.type === 'EmptyStatement'; buffer.concat(st.compiler.indentation); // We don't want EmptyStatements to generate an extra parenthesis except when it is in a while, for, ... buffer.concat(alternateNotIf ? emptyStatement ? 'else' : 'else\n' : 'else ', node); if (alternateNotIf) { st.compiler.indentation += st.compiler.indentStep; } else { st.superNodeIsElse = true; } c(alternate, st, 'Statement'); if (alternateNotIf) st.compiler.indentation = st.compiler.indentation.substring(st.compiler.indentationSize); } }, LabeledStatement: function (node, st, c) { const compiler = st.compiler; const buffer = compiler.jsBuffer; c(node.label, st, 'VariablePattern'); buffer.concat(': ', node); c(node.body, st, 'Statement'); }, BreakStatement: function (node, st, c) { const compiler = st.compiler; const label = node.label; const buffer = compiler.jsBuffer; buffer.concat(st.compiler.indentation); if (label) { buffer.concat('break ', node); c(label, st, 'VariablePattern'); buffer.concat(';\n'); } else { buffer.concat('break;\n', node); } }, ContinueStatement: function (node, st, c) { const compiler = st.compiler; const label = node.label; const buffer = compiler.jsBuffer; buffer.concat(st.compiler.indentation); if (label) { buffer.concat('continue ', node); c(label, st, 'VariablePattern'); buffer.concat(';\n'); } else { buffer.concat('continue;\n', node); } }, WithStatement: function (node, st, c) { const compiler = st.compiler; const buffer = compiler.jsBuffer; buffer.concat(st.compiler.indentation); buffer.concat('with(', node); c(node.object, st, 'Expression'); buffer.concat(')\n', node); st.compiler.indentation += st.compiler.indentStep; c(node.body, st, 'Statement'); st.compiler.indentation = st.compiler.indentation.substring(st.compiler.indentationSize); }, SwitchStatement: function (node, st, c) { const compiler = st.compiler; const buffer = compiler.jsBuffer; buffer.concat(st.compiler.indentation); buffer.concat('switch(', node); c(node.discriminant, st, 'Expression'); buffer.concat(') {\n'); st.compiler.indentation += st.compiler.indentStep; for (let i = 0; i < node.cases.length; ++i) { const cs = node.cases[i]; if (cs.test) { buffer.concat(st.compiler.indentation); buffer.concat('case '); c(cs.test, st, 'Expression'); buffer.concat(':\n'); } else { buffer.concat('default:\n'); } st.compiler.indentation += st.compiler.indentStep; for (let j = 0; j < cs.consequent.length; ++j) { c(cs.consequent[j], st, 'Statement'); } st.compiler.indentation = st.compiler.indentation.substring(st.compiler.indentationSize); } st.compiler.indentation = st.compiler.indentation.substring(st.compiler.indentationSize); buffer.concat(st.compiler.indentation); buffer.concat('}\n'); }, ReturnStatement: function (node, st, c) { const compiler = st.compiler; const buffer = compiler.jsBuffer; buffer.concat(st.compiler.indentation); buffer.concat('return', node); if (node.argument) { buffer.concat(' '); c(node.argument, st, 'Expression'); } buffer.concat(';\n'); }, ThrowStatement: function (node, st, c) { const compiler = st.compiler; const buffer = compiler.jsBuffer; buffer.concat(st.compiler.indentation); buffer.concat('throw', node); buffer.concat(' '); c(node.argument, st, 'Expression'); buffer.concat(';\n'); }, TryStatement: function (node, st, c) { const compiler = st.compiler; const buffer = compiler.jsBuffer; buffer.concat(st.compiler.indentation); buffer.concat('try', node); buffer.concat(' '); st.compiler.indentation += st.compiler.indentStep; st.skipIndentation = true; c(node.block, st, 'Statement'); st.compiler.indentation = st.compiler.indentation.substring(st.compiler.indentationSize); if (node.handler) { const handler = node.handler; const inner = new Scope(st); const param = handler.param; const name = param?.name; if (name) inner.vars[name] = { type: 'catch clause', node: param }; buffer.concat('\n'); buffer.concat(st.compiler.indentation); buffer.concat('catch'); if (param) { buffer.concat('('); c(param, st, 'Pattern'); buffer.concat(') '); } st.compiler.indentation += st.compiler.indentStep; inner.skipIndentation = true; inner.endOfScopeBody = true; c(handler.body, inner, 'BlockStatement'); inner.variablesNotReadWarnings(); st.compiler.indentation = st.compiler.indentation.substring(st.compiler.indentationSize); inner.copyAddedSelfToIvarsToParent(); } if (node.finalizer) { buffer.concat('\n'); buffer.concat(st.compiler.indentation); buffer.concat('finally '); st.compiler.indentation += st.compiler.indentStep; st.skipIndentation = true; c(node.finalizer, st, 'Statement'); st.compiler.indentation = st.compiler.indentation.substring(st.compiler.indentationSize); } buffer.concat('\n'); }, WhileStatement: function (node, st, c) { const compiler = st.compiler; const body = node.body; const buffer = compiler.jsBuffer; buffer.concat(st.compiler.indentation); buffer.concat('while (', node); c(node.test, st, 'Expression'); // We don't want EmptyStatements to generate an extra parenthesis except when it is in a while, for, ... buffer.concat(')'); if (node.body.type !== 'EmptyStatement') buffer.concat('\n'); st.compiler.indentation += st.compiler.indentStep; c(body, st, 'Statement'); st.compiler.indentation = st.compiler.indentation.substring(st.compiler.indentationSize); }, DoWhileStatement: function (node, st, c) { const compiler = st.compiler; const buffer = compiler.jsBuffer; buffer.concat(st.compiler.indentation); buffer.concat('do\n', node); st.compiler.indentation += st.compiler.indentStep; c(node.body, st, 'Statement'); st.compiler.indentation = st.compiler.indentation.substring(st.compiler.indentationSize); buffer.concat(st.compiler.indentation); buffer.concat('while ('); c(node.test, st, 'Expression'); buffer.concat(');\n'); }, ForStatement: function (node, st, c) { const compiler = st.compiler; const body = node.body; const buffer = compiler.jsBuffer; buffer.concat(st.compiler.indentation); buffer.concat('for (', node); if (node.init) c(node.init, st, 'ForInit'); buffer.concat('; '); if (node.test) c(node.test, st, 'Expression'); buffer.concat('; '); if (node.update) c(node.update, st, 'Expression'); // We don't want EmptyStatements to generate an extra parenthesis except when it is in a while, for, ... buffer.concat(')'); if (node.body.type !== 'EmptyStatement') buffer.concat('\n'); st.compiler.indentation += st.compiler.indentStep; c(body, st, 'Statement'); st.compiler.indentation = st.compiler.indentation.substring(st.compiler.indentationSize); }, ForInStatement: function (node, st, c) { const compiler = st.compiler; const body = node.body; const buffer = compiler.jsBuffer; buffer.concat(st.compiler.indentation); buffer.concat('for (', node); c(node.left, st, 'ForInit'); buffer.concat(' in '); c(node.right, st, 'Expression'); // We don't want EmptyStatements to generate an extra parenthesis except when it is in a while, for, ... buffer.concat(body.type === 'EmptyStatement' ? ')\n' : ')\n'); st.compiler.indentation += st.compiler.indentStep; c(body, st, 'Statement'); st.compiler.indentation = st.compiler.indentation.substring(st.compiler.indentationSize); }, ForOfStatement: function (node, st, c) { // TODO: Fix code duplication with 'for in'- const compiler = st.compiler; const body = node.body; const buffer = compiler.jsBuffer; buffer.concat('for', node); if (node.await) buffer.concat(' await '); buffer.concat('('); c(node.left, st, 'ForInit'); buffer.concat(' of '); c(node.right, st, 'Expression'); // We don't want EmptyStatements to generate an extra parenthesis except when it is in a while, for, ... buffer.concat(body.type === 'EmptyStatement' ? ')\n' : ')\n'); st.compiler.indentation += st.compiler.indentStep; c(body, st, 'Statement'); st.compiler.indentation = st.compiler.indentation.substring(st.compiler.indentationSize); }, ForInit: function (node, st, c) { const compiler = st.compiler; const buffer = compiler.jsBuffer; if (node.type === 'VariableDeclaration') { st.isFor = true; c(node, st); delete st.isFor; } else if (node.type === 'BinaryExpression' && node.operator === 'in') { buffer.concat('('); c(node, st, 'Expression'); buffer.concat(')'); } else { c(node, st, 'Expression'); } }, DebuggerStatement: function (node, st, c) { const compiler = st.compiler; const buffer = compiler.jsBuffer; buffer.concat(st.compiler.indentation); buffer.concat('debugger;\n', node); }, Function: function (node, st, c) { const compiler = st.compiler; const buffer = compiler.jsBuffer; const inner = new FunctionScope(st); const decl = node.type === 'FunctionDeclaration'; const id = node.id; inner.isDecl = decl; for (let i = 0; i < node.params.length; ++i) { inner.vars[node.params[i].name] = { type: 'argument', no