compare-ast
Advanced tools
Comparing version 0.1.0 to 0.1.1
var parse = require('esprima').parse; | ||
var Errors = require('./match-error'); | ||
var toLiteral = require('./util/identifier-to-literal'); | ||
@@ -27,3 +28,3 @@ // Given a "template" expected AST that defines abstract identifier names | ||
function _bind(actual, expected) { | ||
var attr, comparator, areEquivalent; | ||
var attr; | ||
@@ -48,8 +49,4 @@ // Literal values | ||
comparator = comparators[actual.type]; | ||
if (comparator) { | ||
areEquivalent = comparator(actual, expected, options); | ||
if (areEquivalent) { | ||
return; | ||
} | ||
if (checkEquivalency(actual, expected, options)) { | ||
return; | ||
} | ||
@@ -73,2 +70,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)) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
}; | ||
// A collection of comparator functions that recognize equivalent nodes that | ||
@@ -82,51 +98,104 @@ // would otherwise be reported as unequal by simple object comparison. These | ||
// - 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. | ||
Identifier: function(actual, expected, options) { | ||
var varPattern = options.varPattern; | ||
var boundVars = options.boundVars; | ||
var unboundName; | ||
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; | ||
if (!varPattern) { | ||
return; | ||
} | ||
expected.name = boundVars[unboundName]; | ||
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; | ||
// 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'];`). | ||
MemberExpression: function(actual, expected) { | ||
var props = {}; | ||
{ | ||
// 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; | ||
if (actual.property.type === 'Literal') { | ||
props.Literal = actual.property.value; | ||
} else if (actual.property.type === 'Identifier') { | ||
props.Identifier = actual.property.name; | ||
// 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 (expected.property.type === 'Literal') { | ||
props.Literal = expected.property.value; | ||
} else if (expected.property.type === 'Identifier') { | ||
props.Identifier = expected.property.name; | ||
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; | ||
} | ||
return 'Identifier' in props && 'Literal' in props && | ||
props.Identifier === props.Literal; | ||
} | ||
}; | ||
]; | ||
@@ -133,0 +202,0 @@ compareAst.Error = Errors; |
{ | ||
"name": "compare-ast", | ||
"version": "0.1.0", | ||
"version": "0.1.1", | ||
"description": "Determine if two strings of JavaScript have equivalent abstract syntax trees.", | ||
@@ -5,0 +5,0 @@ "main": "lib/compare.js", |
@@ -32,2 +32,10 @@ # compareAst | ||
// Allow for "fuzzy" string values by specifying the `stringPattern` option | ||
// as a regular expression: | ||
compareAst( | ||
"var a = 'one', b = 'two', c = 'three';" | ||
"var a = '__s1__', b = '__s2__', c = '__s3__';", | ||
{ stringPattern: /__s\d+__/ } | ||
); | ||
## Tests | ||
@@ -34,0 +42,0 @@ |
@@ -8,4 +8,10 @@ var compareAst = require('..'); | ||
test('dereferencing', function() { | ||
compareAst('a.b;', 'a["b"];'); | ||
suite('dereferencing', function() { | ||
test('identifier to literal', function() { | ||
compareAst('a.b;', 'a["b"];'); | ||
}); | ||
test('literal to identifier', function() { | ||
compareAst('a["b"];', 'a.b;'); | ||
}); | ||
}); | ||
@@ -31,2 +37,26 @@ | ||
test('string binding', function() { | ||
compareAst( | ||
'a["something"];"something2";"something";', | ||
'a["__STR1__"]; "__STR2__"; "__STR1__";', | ||
{ stringPattern: /__STR\d+__/ } | ||
); | ||
}); | ||
test('string binding with object dereferencing', function() { | ||
compareAst( | ||
'a.b;', | ||
'a["_s1_"];', | ||
{ stringPattern: /_s\d_/ } | ||
); | ||
}); | ||
test('variable binding and string binding', function() { | ||
compareAst( | ||
'a["b"];', | ||
'_v1_["_s1_"];', | ||
{ stringPattern: /_s\d_/, varPattern: /_v\d_/ } | ||
); | ||
}); | ||
suite('expected failures', function() { | ||
@@ -71,3 +101,3 @@ | ||
test('double binding', function() { | ||
test('double variable binding', function() { | ||
noMatch([ | ||
@@ -82,2 +112,22 @@ '(function(a, b) { console.log(a); });', | ||
test('double string binding', function() { | ||
noMatch([ | ||
'var a = "one", b = "two", c = "three";', | ||
'var a = "_s1_", b = "_s2_", c = "_s1_";', | ||
{ stringPattern: /_s\d_/ }, | ||
], | ||
2 | ||
); | ||
}); | ||
test('double string binding (through object dereference)', function() { | ||
noMatch([ | ||
'a.a; a.b; a.c;', | ||
'a["_s1_"]; a["_s2_"]; a["_s1_"];', | ||
{ stringPattern: /_s\d/ }, | ||
], | ||
2 | ||
); | ||
}); | ||
test('extra statements', function() { | ||
@@ -87,3 +137,7 @@ noMatch(['a;', ''], 3); | ||
test('unmatched member expression', function() { | ||
noMatch(['a.b;', '3;'], 3); | ||
}); | ||
}); | ||
}); |
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
12793
9
318
50