compare-ast
Advanced tools
Comparing version 0.1.2 to 0.2.0
var parse = require('esprima').parse; | ||
var Errors = require('./match-error'); | ||
var toLiteral = require('./util/identifier-to-literal'); | ||
var normalizeMemberExpr = require('./util/normalize-member-expr'); | ||
@@ -10,7 +10,24 @@ // Given a "template" expected AST that defines abstract identifier names | ||
var actualAst, expectedAst; | ||
options = options || {}; | ||
// Lookup table of bound variable names | ||
options.boundVars = {}; | ||
if (!options.comparators) { | ||
options.comparators = []; | ||
} | ||
/* | ||
* A collection of comparator functions that recognize equivalent nodes | ||
* that would otherwise be reported as unequal by simple object comparison. | ||
* These may: | ||
* | ||
* - return `true` if the given nodes can be verified as equivalent with no | ||
* further processing | ||
* - return an instance of MatchError if the nodes are verified as | ||
* uniquivalent | ||
* - return any other value if equivalency cannot be determined | ||
*/ | ||
Array.prototype.push.apply(options.comparators, [ | ||
require('./comparators/fuzzy-identifiers')(options), | ||
require('./comparators/fuzzy-strings')(options) | ||
]); | ||
try { | ||
@@ -52,3 +69,6 @@ actualAst = parse(actualSrc).body; | ||
if (checkEquivalency(actual, expected, options)) { | ||
normalizeMemberExpr(actual); | ||
normalizeMemberExpr(expected); | ||
if (checkEquivalency(options.comparators, actual, expected)) { | ||
return; | ||
@@ -73,134 +93,21 @@ } | ||
var checkEquivalency = function(actual, expected, options) { | ||
var len = comparators.length; | ||
var comparator, idx, types; | ||
for (idx = 0; idx < len; ++idx) { | ||
comparator = comparators[idx]; | ||
types = comparator.types; | ||
if (!(actual.type === types[0] && expected.type === types[1]) && | ||
!(actual.type === types[1] && expected.type === types[0])) { | ||
continue; | ||
} | ||
if (comparator.handler(actual, expected, options)) { | ||
var checkEquivalency = function(comparators, actual, expected) { | ||
var result = comparators.map(function(comparator) { | ||
return comparator(actual, expected); | ||
}).reduce(function(prev, current) { | ||
if (current === true) { | ||
return true; | ||
} | ||
return prev || current; | ||
}, null); | ||
if (result instanceof Errors) { | ||
throw result; | ||
} else if (result === true) { | ||
return true; | ||
} | ||
return false; | ||
}; | ||
// A collection of comparator functions that recognize equivalent nodes that | ||
// would otherwise be reported as unequal by simple object comparison. These | ||
// may: | ||
// | ||
// - return `true` if the given nodes can be verified as equivalent with no | ||
// further processing | ||
// - throw an error if the nodes are verified as uniquivalent | ||
// - return any other value if equivalency cannot be determined | ||
var comparators = [ | ||
{ | ||
// Update unbound variable names in the expected AST, using the | ||
// previously-bound value when available. | ||
types: ['Identifier', 'Identifier'], | ||
handler: function(actual, expected, options) { | ||
var varPattern = options.varPattern; | ||
var boundVars = options.boundVars; | ||
var unboundName; | ||
if (!varPattern) { | ||
return; | ||
} | ||
if (varPattern.test(expected.name)) { | ||
unboundName = expected.name; | ||
if (!(unboundName in boundVars)) { | ||
boundVars[unboundName] = actual.name; | ||
} | ||
expected.name = boundVars[unboundName]; | ||
// This inequality would be caught by the next recursion, but it is | ||
// asserted here to provide a more specific error--the ASTs do not | ||
// match because a variable was re-bound. | ||
if (expected.name !== actual.name) { | ||
throw new Errors.BindingError(actual.name, expected.name); | ||
} else { | ||
return true; | ||
} | ||
} | ||
} | ||
}, | ||
{ | ||
// Recognize simple string deferencing as equivalent to "dot notation" | ||
// with a matching identifier (i.e. `a.b;` is equivalent to `a['b'];`). | ||
types: ['MemberExpression', 'MemberExpression'], | ||
handler: function(actual, expected, options) { | ||
var literal, props; | ||
// When the actual property is an Identifier, compare its literal | ||
// representation against the expected property. | ||
if (actual.property.type === 'Identifier') { | ||
literal = toLiteral(actual.property); | ||
if (checkEquivalency(literal, expected.property, options)) { | ||
actual.property = literal; | ||
return true; | ||
} | ||
} | ||
props = {}; | ||
if (actual.property.type === 'Literal') { | ||
props.Literal = actual.property.value; | ||
} else if (actual.property.type === 'Identifier') { | ||
props.Identifier = actual.property.name; | ||
} | ||
if (expected.property.type === 'Literal') { | ||
props.Literal = expected.property.value; | ||
} else if (expected.property.type === 'Identifier') { | ||
props.Identifier = expected.property.name; | ||
} | ||
return 'Identifier' in props && 'Literal' in props && | ||
props.Identifier === props.Literal; | ||
} | ||
}, | ||
{ | ||
// Fuzzy string matching | ||
types: ['Literal', 'Literal'], | ||
handler: function(actual, expected, options) { | ||
var stringPattern = options.stringPattern; | ||
var boundStrings; | ||
if (!options.boundStrings) { | ||
options.boundStrings = {}; | ||
} | ||
boundStrings = options.boundStrings; | ||
if (!stringPattern) { | ||
return false; | ||
} | ||
if (!stringPattern.test(expected.value)) { | ||
return false; | ||
} | ||
var expectedValue = boundStrings[expected.value]; | ||
// This string value has previously been bound | ||
if (expectedValue) { | ||
if (expectedValue !== actual.value) { | ||
throw new Errors.BindingError(actual.value, expected.value); | ||
} | ||
} else { | ||
boundStrings[expected.value] = actual.value; | ||
} | ||
expected.value = actual.value; | ||
return true; | ||
} | ||
} | ||
]; | ||
compareAst.Error = Errors; | ||
module.exports = compareAst; |
{ | ||
"name": "compare-ast", | ||
"version": "0.1.2", | ||
"version": "0.2.0", | ||
"description": "Determine if two strings of JavaScript have equivalent abstract syntax trees.", | ||
@@ -15,3 +15,4 @@ "main": "lib/compare.js", | ||
"test": "mocha --ui tdd", | ||
"jshint": "jshint lib/ test/" | ||
"jshint": "jshint lib/ test/", | ||
"ci": "npm run jshint && npm test" | ||
}, | ||
@@ -18,0 +19,0 @@ "author": "Mike Pennisi", |
@@ -6,2 +6,5 @@ # compareAst | ||
[![Build | ||
Status](https://travis-ci.org/jugglinmike/compare-ast.png)](https://travis-ci.org/jugglinmike/compare-ast) | ||
## API | ||
@@ -13,2 +16,33 @@ | ||
### Built-in Relaxing Options | ||
compareAst supports configuration of the following relaxing options: | ||
- `varPattern` - a regular expression describing identifier names that, when | ||
encountered in the "expected" AST, will be "bound" to the corresponding | ||
identifier in the "actual" AST. All further occurences of that identifier | ||
must match the original bound value. | ||
- `stringPattern` - a regular expression describing string values that, when | ||
encountered in the "expected" AST, will be "bound" to the corresponding | ||
string value in the "actual" AST. All further occurences of that string value | ||
must match the original bound value. | ||
See the "Examples" section below for more information on defining these | ||
relaxing options. | ||
### Custom Comparators | ||
The `options` object may specify an array of `comparators`. These functions can | ||
be used to further relax the criteria for equivalency. Each will be invoked for | ||
every node under comparison. These nodes are generated by | ||
[esprima](http://esprima.org/); see the [esprima | ||
documentation](http://esprima.org/doc/index.html) for a description of their | ||
structure. | ||
compareAst recognizes the following comparator return types: | ||
- Instance of `compareAst.Errors` - the two nodes are not equivalent | ||
- `true` - the two nodes are equivalent | ||
- `undefined` - equivalency cannot be determined by this comparator | ||
## Examples | ||
@@ -15,0 +49,0 @@ |
@@ -60,2 +60,21 @@ var compareAst = require('..'); | ||
test('custom comparator', function() { | ||
var vals = [3, 4]; | ||
var threeOrFour = function(actual, expected) { | ||
if (actual.type !== 'Literal' || expected.type !== 'Literal') { | ||
return; | ||
} | ||
if (vals.indexOf(actual.value) > -1 && | ||
vals.indexOf(expected.value) > -1) { | ||
return true; | ||
} | ||
}; | ||
compareAst( | ||
'a.b + 3', | ||
'a["b"] + 4', | ||
{ comparators: [threeOrFour] } | ||
); | ||
}); | ||
suite('expected failures', function() { | ||
@@ -62,0 +81,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
14654
12
340
84