diff --git a/JavascriptTracer/.eslintrc.yml b/JavascriptTracer/.eslintrc.yml new file mode 100644 index 0000000..1121de5 --- /dev/null +++ b/JavascriptTracer/.eslintrc.yml @@ -0,0 +1,9 @@ +env: + browser: true + es2021: true + node: true +extends: eslint:recommended +parserOptions: + ecmaVersion: latest + sourceType: module +rules: {} diff --git a/JavascriptTracer/analysis.js b/JavascriptTracer/analysis.js deleted file mode 100644 index efe91ed..0000000 --- a/JavascriptTracer/analysis.js +++ /dev/null @@ -1,363 +0,0 @@ -// JALANGI DO NOT INSTRUMENT - -const fs = require('fs'); - -const globalPathPrefix = process.env.JS_PATH_PREFIX; - -const testcaseBeginFunctionName = "__MICROWALK_testcaseBegin"; -const testcaseEndFunctionName = "__MICROWALK_testcaseEnd"; - -const traceDirectory = process.env.JS_TRACE_DIRECTORY; - -let currentTestcaseId = -1; // Prefix mode -let traceData = []; -const traceDataSizeLimit = 1000000; -let previousTraceFilePath = "" -let scriptsFile = fs.openSync(`${traceDirectory}/scripts.txt`, "w"); - -let nextCodeFileIndex = 0; -let knownCodeFiles = {}; - -// If set to true, trace compression is disabled. -// WARNING: This may lead to huge files, and is incompatible to Microwalk's preprocessor module! -let disableTraceCompression = false; - -// Compressed lines from the trace prefix can be reused in all other traces -let nextCompressedLineIndex = 0; -let compressedLines = {}; -let prefixNextCompressedLineIndex = 0; -let prefixCompressedLines = {}; - -// Used for computing relative distance between subsequent compressed line IDs. -// If the distance is small, we only print the offset (+1, ...), not the entire line ID. -let lastCompressedLineIndex = -1000; - -// If the last line used a one-character relative encoding, we omit the line break and append the next one directly. -let lastLineWasEncodedRelatively = false; - -// Stores whether the last trace entry was a conditional. -let pendingConditionalState = 0; // 0: No conditional; 1: Pending target instruction; 2: Skip very next expression - -function persistTrace() -{ - let traceFilePath = currentTestcaseId === -1 ? `${traceDirectory}/prefix.trace` : `${traceDirectory}/t${currentTestcaseId}.trace`; - - let writingToNewTrace = false; - if(traceFilePath !== previousTraceFilePath) - { - writingToNewTrace = true; - previousTraceFilePath = traceFilePath; - } - - let traceFile; - if(writingToNewTrace) - { - console.log(`Creating ${traceFilePath}`); - traceFile = fs.openSync(traceFilePath, "w"); - } - else - { - traceFile = fs.openSync(traceFilePath, "a+"); - } - - fs.writeSync(traceFile, traceData.join('\n')); - fs.writeSync(traceFile, '\n'); - - fs.closeSync(traceFile); -} - -function writeTraceLine(line) -{ - if(traceData.length >= traceDataSizeLimit) - { - persistTrace(); - traceData = []; - } - - if(disableTraceCompression) - { - traceData.push(line); - return; - } - - let encodedLine = ""; - - let lineIndex = getCompressedLine(line); - let distance = lineIndex - lastCompressedLineIndex; - let encodeRelatively = (distance >= -9 && distance <= 9); - if(encodeRelatively) - encodedLine = String.fromCharCode(106 + distance); // 'j' + distance => a ... s - else - encodedLine = lineIndex.toString(); - - if(lastLineWasEncodedRelatively && traceData.length > 0) - traceData[traceData.length - 1] += encodedLine; - else - traceData.push(encodedLine); - - lastLineWasEncodedRelatively = encodeRelatively; - lastCompressedLineIndex = lineIndex; -} - -function writePrefixedTraceLine(prefix, line) -{ - if(traceData.length >= traceDataSizeLimit) - { - persistTrace(); - traceData = []; - } - - if(disableTraceCompression) - { - traceData.push(`${prefix}${line}`); - return; - } - - let encodedLine = ""; - - let lineIndex = getCompressedLine(prefix); - let distance = lineIndex - lastCompressedLineIndex; - let encodeRelatively = (distance >= -9 && distance <= 9); - if(encodeRelatively) - encodedLine = `${String.fromCharCode(106 + distance)}|${line}`; // 'j' + distance => a ... s - else - encodedLine = `${lineIndex}|${line}`; - - if(lastLineWasEncodedRelatively && traceData.length > 0) - traceData[traceData.length - 1] += encodedLine; - else - traceData.push(encodedLine); - - lastLineWasEncodedRelatively = false; - lastCompressedLineIndex = lineIndex; -} - -function getCompressedLine(line) -{ - if(line in compressedLines) - return compressedLines[line]; - else - { - let compressed = nextCompressedLineIndex; - ++nextCompressedLineIndex; - - compressedLines[line] = compressed; - traceData.push(`L|${compressed}|${line}`); - - lastLineWasEncodedRelatively = false; - return compressed; - } -} - -(function(sandbox) -{ - function formatIidWithSid(sid, iid) - { - const sdata = J$.smap[sid]; - if(sdata === undefined) - return null; - const idata = sdata[iid]; - if(idata === undefined) - return null; - - let fileIndex = ""; - if(sdata.originalCodeFileName in knownCodeFiles) - fileIndex = knownCodeFiles[sdata.originalCodeFileName]; - else - { - let codeFileName = sdata.originalCodeFileName; - if(codeFileName.startsWith(globalPathPrefix)) - codeFileName = codeFileName.slice(globalPathPrefix.length); - - fileIndex = nextCodeFileIndex.toString(); - ++nextCodeFileIndex; - - knownCodeFiles[sdata.originalCodeFileName] = fileIndex; - fs.writeSync(scriptsFile, `${fileIndex}\t${sdata.originalCodeFileName}\t${codeFileName}\n`); - } - - return `${fileIndex}:${idata[0]}:${idata[1]}:${idata[2]}:${idata[3]}`; - } - - function formatIid(iid) - { - return formatIidWithSid(J$.sid, iid); - } - - function MicrowalkTraceGenerator() - { - this.invokeFunPre = function(iid, f, base, args, isConstructor, isMethod, functionIid, functionSid) - { - let functionName = ""; - if(f && f.name) - functionName = f.name; - else - { - let functionShadowObject = J$.smemory.getShadowObjectOfObject(f); - if(functionShadowObject && functionShadowObject.functionName) - functionName = functionShadowObject.functionName; - } - - // Handle special testcase begin marker function - if(functionName === testcaseBeginFunctionName) - { - // Ensure that previous trace has been fully written (prefix mode) - if(traceData.length > 0) - persistTrace(); - traceData = []; - - // If we were in prefix mode, store compression dictionaries - if(currentTestcaseId === -1) - { - prefixNextCompressedLineIndex = nextCompressedLineIndex; - prefixCompressedLines = compressedLines; - } - - // Enter new testcase - ++currentTestcaseId; - compressedLines = Object.assign({}, prefixCompressedLines); - nextCompressedLineIndex = prefixNextCompressedLineIndex; - lastCompressedLineIndex = -1000; - lastLineWasEncodedRelatively = false; - } - - // Get function information - let functionInfo = formatIidWithSid(functionSid, functionIid); - if(functionInfo == null) - { - if(f && f.name !== undefined) - functionInfo = `E:${f.name}`; - else - functionInfo = "E:?"; - } - - functionInfo += (isConstructor ? ":c" : ":"); - - writeTraceLine(`c;${formatIid(iid)};${functionInfo};${functionName}`); - - pendingConditionalState = 0; - - return {f: f, base: base, args: args, skip: false}; - }; - - this.invokeFun = function(iid, f, base, args, result, isConstructor, isMethod, functionIid, functionSid) - { - // Get function information - let functionInfo = formatIidWithSid(functionSid, functionIid); - if(functionInfo == null) - { - if(f && f.name !== undefined) - functionInfo = `E:${f.name}`; - else - functionInfo = "E:?"; - } - - functionInfo += (isConstructor ? ":c" : ":"); - - writeTraceLine(`R;${functionInfo};${formatIid(iid)}`); - - if(f && f.name === testcaseEndFunctionName) - { - // Close trace - persistTrace(); - traceData = []; - } - - pendingConditionalState = 0; - - return {result: result}; - }; - - this.getFieldPre = function(iid, base, offset, isComputed, isOpAssign, isMethodCall) - { - // Ignore writes (handled in putFieldPre) - if(isOpAssign) - return {base: base, offset: offset, skip: false}; - - // Retrieve shadow object - let shadowObject = J$.smemory.getShadowObject(base, offset); - - if(shadowObject) - { - let formattedOffset = offset; - if(typeof formattedOffset === "string") - formattedOffset = formattedOffset.replace(';', '_'); - - writePrefixedTraceLine(`g;${formatIid(iid)};${shadowObject["owner"]["*J$O*"]};`, formattedOffset); - } - - pendingConditionalState = 0; - - return {base: base, offset: offset, skip: false}; - }; - - this.putFieldPre = function(iid, base, offset, val, isComputed, isOpAssign) - { - // Retrieve shadow object - let shadowObject = J$.smemory.getShadowObject(base, offset); - - if(shadowObject) - { - let formattedOffset = offset; - if(typeof formattedOffset === "string") - formattedOffset = formattedOffset.replace(';', '_'); - - writePrefixedTraceLine(`p;${formatIid(iid)};${shadowObject["owner"]["*J$O*"]};`, formattedOffset); - } - - // If val is an anonymous function, use the property as its name - if(val && typeof val === "function" && val.name === "") - J$.smemory.getShadowObjectOfObject(val).functionName = offset; - - pendingConditionalState = 0; - - return {base: base, offset: offset, val: val, skip: false}; - }; - - this._return = function(iid, val) - { - writeTraceLine(`r;${formatIid(iid)}`); - - pendingConditionalState = 0; - - return {result: val}; - }; - - this.conditional = function(iid, result) - { - writeTraceLine(`C;${formatIid(iid)}`); - - pendingConditionalState = 2; - - return {result: result}; - }; - - this.endExpression = function(iid) - { - // Only record expressions when there is a pending conditional - if(pendingConditionalState === 0) - return; - - // Always skip expressions immediately following a conditional, as those simply span the entire conditional statement - if(pendingConditionalState === 2) - { - pendingConditionalState = 1; - return; - } - - writeTraceLine(`e;${formatIid(iid)}`); - - pendingConditionalState = 0; - }; - - this.onReady = function(cb) - { - cb(); - }; - } - - sandbox.analysis = new MicrowalkTraceGenerator(); -})(J$); - - - diff --git a/JavascriptTracer/constants.cjs b/JavascriptTracer/constants.cjs new file mode 100644 index 0000000..7339814 --- /dev/null +++ b/JavascriptTracer/constants.cjs @@ -0,0 +1,48 @@ +/** + * Common constants used for instrumentation and runtime. + * CommonJS module to support inclusion by the CommonJS runtime module. + */ + +const VALID_SUFFIXES = [".js", ".ts", ".cjs", ".mjs"] +const PLUGIN_SUFFIX = ".mw"; +const PLUGIN_SUFFIX_REGEX = /\.mw\.(js|ts|cjs|mjs)$/i; +const NODE_MODULES_DIR_NAME = "node_modules" + +const COMPUTED_OFFSET_INDICATOR = "___computed___"; +const PRIMITIVE_INDICATOR = "___primitive___"; + +const INSTR_MODULE_NAME = "$$instr"; +const FILE_ID_VAR_NAME = "$$fileId"; +const INSTR_VAR_PREFIX = "$$"; + +const CHAINVARNAME = `${INSTR_VAR_PREFIX}vChain`; +const CALLVARNAME = `${INSTR_VAR_PREFIX}vCall`; +const THISVARNAME = `${INSTR_VAR_PREFIX}vThis`; +const ARGSVARNAME = `${INSTR_VAR_PREFIX}vArgs`; +const SWITCHLABELNAME = `${INSTR_VAR_PREFIX}vSwitchLabel`; +const SWITCHFALLTHROUGHVARNAME = `${INSTR_VAR_PREFIX}vSwitchFallthrough`; +const COMPUTEDVARNAME = `${INSTR_VAR_PREFIX}vComputed`; +const TERNARYIDNAME = `${INSTR_VAR_PREFIX}vTernaryId`; + +module.exports = { + VALID_SUFFIXES, + PLUGIN_SUFFIX, + PLUGIN_SUFFIX_REGEX, + NODE_MODULES_DIR_NAME, + + COMPUTED_OFFSET_INDICATOR, + PRIMITIVE_INDICATOR, + + INSTR_MODULE_NAME, + FILE_ID_VAR_NAME, + INSTR_VAR_PREFIX, + + CHAINVARNAME, + CALLVARNAME, + THISVARNAME, + ARGSVARNAME, + SWITCHLABELNAME, + SWITCHFALLTHROUGHVARNAME, + COMPUTEDVARNAME, + TERNARYIDNAME +}; \ No newline at end of file diff --git a/JavascriptTracer/instrument-file.mjs b/JavascriptTracer/instrument-file.mjs new file mode 100644 index 0000000..4de6cdb --- /dev/null +++ b/JavascriptTracer/instrument-file.mjs @@ -0,0 +1,33 @@ +/** + * Instruments the given file and its children. + * + * @param {string} path The file to instrument. + */ + +import * as path from "node:path"; +import process from "node:process"; + +import { instrumentFileTree, getInstrumentedName } from "./instrument.mjs"; + +const cliArgs = process.argv; + +if (cliArgs.length < 3) { + console.error('No source file provided'); + console.log(`Usage: node ${cliArgs[1]} FILEPATH`) + process.exit(1); +} + +let filePath = cliArgs[2]; + +// Make path absolute +if (!path.isAbsolute(filePath)) { + filePath = path.resolve(process.cwd(), filePath); +} + +// Ensure everything is instrumented +instrumentFileTree(filePath); + +export const instrumentedName = getInstrumentedName(filePath); + +// Remove the instrumentation script from the process arguments +cliArgs.splice(1, 1); \ No newline at end of file diff --git a/JavascriptTracer/instrument-setup.mjs b/JavascriptTracer/instrument-setup.mjs new file mode 100644 index 0000000..43e0535 --- /dev/null +++ b/JavascriptTracer/instrument-setup.mjs @@ -0,0 +1,1014 @@ +/** + * This file contains a setup pass that transforms the AST before it gets instrumented. + */ + +import * as t from "@babel/types"; +import * as util from "./instrument-utility.mjs"; +import * as constants from "./constants.cjs"; +import template from "@babel/template"; + +// counter that gets incremented with each switch statement +// since each switch statement gets their own UNIQUE labeled block +let switchctr = 0; +// counter that gets incremented for each chain split +// since each split gets a UNIQUE array +let chainctr = 0; +// counter for each call split +let callctr = 0; + +/** + * Splits a given member expression chain (>1 member expression) into multiple equivalent expressions and assigns the final result + * to a variable, which then replaces the member expression chain + * @param {NodePath} path NodePath of a member expression chain + * @param {boolean} [genExpressions=true] whether to generate a sequence expression of expressions or a block statement of statements + * @returns + */ +function explodeMemberExpression(path, genExpressions=true) { + const stack = []; + let currentNode = path.node, memberExpressionCount = 0; + + // traverse to base of member expression (from right to left) + while (util.isMemberOrOptExpression(currentNode) || util.isCallOrOptExpression(currentNode)) { + stack.push(currentNode); + switch (true) { + case t.isOptionalMemberExpression(currentNode): + case t.isMemberExpression(currentNode): + // update to next node + currentNode = currentNode.object; + memberExpressionCount++; + + break; + + case t.isOptionalCallExpression(currentNode): + case t.isCallExpression(currentNode): + // update to next node + currentNode = currentNode.callee; + break; + } + } + + // only continue if it's a chain (more than 1 member expression) + if (memberExpressionCount <= 1) { + return; + } + + const baseNumber = chainctr++; + const varBaseString = `${constants.INSTR_VAR_PREFIX}tmp${baseNumber}`; + + + const varDec = template.default.ast(` + let ${constants.CHAINVARNAME}${baseNumber}; + `); + // use location info of nearest statement + const varDecLoc = util.getStatementParent(path).node.loc; + varDec.loc = varDecLoc; + const declarationsArray = varDec.declarations; + // insert declarations directly prior to node + const statParent = util.getStatementParent(path); + statParent.insertBefore(varDec); + + /** + * + * @param {t.Node} node + * @param {number} splitNr + * @returns {t.AssignmentExpression} + */ + const genMemberSplit = function(node, splitNr) { + // ensure correct node type + if (!util.isMemberOrOptExpression(node)) { + throw new Error(`Expected MemberExpression node for splitting; instead got ${node.type}`); + } + + const oldObjLoc = node.object.loc; + // change passed node to use previous var as base + node.object = t.identifier(`${varBaseString}_${splitNr-1}`); + node.object.loc = oldObjLoc; + + // temp var that stores evaluation + const tmpId = t.identifier(`${varBaseString}_${splitNr}`); + tmpId.loc = node.loc; + // add to declarations + declarationsArray.push(t.variableDeclarator(tmpId)); + + const rhs = t.cloneNode(node, true, false); + const assigment = template.default(`%%lhs%% = %%rhs%%;`)({lhs: tmpId, rhs}).expression; + + // mark helper nodes as logged + assigment.writeLogged = true; + tmpId.writeLogged = true; + tmpId.readLogged = true; + + return assigment; + } + + const genCallSplit = function(node, splitNr) { + // ensure correct node type + if (!util.isCallOrOptExpression(node)) { + throw new Error(`Expected CallExpression node for splitting; instead got ${node.type}`); + } + + // tmp var that stores evaluation + const tmpId = t.identifier(`${varBaseString}_${splitNr}`); + tmpId.loc = node.loc; + // add to declarations + declarationsArray.push(t.variableDeclarator(tmpId)); + + // callee id is previous tmp var + const calleeId = t.identifier(`${varBaseString}_${splitNr - 1}`); + let callee; + if (t.isOptionalCallExpression(node)) { + callee = template.default(`%%calleeId%%?.call`)({calleeId}).expression; + } else { + callee = template.default(`%%calleeId%%.call`)({calleeId}).expression; + } + + // "this" id is two prior + if (splitNr - 2 < 0) { + throw new Error(`Illegal call in chain split. Can't reference negative tmp var.`); + } + const thisId = t.identifier(`${varBaseString}_${splitNr - 2}`); + thisId.loc = sequenceArr.at(-1).right.loc; + + // update callee in node + node.callee = callee; + // prepend "this" to args + node.arguments.unshift(thisId); + + const rhs = t.cloneNode(node, true, false); + const assigment = template.default(`%%lhs%% = %%rhs%%;`)({lhs: tmpId, rhs}).expression; + + // mark helper nodes as logged + assigment.writeLogged = true; + tmpId.writeLogged = true; + tmpId.readLogged = true; + thisId.readLogged = true; + rhs.squashed = true; + + return assigment; + } + + // current node is base object -> tmp0 + // tmp var for evaluation result + const tmp0Id = t.identifier(`${varBaseString}_0`); + // add to declarations + declarationsArray.push(t.variableDeclarator(tmp0Id)); + + const firstAssigment = template.default(`%%tmpId%% = %%rhs%%;`)({tmpId: tmp0Id, rhs: currentNode}).expression; + + + // this is the array for the sequence expression that will contain ALL temp results etc. + const sequenceArr = [firstAssigment]; + + // mark helper nodes as logged + firstAssigment.writeLogged = true; + tmp0Id.writeLogged = true; + tmp0Id.readLogged = true; + + + // create temp variables (from left to right of member expression) + let splitNr = sequenceArr.length; + while (stack.length > 2) { + // get top node from stack + currentNode = stack.pop(); + + switch (true) { + case t.isMemberExpression(currentNode): + case t.isOptionalMemberExpression(currentNode): { + // generate and save assignment expression + const assign = genMemberSplit(currentNode, splitNr++); + sequenceArr.push(assign); + + break; + } + // wrap the last var into (optional) call expression + case t.isCallExpression(currentNode): + case t.isOptionalCallExpression(currentNode): { + // generate and save assignment + const assign = genCallSplit(currentNode, splitNr++); + // also add call result var to sequence + sequenceArr.push(assign, assign.left); + + break; + } + } + } + + // second to last split is assigned to chain var + currentNode = stack.pop(); + + const finalVarId = t.identifier(`${constants.CHAINVARNAME}${baseNumber}`); + let finalAssignment + const finalLoc = currentNode.loc; + finalVarId.loc = finalLoc; + + if (util.isMemberOrOptExpression(currentNode)) { + finalAssignment = genMemberSplit(currentNode, splitNr++); + } else if (util.isCallOrOptExpression(currentNode)) { + finalAssignment = genCallSplit(currentNode, splitNr++); + } + else { + throw new Error(`Unexpected node type ${currentNode.type} encountered during member expression split.`); + } + + // replace lhs since + // tmpX = tmpX-1.prop + // was generated + finalAssignment.left = finalVarId; + + // mark as logged + finalAssignment.writeLogged = true; + finalVarId.readLogged = true; + + sequenceArr.push(finalAssignment); + if (util.isCallOrOptExpression(currentNode)) { + sequenceArr.push(finalAssignment.left); + } + + // final member expression is retained + currentNode = stack.pop(); + const finalEx = t.cloneNode(currentNode, true, false); + // replace object with chain id + finalEx.object = finalVarId; + // copy location + finalEx.loc = finalLoc; + + // use sequence expression + if (genExpressions) { + sequenceArr.push(finalEx); + + // create sequence expression + const seqEx = t.sequenceExpression(sequenceArr); + seqEx.loc = path.node.loc; + + // use function method for call if parent is call expression + if (util.isCallOrOptExpression(path.parent) && path.listKey !== 'arguments') { + const callParent = path.parent; + + const callVarId = t.identifier(`${constants.CALLVARNAME}${callctr++}`); + callVarId.loc = callParent.loc; + declarationsArray.push(t.variableDeclarator(callVarId)); + + let callee; + if (t.isOptionalCallExpression(callParent)) { + callee = template.default(`%%seq%%?.call;`)({ + seq: seqEx + }).expression; + } + else { + callee = template.default(`%%seq%%.call;`)({ + seq: seqEx + }).expression; + } + + const callAssignEx = template.default(` + %%callVarId%% = %%callee%%(%%thisVal%%, %%args%%); + `)({ + callVarId, + callee: callee, + thisVal: finalVarId, + args: callParent.arguments + }).expression; + + // copy location + callAssignEx.right.loc = callParent.loc; + callAssignEx.loc = callParent.loc; + + // mark helper nodes as logged + callAssignEx.right.squashed = true; + callVarId.readLogged = true; + callVarId.writeLogged = true; + + const callSeq = t.sequenceExpression([callAssignEx, callVarId]); + callSeq.loc = callParent.loc; + + // replace call parent with sequence expression + // (vCall = (chain...).call(), vCall) + path.parentPath.replaceWith(callSeq); + } + else { + // simply replace chain with sequence expression + path.replaceWith(seqEx); + } + } + // create a block statement + else { + // create block with statements instead of expressions + const block = t.blockStatement(sequenceArr.map((val) => t.expressionStatement(val))); + block.loc = path.node.loc; + + // insert block before node + util.getStatementParent(path).insertBefore(block); + + path.replaceWith(finalEx); + } + + return; +} + +/** + * Squashes call expressions so that every call expression is assigned to a variable with an identifier as the callee. + * example: + before: a()()() + after: + call0_0 = a(), + call0_1 = call0_0(), + call0_2 = call0_1(), + call0_2 + + before: a.b(1,2,3) + after: + this1 = a, + call1_0 = this1.b, + call1_1 = call1_0.call(this1, 1, 2, 3), + call1_1 + + * @param {NodePath} path Path of the call expression + * @returns + */ +function squashCallees(path) { + // sanity checks + if (path.node.squashed || path.node.ignore) { + // path has already been squashed + return; + } + + let callee = path.node.callee; + + if (!util.isCallOrOptExpression(callee) && !util.isMemberOrOptExpression(callee)) { + callee = path.node; + } + + if (!util.isCallOrOptExpression(path.node)) { + // path isn't a call expression or callee isn't applicable + return; + } + + const baseNumber = callctr++; + const varBaseString = `${constants.CALLVARNAME}${baseNumber}`; + + const varDec = template.default.ast(` + let ${varBaseString}_0; + `); + const varDecLoc = util.getStatementParent(path).node.loc; + varDec.loc = varDecLoc; + const declarationsArray = varDec.declarations; + declarationsArray[0].writeLogged = true; + // insert declarations directly prior to statment of node + util.getStatementParent(path).insertBefore(varDec); + + // array containing all expressions from call split + const sequenceArr = []; + + if (util.isMemberOrOptExpression(callee)) { + + // save member object as this value of call + const thisId = t.identifier(`${constants.THISVARNAME}${baseNumber}`); + thisId.loc = callee.object.loc; + // add this var to declarations + declarationsArray.push(t.variableDeclarator(thisId)); + const thisAssign = template.default(` + %%thisId%% = %%callee%%; + `)({ thisId, callee: callee.object }).expression; + // save expression + sequenceArr.push(thisAssign); + + // assign member expression to call var + const callVarId = t.identifier(`${varBaseString}_0`); + callVarId.loc = callee.loc; + const baseAssign = template.default(` + %%callVarId%% = %%thisId%%.%%prop%%; + `)({ callVarId, thisId, prop: callee.property }).expression; + // save expression + sequenceArr.push(baseAssign); + + // mark helper nodes as logged + thisAssign.writeLogged = true; + baseAssign.writeLogged = true; + callVarId.readLogged = true; + callVarId.writeLogged = true; + thisId.readLogged = true; + thisId.writeLogged = true; + + // generate call + // $$call.call + const callMemberEx = t.memberExpression(callVarId, t.identifier("call")); + // update callee to $$call.call + path.node.callee = callMemberEx; + // add $$this as first argument + path.node.arguments.unshift(thisId); + + // mark as logged + path.node.squashed = true; + path.node.readLogged = true; + // callee already gets logged in assignment expression + callMemberEx.readLogged = true; + + // convert args to array accesses + // and generate array of corresponding expressions saving the args + const argsExpArr = convertArgs(path, `${baseNumber}_0`, declarationsArray); + // add to sequence array + sequenceArr.push(...argsExpArr); + + const callResId = t.identifier(`${varBaseString}_1`); + callResId.loc = path.node.loc; + declarationsArray.push(t.variableDeclarator(callResId)); + + // assign call result to final var + const finalAssign = template.default(` + %%callResId%% = %%call%%; + `)({ callResId, call: path.node }).expression; + + // mark helpers as logged + callResId.readLogged = true; + callResId.writeLogged = true; + finalAssign.writeLogged = true; + + + sequenceArr.push(finalAssign); + // final expression in sequence is the result of the call + sequenceArr.push(callResId); + + const seqEx = t.sequenceExpression(sequenceArr); + seqEx.loc = path.node.loc; + + // replace path with sequence expression + path.replaceWith(seqEx); + + return; + } + else if (util.isCallOrOptExpression(callee)) { + let currentCallee = path.get("callee"); + const stack = [path]; + let callCtr = 0; + + // save all calls from chain + while (util.isCallOrOptExpression(currentCallee)) { + stack.push(currentCallee); + currentCallee = currentCallee.get("callee"); + } + + // innermost node is the base object (i.e. not a call expression) + // so we need to wrap that in the first call + const firstCall = stack.pop(); + firstCall.node.callee = currentCallee.node; + // generate first assignment expression with the first call + const firstAssign = template.default(` + %%callResId%% = %%call%%; + `)({ + callResId: t.identifier(`${varBaseString}_0`), + call: firstCall.node + }).expression; + + // mark as logged + firstAssign.writeLogged = true; + firstCall.node.squashed = true; + + // convert args to array accesses + // and generate array of corresponding expressions saving the args + const argsExpArr = convertArgs(firstCall, `${baseNumber}_0`, declarationsArray); + // add to sequence array + sequenceArr.push(...argsExpArr); + + // add to sequence + sequenceArr.push(firstAssign); + callCtr++; + + // unwind stack into separate calls with each call being of the form + // $$call = $$call(); + while (stack.length > 0) { + currentCallee = stack.pop(); + const currentCallNode = currentCallee.node; + + // update callee to $$call + currentCallNode.callee = t.identifier(`${varBaseString}_${callCtr - 1}`); + + // identifier for result of call + const callResId = t.identifier(`${varBaseString}_${callCtr}`); + callResId.loc = currentCallee.node.loc; + // add to declarations + declarationsArray.push(t.variableDeclarator(callResId)); + + const assign = template.default(` + %%callResId%% = %%call%%; + `)({ callResId, call: currentCallNode }).expression; + + // convert args to array accesses + // and generate array of corresponding expressions saving the args + const argsExpArr = convertArgs(currentCallee, `${baseNumber}_${callCtr}`, declarationsArray); + // add to sequence array + sequenceArr.push(...argsExpArr); + + // mark helper nodes as logged + assign.writeLogged = true; + callResId.writeLogged = true; + callResId.readLogged = true; + currentCallNode.squashed = true; + + sequenceArr.push(assign); + callCtr++; + } + + // set final call result as last expression in sequence + sequenceArr.push(sequenceArr.at(-1).left); + + const seqEx = t.sequenceExpression(sequenceArr); + seqEx.loc = path.node.loc; + + // replace chain with sequence expression + path.replaceWith(seqEx); + + return; + } +} + +/** + * Alters the ast around a given call expression to save arguments of the call into an array and then + * pass the array accesses as arguments. Insertion of the generated block statement can be handled by the + * function or the caller. + * @param {NodePath} path path of the call expression whose arguments should be converted + * @param {NodePath} placeholderPath path of the placeholder node that is then replaced with the arg block + * @returns + */ +function convertArgs(path, ctr, declarationsArray) { + // sanity check + if (path.node.convertedArgs || path.node.ignore) { + return t.blockStatement([]); + } + + const argsId = t.identifier(`${constants.ARGSVARNAME}${ctr}`); + argsId.writeLogged = true; + argsId.readLogged = true; + + // array that has the member expressions of all new args + const newCallArgs = []; + + // save original args + const originalArgs = path.node.arguments; + + // init local tmp array + // const args = [] + const varDecor = t.variableDeclarator(argsId, t.arrayExpression([])); + varDecor.ignore = true; + declarationsArray.push(varDecor); + + // array to save all arg expressions + const argSequenceArr = []; + + // save args into local scoped tmp array $$args + for (let i = 0; i < originalArgs.length; i++) { + // skip $$this + if (t.isIdentifier(originalArgs[i]) && originalArgs[i].name == constants.THISVARNAME) { + newCallArgs.push(originalArgs[i]); + continue; + } + + // member expression: $$args.push (for saving the arg) + const argsPushMemberEx = t.memberExpression(argsId, t.identifier("push")); + argsPushMemberEx.readLogged = true; + + // call expression: $$args.unshift() (for arg in call) + const newArgEx = template.default(` + %%argsId%%.shift(); + `)({ argsId }).expression; + + newArgEx.ignore = true; + newCallArgs.push(newArgEx); + + const argPath = path.get(`arguments.${i}`); + + // try to find a member expression in arg + let argMemExPath = argPath; + while (t.isCallExpression(argMemExPath)) { + argMemExPath = argMemExPath.get('callee'); + } + + if (t.isMemberExpression(argMemExPath)) { + // explode member expression chains if applicable + explodeMemberExpression(argMemExPath); + } + + // call expression: $$args.push(originalArgs[i]) + const pushArgEx = t.callExpression(argsPushMemberEx, [originalArgs[i]]); + + // avoid superfluous logging + pushArgEx.ignore = true; + + argSequenceArr.push(pushArgEx); + } + + // replace args in call with array access + path.node.arguments = []; + path.pushContainer("arguments", newCallArgs); + + return argSequenceArr; +} + +/** + * Ensures a given if- or switch statement has a block statement for a body + * @param {NodePath} path path of IfStatement or SwitchStatement + * @returns + */ +function ensureConsequentBlock(path) { + const consPath = path.get('consequent'); + const consNode = consPath.node; + + // already a block statement? + if (t.isBlockStatement(consNode)) + return; + + if (t.isIfStatement(path)) { + // if statements have a singular statement as their consequence + // wrap that in a block statement + const block = t.toBlock(consNode, path.node); + block.loc = consNode.loc; + // replace + consPath.replaceWith(block); + + // also ensure alternate is a block + const altPath = path.get('alternate'); + if (altPath.node) { + // alternate exists + const altBlock = t.toBlock(altPath.node, path.node); + altBlock.loc = altPath.node.loc; + altPath.replaceWith(altBlock); + } + } + else if (t.isSwitchCase(path)) { + // consequent is an array of statements + // add a block statement with all statements as the first item + path.node.consequent[0] = t.blockStatement(path.node.consequent); + // remove all other items + path.node.consequent.splice(1); + } +} + +/** + * Assigns evaluated computed property values to temp variables so they can be traced. + * example + * before: + * a[1+1] + * after: + * computed = 1+1 + * a[computed] + * @param {NodePath} path + */ +function assignComputedToTemp(path) { + const node = path.node; + + // ignore static member expressions + if (!node.computed || node.computeassigned || node.ignore) return; + + // properties that are already identifiers or numeric literals are fine + if (t.isNumericLiteral(node.property) || t.isStringLiteral(node.property)) return; + + // property is guaranteed to be an expression that is not an identifier + // create new statement with assignment + const computedVar = t.identifier(constants.COMPUTEDVARNAME); + computedVar.ignore = true; + // copy loc from expression + computedVar.loc = node.property.loc; + + const computedAssign = template.default(` + %%computedVar%% = %%propertyExpression%%; + `)({ + computedVar, + propertyExpression: node.property + }).expression; + + computedAssign.ignore = true; + computedAssign.loc = node.loc; + + // replace property with assigned var + path.get('property').replaceWith(computedVar); + // make property into sequence expression of assignment and var + // todo: nested computed properties don't get assigned + // todo: probably add to traversal q instead of using injectAst ? + path.get('property').insertBefore(computedAssign) + + node.computeassigned = true; +} + +export const setupVisitor = { + "MemberExpression|OptionalMemberExpression"(path) { + // check parents (until statement parent) for call expression + let parentPath = path, ignore = false, genExpressions = true; + while (parentPath && !t.isStatement(parentPath) && !t.isBlockParent(parentPath)) { + // check the list key (we want to ignore member expressions that are call arguments) + if (parentPath.listKey == "arguments") { + ignore = true; + break; + } + + // generate statements for lhs of assignments + // and update expressions + if (t.isAssignmentExpression(parentPath.parent) && parentPath.key == "left" + || t.isUpdateExpression(parentPath)) { + genExpressions = false; + break; + } + + parentPath = parentPath.parentPath; + } + + + // don't explode member expressions that are call arguments at this point + if (!ignore && !path.node.ignore) { + explodeMemberExpression(path, genExpressions); + } + }, + + // we want all functions to have a proper block body + "Function"(path) { + util.ensureBlock(path); + }, + + // transform switch statement to if/else statements + SwitchStatement(path) { + // wrap everything in a labeled block statement (to ensure break functionality) + const switchBlock = t.blockStatement([]); + switchBlock.loc = path.node.loc; + const switchLabel = t.identifier(`${constants.SWITCHLABELNAME}${switchctr++}`); + + const fallthroughVarId = t.identifier(constants.SWITCHFALLTHROUGHVARNAME); + fallthroughVarId.readLogged = true; + fallthroughVarId.writeLogged = true; + + const genAssignFallthroughStat = (bool) => { + const assignmentExpression = t.assignmentExpression("=", fallthroughVarId, t.booleanLiteral(bool)); + const expressionStatement = t.expressionStatement(assignmentExpression); + expressionStatement.new = true; + return expressionStatement; + } + // set fallthrough var to false + switchBlock.body.push(genAssignFallthroughStat(false)); + + const switchCases = path.get('cases'); + let currentCase, defaultCase, completeTest; + const genTestExpression = (test) => { + // we want each of these to be their own node so we have to clone + const discriminantExp = t.cloneNode(path.node.discriminant, true, false); + return t.binaryExpression("===", discriminantExp, test); + }; + + while (switchCases.length > 0) { + currentCase = switchCases.shift(); + let currentTest = currentCase.node.test; + let newTest; + + if (!currentTest) { + // test == null means it's the default case + defaultCase = currentCase; + } else { + newTest = genTestExpression(currentTest); + } + + // empty consequent means case gets combined with following case + while(currentCase.node.consequent.length == 0 && switchCases.length > 0) { + currentCase = switchCases.shift(); + currentTest = currentCase.node.test; + newTest = currentTest ? + t.logicalExpression("||", newTest, genTestExpression(currentTest)) : + newTest; + } + + // update complete test (for default case) + if (newTest) { + completeTest = completeTest ? t.logicalExpression("||", completeTest, newTest) : newTest; + } + + const consequentStatStack = [].concat(currentCase.node.consequent); + + // update break statements to break out of labeled block + while (consequentStatStack.length > 0) { + let statement = consequentStatStack.pop(); + + switch (true) { + case t.isBreakStatement(statement): { + // update label + statement.label = switchLabel; + break; + } + case t.isSwitchCase(statement): { + consequentStatStack.push(...statement.consequent); + break; + } + case t.isBlockStatement(statement): { + consequentStatStack.push(...statement.body); + break; + } + case t.isWithStatement(statement): + case t.isLabeledStatement(statement): + case t.isWhileStatement(statement): + case t.isDoWhileStatement(statement): + case t.isCatchClause(statement): + case t.isFunctionDeclaration(statement): + case t.isFor(statement): { + consequentStatStack.push(statement.body); + break; + } + case t.isIfStatement(statement): { + consequentStatStack.push(statement.consequent); + if (statement.alternate) { + consequentStatStack.push(statement.alternate); + } + break; + } + case t.isTryStatement(statement): { + consequentStatStack.push(statement.block, statement.finalizer); + break; + } + } + } + + // wrap consequent statement(s) in block since if accepts singular statement as body + const consequentBlock = t.blockStatement([genAssignFallthroughStat(true)].concat(currentCase.node.consequent)); + // test fallthrough first to short-circuit + const finalTest = newTest ? t.logicalExpression("||", fallthroughVarId, newTest) : fallthroughVarId; + const ifReplacement = t.ifStatement(finalTest, consequentBlock); + // copy location info + ifReplacement.loc = currentCase.node.loc; + if (currentCase.node.consequent.length > 0) { + consequentBlock.loc = currentCase.node.consequent[0]?.loc; + consequentBlock.loc.end = currentCase.node.consequent.at(-1)?.loc?.end; + } // copy location of case if there is no consequence (i.e. empty default case) + else { + consequentBlock.loc = currentCase.node.loc; + } + + if (currentCase == defaultCase) { + defaultCase = ifReplacement; + } + + switchBlock.body.push(ifReplacement); + } + + // update default case test + if (defaultCase) { + const defaultTest = t.unaryExpression("!", t.cloneNode(completeTest, true, false), true); + defaultCase.test = t.logicalExpression("||", defaultTest, fallthroughVarId); + } + + // replace switch statement with labeled block + const labeledStatement = t.labeledStatement(switchLabel, switchBlock); + labeledStatement.loc = path.node.loc; + path.replaceWith(labeledStatement); + }, + + "IfStatement"(path) { + ensureConsequentBlock(path); + }, + + // move variable declarations to their own statements + For(path) { + // rewrite for expression to equivalent while loop + const node = path.node; + + // skip for nodes that have already been transformed + if (node.forhoisted) + return; + + switch (true) { + case (t.isForXStatement(node)): { + let loopVarKind = t.isVariableDeclaration(node.left) ? node.left.kind : "var"; + let allBindingsExist = true; + + if (!t.isVariableDeclaration(node.left)) { + // check whether all bindings exist + let [loopVarIds] = util.gatherIdsAndOffsets(node.left); + for (let loopVarId of loopVarIds) { + let idString = t.isIdentifier(loopVarId) ? loopVarId.name : loopVarId; + allBindingsExist &&= path.scope.hasBinding(idString); + } + } + + // only change forX if they have a variable declaration + // OR it's an implicit declaration bc a binding doesn't exist + if (t.isVariableDeclaration(node.left) || !allBindingsExist) { + let leftVarDec = path.get('left'), forPath = path; + + // unwrap destructuring patterns since we only need the vars to exist beforehand + let loopIds = []; + for (let varDeclarator of leftVarDec.node.declarations ?? [leftVarDec.node]) { + [loopIds] = util.gatherIdsAndOffsets(varDeclarator.id ?? varDeclarator); + } + const declarators = []; + for (let id of loopIds) { + declarators.push(t.variableDeclarator(id)); + } + + // check scope -> var is in the same scope as for, others loop local + if (loopVarKind == "var") + path.insertBefore(t.variableDeclaration("var", declarators)); + else { + // loop local -> wrap in block + // replace const with let since constants need to be assigned values upon declaration + const blockWrapper = t.blockStatement([t.variableDeclaration("let", declarators), path.node]); + path.replaceWith(blockWrapper); + forPath = path.get('body')[1]; + } + forPath.node.forhoisted = true; + + // replace variable declaration with init value + if (t.isVariableDeclaration(leftVarDec)) + leftVarDec.replaceWith(leftVarDec.node.declarations.at(-1).id); + } + + break; + } + case t.isForStatement(node): { + const statements = []; + // extract components + const init = node.init; + const test = node.test; + const update = node.update; + + let initStatement; + if (init) { + initStatement = t.isStatement(init) ? init : t.expressionStatement(init); + initStatement.loc = init.loc; + } + let testExpr = test ?? t.booleanLiteral(true); + testExpr.loc = test?.loc; + let updateStatement = update ? t.expressionStatement(update) : null; + + util.ensureBlock(path); + + // find all continue statements + const continues = util.getAllContinues(path); + + // add update statement to end of body + if (updateStatement) { + updateStatement.loc = update.loc; + node.body.body.push(updateStatement); + // and before any continue statements + for (let statement of continues) { + const freshClone = t.cloneNode(updateStatement, true, false); + freshClone.loc = update.loc; + statement.insertBefore(freshClone); + } + } + + // init is first statement of block + if (initStatement) statements.push(initStatement); + // create while with test and body + const whileStatement = t.whileStatement(testExpr, node.body); + // while has loc of for node + whileStatement.loc = node.loc; + // ensure body has a location + if (!whileStatement.body.loc) { + const loc = node.loc; + loc.start = whileStatement.body.body.at(0).loc.start; + // second to last item since the last item is our injected update statement + loc.end = whileStatement.body.body.at(-2).loc.end; + + whileStatement.body.loc = loc; + } + + // add while loop as second statement + statements.push(whileStatement); + + // replace for with block containing init and while loop + path.replaceWith(t.blockStatement(statements)); + } + } + }, + "DoWhileStatement"(path) { + const statements = []; + const node = path.node; + + // copy body statements + if (t.isBlockStatement(node.body)) { + const bodyClone = node.body.body.map(n => t.cloneNode(n, true, false)); + statements.push(...bodyClone); + } else { + statements.push(t.cloneNode(node.body, true, false)); + } + + // create a while statement with the same test and body + const newWhile = t.whileStatement(node.test, node.body); + newWhile.loc = node.loc; + statements.push(newWhile); + + // replace with a block statement that has the entire body and then the new while + path.replaceWith(t.blockStatement(statements)); + }, + + // ensure while has block body + Loop(path) { + util.ensureBlock(path); + } +}; + +export const setupCallExpressionsVisitor = { + "CallExpression|OptionalCallExpression"(path) { + + if (!path.node.squashed && !path.node.ignore) + squashCallees(path); + }, + + "MemberExpression|OptionalMemberExpression"(path) { + if (path.node.computed) { + assignComputedToTemp(path); + } + } +}; \ No newline at end of file diff --git a/JavascriptTracer/instrument-utility.mjs b/JavascriptTracer/instrument-utility.mjs new file mode 100644 index 0000000..f334c8e --- /dev/null +++ b/JavascriptTracer/instrument-utility.mjs @@ -0,0 +1,519 @@ +/** + * Contains utility functions for the instrumentation process. + */ + +import * as t from "@babel/types"; +import * as constants from "./constants.cjs"; + +/** + * + * @param {Node} node - + * @param {boolean} isRead - whether the ids and offsets that are being read should be returned; defaults to true + * @returns {Array} An array consisting of two arrays, the first one has ids that are used while the second one has offsets. The order of the arrays is matching meaning the offset of id[0] is at offsets[0] + */ +export function gatherIdsAndOffsets(node, isRead=true) { + const ids = []; + const properties = []; + + switch (true) { + // ignore meta properties + case (t.isMetaProperty(node)): + return [[], []]; + // ignore window things / self + case (node?.name === "window" + || node?.name === "self" + || (t.isMemberExpression(node) && node.object.name === "window")): + return [[], []]; + + case (t.isIdentifier(node)): + return [[node], properties]; + + case (t.isLiteral(node)): { + const primitive = t.identifier(constants.PRIMITIVE_INDICATOR); + primitive.readLogged = true; + primitive.loc = node.loc; + return [[primitive], properties]; + } + + case t.isUpdateExpression(node): + case t.isUnaryExpression(node): + case t.isYieldExpression(node): + case t.isAwaitExpression(node): + case (t.isRestElement(node)): + return gatherIdsAndOffsets(node.argument); + + case (t.isThisExpression(node)): + ids.push('this'); + break; + + case t.isObjectMember(node): { + return gatherIdsAndOffsets(node.key); + } + + case t.isTupleExpression(node): + case t.isArrayExpression(node): { + for (let e of node.elements) { + const [arrayIds, arrayProperties] = gatherIdsAndOffsets(e); + ids.push(...arrayIds); + properties.push(...arrayProperties); + } + break; + } + + case t.isRecordExpression(node): + case t.isObjectExpression(node): { + for (let p of node.properties) { + const [objIds, objProps] = gatherIdsAndOffsets(p); + ids.push(...objIds); + properties.push(...objProps); + } + break; + } + + case t.isLogicalExpression(node): + case t.isBinaryExpression(node): { + const [leftIds, leftProps] = gatherIdsAndOffsets(node.left); + const [righIds, rightProps] = gatherIdsAndOffsets(node.right); + ids.push(...leftIds); + ids.push(...righIds); + properties.push(...leftProps); + properties.push(...rightProps); + break; + } + + case (isCallOrOptExpression(node)): { + const [calleeIds, calleeProps] = gatherIdsAndOffsets(node.callee); + + if (calleeProps.length !== 0) { + properties.push(...calleeProps); + } + + ids.push(...calleeIds); + break; + } + + // due to previous setup there are no member expression chains (i.e. each member expression is only a single obj.prop / obj[prop] expression) + case (isMemberOrOptExpression(node)): { + ids.push(...gatherIdsAndOffsets(node.object)[0]); + + // gather property ids + // computed properties are a special case -- i.e. o[prop] + if (node.computed) { + const property = node.property; + + // due to setup all computed properties are guaranteed to be either + // numeric/string literals or sequence expressions ending in an identifier + if (t.isNumericLiteral(property) || t.isStringLiteral(property)) { + properties.push(`[${property.value}]`); + } + else if (t.isSequenceExpression) { + properties.push(property.expressions.at(-1)); + } + else if (t.isIdentifier(property)) { + properties.push(property); + } + else if (t.isCallExpression(property) + && t.isMemberExpression(property.callee) + && property.callee.object.name == constants.COMPUTEDVARNAME) { + properties.push(constants.COMPUTED_OFFSET_INDICATOR); + } + } + // regular properties -- i.e. o.prop + else { + properties.push(...gatherIdsAndOffsets(node.property)[0]); + } + + break; + } + + case t.isVariableDeclarator(node): + case t.isAssignmentExpression(node): { + const pat = node.id ?? node.left; + const base = node.init ?? node.right; + // destructuring assignment + if (t.isObjectPattern(pat)) { + const [baseId] = gatherIdsAndOffsets(base, isRead); + const [patIds] = gatherIdsAndOffsets(pat, isRead); + for (let i = 0; i < patIds.length; i++) { + const bId = i >= baseId.length ? baseId[0] : baseId[i]; + ids.push(bId); + properties.push(patIds[i]); + } + } + else if (t.isArrayPattern(pat)) { + + // ignore array expressions on rhs - elements will be handled by later visitors + // override readlogged since elements of array should be logged individually instead + if (t.isArrayExpression(base) && isRead) { + node.readLogged = false; + return [[], []]; + } + + else if (t.isArrayExpression(base) && !isRead) { + const [arrayIds, arrayProps] = gatherIdsAndOffsets(pat); + ids.push(...arrayIds); + properties.push(...arrayProps); + } + + // not an array -> singular object / id + // use right as base + else { + const [baseId] = gatherIdsAndOffsets(base, isRead); + // we only care about the number of elements in the pattern + const eleAmount = pat.elements.length; + // add the base + index for each element in the pattern + for(let i = 0; i < eleAmount; i++) { + if (!pat.elements[i]) { + // skip null elements + continue; + } + + ids.push(baseId[0]); + properties.push(`${i}`); + } + properties.reverse(); + } + } + break; + } + + case (t.isPattern(node)): { + let elementsOrProperties; + switch (true) { + // array + case t.isArrayPattern(node): + elementsOrProperties = node.elements; + break; + + // obj + case t.isObjectPattern(node): + elementsOrProperties = node.properties; + break; + } + + // gather all pattern ids + for (let e of elementsOrProperties) { + // unwrap object property value if need be + if (e && t.isObjectProperty(e)) { + e = isRead ? e.key : e.value ?? e.key; + } + + if (t.isIdentifier(e)) + ids.push(e); + // skip null elements + else if (!e) + continue; + // other (e.g. rest element, pattern, expression) + else { + const extraIdsAndOffsets = gatherIdsAndOffsets(e); + ids.push(...extraIdsAndOffsets[0]); + properties.push(...extraIdsAndOffsets[1]); + } + } + + break; + } + } + // console.log(node); + + return [ids, properties]; +} + +/** + * Injects a given ast into a path as a sibling node + * @param {ast} ast - the ast to inject + * @param {NodePath} path - the path to inject into + * @param {boolean} [insertBefore=true] - whether to insert the given ast before (or after if false); default is true + * @param {boolean} [markAsNew=true] whether to mark the inserted nodes as new, i.e. these nodes and the path will be SKIPPED by instrumentation; default true + * @param {boolean} [insertInto=false] whether to try to insert before the path as a statement or INTO the path as an expression; default false + * @returns {NodePath[]} array of paths that were inserted + */ +export function injectAst(ast, path, insertBefore=true, markAsNew=true, insertInto=false) { + const queueLengths = new Map(); + let parent = path, contexts, newPaths = [], isTest = false; + let containerInsert = path.has('body'); + + while (!insertInto && (!t.isStatement(parent) && !containerInsert)) { + isTest = parent.key == 'test' || isTest; + parent = parent.parentPath; + } + + // save current queue length + contexts = parent._getQueueContexts(); + for (let context of contexts) { + queueLengths.set(context, context.queue?.length ?? 0); + } + + // mark injection ast as new to stop repeated traversal + if (markAsNew) { + ast.new = true; + if (markAsNew && insertInto && ast?.expression) { + ast.expression.new = true; + } + } + + + + // inject as sibling of the statement parent + if (containerInsert || t.isBlockStatement(path)) { + // insert INTO containers and block statements + // this means the new node will be placed at the start or end of the container + + const insertFn = insertBefore ? parent.unshiftContainer : parent.pushContainer + // ensure body is a block statement + const bodyPath = path.get('body'); + if (!t.isBlockStatement(bodyPath) && !Array.isArray(bodyPath)) { + util.ensureBlock(parent); + // copy location info to new block + parent.node.body.loc = bodyPath.node.loc; + } + + // ensure body is an array of statements so we can do a container insertion + if (!Array.isArray(parent.get('body'))) { + parent = parent.get('body'); + } + + newPaths = insertFn.call(parent, 'body', ast); + } else if (t.isReturnStatement(parent) && !insertBefore && t.isExpressionStatement(ast)) { + // special handling of return statements since anything BEHIND a return is unreachable + // this only works with EXPRESSIONS + + const retArgument = parent.get('argument'); + const expression = ast.expression; + expression.new = true; + + if (t.isSequenceExpression(retArgument)) { + // already a sequence expression so simply prepend + retArgument.unshiftContainer('expressions', expression); + } + else { + // wrap argument in a sequence expression + const seqExpression = t.toSequenceExpression([ast, retArgument.node]); + retArgument.replaceWith(seqExpression); + } + newPaths.push(parent.get('argument')); + } + else if (isTest && ( t.isExpression(ast) || t.isExpressionStatement(ast) ) && t.isIfStatement(parent)) { + // special handling when the node is in the test of a statement + // do while is special so it gets its own case + const testExpression = parent.get('test'); + + // an expression can be inserted before by making the test into a sequence expression + if (insertBefore) { + const expression = ast.expression; + expression.new = true; + + if (t.isSequenceExpression(testExpression)) { + testExpression.unshiftContainer('expressions', expression); + } else { + // wrap in a sequence expression + const seqExpression = t.toSequenceExpression([ast, testExpression.node]); + testExpression.replaceWith(seqExpression); + } + + newPaths.push(parent.get('test')); + } + else { + // insert into consequent body + const consequent = parent.get('consequent'); + consequent.unshiftContainer('body', ast); + // insert into alternate body (if applicable) + const alternate = parent.get('alternate'); + if (alternate.node) { + alternate.unshiftContainer('body', ast); + } else { + // create a new block statement for the alternate and insert it there + parent.alternate = t.blockStatement([ast]); + } + + newPaths.push(parent.get('consequent')); + newPaths.push(parent.get('alternate')); + } + } + else if (isTest && ( t.isWhile(parent) || t.isForStatement(parent))) { + const testExpression = parent.get('test'); + + // an expression can be inserted before by making the test into a sequence expression + if (insertBefore) { + if (t.isExpressionStatement(ast)) { + const expression = ast.expression; + expression.new = true; + + if (t.isSequenceExpression(testExpression)) { + testExpression.unshiftContainer('expressions', expression); + } else { + // wrap in a sequence expression + const seqExpression = t.toSequenceExpression([ast, testExpression.node]); + testExpression.replaceWith(seqExpression); + } + + newPaths.push(parent.get('test')); + } else { + // insert before statement + newPaths = parent.insertBefore(ast); + // also insert THE SAME NODE as last statement of body + parent.get('body').pushContainer('body', ast); + // and before every continue + const continues = getAllContinues(parent); + + for (let statPath of continues) { + statPath.insertBefore(ast); + } + } + } else { + // insert into body as first statement + ensureBlock(parent); + const body = parent.get('body'); + body.unshiftContainer('body', ast); + + newPaths.push(body); + } + } + else { + // ensure location info persists + const oldNode = path.node; + + // use the standard library insertBefore / insertAfter + if(insertInto) { + // ensure path has a container + let insertionPath = path; + while (!insertionPath.container) { + insertionPath = insertionPath.parentPath; + } + + // insert around path + const insertFn = insertBefore ? insertionPath.insertBefore : insertionPath.insertAfter; + newPaths = insertFn.call(insertionPath, ast); + } + else { + // insert around statement parent + const insertFn = insertBefore ? parent.insertBefore : parent.insertAfter; + newPaths = insertFn.call(parent, ast); + } + + if (!path.node.loc) { + path.node.loc = oldNode.loc; + } + } + + // remove injected node path(s) from queue + contexts = parent._getQueueContexts(); + for (let context of contexts) { + for (let x = context.queue?.length ?? 0, targetlen = queueLengths.get(context); x > targetlen; x--) { + context.queue.pop(); + } + } + + return newPaths; +} + +/** + * Checks whether a node (path) is an (optional) call expression. + * @param {NodePath|Node} node node to check + * @returns {boolean} + */ +export function isCallOrOptExpression(node) { + return t.isOptionalCallExpression(node) || t.isCallExpression(node); +} + +/** + * Checks whether a node (path) is an (optional) member expression. + * @param {Node|NodePath} node node to check + * @returns {boolean} + */ +export function isMemberOrOptExpression(node) { + return t.isMemberExpression(node) || t.isOptionalMemberExpression(node); +} + +/** + * Finds and returns all continue statements in a loop body + * @param {NodePath} loopPath path to a loop statement that MUST have a block body + * @returns {Array>} an array of found continue statements; empty if none are found + */ +export function getAllContinues(loopPath) { + if (!loopPath.has('body') || !loopPath.get('body').has('body')) { + throw new Error("Can't find continue statements if loop has no block body"); + } + + const statementStack = [].concat(loopPath.get('body').get('body')); + const continues = []; + + // find all continue statements + while (statementStack.length > 0) { + let statementPath = statementStack.pop(); + + switch (true) { + case t.isContinueStatement(statementPath): { + // save + continues.push(statementPath); + break; + } + case t.isBlockStatement(statementPath): { + statementStack.push(...statementPath.get('body')); + break; + } + // skip loops + // since continue breaks out of the *innermost* loop + case t.isLoop(statementPath): { + continue; + } + case t.isWithStatement(statementPath): + case t.isLabeledStatement(statementPath): + case t.isCatchClause(statementPath): + case t.isFunctionDeclaration(statementPath): { + statementStack.push(statementPath.get('body')); + break; + } + case t.isIfStatement(statementPath): { + statementStack.push(statementPath.get('consequent')); + if (statementPath.node.alternate) { + statementStack.push(statementPath.get('alternate')); + } + break; + } + case t.isTryStatement(statementPath): { + statementStack.push(statementPath.get('block'), statementPath.get('finalizer')); + break; + } + } + } + + return continues; +} + +/** + * Ensures that a path has a block statement as its body. + * Also copies the source location from the original node to the new block statement. + * + * @param {NodePath} path path to a statement + */ +export function ensureBlock(path) { + + // Generate block statement and copy source location + let oldNode = path.node; + let blockNode = path.ensureBlock(); + if(!blockNode.loc) + blockNode.loc = oldNode.loc; + + // If we generated a return statement, copy location info there as well + let newBlockStatement; + if (t.isArrowFunctionExpression(blockNode)) + newBlockStatement = blockNode.body; + if (t.isBlockStatement(newBlockStatement) && t.isReturnStatement(newBlockStatement.body[0])) { + newBlockStatement.body[0].loc = oldNode.loc; + } +} + +/** + * Finds the nearest statement parent of a path and returns it. + * @param {NodePath} path + * @returns {NodePath} the nearest statement parent + */ +export function getStatementParent(path) { + let statementParentPath = path; + while (statementParentPath.parentPath && !t.isStatement(statementParentPath)) { + statementParentPath = statementParentPath.parentPath; + } + + return statementParentPath; +} \ No newline at end of file diff --git a/JavascriptTracer/instrument.mjs b/JavascriptTracer/instrument.mjs new file mode 100644 index 0000000..eb716c4 --- /dev/null +++ b/JavascriptTracer/instrument.mjs @@ -0,0 +1,806 @@ +/** + * This file contains the instrumentation logic. + * The instrumentation is done in two steps: First, we have a setup phase that does a few + * necessary tweaks to the AST, and then we do the actual instrumentation. + */ + +import * as t from "@babel/types"; +import parser from "@babel/parser"; +import template from "@babel/template"; +import traverse, { visitors } from "@babel/traverse"; +import generate from "@babel/generator"; +import * as templates from "./templates.mjs"; +import * as setup from "./instrument-setup.mjs"; +import * as util from "./instrument-utility.mjs"; +import * as pathModule from "node:path"; +import * as constants from "./constants.cjs"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { createRequire } from "node:module"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +import * as importMetaResolve from "import-meta-resolve"; + +// Path of runtime. +const runtimePath = pathModule.resolve(pathModule.dirname(fileURLToPath(import.meta.url)), 'runtime.cjs'); + +// Collects files pending instrumentation. +let filesToInstrument = new Set(); + +// Collects files that have already been instrumented. +let filesInstrumented = new Set(); + +// Directory of the currently instrumented file. +let currentDir = null; + +// Full path to the currently instrumented file. +let currentFilePath = null; + +// Utility require function to check for node_modules of the currently instrumented file. +let requireFunc = null; + +// set of functions that are defined in the instrumented file +const functionDefs = new Set(); + +function genPreAst(filePath, isModule) { + let importStatement; + if(isModule) + importStatement = `import * as ${constants.INSTR_MODULE_NAME} from "${runtimePath}";`; + else + importStatement = `const ${constants.INSTR_MODULE_NAME} = require("${runtimePath}");`; + + return template.default.ast(` + ${importStatement} + const ${constants.FILE_ID_VAR_NAME} = ${constants.INSTR_MODULE_NAME}.registerScript("${filePath}"); + + // Temporary variables for various instrumentation operations + let ${constants.INSTR_VAR_PREFIX}vChain = []; + let ${constants.INSTR_VAR_PREFIX}vCall = []; + let ${constants.INSTR_VAR_PREFIX}vThis = []; + let ${constants.INSTR_VAR_PREFIX}vArgs = []; + let ${constants.INSTR_VAR_PREFIX}vSwitchLabel = []; + let ${constants.INSTR_VAR_PREFIX}vSwitchFallthrough = []; + let ${constants.INSTR_VAR_PREFIX}vTernaryId = []; + let ${constants.COMPUTEDVARNAME}; + `); +} + +/** + * Instruments a given node and corresponding path so memory access is logged + * during runtime + * @param {Node} node - the node of the object that is being accessed, + * e.g. left side of an assigment + * @param {NodePath} path - corresponding path of the node + * @param {boolean} isWrite - whether the memory access is a write operation or not + * @param {boolean} insertBefore - (optional) whether logging should be done before the node is executed or after + */ +function logMemoryAccessOfNode(node, path, isWrite, insertBefore=true, insertInto=false) { + const [ids, properties] = util.gatherIdsAndOffsets(node, !isWrite); + + // do injection for each id in the (potential) chain + for (let len = ids.length, index = len - 1; index >= 0; index--) { + let offset; + // don't try to get properties that don't exist + if (properties.length < index) + offset = null; + else + offset = t.isIdentifier(properties[index]) ? properties[index].name : properties[index]; + + let loc = path.node.loc; + // get location info of identifier for writes + if (t.isIdentifier(ids[index])) { + loc = ids[index].loc ?? loc; + } + + let id = t.isIdentifier(ids[index]) ? ids[index].name : ids[index]; + + // generate ast + let templateAst = templates.genMemoryAccessAst(id, offset, loc, isWrite); + // and inject + util.injectAst(templateAst, path, insertBefore, undefined, insertInto); + } +} + +/** + * Checks whether a given path reads a node or not + * @param {NodePath} path - path of the node that is being checked + * @returns {boolean} whether the node of the path is being read or not + */ +function isReadAccess(path) { + const parent = path.parent; + const key = path.listKey ?? path.key; + + // negative list of cases that AREN'T a read access + switch (true) { + case (t.isFunction(parent) && key === 'params'): + case (t.isCatchClause(parent) && key === 'param'): + case ((t.isAssignmentExpression(parent) || t.isAssignmentPattern(parent)) && key === 'left'): + case (util.isMemberOrOptExpression(parent) && key === 'property'): + case (util.isCallOrOptExpression(parent) && key === 'callee'): + case (t.isVariableDeclarator(parent) && parent.init == null): + case (t.isVariableDeclarator(path.parentPath.parent) && path.parentPath.parent.init == null): + case (t.isImportDeclaration(parent)): + case (key === 'id'): + case (key === 'key'): + case (t.isObjectProperty(parent) && path.key == "key"): + case (key === 'label'): + case (t.isForXStatement(parent) && key == 'left'): + return false; + case (t.isIdentifier(path) || util.isMemberOrOptExpression(path)): { + // we need to check all the parents to ensure we aren't on the lhs of an assignment + let currentNode = path, isRead = true; + while (isRead && currentNode.parentPath && !t.isAssignmentExpression(currentNode) && !t.isStatement(currentNode.parentPath)) { + currentNode = currentNode.parentPath; + isRead &&= isReadAccess(currentNode); + } + + return isRead; + } + default: + return true; + } +} + +/** + * Traverses up a nodepath and checks for a given marker key. Returns upon hitting the program node or finding a parent with the marker. + * @param {NodePath} path path for the node that is to be checked + * @param {string} logPropertyName logging marker to check for + * @returns {boolean} whether a parent of the starting node has the given marker + */ +function checkParentsForLogged(path, logPropertyName) { + let p = path; + + while (!t.isStatement(p.node) && p.listKey != "arguments" && p.parentPath) { + p = p.parentPath; + + if (Object.hasOwn(p.node, logPropertyName) || Object.hasOwn(p.node, "ignore")) + return logPropertyName; + } + + return false; +} + +const generalVisitor = { + // skip any injected nodepaths + enter(path) { + if (path.node.new) { + path.skip(); + } + }, + + // inject pre code + Program(path) { + const queueLengths = new Map(); + let contexts; + + // save current queue length + contexts = path._getQueueContexts(); + for (let context of contexts) { + queueLengths.set(context, context.queue.length); + } + + console.log(" type:", path.node.sourceType); + const preAst = genPreAst(currentFilePath, path.node.sourceType === 'module'); + preAst.forEach(e => e.new = true); + path.unshiftContainer('body', preAst); + + // remove injected node path(s) from queue + contexts = path._getQueueContexts(); + for (let context of contexts) { + for (let x = context.queue.length, targetlen = queueLengths.get(context); x > targetlen; x--) { + context.queue.pop(); + } + } + } +} + +const readVisitor = { + "AssignmentExpression|AssignmentPattern|Identifier|MemberExpression|OptionalMemberExpression|CallExpression|OptionalCallExpression|ThisExpression|VariableDeclarator|UnaryExpression"(path) { + // skip visited nodes + if (path.node.readLogged || path.node.ignore) + return; + + const node = path.node; + let readNode = null, insertInto = true; + + switch(true) { + case t.isVariableDeclarator(node): + // for destructuring assignment we need left + right + if (t.isObjectPattern(node.id) || t.isArrayPattern(node.id)) { + readNode = node; + } + break; + + case t.isAssignmentPattern(node): + case t.isAssignmentExpression(node): + // rhs is needed for object destructuring names + if (t.isObjectPattern(node.left) || t.isArrayPattern(node.left)) { + readNode = node.right; + break; + } + + // lhs is not read during simple assignment + if (node.operator === "=") { + node.left.readLogged = true; + return; + } + // rhs will be handled by later traversal + + break; + + case t.isThisExpression(node): + readNode = node; + break; + + case (t.isIdentifier(node) || util.isMemberOrOptExpression(node) || util.isCallOrOptExpression(node)): + // ignore sequence expression abomination calls -> handle lower nodes + if (t.isSequenceExpression(node?.callee?.object)) + return; + + // Ignore export nodes + if(t.isExportSpecifier(path.parentPath) || t.isExportDefaultDeclaration(path.parentPath)) + return; + + if (isReadAccess(path) && !checkParentsForLogged(path, 'readLogged')) { + readNode = node; + + // check whether parent is a postfix operation -> doesn't allow sequence expressions + let parentPath = path.parentPath; + while (!t.isStatement(parentPath) && !t.isProgram(parentPath)) { + if (t.isUpdateExpression(parentPath) || + t.isVariableDeclaration(parentPath)) { + insertInto = false; + break; + } + parentPath = parentPath.parentPath; + } + } + break; + case t.isUnaryExpression(path): + // ignore typeof + if (node.operator === "typeof") { + node.readLogged = true; + return; + } + break; + } + + if (readNode) { + logMemoryAccessOfNode(readNode, path, false, undefined, insertInto); + // mark node as done + readNode.readLogged = true; + } + + } +}; + +const writeVisitor = { + "AssignmentExpression|VariableDeclarator|AssignmentPattern|UpdateExpression|ForXStatement" (path) { + // skip visited nodes + if (path.node.writeLogged || path.node.ignore) + return; + + let leftNode, insertBefore = true; + + // identify lhs + switch (true) { + case (t.isVariableDeclarator(path)): + leftNode = path.node.id; + + // variable declaration without assignment isn't a write + if (!path.node.init) { + path.node.writeLogged = true; + return; + } + // for loop variable declaration + if (t.isForXStatement(path.parentPath.parent) && path.parentPath.key == "left" + || t.isForStatement(path.parentPath.parent) && path.parentPath.key == "init") + // insert before so the injection is the first statement of the body + insertBefore = true; + // other variable declarations + else + insertBefore = false; + break; + + case (t.isForXStatement(path)): + // left side is being written to + leftNode = path.node.left; + break; + + case t.isAssignmentExpression(path): + case t.isAssignmentPattern(path): + // ensure we're not in function parameters + let currentPath = path; + while (!t.isStatement(currentPath) && !t.isProgram(currentPath)) { + if (currentPath.listKey === 'params') { + // mark node as done + path.node.writeLogged = true; + return; + } + + currentPath = currentPath.parentPath; + } + + leftNode = path.node.left; + insertBefore = false; + + // for destructuring assignment we need left + right + if (t.isObjectPattern(leftNode) || t.isArrayPattern(leftNode)) { + leftNode = path.node; + } + break; + + case t.isUpdateExpression(path): + leftNode = path.node.argument; + break; + } + + void logMemoryAccessOfNode(leftNode, path, true, insertBefore); + + // mark node as done + path.node.writeLogged = true; + }, +}; + +const callVisitor = { + "FunctionDeclaration|FunctionExpression|ArrowFunctionExpression|ObjectMethod|ClassMethod"(path) { + const node = path.node; + + if (node.fnlogged) + return; + + let body = path.get('body'); + + const injAst = templates.genFuncInfoAst(node.loc); + util.injectAst(injAst, body, undefined, undefined, true); + + // add to set of function definitions + functionDefs.add(node.loc); + + // ensure non-generator function has a return statement + if (!node.generator && !t.isReturnStatement(body.get('body').at(-1))) { + let returnStat = t.returnStatement(); + returnStat.loc = { + start: node.loc.end, + end: node.loc.end, + filename: node.loc.filename + }; + body.pushContainer('body', returnStat); + } + + node.fnlogged = true; + }, + + CallExpression: { + exit(path) { + const node = path.node; + + // skip visited nodes + if (node.callLogged || node.ignore) + return; + + // ignore import "calls" + if (t.isImport(node)){console.log("ignore import!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + return;} + + let fnName = null, baseNode = node; + + // find the identifier relating to the callee, so we can extract its name at runtime + // due to setup every call is associated with a primary expression as a callee + let isImport = false; + while (!fnName) { + if (t.isIdentifier(baseNode)) + fnName = baseNode.name; + + else if (t.isCallExpression(baseNode)) + baseNode = baseNode.callee; + + else if (t.isMemberExpression(baseNode)) + // calls are only member expressions if they WERE of the form a.b() and are now of the form $$call.call(a) + // (since member expression calls have been squashed) so the OBJECT has the relevant info + baseNode = baseNode.object; + + else if (t.isFunctionExpression(baseNode) || t.isSequenceExpression(baseNode)) { + fnName = "undefined"; + } + else if (t.isImport(baseNode)) { + fnName = "import"; + isImport = true; + } + + else + throw new Error(`Unexpected parent node type ${baseNode.type} in call`); + } + + // Handle `[await] import()` + if (isImport && node.arguments.length === 1) { + + // If the import is known already, instrument it statically + if(t.isStringLiteral(node.arguments[0])) { + path.node.arguments[0] = t.stringLiteral(enqueueForInstrumentation(node.arguments[0].value, true)); + } + else { + // Call runtime.instrumentDynamic(await import.meta.resolve('...')) + const dynamicImportFuncAst = t.memberExpression(t.identifier(constants.INSTR_MODULE_NAME), t.identifier("instrumentDynamic")); + const resolveFuncAst = t.memberExpression(t.memberExpression(t.identifier("import"), t.identifier("meta")), t.identifier("resolve")); + const resolveModuleAst = t.callExpression(resolveFuncAst, [node.arguments[0]]); + const dynamicImportAst = t.callExpression(dynamicImportFuncAst, [resolveModuleAst]); + + // Rewrite required target + path.node.arguments[0] = dynamicImportAst; + } + + // No further handling + return; + } + + // Handle calls to require() + if(fnName === "require" && node.arguments.length === 1) { + + // If the import is known already, instrument it statically + if(t.isStringLiteral(node.arguments[0])) { + path.node.arguments[0] = t.stringLiteral(enqueueForInstrumentation(node.arguments[0].value, false)); + } + else { + // Call runtime.instrumentDynamic(require.resolve(moduleName)) + const dynamicImportFuncAst = t.memberExpression(t.identifier(constants.INSTR_MODULE_NAME), t.identifier("instrumentDynamic")); + const resolveFuncAst = t.memberExpression(t.identifier("require"), t.identifier("resolve")); + const resolveModuleAst = t.callExpression(resolveFuncAst, [node.arguments[0]]); + const dynamicImportAst = t.callExpression(dynamicImportFuncAst, [resolveModuleAst]); + + // Rewrite required target + path.node.arguments[0] = dynamicImportAst; + } + + // No further handling + return; + } + + // generate ast for call trace + const templateAst = templates.genCallAst(fnName, node.loc); + // and inject + util.injectAst(templateAst, path, undefined, undefined, true); + + node.callLogged = true; + + // skip ret2 for yield delegation + if (t.isYieldExpression(path.parent) && path.parent.delegate) + return; + + // inject ret2 for returning from a call + const ret2Ast = templates.genReturnAst(false, node.loc); + + // due to previous setup ALL calls have their result assigned to a var + // implicit return should be directly after the call and before the call result variable + + // find the parent assignment and corresponding sequence expression + let seqEx = path, foundId = false, callIdPath = seqEx.getNextSibling(); + while (!foundId && !t.isStatement(seqEx)) { + if (t.isSequenceExpression(seqEx.parent)) { + // call id should be next sibling in sequence expression + callIdPath = seqEx.getNextSibling(); + if (t.isIdentifier(callIdPath)) { + foundId = true; + break; + } + } + + seqEx = seqEx.parentPath; + } + + // replace with sequence expression of return and result variable + ret2Ast.expression.new = true; + callIdPath.replaceWith(t.sequenceExpression([ret2Ast.expression, callIdPath.node])); + + node.callLogged = true; + } + }, + + + "ReturnStatement": { + // instrument on exit so return(1) is the last emission of the function + exit(path) { + const node = path.node; + + // skip already visited nodes + if(node.returnLogged || node.ignore) + return; + + const ret1Ast = templates.genReturnAst(true, node.loc); + util.injectAst(ret1Ast, path); + + node.returnLogged = true; + } + }, + + "YieldExpression": { + // instrument on exit so injection is last emission + exit(path) { + const node = path.node; + + // skip already visited + if (node.yieldLogged || node.ignore) + return; + + // find statement parent + let statementParentPath = util.getStatementParent(path); + + // inject before to log returning from yield + const ret1Ast = templates.genReturnAst(true, node.loc); + util.injectAst(ret1Ast, path, true); + + // inject after yield to log resumption + // except for the final yield + if (statementParentPath.parent.body.at(-1) != statementParentPath.node) { + const yieldResAst = templates.genYieldAst(true, node.loc); + util.injectAst(yieldResAst, path, false); + } + + node.yieldLogged = true; + } + } +} + +const branchVisitor = { + // general: + // * add "taken" emission to conditional body + + // notes: + // conditional expressions have EXPRESSIONS as consequence/alternate -> use SEQUENCEEXPRESSION + // switchcase has an ARRAY for consequent -> insert statement into array (mb works out of the box?) + "IfStatement|SwitchCase|While|For"(path) { + const node = path.node; + let bodyPath = null; + + if (node.branchlogged) + return; + + // get 'body' path + switch (true) { + case (t.isIfStatement(node)): + bodyPath = path.get("consequent"); + break; + case t.isSwitchCase(node): + // ignore the switch case if it's empty + if (node.consequent.length === 0) + return + + // consequent is an array and we want a node, so wrap everything in a block statement and use that + bodyPath = path.get("consequent")[0]; + bodyPath.replaceWith(t.blockStatement(path.get("consequent").map(p => p.node))); + // remove remaining elements (since they're now in the block statement) + node.consequent.splice(1); + + break; + + case (t.isFor(node) || t.isWhile(node)): + util.ensureBlock(path); + bodyPath = path.get("body"); + break; + } + + const bodyLoc = bodyPath.has('body') ? bodyPath.node.body[0]?.loc : bodyPath.node.loc; + + let bodyAst = templates.genBranchAst(node.loc, bodyLoc ?? bodyPath.node.loc); + util.injectAst(bodyAst, bodyPath); + + // handle alternate (if statement) + if (node.alternate) { + const alternate = path.get('alternate'); + let alternateLoc = alternate.node.body?.loc ?? alternate.node.body?.at(0)?.loc ?? alternate.node.loc; + + bodyAst = templates.genBranchAst(node.loc, alternateLoc); + util.injectAst(bodyAst, alternate); + } + + node.branchlogged = true; + }, + + ConditionalExpression(path) { + if (path.node.branchlogged) { + return; + } + + const node = path.node; + const consequent = path.get('consequent'); + const alternate = path.get('alternate'); + + const branchConsAst = templates.genBranchAst(node.loc, consequent.node.loc); + util.injectAst(branchConsAst, consequent, true, undefined, true); + + const branchAltAst = templates.genBranchAst(node.loc, alternate.node.loc); + util.injectAst(branchAltAst, alternate, true, undefined, true); + + node.branchlogged = true; + }, + + "ContinueStatement|BreakStatement"(path) { + const label = path.node.label?.name; + let dest = path; + + if (label) { + // look for labeled statement with correct label + let pathsToCheck = [path.parentPath]; + let currentPath; + while (pathsToCheck.length > 0) { + currentPath = pathsToCheck.shift(); + + if (t.isLabeledStatement(currentPath) && currentPath.node.label.name == label) { + // ding ding - found the label + dest = currentPath; + break; + } + + // add all siblings to queue + if (currentPath.listKey == 'body') { + pathsToCheck.push(...currentPath.getAllPrevSiblings()); + pathsToCheck.push(...currentPath.getAllNextSiblings()); + } + + // remove children from q + if (currentPath.has('body') && pathsToCheck.length > 0) { + pathsToCheck = pathsToCheck.filter(e => { + currentPath.node.body.includes(e) + }) + } + + // add parent to queue + if (!t.isProgram(currentPath)) { + pathsToCheck.push(currentPath.parentPath); + } + } + } else { + // look for parent loop of statement + while(dest.parentPath && !t.isLoop(dest) && !t.isProgram(dest)) { + dest = dest.parentPath; + } + } + + if (t.isBreakStatement(path)) { + // destination is next sibling of label/loop + const nextSibling = dest.getNextSibling(); + if (nextSibling.node) { + dest = nextSibling; + } + else { + dest.node.loc.start = dest.node.loc.end; + } + } + + const jmpAst = templates.genJumpAst(path.node.loc, dest.node.loc); + util.injectAst(jmpAst, path); + } +} + +const importVisitor = { + ImportDeclaration(path) { + const node = path.node; + + const importModule = node.source.value; + + // Enqueue for static instrumentation and update path + node.source.value = enqueueForInstrumentation(importModule, true); + } +} + +// Merge visitors +const instrumentationVisitor = visitors.merge([generalVisitor, writeVisitor, readVisitor, callVisitor, branchVisitor, importVisitor]); + +// Unwrap enter +instrumentationVisitor['enter'] = instrumentationVisitor['enter'][0]; + +/** + * Enqueues the given module for instrumentation. + * @param {string} module The module to instrument. + * @param {boolean} isEsModule Whether the module is an ES module or not. + * @returns {string} The path to the instrumented module. + */ +function enqueueForInstrumentation(module, isEsModule) +{ + // Get file path of import + let modulePath = module; + if(module.startsWith('.')) + { + let potentialPath = pathModule.resolve(currentDir, module); + if(fs.existsSync(potentialPath)) + modulePath = potentialPath; + } + if(!module.startsWith('/')) + { + // Some named module, try to resolve it + if(isEsModule) + modulePath = importMetaResolve.resolve(module, pathToFileURL(currentFilePath)); //import.meta.resolve(module, pathToFileURL(currentFilePath)); + else + modulePath = requireFunc.resolve(module); + + if(modulePath.startsWith('file://')) + modulePath = fileURLToPath(modulePath); + } + + // Put in instrumentation queue, if we could resolve the file + // We do not translate imports of built-in modules + if(modulePath != module) + { + if(!filesToInstrument.has(modulePath) && !filesInstrumented.has(modulePath)) + console.log(` enqueueing ${module} (-> ${modulePath}) for instrumentation`); + + filesToInstrument.add(modulePath); + return getInstrumentedName(modulePath); + } + else + { + console.log(` SKIPPING ${module} for instrumentation`); + } + + return module; +} + +export function getInstrumentedName(filePath) { + const suffix = constants.VALID_SUFFIXES.find((e) => filePath.endsWith(e)); + return filePath.replace(suffix, `${constants.PLUGIN_SUFFIX}${suffix}`); +} + +import printAST from "ast-pretty-print"; +import { notEqual } from "node:assert"; +import { is } from "@babel/types"; +export function instrumentAst(filePath, ast) { + currentDir = pathModule.dirname(filePath); + currentFilePath = filePath; + + requireFunc = createRequire(filePath); + + // Setup: Simplify AST, split up certain constructs + traverse.default(ast, setup.setupVisitor); + traverse.default(ast, setup.setupCallExpressionsVisitor); + + // Debugging: Dump intermediate AST after setup + fs.writeFileSync(getInstrumentedName(filePath) + ".tmp", generate.default(ast, {comments: false}).code); + fs.writeFileSync(getInstrumentedName(filePath) + ".ast", printAST(ast)); + + // Actual instrumentation + try { + traverse.default(ast, instrumentationVisitor); + } + catch (error) { + console.error(error.message); + console.error(error.stack); + } +} + +export function instrumentFileTree(filePath) { + + // If the file is not already instrumented, instrument it and all its dependencies + let filePathInstrumented = getInstrumentedName(filePath); + if (!fs.existsSync(filePathInstrumented)) { + try { + // Pending files to instrument + filesToInstrument.add(filePath); + while (filesToInstrument.size > 0) { + let currentFile = filesToInstrument.values().next().value; + filesToInstrument.delete(currentFile); + filesInstrumented.add(currentFile); + + // Skip if already instrumented + let currentFileInstrumented = getInstrumentedName(currentFile); + if (fs.existsSync(currentFileInstrumented)) + continue; + + console.log(`Instrumenting ${currentFile}`); + + // Read and parse given file + const code = fs.readFileSync(currentFile, { encoding: "utf-8" }); + const ast = parser.parse(code, { sourceFilename: path.basename(currentFile), sourceType: "unambiguous" }); + + // Do instrumentation + instrumentAst(currentFile, ast); + + // Get absolute path of instrumented file and write it + console.log(` writing ${getInstrumentedName(currentFile)}`); + fs.writeFileSync(getInstrumentedName(currentFile), generate.default(ast, {comments: false}).code); + } + } catch (error) { + console.error(error.message); + console.error(error.stack); + } + } +} \ No newline at end of file diff --git a/JavascriptTracer/package-lock.json b/JavascriptTracer/package-lock.json new file mode 100644 index 0000000..af225a0 --- /dev/null +++ b/JavascriptTracer/package-lock.json @@ -0,0 +1,2546 @@ +{ + "name": "JavascriptTracer", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "@babel/generator": "^7.21.1", + "@babel/parser": "^7.21.2", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.2", + "@babel/types": "^7.21.2", + "ast-pretty-print": "2.0.1", + "import-meta-resolve": "4.0.0" + }, + "devDependencies": { + "eslint": "^8.55.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", + "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/ast-pretty-print": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ast-pretty-print/-/ast-pretty-print-2.0.1.tgz", + "integrity": "sha512-o3Ne0PcZByYAOni7NVQGvJ+EDHWvJ72h895uhKAIJmXxwaS8x+inf8k7Rqs0V5dtQm/To3l8wWqnff5hq4Q/Zg==", + "dependencies": { + "pretty-format-ast": "^1.0.1", + "pretty-format2": "^2.0.3" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", + "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format-ast": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pretty-format-ast/-/pretty-format-ast-1.0.1.tgz", + "integrity": "sha512-xLokxZerpiFBppT0dlY/GIMdvMmmoMpqf1dWCNHWl9kXR/Cu1hdOHCDyIeJdKBDqXWjezUbzLBTP0wdOEUay8Q==", + "dependencies": { + "pretty-format2": "^2.0.3" + } + }, + "node_modules/pretty-format2": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pretty-format2/-/pretty-format2-2.0.4.tgz", + "integrity": "sha512-Tl/GgFDw5IcHMgbjEhqS4BZ713KXkCRQp+FcKhYpXZ56cZv/G/m3XqoL9cUi1nPRVJ3TPNvrV3szDMQ39cQRvg==", + "dependencies": { + "ansi-styles": "^3.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "requires": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + } + }, + "@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "requires": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==" + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" + }, + "@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==" + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", + "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", + "requires": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "requires": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + } + } + }, + "@eslint/js": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "ast-pretty-print": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ast-pretty-print/-/ast-pretty-print-2.0.1.tgz", + "integrity": "sha512-o3Ne0PcZByYAOni7NVQGvJ+EDHWvJ72h895uhKAIJmXxwaS8x+inf8k7Rqs0V5dtQm/To3l8wWqnff5hq4Q/Zg==", + "requires": { + "pretty-format-ast": "^1.0.1", + "pretty-format2": "^2.0.3" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "eslint": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "ignore": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-meta-resolve": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", + "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==" + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "pretty-format-ast": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pretty-format-ast/-/pretty-format-ast-1.0.1.tgz", + "integrity": "sha512-xLokxZerpiFBppT0dlY/GIMdvMmmoMpqf1dWCNHWl9kXR/Cu1hdOHCDyIeJdKBDqXWjezUbzLBTP0wdOEUay8Q==", + "requires": { + "pretty-format2": "^2.0.3" + } + }, + "pretty-format2": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pretty-format2/-/pretty-format2-2.0.4.tgz", + "integrity": "sha512-Tl/GgFDw5IcHMgbjEhqS4BZ713KXkCRQp+FcKhYpXZ56cZv/G/m3XqoL9cUi1nPRVJ3TPNvrV3szDMQ39cQRvg==", + "requires": { + "ansi-styles": "^3.0.0" + } + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/JavascriptTracer/package.json b/JavascriptTracer/package.json new file mode 100644 index 0000000..397b644 --- /dev/null +++ b/JavascriptTracer/package.json @@ -0,0 +1,15 @@ +{ + "dependencies": { + "@babel/generator": "^7.21.1", + "@babel/parser": "^7.21.2", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.2", + "@babel/types": "^7.21.2", + "ast-pretty-print": "2.0.1", + "import-meta-resolve": "4.0.0" + }, + "type": "module", + "devDependencies": { + "eslint": "^8.55.0" + } +} diff --git a/JavascriptTracer/run.js b/JavascriptTracer/run.js new file mode 100644 index 0000000..4989853 --- /dev/null +++ b/JavascriptTracer/run.js @@ -0,0 +1,11 @@ +/** + * Instruments the given file, if necessary, and then runs it. + * + * @param {string} path File to instrument and run + */ + +// Instrument if necessary +const { instrumentedName } = await import("./instrument-file.mjs"); + +// Run instrumented file +await import(instrumentedName); \ No newline at end of file diff --git a/JavascriptTracer/runtime.cjs b/JavascriptTracer/runtime.cjs new file mode 100644 index 0000000..d5954e1 --- /dev/null +++ b/JavascriptTracer/runtime.cjs @@ -0,0 +1,363 @@ +/** + * Contains runtime functions for the instrumented code. + * CommonJS module to support inclusion in both ES and CommonJS modules. + */ + +const constants = require("./constants.cjs"); +const uidUtil = require("./uid.cjs"); +const fs = require("fs"); +const { execSync } = require("child_process"); +const pathModule = require("path"); + +// Names of the testcase begin/end marker functions. +const testcaseBeginFunctionName = "__MICROWALK_testcaseBegin"; +const testcaseEndFunctionName = "__MICROWALK_testcaseEnd"; + +// Ensure trace directory exists +const traceDirectory = process.env.MW_TRACE_DIRECTORY; +if(!traceDirectory) + throw new Error("MW_TRACE_DIRECTORY not set!"); +if (!fs.existsSync(traceDirectory)) { + fs.mkdirSync(traceDirectory, { recursive: true }); +} + +// Tracing state +let currentTestcaseId = -1; // Prefix mode +let isTracing = true; +let traceData = []; +const traceDataSizeLimit = 1000000; +let previousTraceFilePath = ""; + +// (debugging only) If set to true, trace compression is disabled. +// WARNING: This may lead to huge files, and is incompatible to Microwalk's preprocessor module! +let disableTraceCompression = false; + +// Compressed lines from the trace prefix can be reused in all other traces +let nextCompressedLineIndex = 0; +let compressedLines = {}; +let prefixNextCompressedLineIndex = 0; +let prefixCompressedLines = {}; + +// Used for computing relative distance between subsequent compressed line IDs. +// If the distance is small, we only print the offset (+1, ...), not the entire line ID. +let lastCompressedLineIndex = -1000; + +// If the last line used a one-character relative encoding, we omit the line break and append the next one directly. +let lastLineWasEncodedRelatively = false; + +// Path prefix to remove from script file paths (so they are relative to the project root) +const scriptPathPrefix = process.env.MW_PATH_PREFIX; +if(!scriptPathPrefix) + throw new Error("MW_PATH_PREFIX not set!"); + +// Mapping of known script file paths to their IDs. +const scriptNameToIdMap = new Map(); + +// File handle of the script information file. +let scriptsFile = fs.openSync(`${traceDirectory}/scripts.txt`, "w"); + + +/** + * Registers the given script in the trace writer and returns an ID that can be used with the trace writing functions. + * @param {string} filename - Full path of the script file + * @returns {number} ID of the script + */ +function registerScript(filename) +{ + // Remove path prefix + if(!filename.startsWith(scriptPathPrefix)) + throw new Error(`Script path "${filename}" does not start with prefix "${scriptPathPrefix}"`); + filename = filename.substring(scriptPathPrefix.length); + if(filename.startsWith("/")) + filename = filename.substring(1); + + // Check whether script is already known + if(scriptNameToIdMap.has(filename)) + return scriptNameToIdMap.get(filename); + + // No, generate new ID + const id = scriptNameToIdMap.size; + scriptNameToIdMap.set(filename, id); + + fs.writeSync(scriptsFile, `${id}\t${filename}\n`); + + return id; +} + +/** + * Writes the pending trace entries. + */ +function _persistTrace() +{ + if(!isTracing) + return; + + let traceFilePath = currentTestcaseId === -1 ? `${traceDirectory}/prefix.trace` : `${traceDirectory}/t${currentTestcaseId}.trace`; + + let writingToNewTrace = false; + if(traceFilePath !== previousTraceFilePath) + { + writingToNewTrace = true; + previousTraceFilePath = traceFilePath; + } + + let traceFile; + if(writingToNewTrace) + { + console.log(` creating ${traceFilePath}`); + traceFile = fs.openSync(traceFilePath, "w"); + } + else + { + traceFile = fs.openSync(traceFilePath, "a+"); + } + + fs.writeSync(traceFile, traceData.join('\n')); + fs.writeSync(traceFile, '\n'); + + fs.closeSync(traceFile); +} + +/** + * Checks whether we already have a compressed representation of the given line. + * If not, a new one is created. + * @param {string} line - Line to compress + * @returns A compressed representation of the given line. + */ +function _getCompressedLine(line) +{ + if(line in compressedLines) + return compressedLines[line]; + else + { + let compressed = nextCompressedLineIndex; + ++nextCompressedLineIndex; + + compressedLines[line] = compressed; + traceData.push(`L|${compressed}|${line}`); + + lastLineWasEncodedRelatively = false; + return compressed; + } +} + +/** + * Writes a line into the trace file. + * @param {string} line - line to write + */ +function _writeTraceLine(line) +{ + if(traceData.length >= traceDataSizeLimit) + { + _persistTrace(); + traceData = []; + } + + if(disableTraceCompression) + { + traceData.push(line); + return; + } + + let encodedLine = ""; + + // Ensure that compressed line exists, and then output its index (either absolute or relative) + let lineIndex = _getCompressedLine(line); + let distance = lineIndex - lastCompressedLineIndex; + let encodeRelatively = (distance >= -9 && distance <= 9); + if(encodeRelatively) + encodedLine = String.fromCharCode(106 + distance); // 'j' + distance => a ... s + else + encodedLine = lineIndex.toString(); + + // If we are in relative encoding mode, we omit the line break and append the distance marker directly + if(lastLineWasEncodedRelatively && traceData.length > 0) + traceData[traceData.length - 1] += encodedLine; + else + traceData.push(encodedLine); + + lastLineWasEncodedRelatively = encodeRelatively; + lastCompressedLineIndex = lineIndex; +} + +// Tracks a pending call. +// Calls are written in two steps: In the call instruction itself, and when entering the callee. +let callInfo = null; + +function startCall(fileId, sourceLoc, fnObj) +{ + // Extract callee name, fallback if we can not resolve it + let fnName = fnObj?.name; + if (!fnName) + fnName = ""; + + // Handle special testcase begin marker function + if(fnName === testcaseBeginFunctionName) + { + // Ensure that previous trace has been fully written (prefix mode) + if(isTracing && traceData.length > 0) + _persistTrace(); + traceData = []; + + // If we were in prefix mode, store compression dictionaries + if(currentTestcaseId === -1) + { + prefixNextCompressedLineIndex = nextCompressedLineIndex; + prefixCompressedLines = compressedLines; + } + + // Initialize compression dictionaries + compressedLines = Object.assign({}, prefixCompressedLines); + nextCompressedLineIndex = prefixNextCompressedLineIndex; + lastCompressedLineIndex = -1000; + lastLineWasEncodedRelatively = false; + + // Enter new testcase + ++currentTestcaseId; + isTracing = true; + } + + // Handle special testcase end marker function + if(fnName === testcaseEndFunctionName) + { + // Close trace + _persistTrace(); + traceData = []; + isTracing = false; + } + + // Store info for later write when we know the callee location + callInfo = { + sourceFileId: fileId, + sourceLocation: sourceLoc, + destinationFileId: null, + destinationLocation: null, + functionName: fnName + }; +} + +function endCall(fileId, destLoc) +{ + if(!callInfo) { + // We did not observe the call itself; this can happen for callbacks when the caller is an external function. + // Do not produce a call entry in this case, as we will also miss the Return1 entry, so the call tree stays balanced. + return; + } + + callInfo.destinationFileId = fileId; + callInfo.destinationLocation = destLoc; + + writeCall(); +} + +function writeCall() +{ + if(!callInfo) + return; + + let srcFileId = callInfo.sourceFileId; + let srcLoc = callInfo.sourceLocation; + let destFileId = callInfo.destinationFileId ?? "E"; + let destLoc = callInfo.destinationLocation ?? callInfo.functionName; + let fnName = callInfo.functionName; + _writeTraceLine(`c;${srcFileId};${srcLoc};${destFileId};${destLoc};${fnName}`); + + callInfo = null; +} + +function writeReturn(fileId, location, isReturn1) +{ + if(callInfo) + writeCall(); + + const ret = isReturn1 ? 'r' : 'R'; + _writeTraceLine(`${ret};${fileId};${location}`); +} + +function writeYield(fileId, location, isResume) +{ + const res = isResume ? 'Y' : 'Y'; + _writeTraceLine(`${res};${fileId};${location}`); +} + +function writeBranch(fileId, sourceLoc, bodyLocation) +{ + //_writeTraceLine(`b;${fileId};${sourceLoc};${bodyLocation}`); + + // Handle like a jump + // TODO remove separation of branch vs jump altogether? + writeJump(fileId, sourceLoc, bodyLocation); +} + +function writeJump(fileId, sourceLoc, destLoc) +{ + _writeTraceLine(`j;${fileId};${sourceLoc};${destLoc}`); +} + +function writeMemoryAccess(fileId, loc, objId, offset, isWrite, computedVar) +{ + let offsetStr = offset; + if (offset == constants.COMPUTED_OFFSET_INDICATOR) { + offsetStr = `${computedVar}`; + } + + const memAccessType = isWrite ? "w" : "r"; + const objIdStr = uidUtil.getUid(objId); + if (objIdStr && objIdStr != constants.PRIMITIVE_INDICATOR) { + _writeTraceLine(`m;${memAccessType};${fileId};${loc};${objIdStr};${offsetStr}`); + } +} + +/** + * Instruments the given dynamically imported file. + * + * @param {string} path The file to instrument. + */ +function instrumentDynamic(path) +{ + console.log(`Instrumenting dynamic import: ${path}`); + + // Remove file:// if necessary + if (path.startsWith("file://")) { + path = path.substring(7); + } + + // Check whether the file exists + if (!fs.existsSync(path)) { + console.log(` File does not exist, skipping`); + return path; + } + + // We call an instrumentation script and wait for its execution. It would be preferable to just call the instrumentation + // functions here, but they are implemented as an ES module and we are in a CommonJS context here. As soon as we drop + // support for CommonJS, we can clean this up. + + // The script exists in the same folder as this one + const instrumentFileScriptPath = pathModule.resolve(__dirname, 'instrument-file.mjs'); + + // Check whether the file is already instrumented + const suffix = constants.VALID_SUFFIXES.find((e) => path.endsWith(e)); + const instrumentedPath = path.replace(suffix, `${constants.PLUGIN_SUFFIX}${suffix}`); + if (fs.existsSync(instrumentedPath)) { + console.log(` File is already instrumented, skipping`); + return instrumentedPath; + } + + // Run instrumentation process + execSync(`node ${instrumentFileScriptPath} "${path}"`, { stdio: 'inherit' }); + + // Return name of instrumented file + return instrumentedPath; +} + +module.exports = { + registerScript, + writeMemoryAccess, + startCall, + endCall, + writeReturn, + writeYield, + writeBranch, + writeJump, + instrumentDynamic +}; \ No newline at end of file diff --git a/JavascriptTracer/templates.mjs b/JavascriptTracer/templates.mjs new file mode 100644 index 0000000..3faa0c0 --- /dev/null +++ b/JavascriptTracer/templates.mjs @@ -0,0 +1,114 @@ +import template from "@babel/template"; +import * as constants from "./constants.cjs"; + +/** + * Generates a string formatting relevant source information given a SourceLocation object + * @param {t.SourceLocation} loc + * @returns {string} formatted string with location information + */ +export function genLocString(loc) { + if(!loc) + throw new Error("Undefined source location"); + + return `${loc.start.line}:${loc.start.column}:${loc.end.line}:${loc.end.column}`; +} + +/** + * Generates an ast for injecting logging of memory access + * @param {String} objId - identifier name of the object that is being accessed + * @param {String} offset - offset that is being accessed -- i.e. the property for objects or the index for arrays. + * Defaults to null if none is given + * @param {t.SourceLocation} loc - location object of the memory access + * @param {boolean} isWrite - boolean whether the memory access is a write operation + * @returns {ast} an ast that can be inserted + */ +export function genMemoryAccessAst(objId, offset = null, loc, isWrite) { + + let offsetStr = ''; + if (offset) { + if (offset == constants.COMPUTED_OFFSET_INDICATOR) { + offsetStr = constants.COMPUTED_OFFSET_INDICATOR; + } + else if(offset == constants.COMPUTEDVARNAME) { + offsetStr = `${constants.COMPUTED_OFFSET_INDICATOR}`; + } + else { + offsetStr = `${offset}`; + } + } + + let objIdVal = objId; + if (objId == constants.PRIMITIVE_INDICATOR) { + objIdVal = `'${constants.PRIMITIVE_INDICATOR}'`; + } + + // TODO handle optional chains + return template.default.ast(` + ${constants.INSTR_MODULE_NAME}.writeMemoryAccess(${constants.FILE_ID_VAR_NAME}, '${genLocString(loc)}', ${objIdVal}, '${offsetStr}', ${isWrite}, ${constants.COMPUTEDVARNAME}); + `); +} + +/** + * Generates an ast for injecting logging of calls + * @param {string} fnObj - callee + * @param {Object} source - location object of the source of the call (caller) + * @returns + */ +export function genCallAst(fnObj, source) { + return template.default.ast(` + ${constants.INSTR_MODULE_NAME}.startCall(${constants.FILE_ID_VAR_NAME}, '${genLocString(source)}', ${fnObj}); + `); +} + +/** + * Generates an ast for injection logging of function information () + * @param {Object} location - location object of the function definition + * @returns + */ +export function genFuncInfoAst(location) { + return template.default.ast(` + ${constants.INSTR_MODULE_NAME}.endCall(${constants.FILE_ID_VAR_NAME}, '${genLocString(location)}'); + `); +} + +/** + * Generates an ast for injecting logging of returns + * @param {boolean} isReturn1 - whether this is the first return (return statement inside a function) or not (immediately after call) + * @param {Object} location - location object of return statement + * @returns + */ +export function genReturnAst(isReturn1, location) { + return template.default.ast(` + ${constants.INSTR_MODULE_NAME}.writeReturn(${constants.FILE_ID_VAR_NAME}, '${genLocString(location)}', ${isReturn1}); + `); +} + +/** + * Generates an ast for injecting logging of yield expressions + * @param {boolean} isResume - whether the generator is being resumed or paused + * @param {object} location - location object of yield expression + * @returns + */ +export function genYieldAst(isResume, location) { + return template.default.ast(` + ${constants.INSTR_MODULE_NAME}.writeYield(${constants.FILE_ID_VAR_NAME}, '${genLocString(location)}', ${isResume}); + `); +} + +/** + * Generates an ast for injecting logging of branches + * @param {t.SourceLocation} source - location object of the branching statement + * @param {t.SourceLocation} bodyLocation - location object that is being branched to (body of the branching statement) + * @returns + */ +export function genBranchAst(source, bodyLocation) { + return template.default.ast(` + ${constants.INSTR_MODULE_NAME}.writeBranch(${constants.FILE_ID_VAR_NAME}, '${genLocString(source)}', '${genLocString(bodyLocation)}'); + `); +} + +export function genJumpAst(source, dest) { + return template.default.ast(` + ${constants.INSTR_MODULE_NAME}.writeJump(${constants.FILE_ID_VAR_NAME}, '${genLocString(source)}', '${genLocString(dest)}'); + `); +} \ No newline at end of file diff --git a/JavascriptTracer/uid.cjs b/JavascriptTracer/uid.cjs new file mode 100644 index 0000000..aed1775 --- /dev/null +++ b/JavascriptTracer/uid.cjs @@ -0,0 +1,63 @@ +/** + * Runtime utilities for UID generation and management. + */ + +const constants = require("./constants.cjs"); + +const uidCtr = +{ + _ctr: 0, + get ctr() + { + return this._ctr; + }, + + set ctr(x) + { + // can't be changed externally + }, + + next() + { + return ++this._ctr; + }, +}; + +const uidMap = new WeakMap(); + +function getUid(obj) +{ + // We can't get IDs of undefined objects + if (!obj) + return undefined; + + // Primitives don't have IDs + if (typeof obj !== "object" && typeof obj !== "function") + return constants.PRIMITIVE_INDICATOR; + + try + { + // Add uid if object doesn't already have one + const uidSymbol = Symbol.for('uid'); + if (!Object.hasOwn(obj, uidSymbol)) { + Object.defineProperty(obj, uidSymbol, { + writable: false, + value: uidCtr.next() + }); + } + return obj[uidSymbol]; + } + catch + { + // If the object is not extensible, we can't add properties to it + // In this rare case, we use map-based ID tracking + if (!uidMap.has(obj)) { + uidMap.set(obj, uidCtr.next()); + } + return uidMap.get(obj); + } +} + +module.exports = { + getUid +}; \ No newline at end of file diff --git a/Microwalk.Plugins.JavascriptTracer/JsTracePreprocessor.cs b/Microwalk.Plugins.JavascriptTracer/JsTracePreprocessor.cs index b908944..eb9e1db 100644 --- a/Microwalk.Plugins.JavascriptTracer/JsTracePreprocessor.cs +++ b/Microwalk.Plugins.JavascriptTracer/JsTracePreprocessor.cs @@ -60,12 +60,12 @@ public class JsTracePreprocessor : PreprocessorStage /// /// Heap allocations from the trace prefix. /// - private Dictionary? _prefixHeapObjects = null; + private Dictionary? _prefixHeapObjects; /// /// Compressed lines from the trace prefix, indexed by line ID. /// - private Dictionary? _prefixCompressedLinesLookup = null; + private Dictionary? _prefixCompressedLinesLookup; /// /// ID of the external functions image. @@ -82,36 +82,44 @@ public class JsTracePreprocessor : PreprocessorStage /// private readonly List _imageData = new(); + /// + /// Catch-all address for unknown external function locations. + /// Is used for returns from external functions, where we don't know the precise source location. + /// + private uint _catchAllExternalFunctionAddress = 1; + /// /// The next address which is assigned to an external function. + /// Initialized with 2, as is 1. /// - private uint _currentExternalFunctionAddress = 1; + private uint _currentExternalFunctionAddress = 2; /// /// Lookup for addresses assigned to external functions "[extern]:functionName", indexed by functionName. /// - private ConcurrentDictionary _externalFunctionAddresses = null!; + private ConcurrentDictionary? _externalFunctionAddresses; /// /// Lookup for addresses assigned to external functions "[extern]:functionName", indexed by functionName. /// - /// This variable used by the prefix preprocessing step exclusively, the data is copied to afterwards. + /// The variable is used by the prefix preprocessing step exclusively, the data is copied to afterwards. + /// This is purely a performance optimization, as the prefix is processed exclusively at the beginning and we thus don't need the concurrency of . /// - private Dictionary _externalFunctionAddressesPrefix = new(); + private Dictionary? _externalFunctionAddressesPrefix = new(); /// /// Requested MAP entries, which will be generated after preprocessing is done. /// This dictionary is used as a set, so the value is always ignored. /// - private ConcurrentDictionary<(int imageId, uint relativeAddress), object?> _requestedMapEntries = null!; + private ConcurrentDictionary<(int imageId, uint relativeAddress), object?>? _requestedMapEntries; /// /// Requested MAP entries, which will be generated after preprocessing is done. /// This dictionary is used as a set, so the value is always ignored. - /// - /// This variable used by the prefix preprocessing step exclusively, the data is copied to afterwards. + /// + /// During prefix processing, we use a non-thread-safe dictionary for performance. /// - private Dictionary<(int imageId, uint relativeAddress), object?> _requestedMapEntriesPrefix = new(); + private Dictionary<(int imageId, uint relativeAddress), object?>? _requestedMapEntriesPrefix = new(); public override async Task PreprocessTraceAsync(TraceEntity traceEntity) { @@ -151,7 +159,7 @@ public override async Task PreprocessTraceAsync(TraceEntity traceEntity) Interesting = true, StartAddress = (ulong)imageFileId << 32, EndAddress = ((ulong)imageFileId << 32) | 0xFFFFFFFFul, - Name = scriptData[2] + Name = scriptData[1] }; _imageData.Add(new ImageData(imageFile)); @@ -171,7 +179,12 @@ public override async Task PreprocessTraceAsync(TraceEntity traceEntity) EndAddress = ((ulong)_externalFunctionsImageId << 32) | 0xFFFFFFFFul, Name = "[extern]" }; - _imageData.Add(new ImageData(_externalFunctionsImage)); + var externalFunctionsImageData = new ImageData(_externalFunctionsImage); + _imageData.Add(externalFunctionsImageData); + + // Add catch-all entry for returns from unknown locations + _requestedMapEntriesPrefix!.Add((_externalFunctionsImageId, _catchAllExternalFunctionAddress), null); + externalFunctionsImageData.FunctionNameLookupPrefix!.Add((_catchAllExternalFunctionAddress, _catchAllExternalFunctionAddress), "[unknown]"); // Prepare writer for serializing trace data // We initialize it with some robust initial capacity, to reduce amount of copying while keeping memory overhead low @@ -198,16 +211,16 @@ public override async Task PreprocessTraceAsync(TraceEntity traceEntity) } // Initialize shared dictionaries - _externalFunctionAddresses = new ConcurrentDictionary(_externalFunctionAddressesPrefix); - _externalFunctionAddressesPrefix = null!; - _requestedMapEntries = new ConcurrentDictionary<(int imageId, uint relativeAddress), object?>(_requestedMapEntriesPrefix); - _requestedMapEntriesPrefix = null!; + _externalFunctionAddresses = new ConcurrentDictionary(_externalFunctionAddressesPrefix!); + _externalFunctionAddressesPrefix = null; + _requestedMapEntries = new ConcurrentDictionary<(int imageId, uint relativeAddress), object?>(_requestedMapEntriesPrefix!); + _requestedMapEntriesPrefix = null; foreach(var imageData in _imageData) { - imageData.FunctionNameLookup = new ConcurrentDictionary<(uint start, uint end), string>(imageData.FunctionNameLookupPrefix); - imageData.FunctionNameLookupPrefix = null!; - imageData.RelativeAddressLookup = new ConcurrentDictionary(imageData.RelativeAddressLookupPrefix); - imageData.RelativeAddressLookupPrefix = null!; + imageData.FunctionNameLookup = new ConcurrentDictionary<(uint start, uint end), string>(imageData.FunctionNameLookupPrefix!); + imageData.FunctionNameLookupPrefix = null; + imageData.RelativeAddressLookup = new ConcurrentDictionary(imageData.RelativeAddressLookupPrefix!); + imageData.RelativeAddressLookupPrefix = null; } _firstTestcase = false; @@ -272,9 +285,14 @@ private void PreprocessFile(string inputFileName, IFastBinaryWriter traceFileWri HeapAllocation heapAllocationEntry = new(); HeapMemoryAccess heapMemoryAccessEntry = new(); + // Helper function for adding requested MAP entries without having to check _firstTestcase every time + // We cannot cast to IDictionary, as the TryAdd extension does not work with ConcurrentDictionary + Func<(int imageId, uint relativeAddress), object?, bool> tryAddRequestedMapEntry = _firstTestcase + ? (key, value) => _requestedMapEntriesPrefix!.TryAdd(key, value) + : (key, value) => _requestedMapEntries!.TryAdd(key, value); + // Parse trace entries (TracePrefixFile.ImageFileInfo imageFileInfo, uint address)? lastRet1Entry = null; - (TracePrefixFile.ImageFileInfo imageFileInfo, uint address)? lastCondEntry = null; Dictionary heapObjects = _prefixHeapObjects == null ? new() : new(_prefixHeapObjects); Dictionary compressedLinesLookup = _prefixCompressedLinesLookup == null ? new() : new(_prefixCompressedLinesLookup); ulong nextHeapAllocationAddress = _prefixNextHeapAllocationAddress; @@ -452,48 +470,35 @@ private void PreprocessFile(string inputFileName, IFastBinaryWriter traceFileWri case 'c': { // Parse line + var sourceScriptIdPart = NextSplit(ref lineParts, separator); var sourcePart = NextSplit(ref lineParts, separator); + var destinationScriptIdPart = NextSplit(ref lineParts, separator); var destinationPart = NextSplit(ref lineParts, separator); var namePart = NextSplit(ref lineParts, separator); + int sourceScriptId = ParseInt32NotSigned(sourceScriptIdPart); + int? destinationScriptId = destinationScriptIdPart.Equals("E", StringComparison.Ordinal) ? null : ParseInt32NotSigned(destinationScriptIdPart); + // Resolve code locations - var source = ResolveLineInfoToImage(sourcePart); - var destination = ResolveLineInfoToImage(destinationPart); + var source = ResolveLineInfo(sourceScriptId, sourcePart); + var destination = ResolveLineInfo(destinationScriptId, destinationPart); // Produce MAP entries + tryAddRequestedMapEntry((source.imageData.ImageFileInfo.Id, source.relativeStartAddress), null); + tryAddRequestedMapEntry((destination.imageData.ImageFileInfo.Id, destination.relativeStartAddress), null); + tryAddRequestedMapEntry((destination.imageData.ImageFileInfo.Id, destination.relativeEndAddress), null); + if(_firstTestcase) { - _requestedMapEntriesPrefix.TryAdd((source.imageData.ImageFileInfo.Id, source.relativeStartAddress), null); - _requestedMapEntriesPrefix.TryAdd((destination.imageData.ImageFileInfo.Id, destination.relativeStartAddress), null); - _requestedMapEntriesPrefix.TryAdd((destination.imageData.ImageFileInfo.Id, destination.relativeEndAddress), null); // For Ret2-only returns (e.g., void functions) - // Record function name, if it is not already known - destination.imageData.FunctionNameLookupPrefix.TryAdd((destination.relativeStartAddress, destination.relativeEndAddress), new string(namePart)); + destination.imageData.FunctionNameLookupPrefix!.TryAdd((destination.relativeStartAddress, destination.relativeEndAddress), new string(namePart)); // Do not trace branches in prefix mode break; } - _requestedMapEntries.TryAdd((source.imageData.ImageFileInfo.Id, source.relativeStartAddress), null); - _requestedMapEntries.TryAdd((destination.imageData.ImageFileInfo.Id, destination.relativeStartAddress), null); - _requestedMapEntries.TryAdd((destination.imageData.ImageFileInfo.Id, destination.relativeEndAddress), null); // For Ret2-only returns (e.g., void functions) - // Record function name, if it is not already known - destination.imageData.FunctionNameLookup.TryAdd((destination.relativeStartAddress, destination.relativeEndAddress), new string(namePart)); - - // Create branch entry, if there is a pending conditional - if(lastCondEntry != null) - { - branchEntry.BranchType = Branch.BranchTypes.Jump; - branchEntry.Taken = true; - branchEntry.SourceImageId = lastCondEntry.Value.imageFileInfo.Id; - branchEntry.SourceInstructionRelativeAddress = lastCondEntry.Value.address; - branchEntry.DestinationImageId = source.imageData.ImageFileInfo.Id; - branchEntry.DestinationInstructionRelativeAddress = source.relativeStartAddress; - branchEntry.Store(traceFileWriter); - - lastCondEntry = null; - } + destination.imageData.FunctionNameLookup!.TryAdd((destination.relativeStartAddress, destination.relativeEndAddress), new string(namePart)); // Record call branchEntry.BranchType = Branch.BranchTypes.Call; @@ -510,38 +515,22 @@ private void PreprocessFile(string inputFileName, IFastBinaryWriter traceFileWri case 'r': { // Parse line - var sourcePart = NextSplit(ref lineParts, separator); + var scriptIdPart = NextSplit(ref lineParts, separator); + var locationPart = NextSplit(ref lineParts, separator); // Resolve code locations - var source = ResolveLineInfoToImage(sourcePart); + int scriptId = ParseInt32NotSigned(scriptIdPart); + var location = ResolveLineInfo(scriptId, locationPart); // Produce MAP entries - if(_firstTestcase) - { - _requestedMapEntriesPrefix.TryAdd((source.imageData.ImageFileInfo.Id, source.relativeStartAddress), null); + tryAddRequestedMapEntry((location.imageData.ImageFileInfo.Id, location.relativeStartAddress), null); - // Do not trace branches in prefix mode + // Do not trace branches in prefix mode + if(_firstTestcase) break; - } - - _requestedMapEntries.TryAdd((source.imageData.ImageFileInfo.Id, source.relativeStartAddress), null); - - // Create branch entry, if there is a pending conditional - if(lastCondEntry != null) - { - branchEntry.BranchType = Branch.BranchTypes.Jump; - branchEntry.Taken = true; - branchEntry.SourceImageId = lastCondEntry.Value.imageFileInfo.Id; - branchEntry.SourceInstructionRelativeAddress = lastCondEntry.Value.address; - branchEntry.DestinationImageId = source.imageData.ImageFileInfo.Id; - branchEntry.DestinationInstructionRelativeAddress = source.relativeStartAddress; - branchEntry.Store(traceFileWriter); - - lastCondEntry = null; - } // Remember for next Ret2 entry - lastRet1Entry = (source.imageData.ImageFileInfo, source.relativeStartAddress); + lastRet1Entry = (location.imageData.ImageFileInfo, location.relativeStartAddress); break; } @@ -549,33 +538,27 @@ private void PreprocessFile(string inputFileName, IFastBinaryWriter traceFileWri case 'R': { // Parse line - var sourcePart = NextSplit(ref lineParts, separator); - var destinationPart = NextSplit(ref lineParts, separator); + var scriptIdPart = NextSplit(ref lineParts, separator); + var locationPart = NextSplit(ref lineParts, separator); // Resolve code locations - var source = ResolveLineInfoToImage(sourcePart); - var destination = ResolveLineInfoToImage(destinationPart); + int scriptId = ParseInt32NotSigned(scriptIdPart); + var location = ResolveLineInfo(scriptId, locationPart); // Produce MAP entries - if(_firstTestcase) - { - _requestedMapEntriesPrefix.TryAdd((source.imageData.ImageFileInfo.Id, source.relativeStartAddress), null); - _requestedMapEntriesPrefix.TryAdd((destination.imageData.ImageFileInfo.Id, destination.relativeStartAddress), null); + tryAddRequestedMapEntry((location.imageData.ImageFileInfo.Id, location.relativeStartAddress), null); - // Do not trace branches in prefix mode + // Do not trace branches in prefix mode + if(_firstTestcase) break; - } - - _requestedMapEntries.TryAdd((source.imageData.ImageFileInfo.Id, source.relativeStartAddress), null); - _requestedMapEntries.TryAdd((destination.imageData.ImageFileInfo.Id, destination.relativeStartAddress), null); // Create branch entry branchEntry.BranchType = Branch.BranchTypes.Return; branchEntry.Taken = true; - branchEntry.DestinationImageId = destination.imageData.ImageFileInfo.Id; - branchEntry.DestinationInstructionRelativeAddress = destination.relativeStartAddress; + branchEntry.DestinationImageId = location.imageData.ImageFileInfo.Id; + branchEntry.DestinationInstructionRelativeAddress = location.relativeStartAddress; - // Did we see a Ret1 entry? -> more accurate source location info + // Did we see a Ret1 entry? -> accurate source location info if(lastRet1Entry != null) { branchEntry.SourceImageId = lastRet1Entry.Value.imageFileInfo.Id; @@ -585,8 +568,8 @@ private void PreprocessFile(string inputFileName, IFastBinaryWriter traceFileWri } else { - branchEntry.SourceImageId = source.imageData.ImageFileInfo.Id; - branchEntry.SourceInstructionRelativeAddress = source.relativeEndAddress; + branchEntry.SourceImageId = _externalFunctionsImageId; + branchEntry.SourceInstructionRelativeAddress = _catchAllExternalFunctionAddress; } branchEntry.Store(traceFileWriter); @@ -594,108 +577,54 @@ private void PreprocessFile(string inputFileName, IFastBinaryWriter traceFileWri break; } - case 'C': + case 'j': { // Parse line - var locationPart = NextSplit(ref lineParts, separator); - - // Resolve code locations - var location = ResolveLineInfoToImage(locationPart); - - // Produce MAP entries - if(_firstTestcase) - { - _requestedMapEntriesPrefix.TryAdd((location.imageData.ImageFileInfo.Id, location.relativeStartAddress), null); - - // Do not trace branches in prefix mode - break; - } - - _requestedMapEntries.TryAdd((location.imageData.ImageFileInfo.Id, location.relativeStartAddress), null); - - // Create branch entry, if there is a pending conditional - if(lastCondEntry != null) - { - branchEntry.BranchType = Branch.BranchTypes.Jump; - branchEntry.Taken = true; - branchEntry.SourceImageId = lastCondEntry.Value.imageFileInfo.Id; - branchEntry.SourceInstructionRelativeAddress = lastCondEntry.Value.address; - branchEntry.DestinationImageId = location.imageData.ImageFileInfo.Id; - branchEntry.DestinationInstructionRelativeAddress = location.relativeStartAddress; - branchEntry.Store(traceFileWriter); - } - - // We have to wait until the next line before we can produce a meaningful branch entry - lastCondEntry = (location.imageData.ImageFileInfo, location.relativeStartAddress); - - break; - } + var scriptIdPart = NextSplit(ref lineParts, separator); + var sourcePart = NextSplit(ref lineParts, separator); + var destinationPart = NextSplit(ref lineParts, separator); - case 'e': - { - // Parse line - var locationPart = NextSplit(ref lineParts, separator); + int scriptId = ParseInt32NotSigned(scriptIdPart); // Resolve code locations - var location = ResolveLineInfoToImage(locationPart); + var source = ResolveLineInfo(scriptId, sourcePart); + var destination = ResolveLineInfo(scriptId, destinationPart); // Produce MAP entries - if(_firstTestcase) - { - _requestedMapEntriesPrefix.TryAdd((location.imageData.ImageFileInfo.Id, location.relativeStartAddress), null); + tryAddRequestedMapEntry((source.imageData.ImageFileInfo.Id, source.relativeStartAddress), null); + tryAddRequestedMapEntry((destination.imageData.ImageFileInfo.Id, destination.relativeStartAddress), null); - // Do not trace branches in prefix mode + // Do not trace branches in prefix mode + if(_firstTestcase) break; - } - _requestedMapEntries.TryAdd((location.imageData.ImageFileInfo.Id, location.relativeStartAddress), null); - - // Create branch entry, if there is a pending conditional - if(lastCondEntry != null) - { - branchEntry.BranchType = Branch.BranchTypes.Jump; - branchEntry.Taken = true; - branchEntry.SourceImageId = lastCondEntry.Value.imageFileInfo.Id; - branchEntry.SourceInstructionRelativeAddress = lastCondEntry.Value.address; - branchEntry.DestinationImageId = location.imageData.ImageFileInfo.Id; - branchEntry.DestinationInstructionRelativeAddress = location.relativeStartAddress; - branchEntry.Store(traceFileWriter); - - lastCondEntry = null; - } + // Create branch entry + branchEntry.BranchType = Branch.BranchTypes.Jump; + branchEntry.Taken = true; + branchEntry.SourceImageId = source.imageData.ImageFileInfo.Id; + branchEntry.SourceInstructionRelativeAddress = source.relativeStartAddress; + branchEntry.DestinationImageId = destination.imageData.ImageFileInfo.Id; + branchEntry.DestinationInstructionRelativeAddress = destination.relativeStartAddress; + branchEntry.Store(traceFileWriter); break; } - case 'g': + case 'm': { // Parse line + var accessType = NextSplit(ref lineParts, separator); + var scriptIdPart = NextSplit(ref lineParts, separator); var locationPart = NextSplit(ref lineParts, separator); var objectIdPart = NextSplit(ref lineParts, separator); var offsetPart = NextSplit(ref lineParts, separator); // Resolve code locations - var location = ResolveLineInfoToImage(locationPart); + int scriptId = ParseInt32NotSigned(scriptIdPart); + var location = ResolveLineInfo(scriptId, locationPart); // Produce MAP entries - if(_firstTestcase) - _requestedMapEntriesPrefix.TryAdd((location.imageData.ImageFileInfo.Id, location.relativeStartAddress), null); - else - _requestedMapEntries.TryAdd((location.imageData.ImageFileInfo.Id, location.relativeStartAddress), null); - - // Create branch entry, if there is a pending conditional - if(lastCondEntry != null) - { - branchEntry.BranchType = Branch.BranchTypes.Jump; - branchEntry.Taken = true; - branchEntry.SourceImageId = lastCondEntry.Value.imageFileInfo.Id; - branchEntry.SourceInstructionRelativeAddress = lastCondEntry.Value.address; - branchEntry.DestinationImageId = location.imageData.ImageFileInfo.Id; - branchEntry.DestinationInstructionRelativeAddress = location.relativeStartAddress; - branchEntry.Store(traceFileWriter); - - lastCondEntry = null; - } + tryAddRequestedMapEntry((location.imageData.ImageFileInfo.Id, location.relativeStartAddress), null); // Extract access data int objectId = ParseInt32NotSigned(objectIdPart); @@ -748,98 +677,18 @@ private void PreprocessFile(string inputFileName, IFastBinaryWriter traceFileWri heapMemoryAccessEntry.HeapAllocationBlockId = objectId; heapMemoryAccessEntry.MemoryRelativeAddress = offsetRelativeAddress; heapMemoryAccessEntry.Size = 1; - heapMemoryAccessEntry.IsWrite = false; + heapMemoryAccessEntry.IsWrite = accessType == "w"; heapMemoryAccessEntry.Store(traceFileWriter); break; } - case 'p': + /* + case 'Y': { - // Parse line - var locationPart = NextSplit(ref lineParts, separator); - var objectIdPart = NextSplit(ref lineParts, separator); - var offsetPart = NextSplit(ref lineParts, separator); - - // Resolve code locations - var location = ResolveLineInfoToImage(locationPart); - - // Produce MAP entries - if(_firstTestcase) - _requestedMapEntriesPrefix.TryAdd((location.imageData.ImageFileInfo.Id, location.relativeStartAddress), null); - else - _requestedMapEntries.TryAdd((location.imageData.ImageFileInfo.Id, location.relativeStartAddress), null); - - // Create branch entry, if there is a pending conditional - if(lastCondEntry != null) - { - branchEntry.BranchType = Branch.BranchTypes.Jump; - branchEntry.Taken = true; - branchEntry.SourceImageId = lastCondEntry.Value.imageFileInfo.Id; - branchEntry.SourceInstructionRelativeAddress = lastCondEntry.Value.address; - branchEntry.DestinationImageId = location.imageData.ImageFileInfo.Id; - branchEntry.DestinationInstructionRelativeAddress = location.relativeStartAddress; - branchEntry.Store(traceFileWriter); - - lastCondEntry = null; - } - - // Extract access data - int objectId = ParseInt32NotSigned(objectIdPart); - string offset = new string(offsetPart); - - // Did we already encounter this object? - uint offsetRelativeAddress; - if(!heapObjects.TryGetValue(objectId, out var objectData)) - { - objectData = new HeapObjectData { NextPropertyAddress = 0x100000 }; - heapObjects.Add(objectId, objectData); - - heapAllocationEntry.Id = objectId; - heapAllocationEntry.Address = nextHeapAllocationAddress; - heapAllocationEntry.Size = 2 * heapAllocationChunkSize; - heapAllocationEntry.Store(traceFileWriter); - - nextHeapAllocationAddress += 2 * heapAllocationChunkSize; - - // Create entry for accessed offset - // Numeric index, or named property? - offsetRelativeAddress = uint.TryParse(offset, out uint offsetInt) - ? offsetInt - : objectData.NextPropertyAddress++; - objectData.PropertyAddressMapping.TryAdd(offset, offsetRelativeAddress); - } - else - { - // Did we already encounter this offset? - offsetRelativeAddress = objectData.PropertyAddressMapping.GetOrAdd(offset, static (offsetParam, objectDataParam) => - { - // No, create new entry - - // Numeric index? - if(uint.TryParse(offsetParam, out uint offsetInt)) - return offsetInt; - - // Named property - return Interlocked.Increment(ref objectDataParam.NextPropertyAddress); - }, objectData); - } - - // Do not trace memory accesses in prefix mode - if(_firstTestcase) - break; - - // Create memory access - heapMemoryAccessEntry.InstructionImageId = location.imageData.ImageFileInfo.Id; - heapMemoryAccessEntry.InstructionRelativeAddress = location.relativeStartAddress; - heapMemoryAccessEntry.HeapAllocationBlockId = objectId; - heapMemoryAccessEntry.MemoryRelativeAddress = offsetRelativeAddress; - heapMemoryAccessEntry.Size = 1; - heapMemoryAccessEntry.IsWrite = true; - heapMemoryAccessEntry.Store(traceFileWriter); - - break; + // TODO yield/yield resume } + */ default: { @@ -860,36 +709,32 @@ private void PreprocessFile(string inputFileName, IFastBinaryWriter traceFileWri /// /// Resolves a line/column number info into an image and a pair of image-relative start/end addresses. /// + /// ID of the script file containing these lines. /// /// Line number information. /// /// Supported formats: - /// - scriptId:startLine:startColumn:endLine:endColumn - /// - [extern]:functionName:constructor + /// - startLine:startColumn:endLine:endColumn + /// - functionName:constructor /// - private (ImageData imageData, uint relativeStartAddress, uint relativeEndAddress) ResolveLineInfoToImage(ReadOnlySpan lineInfo) + private (ImageData imageData, uint relativeStartAddress, uint relativeEndAddress) ResolveLineInfo(int? scriptFileId, ReadOnlySpan lineInfo) { - const char separator = ':'; - // We use line info as key for caching known addresses string lineInfoString = new string(lineInfo); - var part0 = NextSplit(ref lineInfo, separator); - bool isExternal = part0[0] == 'E'; - // Try to read existing address data, or generate new one if not known yet - var imageData = _imageData[isExternal ? _externalFunctionsImageId : ParseInt32NotSigned(part0)]; + var imageData = _imageData[scriptFileId ?? _externalFunctionsImageId]; (uint start, uint end) addressData; if(_firstTestcase) { - if(!imageData.RelativeAddressLookupPrefix.TryGetValue(lineInfoString, out addressData)) + if(!imageData.RelativeAddressLookupPrefix!.TryGetValue(lineInfoString, out addressData)) { - addressData = GenerateAddressLookupEntry(lineInfoString); - imageData.RelativeAddressLookupPrefix.Add(lineInfoString, addressData); + addressData = GenerateAddressLookupEntry(lineInfoString, (this, scriptFileId)); + imageData.RelativeAddressLookupPrefix!.Add(lineInfoString, addressData); } } else - addressData = imageData.RelativeAddressLookup.GetOrAdd(lineInfoString, GenerateAddressLookupEntry); + addressData = imageData.RelativeAddressLookup!.GetOrAdd(lineInfoString, GenerateAddressLookupEntry, (this, scriptFileId)); return (imageData, addressData.start, addressData.end); } @@ -898,42 +743,48 @@ private void PreprocessFile(string inputFileName, IFastBinaryWriter traceFileWri /// Generates a new entry for the relative address lookup. /// /// Line info. + /// Arguments. /// Relative start and end address. - private (uint start, uint end) GenerateAddressLookupEntry(string lineInfoString) + /// + /// This method is static to avoid an implicit capture of the instance, causing a heap allocation. + /// + private static (uint start, uint end) GenerateAddressLookupEntry(string lineInfoString, (JsTracePreprocessor instance, int? scriptFileId) args) { const char separator = ':'; // Split var lineInfoStringSpan = lineInfoString.AsSpan(); // We can't capture the lineInfo Span directly - var part0 = NextSplit(ref lineInfoStringSpan, separator); // part0 is already handled - var part1 = NextSplit(ref lineInfoStringSpan, separator); // Unknown script / external function? - bool isExternal = part0[0] == 'E'; + bool isExternal = args.scriptFileId == null; if(isExternal) { // Get address of function, or generate a new one if it does not yet exist // Necessary locking is done by the underlying concurrent dictionary (if not in prefix mode) - string functionName = new string(part1); - if(_firstTestcase) + string functionName = lineInfoString; + if(args.instance._firstTestcase) { - if(!_externalFunctionAddressesPrefix.TryGetValue(functionName, out uint externalFunctionAddress)) + if(!args.instance._externalFunctionAddressesPrefix!.TryGetValue(functionName, out uint externalFunctionAddress)) { - externalFunctionAddress = ++_currentExternalFunctionAddress; - _externalFunctionAddressesPrefix.Add(functionName, externalFunctionAddress); + externalFunctionAddress = ++args.instance._currentExternalFunctionAddress; + args.instance._externalFunctionAddressesPrefix.Add(functionName, externalFunctionAddress); } return (externalFunctionAddress, externalFunctionAddress); } else { - uint externalFunctionAddress = _externalFunctionAddresses.GetOrAdd(functionName, _ => Interlocked.Increment(ref _currentExternalFunctionAddress)); + uint externalFunctionAddress = args.instance._externalFunctionAddresses!.GetOrAdd( + functionName, + _ => Interlocked.Increment(ref args.instance._currentExternalFunctionAddress) + ); return (externalFunctionAddress, externalFunctionAddress); } } // Split + var part1 = NextSplit(ref lineInfoStringSpan, separator); var part2 = NextSplit(ref lineInfoStringSpan, separator); var part3 = NextSplit(ref lineInfoStringSpan, separator); var part4 = NextSplit(ref lineInfoStringSpan, separator); @@ -943,8 +794,8 @@ private void PreprocessFile(string inputFileName, IFastBinaryWriter traceFileWri uint startColumn = ParseUInt32(part2); uint endLine = ParseUInt32(part3); uint endColumn = ParseUInt32(part4); - uint startAddress = (startLine << _columnsBits) | startColumn; - uint endAddress = (endLine << _columnsBits) | endColumn; + uint startAddress = (startLine << args.instance._columnsBits) | startColumn; + uint endAddress = (endLine << args.instance._columnsBits) | endColumn; return (startAddress, endAddress); } @@ -984,6 +835,8 @@ public override async Task UnInitAsync() List replaceChars = Path.GetInvalidPathChars().Append('/').Append('\\').Append('.').ToList(); // Save MAP data + if(_requestedMapEntries == null) + return; Dictionary> requestedMapEntriesPerImage = _requestedMapEntries .GroupBy(m => m.Key.imageId) .ToDictionary(m => m.Key, m => m @@ -991,21 +844,26 @@ public override async Task UnInitAsync() .OrderBy(n => n) .ToList() ); - Dictionary> sortedFunctionNameLookup = _imageData - .ToDictionary(i => i.ImageFileInfo.Id, i => new SortedList<(uint start, uint end), string>(i.FunctionNameLookup)); + var sortedFunctionNameLookups = _imageData.ToDictionary(i => i.ImageFileInfo.Id, i => new SortedList<(uint start, uint end), string>(i.FunctionNameLookup ?? new())); foreach(var imageData in _imageData) { int imageFileId = imageData.ImageFileInfo.Id; string mapFileName = Path.Join(_mapDirectory.FullName, replaceChars.Aggregate(imageData.ImageFileInfo.Name, (current, invalidPathChar) => current.Replace(invalidPathChar, '_')) + ".map"); await using var mapFileWriter = new StreamWriter(File.Open(mapFileName, FileMode.Create)); - + await mapFileWriter.WriteLineAsync(imageData.ImageFileInfo.Name); // Create MAP entries - foreach(uint relativeAddress in requestedMapEntriesPerImage[imageFileId]) + if(!requestedMapEntriesPerImage.TryGetValue(imageFileId, out var requestedMapEntries)) + continue; + var functionNameLookup = sortedFunctionNameLookups[imageFileId]; + foreach(uint relativeAddress in requestedMapEntries) { - string name = sortedFunctionNameLookup[imageFileId].LastOrDefault(functionData => functionData.Key.start <= relativeAddress && relativeAddress <= functionData.Key.end, new KeyValuePair<(uint start, uint end), string>((0, 0), "")).Value; + string name = functionNameLookup.LastOrDefault( + functionData => functionData.Key.start <= relativeAddress && relativeAddress <= functionData.Key.end, + new KeyValuePair<(uint start, uint end), string>(default, "?") + ).Value; // Handle [extern] functions separately if(imageFileId == _externalFunctionsImage.Id) @@ -1029,7 +887,7 @@ public override async Task UnInitAsync() /// String to split. /// Split character. /// - private ReadOnlySpan NextSplit(ref ReadOnlySpan str, char separator) + private static ReadOnlySpan NextSplit(ref ReadOnlySpan str, char separator) { // Look for separator for(int i = 0; i < str.Length; ++i) @@ -1055,7 +913,7 @@ private ReadOnlySpan NextSplit(ref ReadOnlySpan str, char separator) /// /// String to parse. /// - private int ParseInt32NotSigned(ReadOnlySpan str) + private static int ParseInt32NotSigned(ReadOnlySpan str) { return unchecked((int)ParseUInt32(str)); } @@ -1066,7 +924,7 @@ private int ParseInt32NotSigned(ReadOnlySpan str) /// /// String to parse. /// - private unsafe uint ParseUInt32(ReadOnlySpan str) + private static unsafe uint ParseUInt32(ReadOnlySpan str) { uint result = 0; fixed(char* strBeginPtr = str) @@ -1089,40 +947,35 @@ private class HeapObjectData public ConcurrentDictionary PropertyAddressMapping { get; } = new(); } - private class ImageData + private class ImageData(TracePrefixFile.ImageFileInfo imageFileInfo) { - public ImageData(TracePrefixFile.ImageFileInfo imageFileInfo) - { - ImageFileInfo = imageFileInfo; - } - /// /// Image file info. /// - public TracePrefixFile.ImageFileInfo ImageFileInfo { get; } + public TracePrefixFile.ImageFileInfo ImageFileInfo { get; } = imageFileInfo; /// /// Lookup for all encoded relative addresses. Indexed by encoding ("script.js:1:2:1:3"). /// - public ConcurrentDictionary RelativeAddressLookup { get; set; } = null!; + public ConcurrentDictionary? RelativeAddressLookup { get; set; } /// /// Lookup for all encoded relative addresses. Indexed by encoding ("script.js:1:2:1:3"). /// /// This variable used by the prefix preprocessing step exclusively, the data is copied to afterwards. /// - public Dictionary RelativeAddressLookupPrefix { get; set; } = new(); + public Dictionary? RelativeAddressLookupPrefix { get; set; } = new(); /// /// Maps encoded start and end addresses to function names. /// - public ConcurrentDictionary<(uint start, uint end), string> FunctionNameLookup { get; set; } = null!; + public ConcurrentDictionary<(uint start, uint end), string>? FunctionNameLookup { get; set; } /// /// Maps encoded start and end addresses to function names. /// /// This variable used by the prefix preprocessing step exclusively, the data is copied to afterwards. /// - public Dictionary<(uint start, uint end), string> FunctionNameLookupPrefix { get; set; } = new(); + public Dictionary<(uint start, uint end), string>? FunctionNameLookupPrefix { get; set; } = new(); } } \ No newline at end of file diff --git a/docker/javascript/Dockerfile b/docker/javascript/Dockerfile new file mode 100644 index 0000000..03b74f1 --- /dev/null +++ b/docker/javascript/Dockerfile @@ -0,0 +1,56 @@ + +## BUILD ## +FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build + +WORKDIR /src + +COPY *.props . +COPY *.sln* . +COPY *.cs . +COPY Microwalk Microwalk +COPY Microwalk.FrameworkBase Microwalk.FrameworkBase +COPY Microwalk.Plugins.JavascriptTracer Microwalk.Plugins.JavascriptTracer +COPY Tools/MapFileGenerator Tools/MapFileGenerator +COPY Tools/CiReportGenerator Tools/CiReportGenerator + +RUN dotnet publish Microwalk/Microwalk.csproj -c Release -o /publish/microwalk +RUN dotnet publish Microwalk.Plugins.JavascriptTracer/Microwalk.Plugins.JavascriptTracer.csproj -c Release -o /publish/microwalk +RUN dotnet publish Tools/MapFileGenerator/MapFileGenerator.csproj -c Release -o /publish/mapfilegenerator +RUN dotnet publish Tools/CiReportGenerator/CiReportGenerator.csproj -c Release -o /publish/CiReportGenerator + + +## RUNTIME ## + +FROM mcr.microsoft.com/dotnet/runtime:8.0-jammy + +# Get some dependencies +RUN apt-get update -y && apt-get install -y \ + wget \ + jq +RUN wget -q -O - https://deb.nodesource.com/setup_21.x | bash - +RUN apt-get install -y nodejs && npm i c8 -g + +# Copy Microwalk binaries +WORKDIR /mw/microwalk +COPY --from=build /publish/microwalk . +ENV MICROWALK_PATH=/mw/microwalk + +# Copy MAP file generator binaries +WORKDIR /mw/mapfilegenerator +COPY --from=build /publish/mapfilegenerator . +ENV MAP_GENERATOR_PATH=/mw/mapfilegenerator + +# Copy code quality report generator binaries +WORKDIR /mw/CiReportGenerator +COPY --from=build /publish/CiReportGenerator . +ENV CQR_GENERATOR_PATH=/mw/CiReportGenerator + +# Copy JavaScript tracer +WORKDIR /mw/jstracer +COPY JavascriptTracer . +RUN npm install +ENV JSTRACER_PATH=/mw/jstracer + +# Prepare working directory +RUN mkdir -p /mw/work +ENV WORK_DIR=/mw/work \ No newline at end of file