stacklizard

0.2.4 • Public • Published

stacklizard

A static-analysis tool to simulate marking one JavaScript function async, and determining what functions and function calls must change.

A simplified scenario

Suppose you have the following code:

// Goal:  Determine where we have to make changes if A.prototype.e becomes asynchronous.

function A() {
  this.x = this.d();
}
A.prototype = {
  a: function() {
    console.log(this.b());
  },

  b: function() {
    void(this.c);
  },

  get c() {
    return this.d() + 1;
  },

  d: function() {
    // just a comment
    let f = this.f();
    let e = this.e(f);
    return e + 1;
  },

  e: function(y) {
    return y + this.g() + 1;
  },

  f: function() {
    return 0;
  },

  g: function() {
    return 1;
  }
};

const B = new A();

In this scenario, B.d() === 3. Everything's fine. But for some reason, you need to mark A.prototype.e asynchronous:

  e: async function(y) {
    return y + this.g() + 1;
  },

Evaluating B.d() results in B.d() = [object Promise]1. That's not desirable. To fix this, we'd have to make the caller await the promise.

  d: function() {
    // just a comment
    let f = this.f();
    let e = await this.e(f);
    return e + 1;
  },

Except that causes SyntaxError: await is only valid in async functions and async generators. So then we mark d as async:

  d: async function() {
    // just a comment
    let f = this.f();
    let e = await this.e(f);
    return e + 1;
  },

B.d() = [object Promise]

This we can await no problem... except that the constructor A() references this.d(). We broke that as well, so we try to fix it:

function A() {
  this.x = await this.d();
}

SyntaxError: await is only valid in async functions and async generators

Okay, mark the constructor async:

async function A() {
  this.x = await this.d();
}
// ...
const B = new A();

TypeError: A is not a constructor

At this point you might throw your hands up in frustration (and rightly so). But if you have to make that original function e() async, it might be helpful to know all the places you need to make changes. StackLizard is for this purpose.

./stacklizard.js standalone docs/use-case/a/a.js 26
- e(), async a.js:26 FunctionExpression[0]
  - d(), await a.js:22 CallExpression[0], async a.js:19 FunctionExpression[0]
    - c(), await a.js:16 CallExpression[0], async a.js:15 FunctionExpression[0], accessor
      - b(), await a.js:12 MemberExpression[0], async a.js:11 FunctionExpression[0]
        - a(), await a.js:8 CallExpression[1], async a.js:7 FunctionExpression[0]
    - A(), await a.js:4 Identifier[1], async a.js:3 FunctionDeclaration[0], constructor
      - A(), await a.js:39 NewExpression[0]
- **SyntaxError**: async a.js:15 FunctionExpression[0], accessor
- **SyntaxError**: async a.js:3 FunctionDeclaration[0], constructor

Notably, StackLizard doesn't fix these problems for you, but it does point them out.

Installation

StackLizard should be treated as a NPM module, and installed as such:

npm install stacklizard

Command-line Usage

From the command-line, you have several subcommands. Generally speaking, I recommend the following:

  1. Using standalone or html subcommands to generate an initial configuration file
  2. Altering the configuration file as necessary
  3. Using the configuration subcommand with the generated configuration file to create revised results.
  4. Repeat as you desire.

standalone

This reads a single JavaScript file, marks one function async as you requested (by line number and optionally a "function index", the index of the function among the list of functions on that line), then generates a stack trace.

Optional arguments:

  • --fnIndex=0 to specify the 0th function on the line to mark async
  • --save-config path/to/json where you can specify a location to write a JSON configuration file for reuse.

configuration

This takes a configuration file you've generated via --save-config with some optional hand-editing, and re-runs the job based on that configuration.

Documentation for the configuration file format is at sample-config.json.yaml in this repository.

Optional arguments:

  • --ignore "pathToFile:line type[index]" to mark a node ignored. Cut & paste the string from an earlier serialization.
  • --save-config path/to/json where you can specify a location to write a JSON configuration file for reuse.

html

This takes a few arguments:

  • A root directory for a HTML project
  • A path to the HTML file where scripts run
  • A path to the HTML or JavaScript file containing the function to mark async
  • The line number of the function
  • --fnIndex=0 to specify the 0th function on the line to mark async
  • --save-config path/to/json where you can specify a location to write a JSON configuration file for reuse.

Usage within Node

Standalone mode

const StackLizard = require("stacklizard");

(async function() {
  const parseDriver = StackLizard.buildDriver("javascript", rootDir, options = {});

  // option 1: load from the file system
  await parseDriver.appendJSFile("path/to/JSFile/from/rootDir"); // always a relative path

  // option 2: load from in-memory string, no file i/o
  parseDriver.appendSource(pathToFile, firstLineInFile, source); 

  // Generate the Abstract Syntax Tree via espree and gather information via estraverse.
  parseDriver.parseSources();

  // Get a function AST node.
  const startAsync = parseDriver.functionNodeFromLine(
    "path/to/JSFile/from/rootDir", lineNumber, functionIndex
  );
  
  // Mark nodes async and await as needed from the function AST node, marked async.  Returns a Map().
  const asyncRefs = parseDriver.getAsyncStacks(startAsync);

  // Build a serializer.
  const serializer = StackLizard.getSerializer(
    "markdown", startAsync, asyncRefs, parseDriver, {nested: true}
  );

  // Serialize the results in a human-readable form.
  console.log(serializer.serialize());
  
  // Get a configuration to save to a file.
  const configuration = {
    driver: parseDriver.getConfiguration(startAsync),
    serializer: serializer.getConfiguration()
  };
})();

HTML mode

(async function() {
  const parseDriver = StackLizard.buildDriver("html", rootDirectory, options = {});

  // load from the file system, and get all the JavaScript code inline and from external files
  await parseDriver.appendSourcesViaHTML(pathToHTML);

  // Generate the Abstract Syntax Tree via espree and gather information via estraverse.
  parseDriver.parseSources();

  // Get a function AST node.
  const startAsync = parseDriver.functionNodeFromLine(
    args.pathToJS, args.line, args.fnIndex
  );

  // Mark nodes async and await as needed from the function AST node, marked async.  Returns a Map().
  const asyncRefs = parseDriver.getAsyncStacks(startAsync);

  // Build a serializer.
  const serializer = StackLizard.getSerializer(
    "markdown", startAsync, asyncRefs, parseDriver, {nested: true}
  );

  // Serialize the results in a human-readable form.
  console.log(serializer.serialize());

  // Get a configuration to save to a file.
  const configuration = {
    driver: parseDriver.getConfiguration(startAsync),
    serializer: serializer.getConfiguration()
  };
})();

Configuration mode

// config is a JSON object, parsed from a configuration file saved in a previous session.
async function doTheAnalysis(config) {
  // Build the parse driver.
  const parseDriver = StackLizard.buildDriver(
    config.driver.type,
    path.resolve(process.cwd(), config.driver.root), // probably something like this
    config.driver.options || {}
  );

  // Analyze everything at once.
  const {startAsync, asyncRefs} = await parseDriver.analyzeByConfiguration(config.driver);

  // Build the serializer.
  const serializer = StackLizard.getSerializer(
    config.serializer.type,
    startAsync,
    asyncRefs,
    parseDriver,
    config.serializer.options || {}
  );

  // Serialize the results in a human-readable form.
  console.log(serializer.serialize());
}

A few notes

  • StackLizard picks up await nodes by their local name ("b", not "A.prototype.b"), and marks them most aggressively, sometimes too much so. You can override this and tell StackLizard to ignore a node via the ignore parameter in a configuration file (recommended) or with code like this:
  const ignorable = this.nodeByLineFilterIndex(
    ignore.path,
    ignore.line,
    ignore.index,
    n => n.type === ignore.type
  );
  this.markIgnored(ignorable);

Readme

Keywords

none

Package Sidebar

Install

npm i stacklizard

Weekly Downloads

0

Version

0.2.4

License

MPL-2.0

Unpacked Size

80.3 kB

Total Files

12

Last publish

Collaborators

  • ajvincent