Skip to content
Browse files

Initial version.

  • Loading branch information...
0 parents commit fe7e7e3e01d180a4709baae1fedb18e12423bf27 @Gozala committed Jan 3, 2013
Showing with 565 additions and 0 deletions.
  1. +5 −0 .travis.yml
  2. +5 −0 History.md
  3. +18 −0 License.md
  4. +97 −0 Readme.md
  5. +96 −0 ast.js
  6. +21 −0 bindings.js
  7. +6 −0 index.js
  8. +46 −0 package.json
  9. +15 −0 properties.js
  10. +20 −0 references.js
  11. +14 −0 scopes.js
  12. +15 −0 test/ast.js
  13. +34 −0 test/bindings.js
  14. +3 −0 test/common.js
  15. +55 −0 test/fixtures/scope.js
  16. +7 −0 test/index.js
  17. +24 −0 test/properties.js
  18. +36 −0 test/references.js
  19. +31 −0 test/scopes.js
  20. +17 −0 tree.js
5 .travis.yml
@@ -0,0 +1,5 @@
+language: node_js
+node_js:
+ - 0.4
+ - 0.5
+ - 0.6
5 History.md
@@ -0,0 +1,5 @@
+# Changes
+
+## 0.0.1 / 2012-12-27
+
+ - Initial release
18 License.md
@@ -0,0 +1,18 @@
+Copyright 2012 Irakli Gozalishvili. All rights reserved.
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
97 Readme.md
@@ -0,0 +1,97 @@
+# episcope
+
+[![Build Status](https://secure.travis-ci.org/Gozala/episcope.png)](http://travis-ci.org/Gozala/episcope)
+
+ECMAScript scope analyzer. Library provides set of functions that perform
+analyzes on the nodes of the AST in the de facto [syntax tree format][ast].
+All the API function take AST nodes denoting a lexical scope and performed
+static analyzes at the given scope level.
+
+
+## API
+
+
+#### references
+
+Returns array of `Identifier` nodes for all the free references with in the
+given scope that are not part of declarations or members access identifiers.
+
+```js
+var esprima = require("esprima")
+var references = require("episcope/references")
+var ast = esprima.parse("console.log('>>>', error)")
+references(ast)
+// => [{ type: "Identifier", name: "console" }, { type: "Identifier", name: "error" }]
+```
+
+#### bindings
+
+Returns array of `Identifier` nodes for all the declared bindings available
+to the given scope, including named arguments if given scope is a function
+form.
+
+```js
+var esprima = require("esprima")
+var bindings = require("episcope/bindings")
+var ast = esprima.parse("function foo(a, b) { var c = a + b; return c * c }")
+ast.body[0].id
+// => { type: 'Identifier', name: 'foo' }
+bindings(ast.body[0])
+// => [ { type: 'Identifier', name: 'a' },
+// { type: 'Identifier', name: 'b' },
+// { type: 'Identifier', name: 'c' } ]
+```
+
+#### scopes
+
+Returns array of nested scope forms for the given one. Note the nested scopes
+of those nested scopes are not included, but this function can be used to
+do the walk through them too.
+
+```js
+var esprima = require("esprima")
+var scopes = require("episcope/scopes")
+var ast = esprima.parse(String(function root() {
+ function nested() { /***/ }
+ try { /***/ } catch(error) { /***/ }
+}))
+ast.body[0].id
+// => { type: 'Identifier', name: 'foo' }
+
+scopes(ast.body[0])
+// => [
+// {
+// type: 'FunctionDeclaration',
+// id: { type: 'Identifier', name: 'nested' },
+// // ...
+// },
+// {
+// type: 'CatchClause',
+// param: { type: 'Identifier', name: 'error' },
+// body: { type: 'BlockStatement', body: [] }
+// }
+//]
+```
+
+#### properties
+
+Returns array of `Identifier` nodes for all the property references within
+the given scope. Mainly used internally to filter out references to free
+variables.
+
+```js
+var esprima = require("esprima")
+var properties = require("episcope/properties")
+var ast = esprima.parse("document.body.appendChild(node)")
+
+properties(ast)
+// => [ { type: 'Identifier', name: 'appendChild' },
+// { type: 'Identifier', name: 'body' } ]
+```
+
+## Install
+
+ npm install episcope
+
+[esprima]:http://esprima.org/
+[ast]:http://esprima.org/doc/index.html#ast
96 ast.js
@@ -0,0 +1,96 @@
+"use strict";
+
+var keys = Object.keys
+function property(name) { return this[name] }
+function values(form) { return keys(form).map(property, form) }
+function isObject(form) { return form && typeof(form) === "object" }
+function isNode(form) { return isObject(form) && !!form.type }
+
+function isBranch(node) { return isObject(node) }
+exports.isBranch = isBranch
+
+function append(left, right) { return left.concat(right) }
+
+var defineProperty = Object.defineProperty
+function defineParent(node) {
+ return defineProperty(node, "parent", {
+ value: this,
+ enumerable: false,
+ // TODO: Remove this
+ configurable: true
+ })
+}
+
+function children(form) {
+ return values(form).
+ // If property is an array then inline it's elements as children.
+ reduce(append, []).
+ // Ignore non node properties
+ filter(isNode).
+ // Define references to the parent nodes
+ map(defineParent, form)
+}
+exports.children = children
+
+var slicer = Array.prototype.slice
+function select(forms) {
+ var types = slicer.call(arguments, 1)
+ return forms.filter(function(form) {
+ return types.indexOf(form.type) >= 0
+ })
+}
+exports.select = select
+
+function isFunctionDeclaration(form) {
+ return form.type === "FunctionDeclaration"
+}
+exports.isFunctionDeclaration = isFunctionDeclaration
+
+function isVariableDeclarator(form) {
+ return form.type === "VariableDeclarator"
+}
+exports.isVariableDeclarator = isVariableDeclarator
+
+function isDeclaration(form) {
+ return isFunctionDeclaration(form) ||
+ isVariableDeclarator(form)
+}
+exports.isDeclaration = isDeclaration
+
+function isFunction(form) {
+ /**
+ Returns `true` if given from is a function expression or declaration.
+ **/
+ return form.type === "FunctionExpression" ||
+ form.type === "FunctionDeclaration"
+}
+exports.isFunction = isFunction
+
+function isCatch(form) {
+ /**
+ Returns `true` if given form is a `catch` clause.
+ **/
+ return form.type === "CatchClause"
+}
+exports.isCatch = isCatch
+
+function isScope(form) {
+ /**
+ Returns `true` if given form forms a `scope`, which means it's either
+ function, catch clause, or a `with` statement.
+ **/
+ return form.type === "Program" ||
+ isFunction(form) ||
+ isCatch(form) ||
+ form.type === "WithStatement"
+}
+exports.isScope = isScope
+
+function isntScope(form) {
+ /**
+ Returns `true` if given form is a branch node, but it isn't a scope form,
+ this way tree walk won't analyze nested scopes.
+ **/
+ return isBranch(form) && !isScope(form)
+}
+exports.isntScope = isntScope
21 bindings.js
@@ -0,0 +1,21 @@
+"use strict";
+
+var tree = require("./tree")
+var ast = require("./ast")
+
+function getId(node) { return node.id }
+
+module.exports = function bindings(scope) {
+ /**
+ Returns array of `Identifier` nodes for all the declared bindings available
+ to the given scope, including named arguments if given scope is a function
+ form.
+ **/
+ var initials = ast.isFunction(scope) ? scope.params.concat(scope.rest || []) :
+ ast.isCatch(scope) ? [scope.param] :
+ []
+ var nodes = tree(scope.body, ast.isntScope, ast.children)
+ var declarations = nodes.filter(ast.isDeclaration).map(getId)
+
+ return initials.concat(declarations)
+}
6 index.js
@@ -0,0 +1,6 @@
+"use strict";
+
+exports.properties = require("./properties")
+exports.references = require("./references")
+exports.scopes = require("./scopes")
+exports.bindings = require("./bindings")
46 package.json
@@ -0,0 +1,46 @@
+{
+ "name": "episcope",
+ "id": "episcope",
+ "version": "0.0.1",
+ "description": "ECMAScript scope analyzer",
+ "keywords": [
+ "episcope",
+ "scope",
+ "AST",
+ "ecmascript",
+ "analizer"
+ ],
+ "author": "Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)",
+ "homepage": "https://github.com/Gozala/episcope",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/Gozala/episcope.git",
+ "web": "https://github.com/Gozala/episcope"
+ },
+ "bugs": {
+ "url": "http://github.com/Gozala/episcope/issues/"
+ },
+ "devDependencies": {
+ "test": "~0.x.0",
+ "phantomify": "~0.x.0",
+ "repl-utils": "~2.0.1"
+ },
+ "main": "./index.js",
+ "scripts": {
+ "repl": "node node_modules/repl-utils",
+ "test": "npm run test-node && npm run test-browser",
+ "test-browser": "node ./node_modules/phantomify/bin/cmd.js ./test/common.js",
+ "test-node": "node ./test/common.js",
+ "postinstall": "npm dedup"
+ },
+ "licenses": [
+ {
+ "type": "MIT",
+ "url": "https://github.com/Gozala/episcope/License.md"
+ }
+ ],
+ "dependencies": {
+ "esprima": "git://github.com/ariya/esprima.git#harmony",
+ "interset": "0.0.1"
+ }
+}
15 properties.js
@@ -0,0 +1,15 @@
+"use strict";
+
+var tree = require("./tree")
+var ast = require("./ast")
+
+module.exports = function properties(scope) {
+ /**
+ Returns array of `Identifier` nodes for all the property references with
+ in the given scope. Mainly useful for filtering out non property references
+ **/
+ var nodes = tree(scope.body, ast.isntScope, ast.children)
+ return ast.select(nodes, "MemberExpression").map(function(node) {
+ return node.property
+ })
+}
20 references.js
@@ -0,0 +1,20 @@
+"use strict";
+
+var tree = require("./tree")
+var difference = require("interset/difference")
+var ast = require("./ast")
+var bindings = require("./bindings")
+var properties = require("./properties")
+
+module.exports = function references(scope) {
+ /**
+ Returns array of `Identifier` nodes for all the free references that are not
+ part of declarations or members access identifiers.
+ **/
+ var nodes = tree(scope.body, ast.isntScope, ast.children)
+ // Get all the identifier nodes.
+ var ids = ast.select(nodes, "Identifier")
+ // Return all identifiers excluding ones that are part of definition
+ // or property names.
+ return difference(ids, bindings(scope), properties(scope))
+}
14 scopes.js
@@ -0,0 +1,14 @@
+"use strict";
+
+var tree = require("./tree")
+var ast = require("./ast")
+
+module.exports = function scopes(scope) {
+ /**
+ Returns array of nested scope forms for the given one. Note the nested scopes
+ of those nested scopes are not included, but this function can be used to
+ do the walk through them too.
+ **/
+ var forms = tree(scope.body, ast.isntScope, ast.children)
+ return forms.filter(ast.isScope)
+}
15 test/ast.js
@@ -0,0 +1,15 @@
+"use strict";
+
+var fixtures = require("./fixtures/scope")
+var ast = require("../ast")
+
+exports["test isScope"] = function(assert) {
+ assert.ok(ast.isScope(fixtures.program),
+ "Program is a scope node")
+ assert.ok(ast.isScope(fixtures.FunctionDeclaration),
+ "FunctionDeclaration creates scope")
+ assert.ok(ast.isScope(fixtures.FunctionExpression),
+ "FunctionExpression creates scope")
+ assert.ok(ast.isScope(fixtures.CatchClause),
+ "CatchClause creates scope")
+}
34 test/bindings.js
@@ -0,0 +1,34 @@
+"use strict";
+
+var fixtures = require("./fixtures/scope")
+var bindings = require("../bindings")
+
+exports["test FunctionExpression"] = function(assert) {
+ assert.deepEqual(bindings(fixtures.FunctionExpression), [
+ { type: "Identifier", name: "c" },
+ { type: "Identifier", name: "d" }
+ ], "both argumentss are included")
+}
+
+exports["test CatchClause"] = function(assert) {
+ assert.deepEqual(bindings(fixtures.CatchClause), [
+ { type: "Identifier", name: "error" }
+ ], "error name in catch is included")
+}
+
+exports["test FunctionDeclaration"] = function(assert) {
+ assert.deepEqual(bindings(fixtures.FunctionDeclaration), [
+ { type: "Identifier", name: "a" },
+ { type: "Identifier", name: "b" },
+ { type: "Identifier", name: "c" },
+ { type: "Identifier", name: "d" },
+ { type: "Identifier", name: "e" },
+ { type: "Identifier", name: "i" },
+ { type: "Identifier", name: "l" },
+ { type: "Identifier", name: "f" },
+ { type: "Identifier", name: "nestedExpression" },
+ { type: "Identifier", name: "nestedDeclaration" },
+ { type: "Identifier", name: "error" },
+ { type: "Identifier", name: "final" }
+ ], "All declarations except ones from nested scopes")
+}
3 test/common.js
@@ -0,0 +1,3 @@
+"use strict";
+
+require("test").run(require("./index"))
55 test/fixtures/scope.js
@@ -0,0 +1,55 @@
+"use strict";
+
+var esprima = require("esprima")
+
+var program = esprima.parse(function fixture() {
+ function foo(a, b) {
+ var c = 3,
+ d
+
+ var e = []
+ d = {}
+
+ for (var i = 0, l = a.length; i < l; i++) {
+ var f = i - 1
+ d["foo"] = b[i]
+ }
+
+ function nestedExpression(f, r) {
+ var nestedVariable
+ }
+
+ var nestedDeclaration = function nested_skip() {
+ }
+
+ try {
+ var error = Error("boom")
+ throw error
+ } catch (error) {
+ var message = error.message
+ console.log(message)
+ } finally {
+ var final = true
+ }
+
+ return a + b + c
+ }
+
+ var bar = function(c, d) {
+ return c.concat(d)
+ }
+
+ try {
+ throw Error("boom!")
+ } catch (error) {
+ console.log(error)
+ }
+})
+
+var expressions = program.body[0].body.body
+
+exports.program = program
+exports.expressions = expressions
+exports.FunctionDeclaration = expressions[0]
+exports.FunctionExpression = expressions[1].declarations[0].init
+exports.CatchClause = expressions[2].handlers[0]
7 test/index.js
@@ -0,0 +1,7 @@
+"use strict";
+
+exports["test ast"] = require("./ast")
+exports["test references"] = require("./references")
+exports["test properties"] = require("./properties")
+exports["test bindings"] = require("./bindings")
+exports["test scopes"] = require("./scopes")
24 test/properties.js
@@ -0,0 +1,24 @@
+"use strict";
+
+var fixtures = require("./fixtures/scope")
+var properties = require("../properties")
+
+exports["test FunctionExpression"] = function(assert) {
+ assert.deepEqual(properties(fixtures.FunctionExpression), [
+ { type: "Identifier", name: "concat" }
+ ], "all properties are returned")
+}
+
+exports["test CatchClause"] = function(assert) {
+ assert.deepEqual(properties(fixtures.CatchClause), [
+ { type: "Identifier", name: "log" }
+ ], "console properties is logged")
+}
+
+exports["test FunctionDeclaration"] = function(assert) {
+ assert.deepEqual(properties(fixtures.FunctionDeclaration), [
+ { type: "Identifier", name: "length" },
+ { type: "Literal", value: "foo", raw: '"foo"' },
+ { type: "Identifier", name: "i" }
+ ], "All properties including non identifiers are returned")
+}
36 test/references.js
@@ -0,0 +1,36 @@
+"use strict";
+
+var fixtures = require("./fixtures/scope")
+var references = require("../references")
+
+exports["test FunctionExpression"] = function(assert) {
+ assert.deepEqual(references(fixtures.FunctionExpression), [
+ { type: "Identifier", name: "c" },
+ { type: "Identifier", name: "d" },
+ ], "all references are returned")
+}
+
+exports["test CatchClause"] = function(assert) {
+ assert.deepEqual(references(fixtures.CatchClause), [
+ { type: "Identifier", name: "console" },
+ { type: "Identifier", name: "error" }
+ ], "Both references identified")
+}
+
+exports["test FunctionDeclaration"] = function(assert) {
+ assert.deepEqual(references(fixtures.FunctionDeclaration), [
+ { type: "Identifier", name: "d" },
+ { type: "Identifier", name: "a" },
+ { type: "Identifier", name: "i" },
+ { type: "Identifier", name: "l" },
+ { type: "Identifier", name: "i" },
+ { type: "Identifier", name: "i" },
+ { type: "Identifier", name: "d" },
+ { type: "Identifier", name: "b" },
+ { type: "Identifier", name: "Error" },
+ { type: "Identifier", name: "error" },
+ { type: "Identifier", name: "a" },
+ { type: "Identifier", name: "b" },
+ { type: "Identifier", name: "c" }
+ ], "Same named references may appear several times if refered several times")
+}
31 test/scopes.js
@@ -0,0 +1,31 @@
+"use strict";
+
+var fixtures = require("./fixtures/scope")
+var scopes = require("../scopes")
+
+exports["test program scopes"] = function(assert) {
+ var actual = scopes(fixtures.program)
+ assert.equal(actual.length, 1, "single scope found")
+ assert.deepEqual(actual[0].id, { type: 'Identifier', name: 'fixture' },
+ "top function is found")
+}
+
+exports["test FunctionExpression"] = function(assert) {
+ assert.deepEqual(scopes(fixtures.FunctionExpression), [],
+ "function expression has no nested scopes")
+}
+
+exports["test FunctionDeclaration"] = function(assert) {
+ var actual = scopes(fixtures.FunctionDeclaration)
+ assert.equal(actual.length, 3, "three nested scopes discovered")
+ assert.deepEqual(actual[0].id, {
+ type: 'Identifier',
+ name: 'nestedExpression'
+ }, "nested function expression")
+ assert.deepEqual(actual[1].id, {
+ type: 'Identifier',
+ name: 'nested_skip'
+ })
+ assert.deepEqual(actual[2].param, { type: 'Identifier', name: 'error' },
+ "last nested scopes is catch clause")
+}
17 tree.js
@@ -0,0 +1,17 @@
+"use strict";
+
+function concat(left, right) {
+ return [].concat(left, right)
+}
+
+function expand(array, f) {
+ return array.map(f).reduce(concat, [])
+}
+
+function tree(form, isBranch, nodes) {
+ function traversable(node) { return tree(node, isBranch, nodes) }
+ var children = isBranch(form) ? expand(nodes(form), traversable) : []
+ return [form].concat(children)
+}
+
+module.exports = tree

0 comments on commit fe7e7e3

Please sign in to comment.
Something went wrong with that request. Please try again.