Skip to content

Commit

Permalink
Utils (#26)
Browse files Browse the repository at this point in the history
* Add utils:
  - applyIteratively
  - treeModifier
  - logger

* Add missing allScopes object

* Add utils tests

* Change imports to relative without __dirname
  • Loading branch information
ctrl-escp authored Jul 5, 2024
1 parent d161823 commit 9bfcad4
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 8 deletions.
2 changes: 1 addition & 1 deletion src/arborist.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const {generateCode, generateFlatAST,} = require(__dirname + '/flast');
const {generateCode, generateFlatAST,} = require('./flast');

const Arborist = class {
/**
Expand Down
1 change: 1 addition & 0 deletions src/flast.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ function generateFlatAST(inputCode, opts = {}) {
if (opts.detailed) {
const scopes = getAllScopes(rootNode);
for (let i = 0; i < tree.length; i++) injectScopeToNode(tree[i], scopes);
tree[0].allScopes = scopes;
}
return tree;
}
Expand Down
7 changes: 4 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
...require(__dirname + '/flast'),
...require(__dirname + '/arborist'),
...require(__dirname + '/types'),
...require('./flast'),
...require('./arborist'),
...require('./types'),
utils: require('./utils'),
};
1 change: 1 addition & 0 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const {Scope} = require('eslint-scope');
/**
* @typedef ASTNode
* @property {string} type
* @property {object} [allScopes]
* @property {ASTNode} [alternate]
* @property {ASTNode} [argument]
* @property {ASTNode[]} [arguments]
Expand Down
65 changes: 65 additions & 0 deletions src/utils/applyIteratively.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const {Arborist} = require('../arborist');
const logger = require('./logger');
const {createHash} = require('node:crypto');

const generateHash = str => createHash('sha256').update(str).digest('hex');


/**
* Apply functions to modify the script repeatedly until they are no long effective or the max number of iterations is reached.
* @param {string} script The target script to run the functions on.
* @param {function[]} funcs
* @param {number?} maxIterations (optional) Stop the loop after this many iterations at most.
* @return {string} The possibly modified script.
*/
function runLoop(script, funcs, maxIterations = 500) {
let scriptSnapshot = '';
let currentIteration = 0;
let changesCounter = 0;
let iterationsCounter = 0;
try {
let scriptHash = generateHash(script);
let arborist = new Arborist(script);
while (arborist.ast?.length && scriptSnapshot !== script && currentIteration < maxIterations) {
const iterationStartTime = Date.now();
scriptSnapshot = script;
// Mark each node with the script hash to distinguish cache of different scripts.
for (let i = 0; i < arborist.ast.length; i++) arborist.ast[i].scriptHash = scriptHash;
for (let i = 0; i < funcs.length; i++) {
const func = funcs[i];
const funcStartTime = +new Date();
try {
logger.debug(`\t[!] Running ${func.name}...`);
arborist = func(arborist);
if (!arborist.ast?.length) break;
// If the hash doesn't exist it means the Arborist was replaced
const numberOfNewChanges = arborist.getNumberOfChanges() + +!arborist.ast[0].scriptHash;
if (numberOfNewChanges) {
changesCounter += numberOfNewChanges;
logger.log(`\t[+] ${func.name} applying ${numberOfNewChanges} new changes!`);
arborist.applyChanges();
script = arborist.script;
scriptHash = generateHash(script);
for (let j = 0; j < arborist.ast.length; j++) arborist.ast[j].scriptHash = scriptHash;
}
} catch (e) {
logger.error(`[-] Error in ${func.name} (iteration #${iterationsCounter}): ${e}\n${e.stack}`);
} finally {
logger.debug(`\t\t[!] Running ${func.name} completed in ` +
`${((+new Date() - funcStartTime) / 1000).toFixed(3)} seconds`);
}
}
++currentIteration;
++iterationsCounter;
logger.log(`[+] ==> Iteartion #${iterationsCounter} completed in ${(Date.now() - iterationStartTime) / 1000} seconds` +
` with ${changesCounter ? changesCounter : 'no'} changes (${arborist.ast?.length || '???'} nodes)`);
changesCounter = 0;
}
if (changesCounter) script = arborist.script;
} catch (e) {
logger.error(`[-] Error on iteration #${iterationsCounter}: ${e}\n${e.stack}`);
}
return script;
}

module.exports = runLoop;
5 changes: 5 additions & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
applyIteratively: require('./applyIteratively'),
logger: require('./logger'),
treeModifier: require('./treeModifier'),
};
44 changes: 44 additions & 0 deletions src/utils/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const logLevels = {
DEBUG: 1,
LOG: 2,
ERROR: 3,
NONE: 9e10,
};

/**
* @param {number} logLevel
* @returns {function(*): void|undefined}
*/
function createLoggerForLevel(logLevel) {
if (!Object.values(logLevels).includes(logLevel)) throw new Error(`Unknown log level ${logLevel}.`);
return msg => logLevel >= logger.currentLogLevel ? logger.logFunc(msg) : undefined;
}

const logger = {
logLevels,
logFunc: console.log,
debug: createLoggerForLevel(logLevels.DEBUG),
log: createLoggerForLevel(logLevels.LOG),
error: createLoggerForLevel(logLevels.ERROR),
currentLogLevel: logLevels.NONE,

/**
* Set the current log level
* @param {number} newLogLevel
*/
setLogLevel(newLogLevel) {
if (!Object.values(this.logLevels).includes(newLogLevel)) throw new Error(`Unknown log level ${newLogLevel}.`);
this.currentLogLevel = newLogLevel;
},

setLogLeveNone() {this.setLogLevel(this.logLevels.NONE);},
setLogLeveDebug() {this.setLogLevel(this.logLevels.DEBUG);},
setLogLeveLog() {this.setLogLevel(this.logLevels.LOG);},
setLogLeveError() {this.setLogLevel(this.logLevels.ERROR);},

setLogFunc(newLogfunc) {
this.logFunc = newLogfunc;
},
};

module.exports = logger;
23 changes: 23 additions & 0 deletions src/utils/treeModifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Boilerplate for filter functions that identify the desired structure and a modifier function that modifies the tree.
* An optional name for the function can be provided for better logging.
* @param {Function} filterFunc
* @param {Function} modFunc
* @param {string} [funcName]
* @returns {function(Arborist): Arborist}
*/
function treeModifier(filterFunc, modFunc, funcName) {
const func = function(arb) {
for (let i = 0; i < arb.ast.length; i++) {
const n = arb.ast[i];
if (filterFunc(n, arb)) {
modFunc(n, arb);
}
}
return arb;
};
if (funcName) Object.defineProperty(func, 'name', {value: funcName});
return func;
}

module.exports = treeModifier;
1 change: 1 addition & 0 deletions tests/functionalityTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ module.exports = [
'generateCode',
'generateFlatAST',
'parseCode',
'utils',
];
function tryImporting(importName) {
const {[importName]: tempImport} = require(importSource);
Expand Down
2 changes: 1 addition & 1 deletion tests/parsingTests.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const assert = require('node:assert');
const {generateFlatAST} = require(__dirname + '/../src/index');
const {generateFlatAST} = require('../src/index');

module.exports = [
{
Expand Down
7 changes: 4 additions & 3 deletions tests/tester.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const tests = {
Parsing: __dirname + '/parsingTests',
Functionality: __dirname + '/functionalityTests',
Arborist: __dirname + '/aboristTests',
Parsing: './parsingTests',
Functionality: './functionalityTests',
Arborist: './aboristTests',
Utils: './utilsTests',
};

let allTests = 0;
Expand Down
32 changes: 32 additions & 0 deletions tests/utilsTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const {utils} = require(__dirname + '/../src/index');
const assert = require('node:assert');
module.exports = [
{
enabled: true,
name: 'treeModifier + applyIteratively',
description: '',
run() {
const code = `console.log('Hello' + ' ' + 'there');`;
const expectedOutput = `console.log('General' + ' ' + 'Kenobi');`;
const expectedFuncName = 'StarWarsDialog';
const replacements = {
Hello: 'General',
there: 'Kenobi',
};
let result = code;
const f = n => n.type === 'Literal' && replacements[n.value];
const m = (n, arb) => arb.markNode(n, {
type: 'Literal',
value: replacements[n.value],
});
const generatedFunc = utils.treeModifier(f, m, expectedFuncName);
result = utils.applyIteratively(result, [generatedFunc]);

assert.equal(result, expectedOutput,
`Result does not match expected output.`);
assert.equal(generatedFunc.name, expectedFuncName,
`The name of the generated function does not match.`);
return true;
},
},
];

0 comments on commit 9bfcad4

Please sign in to comment.