Toolmaking
With
ASTs

Will Klein

Tools

 

Simple Example

function addTen(value) {
  var sum = 0;

  return value + 10
}

if (someCondition) {
  if (someOtherCondition) {
    added = addTen(someValue);
  }
}

This Code...

function addTen(value) {
  return value + 10;
}

if (someCondition &&
    someOtherCondition) {
  added = addTen(someValue);
}

should really be

Talk is Expensive

Talk is Expensive

Deltas are Expensive

Talk is Expensive

Deltas are Expensive

Inaction IS Expensive

Scale Best Practices

Tools

 

Linting

 

ESLINT

  1. Builds an AST of your code
  2. Traverses the tree
  3. For each node, run rules
  4. Rules are built-in, or plugged-in

ESLint: Under the Hood

function addTen(value) {
  var sum = 0;

  return value + 10
}

no-unused-vars

semi

ESLint Rules

if (someCondition) {
  if (someOtherCondition) {
    added = addTen(someValue);
  }
}

???

ESLint Rules

Toolmaking

 

Toolmaking
With
ASTs

Abstract

Syntax
Tree

Wizardry (to non-coders)

Wizardry (to Coders?)

IfStatement

if (someCondition) {}
if (someCondition) {}

IfStatement

{
  "type": "Program",
  "body": [
    {
      "type": "IfStatement",
      "test": {
        "type": "Identifier",
        "name": "someCondition"
      },
      "consequent": {
        "type": "BlockStatement",
        "body": []
      },
      "alternate": null
    }
  ]
}

Nested IfStatement

if (someCondition) {
  if (someOtherCondition) {
    added = addTen(someValue);
  }
}

Nested IfStatement

Nested IfStatement

{
  "type": "IfStatement",
    "test": {
      "type": "Identifier",
      "name": "someCondition"
    },
    "consequent": {
      "type": "BlockStatement",
      "body": [
        {
          "type": "IfStatement",
          "test": {
            "type": "Identifier",
            "name": "someOtherCondition"
          },
          "consequent": {
            "type": "BlockStatement",
            "body": [ ... ] } } ] } }

ESLint: Rule boilerplate

module.exports = function(context) {
  return {






  };
};

ESLint: Rule boilerplate

module.exports = function(context) {
  return {
    "IfStatement": function(node) {




    }
  };
};

ESLint: Rule boilerplate

module.exports = function(context) {
  return {
    "IfStatement": function(node) {
      if (/* test some things */) {
        context.report(node,
            "Unexpected bad code.");
      }
    }
  };
};

Nested IfStatement

ESLint: context

module.exports = function(context) {
  return {
    "IfStatement": function(node) {
      var ancestors = context.getAncestors(),
          parent = ancestors.pop(),
          grandparent = ancestors.pop();

      if (/* test some things */) {
        context.report(node,
            "Unexpected bad code.");
      }
    }
  };
};

Directly nested CASE

if (someCondition)
  if (someOtherCondition) {}

directly nested TEST

if (someCondition)
  if (someOtherCondition) {}
var ancestors = context.getAncestors(),
    parent = ancestors.pop(),
    grandparent = ancestors.pop();

if (parent.type === "IfStatement") {
  context.report(node, "Unexpected bad code.");
}

block Nested CASE

if (someCondition) {
  if (someOtherCondition) {}
}

block Nested Test

var ancestors = context.getAncestors(),
    parent = ancestors.pop(),
    grandparent = ancestors.pop();

if (parent.type === "IfStatement" ||
    (parent.type === "BlockStatement" &&
     grandparent.type === "IfStatement")) {
  context.report(node, "Unexpected bad code.");
}
if (someCondition) {
  if (someOtherCondition) {}
}

Siblings test

var ancestors = context.getAncestors(),
    parent = ancestors.pop(),
    grandparent = ancestors.pop();

if (parent.type === "IfStatement" ||
    (parent.type === "BlockStatement" &&
     parent.body.length === 1 &&
     grandparent.type === "IfStatement")) {
  context.report(node, "Unexpected bad code.");
}
if (someCondition) {
  doSomething();
  if (someOtherCondition) {}
}

consequent test

var ancestors = context.getAncestors(),
    parent = ancestors.pop(),
    grandparent = ancestors.pop();

if (parent.type === "IfStatement" ||
    (parent.type === "BlockStatement" &&
     parent.body.length === 1 &&
     grandparent.type === "IfStatement" &&
     parent === grandparent.consequent)) {
  context.report(node, "Unexpected bad code.");
}
if (someCondition) {
  doSomething();
} else {
  if (someOtherCondition) {}
}

no-nested-if

module.exports = function(context) {
  return {
    "IfStatement": function(node) {
      var ancestors = context.getAncestors(),
          parent = ancestors.pop(),
          grandparent = ancestors.pop();
        
      if (parent.type === "IfStatement" ||
          (parent.type === "BlockStatement" &&
           parent.body.length === 1 &&
           grandparent.type === "IfStatement" &&
           parent === grandparent.consequent)) {
        context.report(node, "Unexpected bad code.");
      }
    }
  };
};

Docs & Code

Will Klein

http://willkle.in

@willslab

Thanks

[Archive] Toolmaking with ASTs - Lightning Edition

By Will Klein

[Archive] Toolmaking with ASTs - Lightning Edition

Prepared for DenverScript's July 2015 meetup

  • 1,895