├── .gitignore ├── test ├── fixture-manipulation-04.js ├── fixture-manipulation-01.js ├── fixture-remove.js ├── fixture-arrays.js ├── fixture-manipulation-03.js ├── fixture-manipulation-02.js ├── fixture-manipulation-05.js ├── fixture-test.js ├── fixture-comments-01.js ├── test.js ├── fixture-rename.js ├── test-detach.js ├── test-attach.js ├── test-array.js ├── test-comments.js └── test-manipulation.js ├── .travis.yml ├── intro.markdown ├── index.js ├── LICENSE ├── package.json ├── lib ├── io.js ├── array.js ├── debug.js ├── tokens.js ├── query.js ├── transformations.js ├── walk.js └── manipulations.js └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /test/fixture-manipulation-04.js: -------------------------------------------------------------------------------- 1 | var WRAP_ME; -------------------------------------------------------------------------------- /test/fixture-manipulation-01.js: -------------------------------------------------------------------------------- 1 | var A; 2 | 3 | A = 0; -------------------------------------------------------------------------------- /test/fixture-remove.js: -------------------------------------------------------------------------------- 1 | var a; 2 | 3 | var b; 4 | var c; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.11" -------------------------------------------------------------------------------- /test/fixture-arrays.js: -------------------------------------------------------------------------------- 1 | var x = [ 2 | "1st-lit", 3 | "2nd-lit", 4 | "3rd-lit" 5 | ]; -------------------------------------------------------------------------------- /test/fixture-manipulation-03.js: -------------------------------------------------------------------------------- 1 | function A() { 2 | var A; 3 | } 4 | 5 | A(); 6 | 7 | var A = 0; -------------------------------------------------------------------------------- /test/fixture-manipulation-02.js: -------------------------------------------------------------------------------- 1 | var T = { 2 | A: 0 3 | }; 4 | 5 | T.A = { 6 | A: 0 7 | }; 8 | 9 | T.A.A = 1; -------------------------------------------------------------------------------- /test/fixture-manipulation-05.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function test() { 4 | // this is a comment 5 | }()) 6 | -------------------------------------------------------------------------------- /test/fixture-test.js: -------------------------------------------------------------------------------- 1 | // this fixture is just to tests (no ci) 2 | 3 | 4 | db.go(function last_injected_edit() { 5 | 6 | }); 7 | 8 | db.go(function () { 9 | 10 | }); -------------------------------------------------------------------------------- /test/fixture-comments-01.js: -------------------------------------------------------------------------------- 1 | function name2() { 2 | //0 3 | var a; 4 | //2 5 | var b; 6 | //4 7 | //5 8 | } 9 | 10 | function name() { 11 | //name 12 | } 13 | 14 | function name3() /* invalid-function-comment */ { 15 | return a * /* invalid-expression-comment */ b; 16 | } -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var tap = require("tap"), 2 | test = tap.test, 3 | utils = require("../index.js"), 4 | esprima = require('esprima'), 5 | parse_ast = esprima.parse, 6 | debug_tree = require("../lib/debug.js").debug_tree; 7 | 8 | //setup 9 | test("Generate file", function(t) { 10 | //var tree = utils.parseFile(__dirname + "/fixture-test.js"); 11 | //var tree = utils.parseFile(__dirname + "/../lib/walk.js"); 12 | var tree = utils.parseFile("/home/luis/noboxout/generator-garu/model/templates/server.model.js"); 13 | 14 | debug_tree(tree, 60, true); 15 | 16 | t.end(); 17 | }); -------------------------------------------------------------------------------- /intro.markdown: -------------------------------------------------------------------------------- 1 | # esprima-ast-utils [![Build Status](https://secure.travis-ci.org/llafuente/esprima-ast-utils.png?branch=master)](http://travis-ci.org/llafuente/esprima-ast-utils) 2 | 3 | Node module to manipulate, transform, query and debug [esprima](https://github.com/ariya/esprima) ASTs. 4 | 5 | ## Objective 6 | 7 | When you edit esprima AST and go back to code with escodegen you lose too much information because primary you don't keep track of ranges, tokens, comments etc. 8 | esprima-ast-utils do this for you, so no escodegen is needed, you can edit the AST directly and code everything is in sync. 9 | 10 | ## API -------------------------------------------------------------------------------- /test/fixture-rename.js: -------------------------------------------------------------------------------- 1 | var XX; 2 | 3 | function A(arg_a) { 4 | arg_a = arg_a + 1; 5 | 6 | return arg_a * arg_a; 7 | } 8 | 9 | (function() { 10 | var A = 0; 11 | 12 | A = 1; 13 | 14 | return A; 15 | }()); 16 | 17 | (function() { 18 | var B, 19 | C, 20 | A = 0; 21 | 22 | A = 10; 23 | 24 | return A; 25 | }()); 26 | 27 | 28 | var x = {}; 29 | 30 | x.A = 0; 31 | 32 | A(); 33 | 34 | (function() { 35 | return A(); 36 | }()); 37 | 38 | module.exports = { 39 | A: A, // this will be replaced by the function 40 | XX: XX // this will be replaced by the var_name 41 | 42 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // removeBetweenComments 4 | // insertBetweenComments 5 | 6 | module.exports = {}; 7 | 8 | [ 9 | require("./lib/walk.js"), 10 | require("./lib/tokens.js"), 11 | require("./lib/io.js"), 12 | require("./lib/query.js"), 13 | require("./lib/manipulations.js"), 14 | require("./lib/transformations.js"), 15 | require("./lib/array.js"), 16 | require("./lib/debug.js") 17 | ].forEach(function(sub_module) { 18 | Object.keys(sub_module).forEach(function(k) { 19 | module.exports[k] = sub_module[k]; 20 | }); 21 | }); 22 | 23 | 24 | /* 25 | 26 | function insertBefore(node, property, index, new_node) { 27 | if (index === null) { 28 | throw new Error("is this legal ?"); 29 | } else { 30 | node[property].splice(index, 0, new_node); 31 | } 32 | } 33 | 34 | function insertAfter(node, property, index, new_node) { 35 | if (index === null) { 36 | throw new Error("is this legal ?"); 37 | } else { 38 | if (node[property].length > index) { 39 | node[property].splice(index + 1, 0, new_node); 40 | } else { 41 | node[property].push(new_node); 42 | } 43 | } 44 | } 45 | */ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # LICENSE 2 | 3 | (The MIT License) 4 | 5 | Copyright (c) 2014 Luis Lafuente 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | 'Software'), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/test-detach.js: -------------------------------------------------------------------------------- 1 | var tap = require("tap"), 2 | test = tap.test, 3 | utils = require("../index.js"); 4 | 5 | 6 | test("utils.remove 1", function(t) { 7 | console.log("***************************************"); 8 | 9 | var tree = utils.parseFile(__dirname + "/fixture-remove.js"); 10 | 11 | utils.detach(tree.body[0]); 12 | //console.log(require("util").inspect(tree, {depth: null, colors: true})); 13 | 14 | 15 | t.equal(tree.$code, '\n\nvar b;\nvar c;'); 16 | t.equal(tree.tokens.length, 6); 17 | 18 | t.end(); 19 | }); 20 | 21 | test("utils.remove 2", function(t) { 22 | console.log("***************************************"); 23 | 24 | var tree = utils.parseFile(__dirname + "/fixture-remove.js"); 25 | 26 | utils.detach(tree.body[1]); 27 | //console.log(require("util").inspect(tree, {depth: null, colors: true})); 28 | 29 | 30 | t.equal(tree.$code, 'var a;\n\n\nvar c;'); 31 | t.equal(tree.tokens.length, 6); 32 | 33 | t.end(); 34 | }); 35 | 36 | test("utils.remove 3", function(t) { 37 | console.log("***************************************"); 38 | 39 | var tree = utils.parseFile(__dirname + "/fixture-remove.js"); 40 | 41 | utils.detach(tree.body[2]); 42 | 43 | t.equal(tree.$code, 'var a;\n\nvar b;\n'); 44 | t.equal(tree.tokens.length, 6); 45 | 46 | t.end(); 47 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esprima-ast-utils", 3 | "version": "0.0.7", 4 | "description": "collection for AST query/manipulation/transformation", 5 | "main": "index.js", 6 | "directories": { 7 | "lib": ".", 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "test": "tap test/test-*.js", 12 | "docr": "docr ./lib/io.js ./lib/walk.js ./lib/debug.js ./lib/query.js ./lib/manipulations.js ./lib/transformations.js ./lib/tokens.js ./lib/arrays.js --prepend intro.markdown --append LICENSE > Readme.md" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/llafuente/esprima-ast-utils.git" 17 | }, 18 | "keywords": [ 19 | "arguments", 20 | "falafel", 21 | "browserify", 22 | "jsdoc" 23 | ], 24 | "author": { 25 | "name": "Luis Lafuente", 26 | "email": "llafuente@noboxout.com", 27 | "url": "http://www.noboxout.com" 28 | }, 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/llafuente/esprima-ast-utils/issues" 32 | }, 33 | "homepage": "https://github.com/llafuente/esprima-ast-utils", 34 | "dependencies": { 35 | "esprima": "^3.1.3", 36 | "cssauron-falafel": "^1.2.1", 37 | "archy": "^1.0.0" 38 | }, 39 | "devDependencies": { 40 | "tap": "^0.4.11", 41 | "object-enhancements": "^0.1.1", 42 | "docr": "0.0.6", 43 | "doctrine": "^0.5.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/test-attach.js: -------------------------------------------------------------------------------- 1 | var tap = require("tap"), 2 | test = tap.test, 3 | utils = require("../index.js"); 4 | 5 | test("utils.attach 1", function(t) { 6 | console.log("***************************************"); 7 | 8 | var tree = utils.parseFile(__dirname + "/fixture-remove.js"); 9 | 10 | utils.attach(tree, "body", 0, "var x;\n\n"); 11 | 12 | t.equal(tree.$code, 'var x;\n\nvar a;\n\nvar b;\nvar c;'); 13 | t.equal(tree.tokens.length, 12); 14 | 15 | t.end(); 16 | }); 17 | 18 | test("utils.attach 2", function(t) { 19 | console.log("***************************************"); 20 | 21 | var tree = utils.parseFile(__dirname + "/fixture-remove.js"); 22 | 23 | //console.log(require("util").inspect(tree.body, {depth: null, colors: true})); 24 | utils.attach(tree, "body", 1, "var x;\n\n"); 25 | //console.log(require("util").inspect(tree, {depth: null, colors: true})); 26 | 27 | 28 | t.deepEqual(tree.range, [0, 29]); 29 | t.equal(tree.$code, 'var a;\n\nvar x;\n\nvar b;\nvar c;'); 30 | t.equal(tree.tokens.length, 12); 31 | 32 | t.end(); 33 | }); 34 | 35 | 36 | test("utils.attach 2", function(t) { 37 | console.log("***************************************"); 38 | 39 | var tree = utils.parseFile(__dirname + "/fixture-remove.js"); 40 | 41 | utils.attach(tree, "body", 1, "var x;\n\n"); 42 | utils.attachPunctuator(tree, ";", tree.range[1]); 43 | //console.log(require("util").inspect(tree, {depth: null, colors: true})); 44 | 45 | 46 | t.deepEqual(tree.range, [0, 30]); 47 | t.equal(tree.$code, 'var a;\n\nvar x;\n\nvar b;\nvar c;;'); 48 | t.equal(tree.tokens.length, 13); 49 | 50 | t.end(); 51 | }); 52 | -------------------------------------------------------------------------------- /test/test-array.js: -------------------------------------------------------------------------------- 1 | var tap = require("tap"), 2 | test = tap.test, 3 | utils = require("../index.js"); 4 | 5 | test("array initial file test", function(t) { 6 | console.log("***************************************"); 7 | 8 | var tree = utils.parseFile(__dirname + "/fixture-arrays.js"); 9 | 10 | console.log(require("util").inspect(tree, {depth: null, colors: true})); 11 | 12 | t.equal(tree.$code, 'var x = [\n \"1st-lit\",\n \"2nd-lit\",\n \"3rd-lit\"\n];'); 13 | t.equal(tree.tokens.length, 11); 14 | 15 | t.end(); 16 | }); 17 | 18 | 19 | 20 | test("utils.arrayPushLiteral no repeat", function(t) { 21 | console.log("***************************************"); 22 | 23 | var tree = utils.parseFile(__dirname + "/fixture-arrays.js"), 24 | ar_exp = tree.body[0].declarations[0].init; 25 | 26 | utils.arrayPushLiteral(ar_exp, "1st-lit", true); 27 | utils.arrayPushLiteral(ar_exp, "2nd-lit", true); 28 | utils.arrayPushLiteral(ar_exp, "3rd-lit", true); 29 | 30 | t.equal(tree.$code, 'var x = [\n \"1st-lit\",\n \"2nd-lit\",\n \"3rd-lit\"\n];'); 31 | t.equal(tree.tokens.length, 11); 32 | 33 | t.end(); 34 | }); 35 | 36 | test("utils.arrayPushLiteral no repeat", function(t) { 37 | console.log("***************************************"); 38 | 39 | var tree = utils.parseFile(__dirname + "/fixture-arrays.js"), 40 | ar_exp = tree.body[0].declarations[0].init; 41 | 42 | utils.arrayPushLiteral(ar_exp, "4st-lit", true); 43 | 44 | t.equal(tree.$code, 'var x = [\n \"1st-lit\",\n \"2nd-lit\",\n \"3rd-lit\",\"4st-lit\"\n];'); 45 | t.equal(tree.tokens.length, 13); 46 | 47 | t.end(); 48 | }); 49 | 50 | 51 | test("utils.arrayPush no repeat", function(t) { 52 | console.log("***************************************"); 53 | 54 | var tree = utils.parseFile(__dirname + "/fixture-arrays.js"), 55 | ar_exp = tree.body[0].declarations[0].init; 56 | 57 | utils.arrayPush(ar_exp, "function xxx(){}"); 58 | 59 | t.equal(tree.$code, 'var x = [\n \"1st-lit\",\n \"2nd-lit\",\n \"3rd-lit\",function xxx(){}\n];'); 60 | t.equal(tree.tokens.length, 18); 61 | 62 | t.end(); 63 | }); 64 | 65 | 66 | test("utils.arrayUnshift no repeat", function(t) { 67 | console.log("***************************************"); 68 | 69 | var tree = utils.parseFile(__dirname + "/fixture-arrays.js"), 70 | ar_exp = tree.body[0].declarations[0].init; 71 | 72 | utils.arrayUnshift(ar_exp, "function xxx(){}"); 73 | 74 | t.equal(tree.$code, 'var x = [\n function xxx(){},\"1st-lit\",\n \"2nd-lit\",\n \"3rd-lit\"\n];'); 75 | t.equal(tree.tokens.length, 18); 76 | 77 | t.end(); 78 | }); 79 | -------------------------------------------------------------------------------- /lib/io.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | parse: parse, 5 | parseWrap: parseWrap, 6 | parseFile: parseFile, 7 | encode: encode 8 | }; 9 | 10 | var walk = require('./walk.js'), 11 | idze = walk.idze, 12 | attachComments = walk.attachComments, 13 | parentize = walk.parentize, 14 | toProgram = require('./transformations.js').toProgram, 15 | __debug = false, 16 | esprima_parse = require('esprima').parse; 17 | 18 | /** 19 | * Parse given str 20 | * @note location it's not supported, and won't sync with changes, range/rokens do. 21 | * 22 | * @param {String} str 23 | * @param {Boolean} [debug] display $id, $parent and $code in console.log (enumerable=true) 24 | * @return {Object} 25 | */ 26 | function parse(str, debug) { 27 | try { 28 | var tree = esprima_parse(str, { 29 | comment: true, 30 | range: true, 31 | //loc: true, // this make no sense right now, escodegen will have problems but not my problem :D 32 | tokens: true, 33 | raw: false, 34 | //attachComment: true this is shitty 35 | }); 36 | } catch(e) { 37 | console.error(str); 38 | throw e; 39 | } 40 | 41 | // this fix first comments :) 42 | tree.range[0] = 0; 43 | // this fix last comments :) 44 | //tree.range[1] = tree.tokens[tree.tokens.length - 1].range[1]; 45 | tree.range[1] = str.length; 46 | 47 | attachComments(tree, debug); 48 | parentize(tree, debug); 49 | idze(tree, debug); 50 | 51 | if (debug || __debug) { 52 | tree.$code = str; 53 | } else { 54 | Object.defineProperty(tree, "$code", {enumerable: false, value: str, writable: true}); 55 | } 56 | 57 | return tree; 58 | } 59 | 60 | /** 61 | * Wrap your code into a function and parse given str. 62 | * Needed if your code contains a `ReturnStatement` at Program level. 63 | * 64 | * @note location it's not supported, and won't sync with changes, range/rokens do. 65 | * 66 | * @param {String} str 67 | * @param {Boolean} [debug] display $id, $parent and $code in console.log (enumerable=true) 68 | * @return {Object} 69 | */ 70 | function parseWrap(str, debug) { 71 | str = ['(function() {', str, '});'].join("\n"); 72 | var parsed = parse(str, debug); 73 | 74 | return toProgram(parsed.body[0].expression.body.body); 75 | } 76 | 77 | /** 78 | * Parse given file 79 | * 80 | * @note: NodeJS only 81 | * 82 | * @param {String} file Path 83 | * @param {Boolean} [debug] display $id, $parent and $code in console.log (enumerable=true) 84 | * @return {Object} 85 | */ 86 | function parseFile(file, debug) { 87 | return parse(require("fs").readFileSync(file, {encoding: "UTF-8"}), debug); 88 | } 89 | 90 | /** 91 | * Return tree.$code, just for API completeness. 92 | * 93 | * @param {Object} tree 94 | * @return {String} 95 | */ 96 | function encode(tree) { 97 | return tree.$code; 98 | } -------------------------------------------------------------------------------- /test/test-comments.js: -------------------------------------------------------------------------------- 1 | var tap = require("tap"), 2 | test = tap.test, 3 | utils = require("../index.js"), 4 | object = require("object-enhancements"); 5 | 6 | test("replaceComment", function(t) { 7 | var tree = utils.parseFile(__dirname + "/fixture-comments-01.js"); 8 | 9 | //console.log(require("util").inspect(tree, {depth: null, colors: true})); 10 | //t.equal(tree.$code, '(function test() {\'use strict\';var WRAP_ME;}())', "valid code"); 11 | 12 | t.deepEqual(tree.body, [ { range: [ 0, 66 ], 13 | type: 'FunctionDeclaration', 14 | id: 15 | { range: [ 9, 14 ], 16 | type: 'Identifier', 17 | name: 'name2' }, 18 | params: [], 19 | body: 20 | { range: [ 17, 66 ], 21 | type: 'BlockStatement', 22 | body: 23 | [ { type: 'Line', 24 | value: '0', 25 | range: [ 23, 26 ] }, 26 | { range: [ 27, 33 ], 27 | type: 'VariableDeclaration', 28 | declarations: 29 | [ { range: [ 31, 32 ], 30 | type: 'VariableDeclarator', 31 | id: 32 | { range: [ 31, 32 ], 33 | type: 'Identifier', 34 | name: 'a' }, 35 | init: null } ], 36 | kind: 'var' }, 37 | { type: 'Line', 38 | value: '2', 39 | range: [ 38, 41 ] }, 40 | { range: [ 42, 48 ], 41 | type: 'VariableDeclaration', 42 | declarations: 43 | [ { range: [ 46, 47 ], 44 | type: 'VariableDeclarator', 45 | id: 46 | { range: [ 46, 47 ], 47 | type: 'Identifier', 48 | name: 'b' }, 49 | init: null } ], 50 | kind: 'var' }, 51 | { type: 'Line', 52 | value: '4', 53 | range: [ 53, 56 ] }, 54 | { type: 'Line', 55 | value: '5', 56 | range: [ 61, 64 ] } ] }, 57 | generator: false, 58 | expression: false }, 59 | { range: [ 68, 98 ], 60 | type: 'FunctionDeclaration', 61 | id: 62 | { range: [ 77, 81 ], 63 | type: 'Identifier', 64 | name: 'name' }, 65 | params: [], 66 | body: 67 | { range: [ 84, 98 ], 68 | type: 'BlockStatement', 69 | body: 70 | [ { type: 'Line', 71 | value: 'name', 72 | range: [ 90, 96 ] } ] }, 73 | generator: false, 74 | expression: false }, 75 | { range: [ 100, 202 ], 76 | type: 'FunctionDeclaration', 77 | id: 78 | { range: [ 109, 114 ], 79 | type: 'Identifier', 80 | name: 'name3' }, 81 | params: [], 82 | body: 83 | { range: [ 148, 202 ], 84 | type: 'BlockStatement', 85 | body: 86 | [ { range: [ 154, 200 ], 87 | type: 'ReturnStatement', 88 | argument: 89 | { range: [ 161, 199 ], 90 | type: 'BinaryExpression', 91 | operator: '*', 92 | left: 93 | { range: [ 161, 162 ], 94 | type: 'Identifier', 95 | name: 'a' }, 96 | right: 97 | { range: [ 198, 199 ], 98 | type: 'Identifier', 99 | name: 'b' } } } ] }, 100 | generator: false, 101 | expression: false } ]) 102 | 103 | t.end(); 104 | }); 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /lib/array.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrayPush: arrayPush, 3 | arrayPushLiteral: arrayPushLiteral, 4 | arrayUnshift: arrayUnshift 5 | }; 6 | 7 | var traverse = require("./walk.js").traverse, 8 | getRoot = require("./walk.js").getRoot, 9 | removeTokens = require("./tokens.js").removeTokens, 10 | replaceCodeRange = require("./tokens.js").replaceCodeRange, 11 | pushTokens = require("./tokens.js").pushTokens, 12 | tokenAt = require("./tokens.js").tokenAt, 13 | addTokens = require("./tokens.js").addTokens, 14 | getCode = require("./query.js").getCode, 15 | getComment = require("./query.js").getComment, 16 | toProgram = require("./transformations.js").toProgram, 17 | attachPunctuator = require("./manipulations.js").attachPunctuator, 18 | attach = require("./manipulations.js").attach, 19 | parse = require("./io.js").parse; 20 | 21 | /** 22 | * Push a Literal into an ArrayExpression. 23 | * 24 | * @todo: support for other node types 25 | * 26 | * @param {Object} node 27 | * @param {String} literal_value 28 | * @param {Boolean} unique 29 | * @param {String} [pre_puntuator] 30 | */ 31 | function arrayPushLiteral(node, literal_value, unique, pre_puntuator) { 32 | 33 | var i; 34 | 35 | if (node.type === "ArrayExpression") { 36 | if (unique) { 37 | for (i = 0; i < node.elements.length; ++i) { 38 | if (node.elements[i].value === literal_value) { 39 | return true; 40 | } 41 | } 42 | } 43 | 44 | var tree = parse(JSON.stringify(literal_value)), 45 | literal = toProgram(tree.body[0].expression); // get rid of Expression 46 | 47 | arrayPush(node, literal, pre_puntuator); 48 | 49 | return true; 50 | } 51 | 52 | throw new Error("node must be an ArrayExpression"); 53 | } 54 | 55 | /** 56 | * Push anything into an ArrayExpression. 57 | * 58 | * @todo: support for other node types 59 | * 60 | * @param {Object} node 61 | * @param {String|Object} code 62 | * @param {String} [pre_puntuator] 63 | */ 64 | function arrayPush(node, code, pre_puntuator) { 65 | 66 | var i, 67 | tree = getRoot(node), 68 | inject_tree; 69 | 70 | if (node.type === "ArrayExpression") { 71 | 72 | var last_node = node.elements[node.elements.length - 1]; 73 | 74 | if ("object" === typeof code) { 75 | inject_tree = code; 76 | } else { 77 | inject_tree = parse(code); 78 | } 79 | 80 | attachPunctuator(tree, pre_puntuator || ",", last_node.range[1]); 81 | 82 | // attach the literal 83 | attach(node, "elements", -1, inject_tree); 84 | 85 | return true; 86 | } 87 | 88 | throw new Error("node must be an ArrayExpression"); 89 | } 90 | 91 | /** 92 | * Unshift anything into an ArrayExpression. 93 | * 94 | * @todo: support for other node types 95 | * 96 | * @param {Object} node 97 | * @param {String|Object} code 98 | * @param {String} [pre_puntuator] 99 | */ 100 | function arrayUnshift(node, code, post_puntuator) { 101 | 102 | var i, 103 | tree = getRoot(node), 104 | inject_tree; 105 | 106 | if (node.type === "ArrayExpression") { 107 | 108 | if ("object" === typeof code) { 109 | inject_tree = code; 110 | } else { 111 | inject_tree = parse(code); 112 | } 113 | 114 | // attach the literal 115 | attach(node, "elements", 0, inject_tree); 116 | 117 | var last_node = node.elements[0]; 118 | attachPunctuator(tree, post_puntuator || ",", last_node.range[1]); 119 | 120 | return true; 121 | } 122 | 123 | throw new Error("node must be an ArrayExpression"); 124 | } -------------------------------------------------------------------------------- /lib/debug.js: -------------------------------------------------------------------------------- 1 | var traverse = require("./walk.js").traverse, 2 | getCode = require("./query.js").getCode, 3 | archy = require("archy"); 4 | 5 | /** 6 | * Show your tree in various ways to easy debug 7 | * Big trees will be always a pain, so keep it small if possible 8 | * 9 | * @param {Object} tree Any node, if root tokens & source will be displayed 10 | * @param {Number} [max_width] max tokens per line 11 | * @param {Boolean} [display_code_in_tree] when display the tree attach the code on the right 12 | */ 13 | function debug_tree(tree, max_width, display_code_in_tree) { 14 | max_width = max_width || 50; 15 | display_code_in_tree = display_code_in_tree || false; 16 | 17 | var i, 18 | ci = -1, 19 | idxs = [], 20 | chars = []; 21 | 22 | if (tree.$code && tree.tokens) { 23 | for (i = 0; i < tree.$code.length; ++i) { 24 | if (i % max_width === 0) { 25 | ++ci; 26 | idxs[ci] = []; 27 | chars[ci] = []; 28 | } 29 | 30 | idxs[ci].push(i + 1); 31 | if (tree.$code[i] === "\n") { 32 | chars[ci].push('\\n'); 33 | } else { 34 | chars[ci].push(tree.$code[i]); 35 | } 36 | } 37 | 38 | 39 | var Table = require('cli-table'); 40 | var table = new Table({style: { 'padding-left': 0, 'padding-right': 0 }}); 41 | 42 | for (i = 0; i < idxs.length; ++i) { 43 | table.push(idxs[i]); 44 | table.push(chars[i]); 45 | } 46 | 47 | console.log("** CODE (char) **"); 48 | console.log(table.toString()); 49 | 50 | ci = 0; 51 | var ranges = [[]], 52 | tokens = [[]]; 53 | for (i = 0; i < tree.tokens.length; ++i) { 54 | if (tree.tokens[i].range[1] > (ci + 1) * max_width) { 55 | ++ci; 56 | ranges[ci] = []; 57 | tokens[ci] = []; 58 | } 59 | 60 | ranges[ci].push(tree.tokens[i].range.join(",")); 61 | tokens[ci].push(tree.tokens[i].value); 62 | } 63 | 64 | table = new Table({style: { 'padding-left': 0, 'padding-right': 0 }}); 65 | 66 | for (i = 0; i < ranges.length; ++i) { 67 | table.push(ranges[i]); 68 | table.push(tokens[i]); 69 | } 70 | 71 | console.log("** TOKENS **"); 72 | console.log(table.toString()); 73 | 74 | table = new Table({style: { 'padding-left': 0, 'padding-right': 0 }}); 75 | 76 | tree.$code.split("\n").forEach(function(val, k) { 77 | table.push([k, val]); 78 | }); 79 | 80 | console.log("** CODE (line) **"); 81 | console.log(table.toString()); 82 | } 83 | 84 | 85 | var mtree = { 86 | label: tree.type, 87 | nodes: [] 88 | }, 89 | cnodes = mtree.nodes, 90 | ptree = mtree, 91 | 92 | lindex = null, 93 | ldepth = 1; 94 | 95 | traverse(tree, function(node, parent, property, index, depth) { 96 | if (node.type == "Program") return; 97 | 98 | if (tree.type !== "Program") { 99 | depth = depth + 1; 100 | } 101 | 102 | if (ldepth < depth) { 103 | cnodes[cnodes.length - 1].nodes = []; 104 | cnodes[cnodes.length - 1].parent = ptree; 105 | 106 | ptree = cnodes[cnodes.length - 1]; 107 | cnodes = cnodes[cnodes.length - 1].nodes; 108 | 109 | } else if (ldepth > depth) { 110 | while (ldepth != depth) { 111 | ptree = ptree.parent; 112 | cnodes = ptree.nodes; 113 | 114 | --ldepth; 115 | } 116 | } 117 | 118 | ldepth = depth; 119 | 120 | cnodes.push({ 121 | label: node.type + " [" + node.range.join(":") + "]" + 122 | (display_code_in_tree ? " - " + getCode(node).replace(/\n/g, "\\n"): ""), 123 | parent: ptree 124 | }); 125 | }); 126 | 127 | console.log("** TREE **"); 128 | console.log(archy(mtree)); 129 | console.log("---------------------------"); 130 | 131 | } 132 | 133 | module.exports = { 134 | debug_tree: debug_tree 135 | }; -------------------------------------------------------------------------------- /lib/tokens.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | getToken: getToken, 5 | getTokens: getTokens, 6 | pushTokens: pushTokens, 7 | growTokens: growTokens, 8 | tokenAt: tokenAt, 9 | addTokens: addTokens, 10 | replaceCodeRange: replaceCodeRange, 11 | removeTokens: removeTokens 12 | }; 13 | 14 | var traverse = require("./walk.js").traverse; 15 | 16 | /** 17 | * Get token based on given range 18 | * 19 | * @param {Object} tree 20 | * @param {Number} start 21 | * @param {Number} end 22 | * @return {Object|null} 23 | */ 24 | function getToken(tree, start, end) { 25 | var i, 26 | token_list = tree.tokens, 27 | max = token_list.length; 28 | 29 | for (i = 0; i < max; ++i) { 30 | if (token_list[i].range[0] === start && token_list[i].range[1] === end) { 31 | return token_list[i]; 32 | } 33 | } 34 | 35 | return null; 36 | } 37 | 38 | /** 39 | * Get tokens in range 40 | * 41 | * @param {Object} tree 42 | * @param {Number} start 43 | * @param {Number} end 44 | * @return {Array|null} 45 | */ 46 | function getTokens(tree, start, end) { 47 | var i, 48 | token_list = tree.tokens, 49 | max = token_list.length, 50 | list = []; 51 | 52 | for (i = 0; i < max; ++i) { 53 | if (token_list[i].range[0] >= start && token_list[i].range[1] <= end) { 54 | list.push(token_list[i]); 55 | } 56 | } 57 | 58 | return list; 59 | } 60 | 61 | /** 62 | * Push tokens range from start 63 | * @note Update nodes range 64 | * 65 | * @param {Object} tree 66 | * @param {Number} start 67 | * @param {Number} amount 68 | */ 69 | function pushTokens(tree, start, amount) { 70 | var i, 71 | token_list = tree.tokens, 72 | max = token_list.length; 73 | 74 | for (i = 0; i < max; ++i) { 75 | if (start <= token_list[i].range[0]) { 76 | token_list[i].range[0] += amount; 77 | token_list[i].range[1] += amount; 78 | } 79 | } 80 | 81 | traverse(tree, function(n) { 82 | if (n.range) { 83 | if (start <= n.range[0]) { 84 | n.range[0] += amount; 85 | n.range[1] += amount; 86 | } else if (start <= n.range[1]) { 87 | n.range[1] += amount; 88 | } 89 | } 90 | }); 91 | } 92 | 93 | /** 94 | * Grow tokens in given range 95 | * @note Update nodes range 96 | * 97 | * @param {Object} tree 98 | * @param {Number} start 99 | * @param {Number} end 100 | * @param {Number} amount 101 | */ 102 | function growTokens(tree, start, end, amount) { 103 | var i, 104 | token_list = tree.tokens, 105 | max = token_list.length; 106 | 107 | for (i = 0; i < max; ++i) { 108 | if (start >= token_list[i].range[0] && end <= token_list[i].range[0]) { 109 | token_list[i].range[1] += amount; 110 | } 111 | } 112 | 113 | traverse(tree, function(n) { 114 | if (n.range && start >= n.range[0] && max <= n.range[1]) { 115 | n.range[1] += amount; 116 | } 117 | }); 118 | } 119 | 120 | /** 121 | * Get the first token 122 | * 123 | * @param {Object} tree 124 | * @param {Number} start 125 | * @return {Object} 126 | */ 127 | function tokenAt(tree, start) { 128 | // add new tokens 129 | var i = 0, 130 | token_list = tree.tokens, 131 | max = token_list.length; 132 | 133 | while (i < max && token_list[i].range[0] < start) { 134 | ++i; 135 | } 136 | 137 | return i === max ? -1 : i; 138 | } 139 | 140 | /** 141 | * Add `src` tokens to `dst` since `start` (so keep the order) 142 | * @note Remember to push `src` tokens before `addTokens` otherwise won't be synced 143 | * 144 | * @param {Object} dst_tree 145 | * @param {Object|Array} src 146 | * @param {Number} start 147 | */ 148 | function addTokens(dst_tree, src, start) { 149 | src = Array.isArray(src) ? src : src.tokens; 150 | 151 | var i, 152 | dst = dst_tree.tokens, 153 | max = src.length; 154 | 155 | for (i = 0; i < max; ++i) { 156 | dst.splice(start + i, 0, src[i]); 157 | } 158 | } 159 | 160 | /** 161 | * Replace code range with given text. 162 | * 163 | * @param {Object} tree 164 | * @param {Array} range 165 | * @param {String} new_text 166 | */ 167 | function replaceCodeRange(tree, range, new_text) { 168 | tree.$code = [ 169 | tree.$code.substring(0, range[0]), 170 | new_text, 171 | tree.$code.substring(range[1]) 172 | ].join(""); 173 | } 174 | 175 | /** 176 | * Remove tokens in range and update ranges 177 | * @note Do not remove nodes. 178 | * 179 | * @param {Object} tree 180 | * @param {Number} start 181 | * @param {Number} end 182 | */ 183 | function removeTokens(tree, start, end) { 184 | //console.log(require("util").inspect(tree.tokens, {depth: null, colors: true})); 185 | 186 | var diff = (end - start); 187 | tree.tokens = tree.tokens.filter(function(n) { 188 | return n.range[1] < start || n.range[0] > end; 189 | }).map(function(n) { 190 | if (n.range[0] > end) { // right 191 | n.range[0] -= diff; 192 | n.range[1] -= diff; 193 | } 194 | 195 | return n; 196 | }); 197 | 198 | traverse(tree, function(n) { 199 | if (n.range[0] > end) { // right 200 | n.range[0] -= diff; 201 | n.range[1] -= diff; 202 | } else if (n.range[0] < start && n.range[1] > end) { // inside 203 | n.range[1] -= diff; 204 | } 205 | }); 206 | 207 | //console.log(require("util").inspect(tree.tokens, {depth: null, colors: true})); 208 | } 209 | -------------------------------------------------------------------------------- /test/test-manipulation.js: -------------------------------------------------------------------------------- 1 | var tap = require("tap"), 2 | test = tap.test, 3 | utils = require("../index.js"), 4 | object = require("object-enhancements"); 5 | 6 | test("renameVariable", function(t) { 7 | var tree = utils.parseFile(__dirname + "/fixture-manipulation-01.js"); 8 | 9 | var tokens = object.clone(tree.tokens); 10 | 11 | utils.renameVariable(tree, {"A": "B"}); 12 | 13 | var i, 14 | offset = 0; 15 | 16 | t.equal(tree.tokens.length, tokens.length, "same token amount"); 17 | 18 | for (i = 0; i < tree.tokens.length; ++i) { 19 | t.deepEquals(tree.tokens[i].range, tokens[i].range, "range is ok"); 20 | } 21 | 22 | utils.renameVariable(tree, {"B": "BBB"}); 23 | 24 | t.equal(tree.tokens.length, tokens.length, "same token amount"); 25 | 26 | for (i = 0; i < tree.tokens.length; ++i) { 27 | if (tokens[i].value === "A") { 28 | t.deepEquals(tree.tokens[i].range[0], tokens[i].range[0] + offset, "range is ok"); 29 | offset += 2; 30 | t.deepEquals(tree.tokens[i].range[1], tokens[i].range[1] + offset, "range is ok"); 31 | 32 | } else { 33 | t.deepEquals(tree.tokens[i].range[0], tokens[i].range[0] + offset, "range is ok"); 34 | t.deepEquals(tree.tokens[i].range[1], tokens[i].range[1] + offset, "range is ok"); 35 | } 36 | } 37 | 38 | t.equal(tree.$code.indexOf("A"), -1, "there is A in the final code"); 39 | 40 | //console.log(require("util").inspect(tree, {depth: null, colors: true})); 41 | console.log(tree.$code); 42 | 43 | t.end(); 44 | }); 45 | 46 | test("renameProperty", function(t) { 47 | var tree = utils.parseFile(__dirname + "/fixture-manipulation-02.js"); 48 | 49 | var tokens = object.clone(tree.tokens); 50 | 51 | utils.renameProperty(tree, {"A": "B"}); 52 | 53 | var i, 54 | offset = 0; 55 | 56 | t.equal(tree.tokens.length, tokens.length, "same token amount"); 57 | 58 | for (i = 0; i < tree.tokens.length; ++i) { 59 | t.deepEquals(tree.tokens[i].range, tokens[i].range, "range is ok"); 60 | } 61 | 62 | utils.renameProperty(tree, {"B": "BBB"}); 63 | 64 | t.equal(tree.tokens.length, tokens.length, "same token amount"); 65 | 66 | for (i = 0; i < tree.tokens.length; ++i) { 67 | if (tokens[i].value === "A") { 68 | t.deepEquals(tree.tokens[i].range[0], tokens[i].range[0] + offset, "range is ok"); 69 | offset += 2; 70 | t.deepEquals(tree.tokens[i].range[1], tokens[i].range[1] + offset, "range is ok"); 71 | 72 | } else { 73 | t.deepEquals(tree.tokens[i].range[0], tokens[i].range[0] + offset, "range is ok"); 74 | t.deepEquals(tree.tokens[i].range[1], tokens[i].range[1] + offset, "range is ok"); 75 | } 76 | } 77 | 78 | t.equal(tree.$code.indexOf("A"), -1, "there is A in the final code"); 79 | 80 | //console.log(require("util").inspect(tree, {depth: null, colors: true})); 81 | console.log(tree.$code); 82 | 83 | t.end(); 84 | }); 85 | 86 | 87 | test("renameFunction", function(t) { 88 | var tree = utils.parseFile(__dirname + "/fixture-manipulation-03.js"); 89 | 90 | var tokens = object.clone(tree.tokens); 91 | 92 | utils.renameFunction(tree, {"A": "B"}); 93 | 94 | var i, 95 | offset = 0, 96 | odd = 0; 97 | 98 | t.equal(tree.tokens.length, tokens.length, "same token amount"); 99 | 100 | for (i = 0; i < tree.tokens.length; ++i) { 101 | t.deepEquals(tree.tokens[i].range, tokens[i].range, "range is ok"); 102 | } 103 | 104 | utils.renameFunction(tree, {"B": "BBB"}); 105 | 106 | t.equal(tree.tokens.length, tokens.length, "same token amount"); 107 | 108 | for (i = 0; i < tree.tokens.length; ++i) { 109 | if (tokens[i].value === "A") { 110 | 111 | t.deepEquals(tree.tokens[i].range[0], tokens[i].range[0] + offset, "range is ok"); 112 | if ((odd % 2) == 0) { 113 | offset += 2; 114 | } 115 | ++odd; 116 | 117 | t.deepEquals(tree.tokens[i].range[1], tokens[i].range[1] + offset, "range is ok"); 118 | 119 | } else { 120 | t.deepEquals(tree.tokens[i].range[0], tokens[i].range[0] + offset, "range is ok"); 121 | t.deepEquals(tree.tokens[i].range[1], tokens[i].range[1] + offset, "range is ok"); 122 | } 123 | } 124 | 125 | t.equal(tree.$code.match(/A/g).length, 2, "there are two A in the final code"); 126 | 127 | t.equal(utils.isFunctionDeclared(tree, "A"), false, "Function A is not defined"); 128 | t.equal(utils.isFunctionDeclared(tree, "BBB"), true, "Function BBB is defined"); 129 | 130 | console.log(tree.$code); 131 | 132 | t.end(); 133 | }); 134 | 135 | test("wrap", function(t) { 136 | var tree = utils.parseFile(__dirname + "/fixture-manipulation-04.js"); 137 | 138 | var node = tree.body[0]; 139 | var text = utils.detach(node); 140 | 141 | utils.attach(tree, "body", -1, "(function test() {'use strict';}())"); 142 | 143 | var block = utils.getFunctionBlock(tree, "test"); 144 | utils.attach(block, "body", -1, text); 145 | 146 | t.equal(tree.$code, '(function test() {\'use strict\';var WRAP_ME;}())', "valid code"); 147 | 148 | t.end(); 149 | }); 150 | 151 | 152 | test("replaceComment", function(t) { 153 | var tree = utils.parseFile(__dirname + "/fixture-manipulation-05.js"); 154 | 155 | //utils.debug_tree(tree); 156 | 157 | //console.log(require("util").inspect(tree, {depth: null, colors: true})); 158 | var text = utils.replaceComment(tree, "this is a comment", "var XXX;"); 159 | 160 | 161 | //console.log(require("util").inspect(tree, {depth: null, colors: true})); 162 | //console.log(require("util").inspect(tree.$code, {depth: null, colors: true})); 163 | 164 | t.equal(tree.$code, '\'use strict\';\n\n(function test() {var XXX;\n \n}())\n', "valid code"); 165 | 166 | t.end(); 167 | }); 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /lib/query.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | getFunction: getFunction, 5 | getFunctionBlock: getFunctionBlock, 6 | isFunctionDeclared: isFunctionDeclared, 7 | isComment: isComment, 8 | hasVarDeclaration: hasVarDeclaration, 9 | isVarDeclared: isVarDeclared, 10 | contains: contains, 11 | hasBody: hasBody, 12 | getComment: getComment, 13 | getCode: getCode, 14 | getArgumentList: getArgumentList, 15 | getVarDeclaration: getVarDeclaration, 16 | getDefaultProperty: getDefaultProperty 17 | }; 18 | 19 | var filter = require("./walk.js").filter, 20 | traverse = require("./walk.js").traverse, 21 | getRoot = require("./walk.js").getRoot, 22 | is_function = function (node) { 23 | return node.type === "FunctionDeclaration" || node.type === "FunctionExpression"; 24 | }; 25 | 26 | /** 27 | * `filter` the AST and return the function with given name, null otherwise. 28 | * 29 | * @param {Object} node 30 | * @param {String} fn_name 31 | * @return {Object|null} 32 | */ 33 | function getFunction(node, fn_name) { 34 | // search nearest function 35 | var fn = filter(node, function(node) { 36 | return is_function(node) && node.id && node.id.name == fn_name; 37 | }); 38 | 39 | if (fn && fn.length) { 40 | return fn[0]; 41 | } 42 | 43 | return null; 44 | } 45 | /** 46 | * `filter` the AST and return the function > block with given name, null otherwise. 47 | * 48 | * @param {Object} node 49 | * @param {String} fn_name 50 | * @return {Object|null} 51 | */ 52 | function getFunctionBlock(node, fn_name) { 53 | var ret = null; 54 | 55 | traverse(node, function(node) { 56 | if ( 57 | node.type === "BlockStatement" && 58 | node.$parent.type === "FunctionExpression" && 59 | node.$parent.id && node.$parent.id.name === fn_name 60 | ) { 61 | ret = node; 62 | return false; 63 | } 64 | }); 65 | 66 | return ret; 67 | } 68 | /** 69 | * shortcut 70 | * 71 | * @param {Object} node 72 | * @param {String} fn_name 73 | * @return {Boolean} 74 | */ 75 | function isFunctionDeclared(node, fn_name) { 76 | return getFunction(node, fn_name) !== null; 77 | } 78 | 79 | function getVarDeclaration(node, var_name) { 80 | var found = null; 81 | traverse(node, function(n) { 82 | if (n.$parent && n.$parent.type === "VariableDeclarator" && 83 | n.type === "Identifier" && 84 | n.name === var_name 85 | ) { 86 | found = n; 87 | } 88 | }); 89 | 90 | if (found) { 91 | // identifier -> declarator -> declaration 92 | return found.$parent.$parent; 93 | } 94 | 95 | return null; 96 | 97 | } 98 | /** 99 | * shortcut 100 | * 101 | * @param {Object} node 102 | * @param {String} var_name 103 | * @return {Boolean} 104 | */ 105 | function hasVarDeclaration(node, var_name) { 106 | var found = false; 107 | traverse(node, function(n) { 108 | if (n.$parent.type === "VariableDeclarator" && 109 | n.type === "Identifier" && 110 | n.name === var_name 111 | ) { 112 | found = true; 113 | } 114 | }); 115 | 116 | return found; 117 | } 118 | /** 119 | * reverse from node to root and look for a Variable declaration 120 | * @note It's not perfect because `VariableDeclaration` it's not hoisted 121 | * 122 | * @param {Object} node 123 | * @param {String} var_name 124 | * @return {Boolean} 125 | */ 126 | function isVarDeclared(node, var_name) { 127 | var found = false; 128 | 129 | getParent(node, function(n) { 130 | var i, 131 | j; 132 | 133 | //console.log(n.type, n.body && n.body.length); 134 | 135 | if (n.body && n.body.length) { 136 | for (i = 0; i < n.body.length; ++i) { 137 | if (n.body[i].type === "VariableDeclaration") { 138 | if (hasVarDeclaration(n.body[i], var_name)) { 139 | found = true; 140 | } 141 | 142 | } 143 | } 144 | } 145 | }); 146 | 147 | //console.log("isVarDeclared", var_name, found); 148 | 149 | return found; 150 | } 151 | /** 152 | * `node` constains `subnode` 153 | * 154 | * @param {Object} node 155 | * @param {Object} subnode 156 | * @return {Boolean} 157 | */ 158 | function contains(node, subnode) { 159 | return node.range[0] <= subnode.range[0] && node.range[1] >= subnode.range[1]; 160 | } 161 | /** 162 | * Has a body property, use to freely attach/detach 163 | * 164 | * @param {Object} node 165 | * @return {Boolean} 166 | */ 167 | function hasBody(node) { 168 | return [ 169 | "Program", 170 | "BlockStatement", 171 | ].indexOf(node.type) !== -1; 172 | } 173 | /** 174 | * shortcut: Is a comment (Line or Block) and has text 175 | * 176 | * @param {Object} node 177 | * @return {Boolean} 178 | */ 179 | function isComment(node) { 180 | return ( 181 | (node.type === "Line" && node.value && node.value.length) || 182 | (node.type === "Block" && node.value.length) 183 | ) > 0; 184 | } 185 | /** 186 | * shortcut: search for a comment (trim it's content for maximum compatibility) 187 | * 188 | * @param {Object} node 189 | * @param {String} comment 190 | * @return {Object} 191 | */ 192 | function getComment(node, comment) { 193 | var output = null; 194 | traverse(node, function(n, parent, property, index, depth) { 195 | if(isComment(n) && n.value.trim() === comment) { 196 | output = n; 197 | return false; 198 | } 199 | }); 200 | 201 | return output; 202 | } 203 | /** 204 | * shortcut: Return node code 205 | * 206 | * @param {Object} node 207 | * @return {String} 208 | */ 209 | function getCode(node) { 210 | var root = getRoot(node); 211 | 212 | return root.$code.substring(node.range[0], node.range[1]); 213 | } 214 | /** 215 | * Return `FunctionDeclaration` arguments name as a list 216 | * 217 | * @param {Object} node 218 | * @return {Array} 219 | */ 220 | function getArgumentList(node) { 221 | var args = [], 222 | arg, 223 | i, 224 | max; 225 | 226 | if (node.type === "FunctionDeclaration" && node.id && node.id.name) { 227 | if (node.id.name[0] === "_") { 228 | //internal skip! 229 | return; 230 | } 231 | 232 | // define a new function! 233 | 234 | 235 | for (i = 0, max = node.params.length; i < max; ++i) { 236 | args.push(node.params[i].name); 237 | } 238 | } 239 | 240 | return args; 241 | } 242 | 243 | 244 | /* 245 | * @TODO add every Node that has only one possible detachable node/list of nodes 246 | * 247 | * @param {Object} node 248 | */ 249 | function getDefaultProperty(node) { 250 | var property; 251 | 252 | switch(node.type) { 253 | case "ArrayExpression": 254 | property = "elements"; 255 | break; 256 | case "Program": 257 | case "BlockStatement": 258 | default: 259 | property = "body"; 260 | } 261 | 262 | return property; 263 | } -------------------------------------------------------------------------------- /lib/transformations.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | setIdentifier: setIdentifier, 5 | renameProperty: renameProperty, 6 | renameVariable: renameVariable, 7 | renameFunction: renameFunction, 8 | toProgram: toProgram 9 | }; 10 | 11 | var traverse = require("./walk.js").traverse, 12 | getRoot = require("./walk.js").getRoot, 13 | parentize = require("./walk.js").parentize, 14 | idze = require("./walk.js").idze, 15 | clone = require("./walk.js").clone, 16 | getToken = require("./tokens.js").getToken, 17 | getTokens = require("./tokens.js").getTokens, 18 | pushTokens = require("./tokens.js").pushTokens, 19 | growTokens = require("./tokens.js").growTokens, 20 | isVarDeclared = require("./query.js").isVarDeclared, 21 | isFunctionDefined = require("./query.js").isFunctionDefined, 22 | getCode = require("./query.js").getCode, 23 | replaceCodeRange = require("./tokens.js").replaceCodeRange; 24 | /** 25 | * rename `Identifier` 26 | * 27 | * @param {Object} node 28 | * @param {String} new_name 29 | */ 30 | function setIdentifier(node, new_name) { 31 | if (node.type !== "Identifier") { 32 | throw new Error("node must be an Identifier"); 33 | } 34 | 35 | var root = getRoot(node), 36 | diff = (new_name.length - node.name.length), 37 | token = getToken(root, node.range[0], node.range[1]); 38 | 39 | if (!token) { 40 | console.log(node); 41 | console.log(root.tokens); 42 | throw new Error("token cannot be found"); 43 | } 44 | 45 | token.value = new_name; 46 | token.range[1] += diff; 47 | 48 | replaceCodeRange(root, node.range, new_name); 49 | 50 | pushTokens(root, node.range[1], diff); 51 | growTokens(root, node.range[0], node.range[1], diff); 52 | 53 | node.name = new_name; 54 | } 55 | 56 | /** 57 | * `traverse` and apply given `replacements` 58 | * 59 | * @param {Object} node 60 | * @param {Object} replacements 61 | * 62 | * @example 63 | * renameProperty(node, {"old_var": "new_var", "much_older": "shinnig_new"}) 64 | */ 65 | function renameProperty(node, replacements) { 66 | if (node.$parent === undefined) { 67 | throw new Error("parentize is required"); 68 | } 69 | 70 | if (node.$id === undefined) { 71 | throw new Error("idze is required"); 72 | } 73 | 74 | 75 | traverse(node, function(node) { 76 | if (node.type == "Identifier" && 77 | replacements[node.name] && 78 | ( 79 | "Property" === node.$parent.type || 80 | ( 81 | "MemberExpression" === node.$parent.type && 82 | node.$parent.property === node 83 | ) 84 | ) 85 | ) { 86 | 87 | setIdentifier(node, replacements[node.name]); 88 | } 89 | }); 90 | } 91 | 92 | /** 93 | * `traverse` and apply given `replacements` 94 | * 95 | * @param {Object} node 96 | * @param {Object} replacements 97 | * 98 | * @example 99 | * renameVariable(node, {"old_var": "new_var", "much_older": "shinnig_new"}) 100 | */ 101 | function renameVariable(node, replacements) { 102 | if (node.$parent === undefined) { 103 | throw new Error("parentize is required"); 104 | } 105 | 106 | if (node.$id === undefined) { 107 | throw new Error("idze is required"); 108 | } 109 | 110 | var root = getRoot(node); 111 | 112 | traverse(node, function(node) { 113 | //if (node.type == "Identifier") 114 | //console.log("*", node.type, node.$parent.type, node.$parent); 115 | 116 | if (node.type == "Identifier" && 117 | replacements[node.name] && 118 | ( 119 | ["VariableDeclarator", "AssignmentExpression", "ReturnStatement"] 120 | .indexOf(node.$parent.type) !== -1 || 121 | ( 122 | "Property" === node.$parent.type && 123 | node.$parent.value.$id === node.$id && 124 | isVarDeclared(node, replacements[node.name]) 125 | ) 126 | ) 127 | ) { 128 | setIdentifier(node, replacements[node.name]); 129 | } 130 | }); 131 | } 132 | 133 | /** 134 | * traverse and apply given `replacements` 135 | * 136 | * @param {Object} node 137 | * @param {Object} replacements 138 | * 139 | * @example 140 | * renameFunction(node, {"old_var": "new_var", "much_older": "shinnig_new"}) 141 | */ 142 | function renameFunction(node, replacements) { 143 | if (node.$parent === undefined) { 144 | throw new Error("parentize is required"); 145 | } 146 | 147 | if (node.$id === undefined) { 148 | throw new Error("idze is required"); 149 | } 150 | 151 | 152 | traverse(node, function(node) { 153 | if (node.type == "Identifier" && replacements[node.name]) { 154 | if ( 155 | ["FunctionDeclaration", "CallExpression"].indexOf(node.$parent.type) !== -1 || 156 | ( 157 | "Property" === node.$parent.type && 158 | node.$parent.value.$id === node.$id && 159 | !isVarDeclared(node, replacements[node.name]) && 160 | ( 161 | isFunctionDefined(node, replacements[node.name]) || 162 | // could be below 163 | isFunctionDefined(node, node.name) 164 | ) 165 | ) 166 | ) { 167 | setIdentifier(node, replacements[node.name]); 168 | } 169 | } 170 | }); 171 | } 172 | 173 | /** 174 | * Clone given node(s) and extract tokens & code from root to given you a Program-like attachable node 175 | * 176 | * @param {Object|Array} node 177 | * if array is provided will add all nodes to program.body 178 | */ 179 | function toProgram(node) { 180 | var root, 181 | program_range, 182 | tokens, 183 | program, 184 | i; 185 | 186 | if (Array.isArray(node)) { 187 | root = getRoot(node[0]); 188 | program_range = [Infinity, -Infinity]; 189 | 190 | for(i = 0; i < node.length; ++i) { 191 | program_range[0] = Math.min(program_range[0], node[i].range[0]); 192 | program_range[1] = Math.max(program_range[1], node[i].range[1]); 193 | } 194 | 195 | } else { 196 | root = getRoot(node); 197 | program_range = node.range; 198 | 199 | } 200 | 201 | tokens = getTokens(root, program_range[0], program_range[1]); 202 | 203 | program = { 204 | type: "Program", 205 | body: null, 206 | range: [program_range[0], program_range[1]], 207 | tokens: tokens, 208 | comments: [], 209 | $code: null 210 | }; 211 | 212 | if (Array.isArray(node)) { 213 | program.$code = getCode({$parent: root, range: program_range}); // fake 214 | program.body = node.slice().map(clone); // clone! 215 | } else { 216 | program.$code = getCode(node); 217 | program.body = [ 218 | clone(node) 219 | ]; 220 | } 221 | 222 | parentize(program); 223 | idze(program); 224 | 225 | pushTokens(program, 0, -program_range[0]); 226 | 227 | return program; 228 | } -------------------------------------------------------------------------------- /lib/walk.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //nice hack :! 4 | module.exports = { 5 | traverse: traverse, 6 | parentize: parentize, 7 | idze: idze, 8 | attachComments: attachComments, 9 | filter: filter, 10 | getParent: getParent, 11 | getRoot: getRoot, 12 | clone: clone 13 | }; 14 | 15 | var __debug = false, 16 | contains = require("./query.js").contains, 17 | hasBody = require("./query.js").hasBody; 18 | 19 | var objectKeys = Object.keys || function (obj) { 20 | var keys = []; 21 | for (var key in obj) keys.push(key); 22 | return keys; 23 | }; 24 | var forEach = function (xs, fn) { 25 | if (xs.forEach) return xs.forEach(fn); 26 | for (var i = 0; i < xs.length; i++) { 27 | fn.call(xs, xs[i], i, xs); 28 | } 29 | }; 30 | 31 | 32 | /** 33 | * traverse AST 34 | * 35 | * @param {Object} node 36 | * @param {Function} callback 37 | * function(node, parent, property, index, depth) 38 | * You can return `false` to stop traverse 39 | * @param {Number} [depth] (0) current depth 40 | * @param {Boolean} [recursive] (true) recursively traverse 41 | */ 42 | function traverse(node, callback, depth, recursive) { 43 | if ("object" !== typeof node) { 44 | throw new Error("node must be provided"); 45 | } 46 | 47 | if ("function" !== typeof callback) { 48 | throw new Error("callback must be provided"); 49 | } 50 | 51 | if (depth === undefined) { 52 | if (!node) { 53 | throw new Error("WTF!?"); 54 | } 55 | callback(node, null, null, null, 0); 56 | } 57 | 58 | depth = depth || 0; 59 | 60 | forEach(objectKeys(node), function (key) { 61 | // do not follow internal vars 62 | // or extra keys 63 | if (key[0] === '$' || key === "loc" || key === "comments" || key === "tokens" || key === "range") return; 64 | 65 | var child = node[key]; 66 | if (Array.isArray(child)) { 67 | forEach(child, function (c, idx) { 68 | if (c && typeof c.type === 'string') { 69 | callback(c, node, key, idx, depth + 1); 70 | recursive !== false && traverse(c, callback, depth + 1, recursive); 71 | } 72 | }); 73 | } 74 | else if (child && typeof child.type === 'string') { 75 | callback(child, node, key, null, depth + 1); 76 | recursive !== false && traverse(child, callback, depth + 1, recursive); 77 | } 78 | }); 79 | } 80 | 81 | function __set_node(node, value, property, debug) { 82 | if (debug) { 83 | node[property] = parent; 84 | } else { 85 | Object.defineProperty(node, property, {enumerable: false, value: value, writable: true}); 86 | } 87 | } 88 | 89 | /** 90 | * `traverse` AST and set $parent node 91 | * 92 | * @param {Object} root 93 | * @param {Boolean} [debug] display $parent in console.log (enumerable=true) 94 | */ 95 | function parentize(root, debug) { 96 | debug = debug || __debug; 97 | 98 | traverse(root, function(node, parent) { 99 | __set_node(node, parent, "$parent", debug); 100 | }); 101 | } 102 | 103 | var node_ids = 0; 104 | /** 105 | * `traverse` AST and set an unique `$id` to every node 106 | * 107 | * @param {Object} node 108 | * @param {Boolean} [debug] display $id in console.log (enumerable=true) 109 | */ 110 | function idze(node, debug) { 111 | debug = debug || __debug; 112 | 113 | traverse(node, function(node, parent) { 114 | if (node.$id === undefined) { // do not overwrite! 115 | __set_node(node, ++node_ids, "$id", debug); 116 | } 117 | }); 118 | } 119 | 120 | function __containmentFactor(anode, bnode) { 121 | return (bnode.range[0] - anode.range[0]) + (anode.range[1] - bnode.range[1]); 122 | } 123 | 124 | /** 125 | * Traverse the AST and add comments as nodes, so you can query them. 126 | * Loop thought comments and find a proper place to inject (BlockStament or alike) 127 | * * attach the comment to the before nearest children 128 | * * if a children contains the comment it's considered invalid 129 | * * push otherwise 130 | * @param {Object} root 131 | */ 132 | function attachComments(root) { 133 | var comments = root.comments, 134 | c, 135 | i, 136 | j, 137 | jmax, 138 | max = comments.length, 139 | blocks = filter(root, hasBody), 140 | nearest_container, 141 | min_f, 142 | f, 143 | container_target_idx; 144 | 145 | if (!blocks) { 146 | return ; 147 | } 148 | 149 | for (i = 0; i< max; ++i) { 150 | // find nearest blockstatement 151 | min_f = Infinity; 152 | c = comments[i]; 153 | nearest_container = null; 154 | 155 | jmax = blocks.length; 156 | for (j = 0; j < jmax; ++j) { 157 | if (contains(blocks[j], c)) { 158 | f = __containmentFactor(blocks[j], c); 159 | if (f < min_f) { 160 | min_f = f; 161 | nearest_container = blocks[j]; 162 | } 163 | } 164 | } 165 | 166 | //console.log("*******************************************"); 167 | //console.log("valid", c); 168 | //console.log(require("util").inspect(nearest_container, {depth: null, colors: true})); 169 | 170 | if (nearest_container) { 171 | // search the first son that exceed comment.range[1] 172 | jmax = nearest_container.body.length; 173 | container_target_idx = -1; // push 174 | min_f = Infinity; 175 | 176 | for (j = 0; j < jmax; ++j) { 177 | // left side 178 | if (c.range[1] < nearest_container.body[j].range[0]) { 179 | // min 180 | f = nearest_container.body[j].range[0] - c.range[1]; 181 | 182 | //console.log("left to", j, f); 183 | 184 | if (f < min_f) { 185 | min_f = f; 186 | container_target_idx = j; 187 | } 188 | } 189 | // if comment is contained by a children, means invalid comment 190 | if (contains(nearest_container.body[j], c)) { 191 | container_target_idx = null; 192 | break; 193 | } 194 | } 195 | 196 | //console.log("container_target_idx", container_target_idx); 197 | 198 | if (container_target_idx !== null) { 199 | if (container_target_idx === -1) { 200 | nearest_container.body.push(c); 201 | } else { 202 | nearest_container.body.splice(container_target_idx, 0, c); 203 | } 204 | } else { 205 | console.error("(info) 1.- Found a comment that cannot be attached", c); 206 | } 207 | } else { 208 | console.error("(info) 2.- Found a comment that cannot be attached", c); 209 | } 210 | 211 | //console.log("*******************************************"); 212 | } 213 | //process.exit(); 214 | //console.log(require("util").inspect(root.body, {depth: null, colors: true})); 215 | 216 | } 217 | 218 | /** 219 | * `traverse` and `filter` given AST based on given `callback` 220 | * 221 | * @param {Object} node 222 | * @param {Function} callback 223 | * @param {Function} [traverse_fn] 224 | * @return {Array} Every match of the `callback` 225 | */ 226 | function filter(node, callback, traverse_fn) { 227 | var out = []; 228 | 229 | traverse_fn = traverse_fn || traverse; 230 | 231 | traverse_fn(node, function(node, parent) { 232 | if (callback(node)) { 233 | out.push(node); 234 | } 235 | }); 236 | 237 | return out.length ? out : null; 238 | } 239 | 240 | /** 241 | * Get parent node based on given callback, stops on `true` 242 | * 243 | * @param {Object} node 244 | * @param {Function} callback 245 | * @return {Object|null} 246 | */ 247 | function getParent(node, callback) { 248 | var n = node, 249 | depth = 0; 250 | while(n.$parent) { 251 | n = n.$parent; 252 | if (callback(n, ++depth)) { 253 | return n; 254 | } 255 | } 256 | 257 | return null; 258 | } 259 | 260 | /** 261 | * get the root of the AST 262 | * 263 | * @param {Object} node 264 | * @return {Object} 265 | */ 266 | function getRoot(node) { 267 | var n = node; 268 | 269 | while(n.$parent) { 270 | n = n.$parent; 271 | } 272 | 273 | return n; 274 | } 275 | 276 | /** 277 | * Recursive clone a node. Do no include "$" properties like $parent or $id 278 | * If you want those, call `parentize` - `idze` after cloning 279 | * 280 | * @param {Object} node 281 | * @return {Object} 282 | */ 283 | function clone(node) { 284 | if (Array.isArray(node)) { 285 | return node.map(clone); 286 | } 287 | if ("object" !== typeof node) { 288 | return node; 289 | } 290 | if (node === null) { 291 | return null; 292 | } 293 | 294 | var copy = {}; 295 | 296 | 297 | forEach(objectKeys(node), function(name) { 298 | // ignore auto generated 299 | if (name[0] === "$") return; 300 | 301 | var value = node[name], 302 | cvalue; 303 | 304 | //recursion! 305 | if (Array.isArray(value)) { 306 | cvalue = value.map(clone); 307 | } else if ("object" === typeof value) { 308 | cvalue = clone(value); 309 | } 310 | 311 | // Note that undefined fields will be visited too, according to 312 | // the rules associated with node.type, and default field values 313 | // will be substituted if appropriate. 314 | copy[name] = cvalue || value; 315 | }); 316 | 317 | // enumerable? 318 | copy.$cloned = true; 319 | 320 | return copy; 321 | } -------------------------------------------------------------------------------- /lib/manipulations.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | attach: attach, 5 | detach: detach, 6 | attachPunctuator: attachPunctuator, 7 | attachBefore: attachBefore, 8 | attachAfterComment: attachAfterComment, 9 | attachAfter: attachAfter, 10 | replace: replace, 11 | replaceComment: replaceComment, 12 | injectCode: injectCode 13 | }; 14 | 15 | var traverse = require("./walk.js").traverse, 16 | getRoot = require("./walk.js").getRoot, 17 | attachComments = require("./walk.js").attachComments, 18 | parentize = require("./walk.js").parentize, 19 | idze = require("./walk.js").idze, 20 | removeTokens = require("./tokens.js").removeTokens, 21 | replaceCodeRange = require("./tokens.js").replaceCodeRange, 22 | pushTokens = require("./tokens.js").pushTokens, 23 | tokenAt = require("./tokens.js").tokenAt, 24 | addTokens = require("./tokens.js").addTokens, 25 | getCode = require("./query.js").getCode, 26 | getComment = require("./query.js").getComment, 27 | getDefaultProperty = require("./query.js").getDefaultProperty, 28 | toProgram = require("./transformations.js").toProgram, 29 | parse = require("./io.js").parse; 30 | 31 | /** 32 | * Attach Code/Program to given node. 33 | * @note tokens are updated 34 | * @note range is updated 35 | * @note comments are not attached to root.comments (invalid-comments) 36 | * 37 | * @param {Object} node node to attach 38 | * @param {String} property Where attach, could be an array or an object 39 | * @param {Number|null} position index if an array is used as target property 40 | * @param {String|Object} str String is preferred if not possible remember that only Program can be attached, you may consider using `toProgram` 41 | */ 42 | function attach(node, property, position, str) { 43 | var root = getRoot(node); 44 | 45 | if (!node[property] && Array.isArray(node[property])) { 46 | throw new Error("invalid attach point"); 47 | } 48 | 49 | // parse str to ast 50 | var tree; 51 | if ("string" === typeof str) { 52 | tree = parse(str); 53 | } else { 54 | tree = str; 55 | 56 | if (tree.type !== "Program") { 57 | throw new Error("only Program can be attached"); 58 | } 59 | } 60 | 61 | // search the entry where we attach new code 62 | var entry, 63 | push_range_start; 64 | 65 | if (Array.isArray(node[property])) { 66 | if (node[property].length === 0) { 67 | // empty program/block 68 | push_range_start = node.range[0]; 69 | 70 | // move until any text 71 | while(root.$code[push_range_start] === " " || root.$code[push_range_start] === "\n") { 72 | ++push_range_start; 73 | } 74 | 75 | if (root.$code[push_range_start] === "{") { 76 | ++push_range_start; 77 | } 78 | 79 | } else { 80 | if (position == -1) { // last 81 | entry = node[property][node[property].length -1]; 82 | push_range_start = entry.range[1]; 83 | } else { 84 | entry = node[property][position]; 85 | 86 | if (entry) { 87 | push_range_start = entry.range[0]; 88 | } else { 89 | entry = node[property][position - 1]; 90 | if (!entry) { 91 | throw new Error("cannot determine entry range"); 92 | } 93 | 94 | push_range_start = entry.range[0]; 95 | } 96 | } 97 | } 98 | } else { // object 99 | push_range_start = node.range[0]; 100 | } 101 | 102 | var clean_str = tree.$code.substring(tree.range[0], tree.range[1]), 103 | i, 104 | j; 105 | 106 | // push new tokens 107 | pushTokens(tree, 0, push_range_start); 108 | // push old tokens 109 | pushTokens(root, push_range_start, clean_str.length); 110 | 111 | if (root.tokens.length === 0) { 112 | i = 0; 113 | } else { 114 | i = tokenAt(root, push_range_start); 115 | 116 | if (i === -1) { 117 | throw new Error("Cannot determine token entry?"); 118 | } 119 | } 120 | 121 | addTokens(root, tree, i); 122 | replaceCodeRange(root, [push_range_start, push_range_start], clean_str); 123 | 124 | if (Array.isArray(node[property])) { 125 | for (i = 0; i < tree.body.length; ++i) { 126 | if (position == -1) { 127 | node[property].push(tree.body[i]); 128 | } else { 129 | node[property].splice(position + i, 0, tree.body[i]); 130 | } 131 | } 132 | } else { // object 133 | if (node[property] !== null) { 134 | throw new Error("node[" + property +"] is not null, detach first"); 135 | } 136 | 137 | if (tree.body.length > 1) { 138 | throw new Error("Only one node can be attached to an object property"); 139 | } 140 | 141 | node[property] = tree.body[0]; 142 | } 143 | 144 | // rebuild parents 145 | parentize(root); 146 | idze(root); 147 | // should we re-attach comments ? 148 | } 149 | 150 | /** 151 | * Attach a punctuator and keep the tree ranges sane. 152 | * The Punctuator can be anything... be careful! 153 | * 154 | * @note The Punctuator is not parsed and could be assigned to nearest literal or alike. 155 | * 156 | * @param {Object} tree 157 | * @param {String} punctuator 158 | * @param {Number} position 159 | * @return {String} detached code string 160 | */ 161 | function attachPunctuator(tree, punctuator, position) { 162 | var list = [{ 163 | type: 'Punctuator', 164 | value: punctuator, 165 | range: [position, position + punctuator.length] 166 | }]; 167 | 168 | pushTokens(tree, position, punctuator.length); 169 | 170 | addTokens(tree, list, tokenAt(tree, position)); 171 | 172 | replaceCodeRange(tree, [position, position], punctuator); 173 | } 174 | 175 | /** 176 | * Detach given node from it's parent 177 | * @note `node.$parent` is set to `null`, remember to save it first if you need it. 178 | * 179 | * @param {Object} node 180 | * @param {String} property 181 | * @return {String} detached code string 182 | */ 183 | function detach(node, property) { 184 | var parent = node.$parent, 185 | root = getRoot(node); 186 | 187 | if (!node) { 188 | throw new Error("invalid node given"); 189 | } 190 | 191 | if (!parent) { 192 | throw new Error("node cannot be detached, no parent."); 193 | } 194 | 195 | property = property || getDefaultProperty(parent); 196 | 197 | // invalid detachable parents 198 | switch(parent.type) { 199 | case "VariableDeclarator": 200 | throw new Error("cannot detach from VariableDeclarator, detach the VariableDeclarator itself"); 201 | break; 202 | } 203 | 204 | if (!parent[property]) { 205 | throw new Error("cannot detach from " + property); 206 | } 207 | 208 | if (Array.isArray(parent[property])) { 209 | if (!parent[property].length) { 210 | throw new Error("cannot detach from empty: " + property); 211 | } 212 | 213 | var idx = parent[property].indexOf(node); 214 | if (idx !== -1) { 215 | parent[property].splice(idx, 1); 216 | } 217 | } else if ("object" === typeof parent[property] && parent[property] !== null) { 218 | parent[property] = null; 219 | } else { 220 | throw new Error("cannot find node in parent[" + property + "] body"); 221 | } 222 | 223 | removeTokens(root, node.range[0], node.range[1]); 224 | 225 | var text = getCode(node); 226 | 227 | replaceCodeRange(root, node.range, ""); 228 | 229 | node.$parent = null; 230 | 231 | return text; 232 | } 233 | /** 234 | * Attach after node, that means `node.$parent.type` is a `BockStament` 235 | * 236 | * @param {Object} node 237 | * @param {String|Object} str String is preferred if not possible remember that only Program can be attached, you may consider using `toProgram` 238 | * @param {String} [property] where to search node in the parent 239 | */ 240 | function attachAfter(node, str, property) { 241 | if (node.$parent) { 242 | 243 | property = property || getDefaultProperty(node.$parent); 244 | 245 | var idx = node.$parent[property].indexOf(node); 246 | 247 | //require("./debug.js").debug_tree(node.$parent); 248 | attach(node.$parent, "body", idx + 1, str); 249 | //require("./debug.js").debug_tree(node.$parent); 250 | return true; 251 | } 252 | return false; 253 | } 254 | 255 | /** 256 | * Attach before node, that means `node.$parent.type` is a `BockStament` 257 | * 258 | * @param {Object} node 259 | * @param {String|Object} str String is preferred if not possible remember that only Program can be attached, you may consider using `toProgram` 260 | */ 261 | function attachBefore(node, str) { 262 | if (node.$parent) { 263 | var idx = node.$parent.body.indexOf(node); 264 | 265 | //require("./debug.js").debug_tree(getRoot(node)); 266 | attach(node.$parent, "body", idx, str); 267 | //require("./debug.js").debug_tree(node); 268 | 269 | return true; 270 | } 271 | 272 | return false; 273 | } 274 | 275 | /** 276 | * Shortcut: Search for given comment, and attachAfter 277 | * 278 | * @param {Object} node 279 | * @param {String} comment 280 | * @param {String|Object} str String is preferred if not possible remember that only Program can be attached, you may consider using `toProgram` 281 | * @return {Boolean} success 282 | */ 283 | function attachAfterComment(node, comment, str) { 284 | var n = getComment(node, comment); 285 | 286 | if (n) { 287 | return attachAfter(n, str); 288 | } 289 | 290 | return false; 291 | } 292 | 293 | /** 294 | * Shortcut: detach/attach 295 | * 296 | * @param {Object} node 297 | * @param {String|Object} str String is preferred if not possible remember that only Program can be attached, you may consider using `toProgram` 298 | */ 299 | function replace(node, str) { 300 | if (node.$parent) { 301 | var idx = node.$parent.body.indexOf(node), 302 | parent = node.$parent, 303 | prev_code = detach(node); 304 | 305 | attach(parent, "body", idx, str); 306 | 307 | return prev_code; 308 | } 309 | 310 | return false; 311 | } 312 | 313 | /** 314 | * Shortcut: Search for a comment and replace 315 | * 316 | * @param {Object} node 317 | * @param {String} comment 318 | * @param {String|Object} str String is preferred if not possible remember that only Program can be attached, you may consider using `toProgram` 319 | */ 320 | function replaceComment(node, comment, str) { 321 | var n = getComment(node, comment); 322 | 323 | if (n) { 324 | replace(n, str); 325 | 326 | return true; 327 | } 328 | 329 | return false; 330 | } 331 | 332 | /** 333 | * Inject code directly intro the given range. 334 | * After the injection the code will be parsed again so original `$id` will be lost 335 | * 336 | * @note this is dangerous and powerful 337 | * 338 | * @param {Object} tree 339 | * @param {Array} range 340 | * @param {String|Object} str String is preferred if not possible remember that only Program can be attached, you may consider using `toProgram` 341 | * @param {Boolean} debug display $id, $parent and $code in console.log (enumerable=true) 342 | */ 343 | function injectCode(tree, range, str, debug) { 344 | var clone = { 345 | $code: tree.$code 346 | }; 347 | replaceCodeRange(clone, range, str); 348 | var ntree = parse(clone.$code, debug || false); 349 | 350 | Object.keys(ntree).forEach(function(k) { 351 | tree[k] = ntree[k]; 352 | }); 353 | 354 | tree.$code = ntree.$code; 355 | 356 | parentize(tree); 357 | idze(tree); 358 | } -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # esprima-ast-utils [![Build Status](https://secure.travis-ci.org/llafuente/esprima-ast-utils.png?branch=master)](http://travis-ci.org/llafuente/esprima-ast-utils) 2 | 3 | Node module to manipulate, transform, query and debug [esprima](https://github.com/ariya/esprima) ASTs. 4 | 5 | ## Objective 6 | 7 | When you edit esprima AST and go back to code with escodegen you lose too much information because primary you don't keep track of ranges, tokens, comments etc. 8 | esprima-ast-utils do this for you, so no escodegen is needed, you can edit the AST directly and code everything is in sync. 9 | 10 | ## API 11 | 12 | #### io 13 | 14 | 15 | ##### `parse` (String:str [, Boolean:debug]) -> Object 16 | 17 | Parse given str 18 | 19 | *Parameters:* 20 | 21 | * `str` 22 | 23 | * `debug`: display $id, $parent and $code in console.log (enumerable=true) 24 | 25 | 26 | *Returns:* 27 | 28 | * `Object` 29 | 30 | *Note*: location it's not supported, and won't sync with changes, range/rokens do. 31 | 32 | 33 |

34 | 35 | ##### `parseWrap` (String:str [, Boolean:debug]) -> Object 36 | 37 | Wrap your code into a function and parse given str. 38 | Needed if your code contains a `ReturnStatement` at Program level. 39 | 40 | *Parameters:* 41 | 42 | * `str` 43 | 44 | * `debug`: display $id, $parent and $code in console.log (enumerable=true) 45 | 46 | 47 | *Returns:* 48 | 49 | * `Object` 50 | 51 | *Note*: location it's not supported, and won't sync with changes, range/rokens do. 52 | 53 | 54 |

55 | 56 | ##### `parseFile` (String:file [, Boolean:debug]) -> Object 57 | 58 | Parse given file 59 | 60 | *Parameters:* 61 | 62 | * `file`: Path 63 | 64 | * `debug`: display $id, $parent and $code in console.log (enumerable=true) 65 | 66 | 67 | *Returns:* 68 | 69 | * `Object` 70 | 71 | *Note*: : NodeJS only 72 | 73 | 74 |

75 | 76 | ##### `encode` (Object:tree) -> String 77 | 78 | Return tree.$code, just for API completeness. 79 | 80 | *Parameters:* 81 | 82 | * `tree` 83 | 84 | 85 | *Returns:* 86 | 87 | * `String` 88 | 89 | 90 |

91 | 92 | #### walk 93 | 94 | 95 | ##### `traverse` (Object:node, Function:callback [, Number:depth] [, Boolean:recursive]) 96 | 97 | traverse AST 98 | 99 | *Parameters:* 100 | 101 | * `node` 102 | 103 | * `callback`: function(node, parent, property, index, depth) 104 | You can return `false` to stop traverse 105 | 106 | * `depth`: (0) current depth 107 | 108 | * `recursive`: (true) recursively traverse 109 | 110 | 111 | 112 |

113 | 114 | ##### `parentize` (Object:root [, Boolean:debug]) 115 | 116 | `traverse` AST and set $parent node 117 | 118 | *Parameters:* 119 | 120 | * `root` 121 | 122 | * `debug`: display $parent in console.log (enumerable=true) 123 | 124 | 125 | 126 |

127 | 128 | ##### `idze` (Object:node [, Boolean:debug]) 129 | 130 | `traverse` AST and set an unique `$id` to every node 131 | 132 | *Parameters:* 133 | 134 | * `node` 135 | 136 | * `debug`: display $id in console.log (enumerable=true) 137 | 138 | 139 | 140 |

141 | 142 | ##### `attachComments` (Object:root) 143 | 144 | Traverse the AST and add comments as nodes, so you can query them. 145 | Loop thought comments and find a proper place to inject (BlockStament or alike) 146 | * attach the comment to the before nearest children 147 | * if a children contains the comment it's considered invalid 148 | * push otherwise 149 | 150 | *Parameters:* 151 | 152 | * `root` 153 | 154 | 155 | 156 |

157 | 158 | ##### `filter` (Object:node, Function:callback [, Function:traverse_fn]) -> Array 159 | 160 | `traverse` and `filter` given AST based on given `callback` 161 | 162 | *Parameters:* 163 | 164 | * `node` 165 | 166 | * `callback` 167 | 168 | * `traverse_fn` 169 | 170 | 171 | *Returns:* 172 | 173 | * `Array`: Every match of the `callback` 174 | 175 | 176 |

177 | 178 | ##### `getParent` (Object:node, Function:callback) -> Object|NULL 179 | 180 | Get parent node based on given callback, stops on `true` 181 | 182 | *Parameters:* 183 | 184 | * `node` 185 | 186 | * `callback` 187 | 188 | 189 | *Returns:* 190 | 191 | * `Object|NULL` 192 | 193 | 194 |

195 | 196 | ##### `getRoot` (Object:node) -> Object 197 | 198 | get the root of the AST 199 | 200 | *Parameters:* 201 | 202 | * `node` 203 | 204 | 205 | *Returns:* 206 | 207 | * `Object` 208 | 209 | 210 |

211 | 212 | ##### `clone` (Object:node) -> Object 213 | 214 | Recursive clone a node. Do no include "$" properties like $parent or $id 215 | If you want those, call `parentize` - `idze` after cloning 216 | 217 | *Parameters:* 218 | 219 | * `node` 220 | 221 | 222 | *Returns:* 223 | 224 | * `Object` 225 | 226 | 227 |

228 | 229 | #### debug 230 | 231 | 232 | ##### `debug_tree` (Object:tree [, Number:max_width] [, Boolean:display_code_in_tree]) 233 | 234 | Show your tree in various ways to easy debug 235 | Big trees will be always a pain, so keep it small if possible 236 | 237 | *Parameters:* 238 | 239 | * `tree`: Any node, if root tokens & source will be displayed 240 | 241 | * `max_width`: max tokens per line 242 | 243 | * `display_code_in_tree`: when display the tree attach the code on the right 244 | 245 | 246 | 247 |

248 | 249 | #### query 250 | 251 | 252 | ##### `getFunction` (Object:node, String:fn_name) -> Object|NULL 253 | 254 | `filter` the AST and return the function with given name, null otherwise. 255 | 256 | *Parameters:* 257 | 258 | * `node` 259 | 260 | * `fn_name` 261 | 262 | 263 | *Returns:* 264 | 265 | * `Object|NULL` 266 | 267 | 268 |

269 | 270 | ##### `getFunctionBlock` (Object:node, String:fn_name) -> Object|NULL 271 | 272 | `filter` the AST and return the function > block with given name, null otherwise. 273 | 274 | *Parameters:* 275 | 276 | * `node` 277 | 278 | * `fn_name` 279 | 280 | 281 | *Returns:* 282 | 283 | * `Object|NULL` 284 | 285 | 286 |

287 | 288 | ##### `isFunctionDeclared` (Object:node, String:fn_name) -> Boolean 289 | 290 | shortcut 291 | 292 | *Parameters:* 293 | 294 | * `node` 295 | 296 | * `fn_name` 297 | 298 | 299 | *Returns:* 300 | 301 | * `Boolean` 302 | 303 | 304 |

305 | 306 | ##### `hasVarDeclaration` (Object:node, String:var_name) -> Boolean 307 | 308 | shortcut 309 | 310 | *Parameters:* 311 | 312 | * `node` 313 | 314 | * `var_name` 315 | 316 | 317 | *Returns:* 318 | 319 | * `Boolean` 320 | 321 | 322 |

323 | 324 | ##### `isVarDeclared` (Object:node, String:var_name) -> Boolean 325 | 326 | reverse from node to root and look for a Variable declaration 327 | 328 | *Parameters:* 329 | 330 | * `node` 331 | 332 | * `var_name` 333 | 334 | 335 | *Returns:* 336 | 337 | * `Boolean` 338 | 339 | *Note*: It's not perfect because `VariableDeclaration` it's not hoisted 340 | 341 | 342 |

343 | 344 | ##### `contains` (Object:node, Object:subnode) -> Boolean 345 | 346 | `node` constains `subnode` 347 | 348 | *Parameters:* 349 | 350 | * `node` 351 | 352 | * `subnode` 353 | 354 | 355 | *Returns:* 356 | 357 | * `Boolean` 358 | 359 | 360 |

361 | 362 | ##### `hasBody` (Object:node) -> Boolean 363 | 364 | Has a body property, use to freely attach/detach 365 | 366 | *Parameters:* 367 | 368 | * `node` 369 | 370 | 371 | *Returns:* 372 | 373 | * `Boolean` 374 | 375 | 376 |

377 | 378 | ##### `isComment` (Object:node) -> Boolean 379 | 380 | shortcut: Is a comment (Line or Block) and has text 381 | 382 | *Parameters:* 383 | 384 | * `node` 385 | 386 | 387 | *Returns:* 388 | 389 | * `Boolean` 390 | 391 | 392 |

393 | 394 | ##### `getComment` (Object:node, String:comment) -> Object 395 | 396 | shortcut: search for a comment (trim it's content for maximum compatibility) 397 | 398 | *Parameters:* 399 | 400 | * `node` 401 | 402 | * `comment` 403 | 404 | 405 | *Returns:* 406 | 407 | * `Object` 408 | 409 | 410 |

411 | 412 | ##### `getCode` (Object:node) -> String 413 | 414 | shortcut: Return node code 415 | 416 | *Parameters:* 417 | 418 | * `node` 419 | 420 | 421 | *Returns:* 422 | 423 | * `String` 424 | 425 | 426 |

427 | 428 | ##### `getArgumentList` (Object:node) -> Array 429 | 430 | Return `FunctionDeclaration` arguments name as a list 431 | 432 | *Parameters:* 433 | 434 | * `node` 435 | 436 | 437 | *Returns:* 438 | 439 | * `Array` 440 | 441 | 442 |

443 | 444 | ##### `getDefaultProperty` (Object:node) 445 | 446 | 447 | *Parameters:* 448 | 449 | * `node` 450 | 451 | 452 | 453 |

454 | 455 | #### manipulations 456 | 457 | 458 | ##### `attach` (Object:node, String:property, Number|NULL:position, String|Object:str) 459 | 460 | Attach Code/Program to given node. 461 | 462 | *Parameters:* 463 | 464 | * `node`: node to attach 465 | 466 | * `property`: Where attach, could be an array or an object 467 | 468 | * `position`: index if an array is used as target property 469 | 470 | * `str`: String is preferred if not possible remember that only Program can be attached, you may consider using `toProgram` 471 | 472 | 473 | *Note*: tokens are updated 474 | 475 | *Note*: range is updated 476 | 477 | *Note*: comments are not attached to root.comments (invalid-comments) 478 | 479 | 480 |

481 | 482 | ##### `attachPunctuator` (Object:tree, String:punctuator, Number:position) -> String 483 | 484 | Attach a punctuator and keep the tree ranges sane. 485 | The Punctuator can be anything... be careful! 486 | 487 | *Parameters:* 488 | 489 | * `tree` 490 | 491 | * `punctuator` 492 | 493 | * `position` 494 | 495 | 496 | *Returns:* 497 | 498 | * `String`: detached code string 499 | 500 | *Note*: The Punctuator is not parsed and could be assigned to nearest literal or alike. 501 | 502 | 503 |

504 | 505 | ##### `detach` (Object:node, String:property) -> String 506 | 507 | Detach given node from it's parent 508 | 509 | *Parameters:* 510 | 511 | * `node` 512 | 513 | * `property` 514 | 515 | 516 | *Returns:* 517 | 518 | * `String`: detached code string 519 | 520 | *Note*: `node.$parent` is set to `null`, remember to save it first if you need it. 521 | 522 | 523 |

524 | 525 | ##### `attachAfter` (Object:node, String|Object:str [, String:property]) 526 | 527 | Attach after node, that means `node.$parent.type` is a `BockStament` 528 | 529 | *Parameters:* 530 | 531 | * `node` 532 | 533 | * `str`: String is preferred if not possible remember that only Program can be attached, you may consider using `toProgram` 534 | 535 | * `property`: where to search node in the parent 536 | 537 | 538 | 539 |

540 | 541 | ##### `attachBefore` (Object:node, String|Object:str) 542 | 543 | Attach before node, that means `node.$parent.type` is a `BockStament` 544 | 545 | *Parameters:* 546 | 547 | * `node` 548 | 549 | * `str`: String is preferred if not possible remember that only Program can be attached, you may consider using `toProgram` 550 | 551 | 552 | 553 |

554 | 555 | ##### `attachAfterComment` (Object:node, String:comment, String|Object:str) -> Boolean 556 | 557 | Shortcut: Search for given comment, and attachAfter 558 | 559 | *Parameters:* 560 | 561 | * `node` 562 | 563 | * `comment` 564 | 565 | * `str`: String is preferred if not possible remember that only Program can be attached, you may consider using `toProgram` 566 | 567 | 568 | *Returns:* 569 | 570 | * `Boolean`: success 571 | 572 | 573 |

574 | 575 | ##### `replace` (Object:node, String|Object:str) 576 | 577 | Shortcut: detach/attach 578 | 579 | *Parameters:* 580 | 581 | * `node` 582 | 583 | * `str`: String is preferred if not possible remember that only Program can be attached, you may consider using `toProgram` 584 | 585 | 586 | 587 |

588 | 589 | ##### `replaceComment` (Object:node, String:comment, String|Object:str) 590 | 591 | Shortcut: Search for a comment and replace 592 | 593 | *Parameters:* 594 | 595 | * `node` 596 | 597 | * `comment` 598 | 599 | * `str`: String is preferred if not possible remember that only Program can be attached, you may consider using `toProgram` 600 | 601 | 602 | 603 |

604 | 605 | ##### `injectCode` (Object:tree, Array:range, String|Object:str, Boolean:debug) 606 | 607 | Inject code directly intro the given range. 608 | After the injection the code will be parsed again so original `$id` will be lost 609 | 610 | *Parameters:* 611 | 612 | * `tree` 613 | 614 | * `range` 615 | 616 | * `str`: String is preferred if not possible remember that only Program can be attached, you may consider using `toProgram` 617 | 618 | * `debug`: display $id, $parent and $code in console.log (enumerable=true) 619 | 620 | 621 | *Note*: this is dangerous and powerful 622 | 623 | 624 |

625 | 626 | #### transformations 627 | 628 | 629 | ##### `setIdentifier` (Object:node, String:new_name) 630 | 631 | rename `Identifier` 632 | 633 | *Parameters:* 634 | 635 | * `node` 636 | 637 | * `new_name` 638 | 639 | 640 | 641 |

642 | 643 | ##### `renameProperty` (Object:node, Object:replacements) 644 | 645 | `traverse` and apply given `replacements` 646 | 647 | *Parameters:* 648 | 649 | * `node` 650 | 651 | * `replacements` 652 | 653 | 654 | *Example*: 655 | ```js 656 | renameProperty(node, {"old_var": "new_var", "much_older": "shinnig_new"}) 657 | ``` 658 | 659 | 660 |

661 | 662 | ##### `renameVariable` (Object:node, Object:replacements) 663 | 664 | `traverse` and apply given `replacements` 665 | 666 | *Parameters:* 667 | 668 | * `node` 669 | 670 | * `replacements` 671 | 672 | 673 | *Example*: 674 | ```js 675 | renameVariable(node, {"old_var": "new_var", "much_older": "shinnig_new"}) 676 | ``` 677 | 678 | 679 |

680 | 681 | ##### `renameFunction` (Object:node, Object:replacements) 682 | 683 | traverse and apply given `replacements` 684 | 685 | *Parameters:* 686 | 687 | * `node` 688 | 689 | * `replacements` 690 | 691 | 692 | *Example*: 693 | ```js 694 | renameFunction(node, {"old_var": "new_var", "much_older": "shinnig_new"}) 695 | ``` 696 | 697 | 698 |

699 | 700 | ##### `toProgram` (Object|Array:node) 701 | 702 | Clone given node(s) and extract tokens & code from root to given you a Program-like attachable node 703 | 704 | *Parameters:* 705 | 706 | * `node`: if array is provided will add all nodes to program.body 707 | 708 | 709 | 710 |

711 | 712 | #### tokens 713 | 714 | 715 | ##### `getToken` (Object:tree, Number:start, Number:end) -> Object|NULL 716 | 717 | Get token based on given range 718 | 719 | *Parameters:* 720 | 721 | * `tree` 722 | 723 | * `start` 724 | 725 | * `end` 726 | 727 | 728 | *Returns:* 729 | 730 | * `Object|NULL` 731 | 732 | 733 |

734 | 735 | ##### `getTokens` (Object:tree, Number:start, Number:end) -> Array|NULL 736 | 737 | Get tokens in range 738 | 739 | *Parameters:* 740 | 741 | * `tree` 742 | 743 | * `start` 744 | 745 | * `end` 746 | 747 | 748 | *Returns:* 749 | 750 | * `Array|NULL` 751 | 752 | 753 |

754 | 755 | ##### `pushTokens` (Object:tree, Number:start, Number:amount) 756 | 757 | Push tokens range from start 758 | 759 | *Parameters:* 760 | 761 | * `tree` 762 | 763 | * `start` 764 | 765 | * `amount` 766 | 767 | 768 | *Note*: Update nodes range 769 | 770 | 771 |

772 | 773 | ##### `growTokens` (Object:tree, Number:start, Number:end, Number:amount) 774 | 775 | Grow tokens in given range 776 | 777 | *Parameters:* 778 | 779 | * `tree` 780 | 781 | * `start` 782 | 783 | * `end` 784 | 785 | * `amount` 786 | 787 | 788 | *Note*: Update nodes range 789 | 790 | 791 |

792 | 793 | ##### `tokenAt` (Object:tree, Number:start) -> Object 794 | 795 | Get the first token 796 | 797 | *Parameters:* 798 | 799 | * `tree` 800 | 801 | * `start` 802 | 803 | 804 | *Returns:* 805 | 806 | * `Object` 807 | 808 | 809 |

810 | 811 | ##### `addTokens` (Object:dst_tree, Object|Array:src, Number:start) 812 | 813 | Add `src` tokens to `dst` since `start` (so keep the order) 814 | 815 | *Parameters:* 816 | 817 | * `dst_tree` 818 | 819 | * `src` 820 | 821 | * `start` 822 | 823 | 824 | *Note*: Remember to push `src` tokens before `addTokens` otherwise won't be synced 825 | 826 | 827 |

828 | 829 | ##### `replaceCodeRange` (Object:tree, Array:range, String:new_text) 830 | 831 | Replace code range with given text. 832 | 833 | *Parameters:* 834 | 835 | * `tree` 836 | 837 | * `range` 838 | 839 | * `new_text` 840 | 841 | 842 | 843 |

844 | 845 | ##### `removeTokens` (Object:tree, Number:start, Number:end) 846 | 847 | Remove tokens in range and update ranges 848 | 849 | *Parameters:* 850 | 851 | * `tree` 852 | 853 | * `start` 854 | 855 | * `end` 856 | 857 | 858 | *Note*: Do not remove nodes. 859 | 860 | 861 |

862 | 863 | # LICENSE 864 | 865 | (The MIT License) 866 | 867 | Copyright (c) 2014 Luis Lafuente 868 | 869 | Permission is hereby granted, free of charge, to any person obtaining 870 | a copy of this software and associated documentation files (the 871 | 'Software'), to deal in the Software without restriction, including 872 | without limitation the rights to use, copy, modify, merge, publish, 873 | distribute, sublicense, and/or sell copies of the Software, and to 874 | permit persons to whom the Software is furnished to do so, subject to 875 | the following conditions: 876 | 877 | The above copyright notice and this permission notice shall be 878 | included in all copies or substantial portions of the Software. 879 | 880 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 881 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 882 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 883 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 884 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 885 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 886 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 887 | 888 | --------------------------------------------------------------------------------