AST Rewriting Using
recast and esprima
Boston Node
March 22, 2017
Stephen Vance
Ember.js Routes
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType,
rootURL: config.rootURL
});
Router.map(function() {
});
export default Router;
2
Ember Route Generator
$ ember generate route foo
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType,
rootURL: config.rootURL
});
Router.map(function() {
this.route('foo');
});
export default Router;
3
Bug: Route Removal
$ ember destroy route bar
Router.map(function() {
this.route('bar');
this.route('foo', function() {
this.route('bar');
});
});
export default Router;
Fixed in Fix like-named route removal bug
4
The Tools
Esprima
ECMAScript parsing
infrastructure for multipurpose
analysis
Site: http://esprima.org/
by Ariya Hidayat
recast
JavaScript syntax tree
transformer, nondestructive
pretty-printer, and automatic
source map generator
by Ben Newman
5
Visualizing ASTs
6
Rendered using http://resources.jointjs.com/demos/javascript-ast
You can also use AST Explorer
AST Types
def("Property")
.bases("Node")
.build("kind", "key", "value")
.field(“kind",
or("init", "get", "set"))
.field(“key",
or(def(“Literal”),
def("Identifier")))
.field(“value”,
def("Expression"));
7
def("Literal")
.bases("Node", "Expression")
.build("value")
.field(“value",
or(String, Boolean, null,
Number, RegExp))
.field("regex", or({
pattern: String,
flags: String
}, null), function () {
if (this.value instanceof RegExp) {
var flags = "";
if (this.value.ignoreCase)
flags += "i";
if (this.value.multiline)
flags += "m";
if (this.value.global)
flags += "g";
return {
pattern: this.value.source,
flags: flags
};
}
return null;
});
Using recast
const recast = require('recast');
const fs = require('fs');
const source = fs.readFileSync('some-code.js', 'utf-8');
let ast = recast.parse(source);
// Manipulate ast
let output = recast.print(ast).code;
8
A Real-Life Use
// ember-cli-build.js
module.exports = function(defaults) {

var app = new EmberApp(defaults, {

// Add options here

});



return app.toTree();

};
9
Examples taken from ember-cli-build-config-editor
Finding a Marker
recast.visit(ast, {

visitNewExpression: function (path) {

var node = path.node;



if (node.callee.name === 'EmberApp') {

editor.configNode = node.arguments.find(isObjectExpression);



return false;

} else {

this.traverse(path);

}

}

})
10
Adding Syntax
function findOrAddConfigKey(key) {
var configKey = this.configNode.properties.find(isKey(key));
if (!configKey) {
configKey = builders.property(
'init',
builders.literal(key),
builders.objectExpression([])
);
this.configNode.properties.push(configKey);
}
return configKey;
}
11
What Type Is It?
function isKey(key) {

return function (property) {

return (property.key.type === 'Literal'
&& property.key.value === key)

|| (property.key.type === 'Identifier'
&& property.key.name === key);

};

}
12
Now we have …
module.exports = function(defaults) {

var app = new EmberApp(defaults, {

'some-addon': {

}

});



return app.toTree();

};
13
Editing Simple Values
function addOrUpdateConfigProperty(configObject, property, config) {

var existingProperty = configObject.properties.find(isKey(property));



if (existingProperty) {

existingProperty.value.value = config[property];

} else {

var newProperty = builders.property(

'init',

builders.literal(property),

builders.literal(config[property])

);

configObject.properties.push(newProperty);

}

}
14
The Result
module.exports = function(defaults) {
var app = new EmberApp(defaults, {
'some-addon': {
'booleanProperty': false,
'numericProperty': 17,
'stringProperty': 'wow'
}
});
return app.toTree();
};
15
Adding a Function Call
var node = builders.expressionStatement(
builders.callExpression(
builders.memberExpression(
builders.thisExpression(),
builders.identifier(options.identifier || 'route'),
false
),
[builders.literal(name)]
)
);
16
Example from ember-router-generator
Go forth and rewrite ASTs!
17
Contact Me
Stephen Vance
http://www.vance.com
steve@vance.com
@StephenRVance
srvance on GitHub and LinkedIn
18

AST Rewriting Using recast and esprima

  • 1.
    AST Rewriting Using recastand esprima Boston Node March 22, 2017 Stephen Vance
  • 2.
    Ember.js Routes import Emberfrom 'ember'; import config from './config/environment'; const Router = Ember.Router.extend({ location: config.locationType, rootURL: config.rootURL }); Router.map(function() { }); export default Router; 2
  • 3.
    Ember Route Generator $ember generate route foo import Ember from 'ember'; import config from './config/environment'; const Router = Ember.Router.extend({ location: config.locationType, rootURL: config.rootURL }); Router.map(function() { this.route('foo'); }); export default Router; 3
  • 4.
    Bug: Route Removal $ember destroy route bar Router.map(function() { this.route('bar'); this.route('foo', function() { this.route('bar'); }); }); export default Router; Fixed in Fix like-named route removal bug 4
  • 5.
    The Tools Esprima ECMAScript parsing infrastructurefor multipurpose analysis Site: http://esprima.org/ by Ariya Hidayat recast JavaScript syntax tree transformer, nondestructive pretty-printer, and automatic source map generator by Ben Newman 5
  • 6.
    Visualizing ASTs 6 Rendered usinghttp://resources.jointjs.com/demos/javascript-ast You can also use AST Explorer
  • 7.
    AST Types def("Property") .bases("Node") .build("kind", "key","value") .field(“kind", or("init", "get", "set")) .field(“key", or(def(“Literal”), def("Identifier"))) .field(“value”, def("Expression")); 7 def("Literal") .bases("Node", "Expression") .build("value") .field(“value", or(String, Boolean, null, Number, RegExp)) .field("regex", or({ pattern: String, flags: String }, null), function () { if (this.value instanceof RegExp) { var flags = ""; if (this.value.ignoreCase) flags += "i"; if (this.value.multiline) flags += "m"; if (this.value.global) flags += "g"; return { pattern: this.value.source, flags: flags }; } return null; });
  • 8.
    Using recast const recast= require('recast'); const fs = require('fs'); const source = fs.readFileSync('some-code.js', 'utf-8'); let ast = recast.parse(source); // Manipulate ast let output = recast.print(ast).code; 8
  • 9.
    A Real-Life Use //ember-cli-build.js module.exports = function(defaults) {
 var app = new EmberApp(defaults, {
 // Add options here
 });
 
 return app.toTree();
 }; 9 Examples taken from ember-cli-build-config-editor
  • 10.
    Finding a Marker recast.visit(ast,{
 visitNewExpression: function (path) {
 var node = path.node;
 
 if (node.callee.name === 'EmberApp') {
 editor.configNode = node.arguments.find(isObjectExpression);
 
 return false;
 } else {
 this.traverse(path);
 }
 }
 }) 10
  • 11.
    Adding Syntax function findOrAddConfigKey(key){ var configKey = this.configNode.properties.find(isKey(key)); if (!configKey) { configKey = builders.property( 'init', builders.literal(key), builders.objectExpression([]) ); this.configNode.properties.push(configKey); } return configKey; } 11
  • 12.
    What Type IsIt? function isKey(key) {
 return function (property) {
 return (property.key.type === 'Literal' && property.key.value === key)
 || (property.key.type === 'Identifier' && property.key.name === key);
 };
 } 12
  • 13.
    Now we have… module.exports = function(defaults) {
 var app = new EmberApp(defaults, {
 'some-addon': {
 }
 });
 
 return app.toTree();
 }; 13
  • 14.
    Editing Simple Values functionaddOrUpdateConfigProperty(configObject, property, config) {
 var existingProperty = configObject.properties.find(isKey(property));
 
 if (existingProperty) {
 existingProperty.value.value = config[property];
 } else {
 var newProperty = builders.property(
 'init',
 builders.literal(property),
 builders.literal(config[property])
 );
 configObject.properties.push(newProperty);
 }
 } 14
  • 15.
    The Result module.exports =function(defaults) { var app = new EmberApp(defaults, { 'some-addon': { 'booleanProperty': false, 'numericProperty': 17, 'stringProperty': 'wow' } }); return app.toTree(); }; 15
  • 16.
    Adding a FunctionCall var node = builders.expressionStatement( builders.callExpression( builders.memberExpression( builders.thisExpression(), builders.identifier(options.identifier || 'route'), false ), [builders.literal(name)] ) ); 16 Example from ember-router-generator
  • 17.
    Go forth andrewrite ASTs! 17
  • 18.