0.3.0 • Published 2 years ago

@alu0101229775/espree-logging v0.3.0

Weekly downloads
-
License
-
Repository
-
Last release
2 years ago

Open in Codespaces

Práctica Espree logging

Introducción

Volver al principio 🔝

En el repo encontrará el programa logging-espree.js el cual implementa una función addLogging que:

  • cuando se llama analiza el código JS que se la da como entrada
  • produciendo como salida un código JS equivalente que inserta mensajes de console.log a la entrada de cada función.

Resumen de lo aprendido

Volver al principio 🔝

El programa genera un arbol AST a partir de un fichero de entrada con código y, una vez se tiene dicho arbol, se recorre para generar el código de nuevo pero modificado con los console.log() a través el escodegen y el transpile.

Asimismo, se ha aprendido a utilizar el debugger de Chrome para inspeccionar el programa.

El ejecutable

Volver al principio 🔝

El ejecutable está en bin/log.js y se puede ejecutar con npm start o node bin/log.js.

Contenido de bin/log.js:

#!/usr/bin/env node
import { program } from "commander";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { version } = require("../package.json");
import { transpile } from "../src/logging-espree.js";

program
  .version(version)
  .argument("<filename>", 'file with the original code')
  .option("-V, --version", "output the version number")
  .option("-o, --output <filename>", "file in which to write the output", "output.js")
  .option("-h, --help", "output usage information")
  .action((filename, options) => {
    transpile(filename, options.output);
  });

program.parse(process.argv);

El programa

Volver al principio 🔝

El programa está en src/logging-espree.js:

import * as escodegen from "escodegen";
import * as espree from "espree";
import * as estraverse from "estraverse";
import * as fs from "fs/promises";

/**
 * Read the file with the js program, calls addLogin to add the login messages and writes the output
 * @param {string} input_file - The name of the input file
 * @param {string} output_file - The name of the output file (default: output.js)
 */
export async function transpile(inputFile, outputFile) {
  try {
    if (inputFile) {
      // console.log(`Transpiling '${inputFile}' to '${outputFile}'`);
      /* Al leer un fichero se usa una función asíncrona
      * Todas las funciones asíncronas llevan una callback que es el único
      * lugar donde estamos seguros que el código se va a ejecutar después
      * de haber ejecutado la función
      * Normalmente se pasa como argumento el error y resultado de la 
      * función asíncrona, para el caso de que no hubiera error.
      */
      let input = await fs.readFile(inputFile, 'utf8', (err) => {
        console.log(`Input read from file '${inputFile}'`);
        /// Si hay error se envía un throw
        if (err) throw `Error reading '${inputFile}': ${err}`;
      });
      /// Se llama al addLoggin y se guarda en output
      const output = addLogging(input);
      /// Se muesta la cadena de entrada al programador
      console.error(`input:\n${input}\n---`);
      /// Manera correcta de realizar el write después del read 
      /// (dentro de la callback). Tendencia hacia la diagonalidad
      await fs.writeFile(outputFile, output, err => {
        /// Se comprueba si ha habido o no error y se imprime la salida por pantalla
        if (err) throw `Can't write to '${outputFile}': ${err}`;
        console.log(`Output in file '${outputFile}'`);
      });
    }
    else program.help();  //< En caso de no usar la sintaxis correcta se imprime la ayuda
  }
  catch (e) {
    console.error(`Hubo errores: ${e}`);
  }
}

/**
 * Builds the AST and
 * Traverses it searching for function nodes and callas addBeforeNode to transform the AST
 * @param {string} code - The source code 
 * @returns -- The transformed AST 
 */
export function addLogging(code) {
  const ast = espree.parse(code, {ecmaVersion: espree.latestEcmaVersion, loc: true});  //< Builds the AST
  estraverse.traverse(ast, {                                                           //< Traverses the AST searching for function nodes
    enter: function(node, parent) {
      if (node.type === 'FunctionDeclaration' ||
        node.type === 'FunctionExpression' ||
        node.type === 'ArrowFunctionExpression') {                                     //< If the node is a function node, calls addBeforeNode
        addBeforeCode(node);                                                           //< to transform the AST
      }
    }
  });
  return escodegen.generate(ast);                                                      //< Generates the code from the AST
}

/**
 * AST transformation
 * @param {AST function type node} node - The function node to be transformed
 */
function addBeforeCode(node) {
  const name = node.id ? node.id.name : '<anonymous function>';                                                        //< Gets the name of the function
  const parameters = node.params.map(param => `\$\{ ${param.name} \}`);                                                //< Gets the parameters of the function
  const beforeCode = "console.log('Entering " + name + "(" + parameters + ") at line " + node.loc.start.line + "');";  //< Builds the code to be added
  const beforeNodes = espree.parse(beforeCode, { ecmaVersion: 6 }).body;                                               //< Builds the AST from the code to be added
  node.body.body = beforeNodes.concat(node.body.body);                                                                 //< Adds the code to the AST
}

Indicar los valores de los argumentos

Se ha modificado el código de logging-espree.js para que el log también indique los valores de los argumentos que se pasaron a la función. Ejemplo:

function foo(a, b) {
  var x = 'blah';
  var y = (function (z) {
    return z+3;
  })(2);
}
foo(1, 'wut', 3);
function foo(a, b) {
    console.log(`Entering foo(${ a }, ${ b })`);
    var x = 'blah';
    var y = function (z) {
        console.log(`Entering <anonymous function>(${ z })`);
        return z + 3;
    }(2);
}
foo(1, 'wut', 3);

Para ello, se ha modificado la función addBeforeCode para que añada el código de log de los argumentos de la función.

/**
 * AST transformation
 * @param {AST function type node} node - The function node to be transformed
 */
function addBeforeCode(node) {
  const name = node.id ? node.id.name : '<anonymous function>';                                                        //< Gets the name of the function
  const parameters = node.params.map(param => `\$\{ ${param.name} \}`);                                                //< Gets the parameters of the function
  const beforeCode = "console.log('Entering " + name + "(" + parameters + ") at line " + node.loc.start.line + "');";  //< Builds the code to be added
  const beforeNodes = espree.parse(beforeCode, { ecmaVersion: 6 }).body;                                               //< Builds the AST from the code to be added
  node.body.body = beforeNodes.concat(node.body.body);                                                                 //< Adds the code to the AST
}

CLI con Commander.js

Se hace un parsing de la línea de comandos mediante el módulo npm commander.js. Contenido de bin/log.js:

#!/usr/bin/env node
import { program } from "commander";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { version } = require("../package.json");
import { transpile } from "../src/logging-espree.js";

program
  .version(version)
  .argument("<filename>", 'file with the original code')
  .option("-V, --version", "output the version number")
  .option("-o, --output <filename>", "file in which to write the output", "output.js")
  .option("-h, --help", "output usage information")
  .action((filename, options) => {
    transpile(filename, options.output);
  });

program.parse(process.argv);

Reto 1: Soportar funciones flecha

Para soportar funciones flecha basta con añadir node.type === 'ArrowFunctionExpression' en el condicional if de la función addLogging en el fichero logging-espree.js:

/**
 * Builds the AST and
 * Traverses it searching for function nodes and callas addBeforeNode to transform the AST
 * @param {string} code - The source code 
 * @returns -- The transformed AST 
 */
export function addLogging(code) {
  const ast = espree.parse(code);                                                      //< Builds the AST
  estraverse.traverse(ast, {                                                           //< Traverses the AST searching for function nodes
    enter: function(node, parent) {
      if (node.type === 'FunctionDeclaration' ||
        node.type === 'FunctionExpression' ||
        node.type === 'ArrowFunctionExpression') {                                     //< If the node is a function node, calls addBeforeNode
        addBeforeCode(node);                                                           //< to transform the AST
      }
    }
  });
  return escodegen.generate(ast);                                                      //< Generates the code from the AST
}

Reto 2: Añadir el número de línea

Para añadir el número de línea en el logging hemos añadido node.loc.start.line al console.log() y {ecmaVersion: espree.latestEcmaVersion, loc: true} en el parser de espree en el fichero logging-espree.js:

/**
 * AST transformation
 * @param {AST function type node} node - The function node to be transformed
 */
function addBeforeCode(node) {
  const name = node.id ? node.id.name : '<anonymous function>';                                                        //< Gets the name of the function
  const parameters = node.params.map(param => `\$\{ ${param.name} \}`);                                                //< Gets the parameters of the function
  const beforeCode = "console.log('Entering " + name + "(" + parameters + ") at line " + node.loc.start.line + "');";  //< Builds the code to be added
  const beforeNodes = espree.parse(beforeCode, { ecmaVersion: 6 }).body;                                               //< Builds the AST from the code to be added
  node.body.body = beforeNodes.concat(node.body.body);                                                                 //< Adds the code to the AST
}
/**
 * Builds the AST and
 * Traverses it searching for function nodes and callas addBeforeNode to transform the AST
 * @param {string} code - The source code 
 * @returns -- The transformed AST 
 */
export function addLogging(code) {
  const ast = espree.parse(code, {ecmaVersion: espree.latestEcmaVersion, loc: true});  //< Builds the AST
  estraverse.traverse(ast, {                                                           //< Traverses the AST searching for function nodes
    enter: function(node, parent) {
      if (node.type === 'FunctionDeclaration' ||
        node.type === 'FunctionExpression' ||
        node.type === 'ArrowFunctionExpression') {                                     //< If the node is a function node, calls addBeforeNode
        addBeforeCode(node);                                                           //< to transform the AST
      }
    }
  });
  return escodegen.generate(ast);                                                      //< Generates the code from the AST
}

Publicación como paquete npm

Volver al principio 🔝

Seguimos los pasos de la documentación de npm para publicar nuestro paquete en npmjs.com también disponible en los apuntes de la asignatura.

Creeamos un usuario en npmjs.com:

npm adduser

Iniciamos sesión en npmjs.com:

npm login
npm whoami

Configuramos nuestro paquete:

npm set init.author.name "Gerard Antony Caramazza Vilá"
npm set init.author.email "alu0101229775@ull.edu.es"
npm set init.author.url "https://github.com/alu0101229775"

Cambiamos la visibilidad del repositorio a público: Visibilidad Repositorio

A continuación ejecutamos el siguiente comando para publicar el paquete:

npm publish --access=public

Tests and Covering

Volver al principio 🔝

Para ejecutar los test añadimos las siguientes lineas en nuestro fichero package.json:

"scripts": {
    "test": "mocha test/test.mjs",
    "cov": "c8 npm test",
    "cov-doc": "c8 --reporter=html --reporter=text --report-dir docs/coverage mocha",
    "doc": "documentation build ./src/** -f html -o docs",
    "test1": "bin/log.js test/data/test1.js",
    "test2": "bin/log.js test/data/test2.js",
    "test3": "bin/log.js test/data/test3.js"
}

Modificamos el fichero test.mjs para que se ejecuten los tests:

import { transpile } from "../src/logging-espree.js";
import assert from 'assert';
import * as fs from "fs/promises";
import { dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
import Tst from './test-description.mjs';

const Test = Tst.map(t => ({
  input: __dirname + '/data/' + t.input,
  output: __dirname + '/data/' + t.output,
  correctLogged: __dirname + '/data/' + t.correctLogged,
  correctOut: __dirname + '/data/' + t.correctOut,
})
)

function removeSpaces(s) {
  return s.replace(/\s/g, '');
}

for (let i = 0; i < Test.length; i++) {
  it(`transpile(${Tst[i].input}, ${Tst[i].output})`, async () => {
    /// Compile the input and check the output program is what expected
    await transpile(Test[i].input, Test[i].output);
    let output = await fs.readFile(Test[i].output, 'utf-8')
    let correctLogged = await fs.readFile(Test[i].correctLogged, 'utf-8')
    assert.equal(removeSpaces(output), removeSpaces(correctLogged));
    await fs.unlink(Test[i].output);

    /// Run the output program and check the logged output is what expected
    let correctOut = await fs.readFile(Test[i].correctOut, 'utf-8')
    let oldLog = console.log; // mocking console.log
    let result = "";
    console.log = function (...s) { result += s.join('') }
      eval(output);
      assert.equal(removeSpaces(result), removeSpaces(correctOut))
    console.log = oldLog;
  }); 
}

Añadimos 3 tests más en la carpeta test/data: Test 1:

function foo(a) {
  console.log(a);
  let b = () => {
    console.log('pl');
  }
  b();
}
foo(() => console.log('hi'));

Rúbrica

Volver al principio 🔝

  • Opciones en línea de comandos (-o, -V, -h, etc.)
  • Añade mensajes de logs a la entrada de las function()
  • Añade mensajes de logs a la entrada de las arrow () => { ... }
  • Tutorial README.md y paneles bien presentados
  • Da información correcta de los números de línea
  • El package.json tiene scripts para ejecutar el programa
  • El paquete está publicado en npmjs con ámbito aluXXX
  • Contiene un ejecutable que se ejecuta correctamente (--help, etc.)
  • El módulo exporta las funciones adecuadas
  • Contiene suficientes tests
  • Estudio de covering
  • Se ha hecho CI con GitHub Actions
  • La documentación es completa: API, ejecutable, instalación, etc.
  • Se ha probado que la librería está accesible y funciona
  • Se ha probado que el ejecutable queda correctamente instalado, puede ser ejecutado con el nombre publicado y produce salidas correctas
  • Se ha hecho un buen uso del versionado semántico en la evolución del módulo