Comparing version 0.1.0 to 0.1.1
@@ -9,32 +9,60 @@ /* Simple control code for allowing user to type code into two editors, | ||
// Set up the editors (disable workers so we can run locally as well.) | ||
var editorStructure = ace.edit("structure"); | ||
setupEditor(editorStructure); | ||
var editorTest = ace.edit("test-code"); | ||
var editorStructure = ace.edit("structure"); | ||
editorTest.getSession().setUseWorker(false); | ||
editorStructure.getSession().setUseWorker(false); | ||
editorTest.getSession().setMode("ace/mode/javascript"); | ||
editorStructure.getSession().setMode("ace/mode/javascript"); | ||
setupEditor(editorTest); | ||
var editorCallbacks = ace.edit("var-callbacks"); | ||
setupEditor(editorCallbacks); | ||
var oldFocus = editorStructure; | ||
// Save the user's focus so we can restore it after the button is pressed. | ||
var oldFocus = editorTest; | ||
editorTest.on("focus", function(){oldFocus = editorTest;}); | ||
editorStructure.on("focus", function(){oldFocus = editorStructure;}); | ||
// Run Structured.match when the user presses the button. | ||
$("#run-button").click(function(evt) { | ||
var structure = editorStructure.getValue() | ||
var structure = "function() {\n" + editorStructure.getValue() + "\n}"; | ||
var code = editorTest.getValue(); | ||
var message; | ||
// Pull in the object with function callbacks. | ||
eval("var varCallbacks = " + editorCallbacks.getValue()); | ||
var message, errorMessage; | ||
try { | ||
var result = Structured.match(code, structure); | ||
var result = Structured.match(code, structure, | ||
{varCallbacks: varCallbacks}); | ||
message = "Match: " + result; | ||
errorMessage = varCallbacks.failure || ""; | ||
} catch (error) { | ||
message = "Error: " + error; | ||
message = ""; | ||
errorMessage = error; | ||
} | ||
$("#results").hide().html(message).fadeIn(); | ||
$(".match-fail-message").html(errorMessage); | ||
$(".test-wrapper").hide(); | ||
oldFocus.focus(); | ||
makeTest(structure, code, result); | ||
makeTest(structure, code, editorCallbacks.getValue(), result); | ||
}); | ||
// Show QUnit test code | ||
$(".gen-test").click(function(evt) { | ||
$("#run-button").click(); | ||
$(".test-wrapper").show(); | ||
}); | ||
$(".var-callbacks-show-hide").click(function(evt) { | ||
$("#var-callbacks").css('visibility', function(i, visibility) { | ||
return visibility === "visible" ? "hidden" : "visible"; | ||
}); | ||
}); | ||
// Output results on the initial load. | ||
$("#run-button").click(); | ||
function setupEditor(editor) { | ||
editor.getSession().setUseWorker(false); | ||
editor.getSession().setMode("ace/mode/javascript"); | ||
editor.renderer.setShowGutter(false); | ||
editor.renderer.setPadding(6); | ||
// Save the user's focus so we can restore it afterwards. | ||
editor.on("focus", function() { | ||
oldFocus = editor; | ||
}); | ||
} | ||
}); | ||
@@ -44,10 +72,15 @@ | ||
Handles multiline string nonsense. */ | ||
function makeTest(structure, code, result) { | ||
var testCode = ""; | ||
testCode += "structure = " + structure + ";"; | ||
function makeTest(structure, code, editorCallbacks, result) { | ||
var testCode = "// QUnit test code \n"; | ||
testCode += "editorCallbacks = " + editorCallbacks + ";"; | ||
testCode += "\nstructure = " + structure + ";"; | ||
testCode += "\ncode = \" \\n \\ \n"; | ||
_.each(code.split("\n"), function(line) { testCode += line + " \\n \\ \n"}) | ||
_.each(code.split("\n"), function(line) { | ||
testCode += line + " \\n \\ \n"; | ||
}); | ||
testCode += "\"; \n"; | ||
testCode += "equal(Detect.matchStructure(code, structure),\n\t" + result + ", \"message\");"; | ||
testCode += "equal(Structured.match(code, structure, " + | ||
"{editorCallbacks: editorCallbacks}),\n\t" + | ||
result + ", \"message\");"; | ||
$(".test-code").hide().html(testCode).fadeIn(); | ||
} | ||
} |
{ | ||
"name": "structured", | ||
"version": "0.1.0", | ||
"version": "0.1.1", | ||
"description": "Simple interface for checking structure of JS code against a template, backed by Esprima.", | ||
@@ -5,0 +5,0 @@ "main": "structured.js", |
@@ -1,21 +0,72 @@ | ||
**structured.js** is a Javascript library that provides a simple interface for static analysis of Javascript code, backed by the abstract syntax tree generated by Esprima. Structured.js works in-browser `<script src='structured.js'></script>`, or as a standalone npm module. | ||
**structured.js** is a Javascript library that provides a simple interface for verifying the structure of Javascript code, backed by the abstract syntax tree generated by Esprima. It is particularly useful in checking beginner code to provide feedback as part of [Khan Academy's CS curriculum](https://www.khanacademy.org/cs). | ||
Structured.js works in-browser `<script src='structured.js'></script>`, or as a standalone npm module. | ||
A structure is any valid Javascript code which contains blanks ( _ characters) and stand-ins ($str) to match values. The structure is sensitive to nesting and ordering. The matcher only requires that code contain the structure -- extra code has no effect. | ||
### Demo | ||
**[Try structured.js yourself](http://khan.github.io/structuredjs/index.html)** to see it in action. | ||
Also check out the [pretty display demo](http://khan.github.io/structuredjs/pretty-display/index.html) for more user-friendly structures. | ||
### Examples | ||
var structure = function structure() { | ||
var _ = _; | ||
if (_ % 2 == 1) { | ||
_ += 1; | ||
var structure = function() { | ||
if (_) { | ||
_ += _; | ||
for (var $a = _; $a < $b; $a += _) { | ||
_($a, $b, 30, 30); | ||
} | ||
} | ||
}; | ||
var code = "var a = 11; var b = 1; if (a % 2 == 1) { b += 1;}" | ||
var result = Structured.match(structure, code); // true | ||
Check out the test suite for more. | ||
var code = "/* some code */"; | ||
### Demo | ||
var result = Structured.match(structure, code); | ||
`demo/demo.html` provides a user interface to create and test wildcard structures. The demo page is a good way to get a feel for the analysis and generate QUnit tests. | ||
Returns true for the code: | ||
if (y > 30 && x > 13) { | ||
x += y; | ||
for (var i = 0; i < 100; i += 1) { | ||
rect(i, 100, 30, 30); | ||
bar(); | ||
} | ||
} | ||
**[Check out the demo](http://khan.github.io/structuredjs/index.html)** for more, or look at the tests. | ||
### Advanced -- Variable Callbacks | ||
To allow tighter control over what exactly is allowed to match your $variable, you may provide a mapping from variable names to function callbacks. These callbacks can enable NOT, OR, and AND functionality on the wildcard variables, for example. | ||
The keys can be of the form "$a", or "$a, $b, $c" if you would like to check multiple values at at time. The callback takes in a proposed value for the variable and accepts/rejects it by returning a boolean. The callback may instead return an object such as `{failure: "failure message"}` as well if you'd like to explain exactly why this value is not allowed. | ||
For instance, say we want to check the value we assign to a var -- check that it is really big, and that it is bigger than whatever we increment it by. It would look like this: | ||
var structure = function() {var _ = $num; $num += $incr; }; | ||
var code = "var foo = 400; foo += 3;"; | ||
var varCallbacks = { | ||
"$num": function(num) { | ||
return num.value > 100; // Just return true/false | ||
}, | ||
"$num, $incr": function(num, incr) { | ||
if (num.value <= incr.value) { | ||
// Return the failure message | ||
return {failure: "The increment must be smaller than the number."}; | ||
} | ||
return true; | ||
} | ||
}; | ||
var match = Structured.match(structure, code, {varCallbacks: varCallbacks}); | ||
if (!match) { | ||
// varCallbacks.failure contains the error message, if any. | ||
console.log("The problem is: " + varCallbacks.failure); | ||
} | ||
Note that the callbacks receive objects that contain a subtree of the [Esprima](http://esprima.org) parse tree, not a raw value. Also note that the callbacks run statically, not dynamically -- so, you will only be able to directly check literal values (i.e., 48), not computed values (24*2, myVar, etc). The callbacks also ignore any variable callbacks for variables that do not actually appear in the structure you've passed in. | ||
Go to [the demo](http://khan.github.io/structuredjs/index.html) to try it out. | ||
### Tests | ||
@@ -28,2 +79,3 @@ | ||
[Esprima](http://esprima.org) and [UnderscoreJS](http://underscorejs.org) for the framework, | ||
[QUnit](http://qunitjs.com/) for the test suite. | ||
[QUnit](http://qunitjs.com/) for the test suite, | ||
[RainbowJS](http://craig.is/making/rainbows/) for prettified structures. |
@@ -8,18 +8,29 @@ /* | ||
*/ | ||
(function(global) { | ||
/* Detect npm versus browser usage */ | ||
var exports; | ||
var esprima; | ||
var _; | ||
/* Detect npm versus browser usage */ | ||
var exports; | ||
if (typeof module !== "undefined" && module.exports) { | ||
exports = module.exports = {}; | ||
var esprima = require("esprima"); | ||
var _ = require("underscore"); | ||
} else { | ||
exports = this.Structured = {}; | ||
if (!this.esprima || !this._) { | ||
// Cache all the structure tests | ||
var structureCache = {}; | ||
// Cache the most recently-parsed code and tree | ||
var cachedCode; | ||
var cachedCodeTree; | ||
if (typeof module !== "undefined" && module.exports) { | ||
exports = module.exports = {}; | ||
esprima = require("esprima"); | ||
_ = require("underscore"); | ||
} else { | ||
exports = this.Structured = {}; | ||
esprima = global.esprima; | ||
_ = global._; | ||
} | ||
if (!esprima || !_) { | ||
throw "Error: Both Esprima and UnderscoreJS are required dependencies."; | ||
} | ||
} | ||
(function(exports) { | ||
/* | ||
@@ -30,9 +41,53 @@ * Returns true if the code (a string) matches the structure in rawStructure | ||
* Example: | ||
* code = "if (y > 30 && x > 13) {x += y;}"; | ||
* rawStructure = function structure() { if(_) {} }; | ||
* var code = "if (y > 30 && x > 13) {x += y;}"; | ||
* var rawStructure = function structure() { if(_) {} }; | ||
* match(code, rawStructure); | ||
* | ||
* options.varCallbacks is an object that maps user variable strings like | ||
* "$myVar", "$a, $b, $c" etc to callbacks. These callbacks receive the | ||
* potential Esprima structure values assigned to each of the user | ||
* variables specified in the string, and can accept/reject that value | ||
* by returning true/false. The callbacks can also specify a failure | ||
* message instead by returning an object of the form | ||
* {failure: "Your failure message"}, in which case the message will be | ||
* returned as the property "failure" on the varCallbacks object if | ||
* there is no valid match. A valid matching requires that every | ||
* varCallback return true. | ||
* | ||
* Advanced Example: | ||
* var varCallbacks = { | ||
* "$foo": function(fooObj) { | ||
* return fooObj.value > 92; | ||
* }, | ||
* "$foo, $bar, $baz": function(fooObj, barObj, bazObj) { | ||
* if (fooObj.value > barObj.value) { | ||
* return {failure: "Check the relationship between values."}; | ||
* } | ||
* return bazObj !== 48; | ||
* } | ||
* }; | ||
* var code = "var a = 400; var b = 120; var c = 500; var d = 49;"; | ||
* var rawStructure = function structure() { | ||
* var _ = $foo; var _ = $bar; var _ = $baz; | ||
* }; | ||
* match(code, rawStructure, {varCallbacks: varCallbacks}); | ||
*/ | ||
function match(code, rawStructure) { | ||
var structure = parseStructure(rawStructure); | ||
var codeTree = esprima.parse(code); | ||
function match(code, rawStructure, options) { | ||
options = options || {}; | ||
var varCallbacks = options.varCallbacks || {}; | ||
var wildcardVars = {order: [], skipData: {}, values: {}}; | ||
// Note: After the parse, structure contains object references into | ||
// wildcardVars[values] that must be maintained. So, beware of | ||
// JSON.parse(JSON.stringify), etc. as the tree is no longer static. | ||
var structure = parseStructureWithVars(rawStructure, wildcardVars); | ||
// Cache the parsed code tree, or pull from cache if it exists | ||
var codeTree = (cachedCode === code ? | ||
cachedCodeTree : | ||
esprima.parse(code)); | ||
cachedCode = code; | ||
cachedCodeTree = codeTree; | ||
foldConstants(codeTree); | ||
var toFind = structure.body; | ||
@@ -44,7 +99,177 @@ var peers = []; | ||
} | ||
var result = checkMatchTree(codeTree, toFind, peers); | ||
var result; | ||
if (wildcardVars.order.length === 0) { | ||
// With no vars to match, our normal greedy approach works great. | ||
result = checkMatchTree(codeTree, toFind, peers, wildcardVars); | ||
} else { | ||
// If there are variables to match, we must do a potentially | ||
// exhaustive search across the possible ways to match the vars. | ||
result = anyPossible(0, wildcardVars, varCallbacks); | ||
} | ||
return result; | ||
/* | ||
* Checks whether any possible valid variable assignment for this i | ||
* results in a valid match. | ||
* | ||
* We orchestrate this check by building skipData, which specifies | ||
* for each variable how many possible matches it should skip before | ||
* it guesses a match. The iteration over the tree is the same | ||
* every time -- if the first guess fails, the next run will skip the | ||
* first guess and instead take the second appearance, and so on. | ||
* | ||
* When there are multiple variables, changing an earlier (smaller i) | ||
* variable guess means that we must redo the guessing for the later | ||
* variables (larger i). | ||
* | ||
* Returning false involves exhausting all possibilities. In the worst | ||
* case, this will mean exponentially many possibilities -- variables | ||
* are expensive for all but small tests. | ||
* | ||
* wildcardVars = wVars: | ||
* .values[varName] contains the guessed node value of each | ||
* variable, or the empty object if none. | ||
* .skipData[varName] contains the number of potential matches of | ||
* this var to skip before choosing a guess to assign to values | ||
* .leftToSkip[varName] stores the number of skips left to do | ||
* (used during the match algorithm) | ||
* .order[i] is the name of the ith occurring variable. | ||
*/ | ||
function anyPossible(i, wVars, varCallbacks) { | ||
var order = wVars.order; // Just for ease-of-notation. | ||
wVars.skipData[order[i]] = 0; | ||
do { | ||
// Reset the skip # for all later variables. | ||
for (var rest = i + 1; rest < order.length; rest += 1) { | ||
wVars.skipData[order[rest]] = 0; | ||
} | ||
// Check for a match only if we have reached the last var in | ||
// order (and so set skipData for all vars). Otherwise, | ||
// recurse to check all possible values of the next var. | ||
if (i === order.length - 1) { | ||
// Reset the wildcard vars' guesses. Delete the properties | ||
// rather than setting to {} in order to maintain shared | ||
// object references in the structure tree (toFind, peers) | ||
_.each(wVars.values, function(value, key) { | ||
_.each(wVars.values[key], function(v, k) { | ||
delete wVars.values[key][k]; | ||
}); | ||
}); | ||
wVars.leftToSkip = _.extend({}, wVars.skipData); | ||
// Use a copy of peers because peers is destructively | ||
// modified in checkMatchTree (via checkNodeArray). | ||
if (checkMatchTree(codeTree, toFind, peers.slice(), wVars) && | ||
checkUserVarCallbacks(wVars, varCallbacks)) { | ||
return true; | ||
} | ||
} else if (anyPossible(i + 1, wVars, varCallbacks)) { | ||
return true; | ||
} | ||
// This guess didn't work out -- skip it and try the next. | ||
wVars.skipData[order[i]] += 1; | ||
// The termination condition is when we have run out of values | ||
// to skip and values is no longer defined for this var after | ||
// the match algorithm. That means that there is no valid | ||
// assignment for this and later vars given the assignments to | ||
// previous vars (set by skipData). | ||
} while (!_.isEmpty(wVars.values[order[i]])); | ||
return false; | ||
} | ||
} | ||
/* | ||
* Checks the user-defined variable callbacks and returns a boolean for | ||
* whether or not the wVars assignment of the wildcard variables results | ||
* in every varCallback returning true as required. | ||
* | ||
* If any varCallback returns false, this function also returns false. | ||
* | ||
* Format of varCallbacks: An object containing: | ||
* keys of the form: "$someVar" or "$foo, $bar, $baz" to mimic an | ||
* array (as JS keys must be strings). | ||
* values containing function callbacks. These callbacks must return | ||
* true/false. They may alternately return an object of the form | ||
* {failure: "The failure message."}. If the callback returns the | ||
* failure object, then the relevant failure message will be returned | ||
* via varCallbacks.failure. | ||
* These callbacks are passed a parameter list corresponding to | ||
* the Esprima parse structures assigned to the variables in | ||
* the key (see example). | ||
* | ||
* Example varCallbacks object: | ||
* { | ||
* "$foo": function(fooObj) { | ||
* return fooObj.value > 92; | ||
* }, | ||
* "$foo, $bar, $baz": function(fooObj, barObj, bazObj) { | ||
* if (fooObj.value > barObj.value) { | ||
* return {failure: "Check the relationship between values."} | ||
* } | ||
* return bazObj !== 48; | ||
* } | ||
* } | ||
*/ | ||
function checkUserVarCallbacks(wVars, varCallbacks) { | ||
// Clear old failure message if needed | ||
delete varCallbacks.failure; | ||
for (var property in varCallbacks) { | ||
// Property strings may be "$foo, $bar, $baz" to mimic arrays. | ||
var varNames = property.split(","); | ||
var varValues = _.map(varNames, function(varName) { | ||
varName = stringLeftTrim(varName); // Trim whitespace | ||
// If the var name is in the structure, then it will always | ||
// exist in wVars.values after we find a match prior to | ||
// checking the var callbacks. So, if a variable name is not | ||
// defined here, it is because that var name does not exist in | ||
// the user-defined structure. | ||
if (!_.has(wVars.values, varName)) { | ||
console.error("Callback var " + varName + " doesn't exist"); | ||
return undefined; | ||
} | ||
// Convert each var name to the Esprima structure it has | ||
// been assigned in the parse. Make a deep copy. | ||
return JSON.parse(JSON.stringify(wVars.values[varName])); | ||
}); | ||
// Call the user-defined callback, passing in the var values as | ||
// parameters in the order that the vars were defined in the | ||
// property string. | ||
var result = varCallbacks[property].apply(null, varValues); | ||
if (!result || _.has(result, "failure")) { | ||
// Set the failure message if the user callback provides one. | ||
if (_.has(result, "failure")) { | ||
varCallbacks.failure = result.failure; | ||
} | ||
return false; | ||
} | ||
} | ||
return true; | ||
/* Trim is only a string method in IE9+, so use a regex if needed. */ | ||
function stringLeftTrim(str) { | ||
if (String.prototype.trim) { | ||
return str.trim(); | ||
} | ||
return str.replace(/^\s+|\s+$/g, ""); | ||
} | ||
} | ||
function parseStructure(structure) { | ||
if (structureCache[structure]) { | ||
return JSON.parse(structureCache[structure]); | ||
} | ||
// Wrapped in parentheses so function() {} becomes valid Javascript. | ||
var fullTree = esprima.parse("(" + structure + ")"); | ||
if (fullTree.body[0].expression.type !== "FunctionExpression" || | ||
!fullTree.body[0].expression.body) { | ||
throw "Poorly formatted structure code"; | ||
} | ||
var tree = fullTree.body[0].expression.body; | ||
structureCache[structure] = JSON.stringify(tree); | ||
return tree; | ||
} | ||
/* | ||
* Returns a tree parsed out of the structure. The returned tree is an | ||
@@ -59,11 +284,6 @@ * abstract syntax tree with wildcard properties set to undefined. | ||
*/ | ||
function parseStructure(structure) { | ||
var fullTree = esprima.parse(structure.toString()); | ||
if (!fullTree.type === "Program" || !fullTree.body.length === 1 || | ||
!fullTree.body[0].type === "FunctionDeclaration" || | ||
!fullTree.body[0].body) { | ||
throw "Poorly formatted structure code."; | ||
} | ||
var tree = fullTree.body[0].body; | ||
simplifyTree(tree); | ||
function parseStructureWithVars(structure, wVars) { | ||
var tree = parseStructure(structure); | ||
foldConstants(tree); | ||
simplifyTree(tree, wVars); | ||
return tree; | ||
@@ -73,17 +293,61 @@ } | ||
/* | ||
* Constant folds the syntax tree | ||
*/ | ||
function foldConstants(tree) { | ||
for (var key in tree) { | ||
if (!tree.hasOwnProperty(key)) { | ||
continue; // Inherited property | ||
} | ||
var ast = tree[key]; | ||
if (_.isObject(ast)) { | ||
foldConstants(ast); | ||
/* | ||
* Currently, we only fold + and - applied to a number literal. | ||
* This is easy to extend, but it means we lose the ability to match | ||
* potentially useful expressions like 5 + 5 with a pattern like _ + _. | ||
*/ | ||
if (ast.type == esprima.Syntax.UnaryExpression) { | ||
var argument = ast.argument; | ||
if (argument.type === esprima.Syntax.Literal && | ||
_.isNumber(argument.value)) { | ||
if (ast.operator === "-") { | ||
argument.value = -argument.value; | ||
tree[key] = argument; | ||
} else if (ast.operator === "+") { | ||
argument.value = +argument.value; | ||
tree[key] = argument; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
/* | ||
* Recursively traverses the tree and sets _ properties to undefined | ||
* and empty bodies to null. | ||
* | ||
* Wildcards are explicitly set to undefined -- these undefined properties | ||
* Wildcards are explicitly set to undefined -- these undefined properties | ||
* must exist and be non-null in order for code to match the structure. | ||
* | ||
* Wildcard variables are set up such that the first occurrence of the | ||
* variable in the structure tree is set to {wildcardVar: varName}, | ||
* and all later occurrences just refer to wVars.values[varName], | ||
* which is an object assigned during the matching algorithm to have | ||
* properties identical to our guess for the node matching the variable. | ||
* (maintaining the reference). In effect, these later accesses | ||
* to tree[key] mimic tree[key] simply being set to the variable value. | ||
* | ||
* Empty statements are deleted from the tree -- they need not be matched. | ||
* | ||
* If the subtree is an array, we just iterate over the array using | ||
* If the subtree is an array, we just iterate over the array using | ||
* for (var key in tree) | ||
* | ||
*/ | ||
function simplifyTree(tree) { | ||
function simplifyTree(tree, wVars) { | ||
for (var key in tree) { | ||
if (!tree.hasOwnProperty(key)) { | ||
continue; // inherited property | ||
continue; // Inherited property | ||
} | ||
@@ -93,2 +357,13 @@ if (_.isObject(tree[key])) { | ||
tree[key] = undefined; | ||
} else if (isWildcardVar(tree[key])) { | ||
var varName = tree[key].name; | ||
if (!wVars.values[varName]) { | ||
// Perform setup for the first occurrence. | ||
wVars.values[varName] = {}; // Filled in later. | ||
tree[key] = {wildcardVar: varName}; | ||
wVars.order.push(varName); | ||
wVars.skipData[varName] = 0; | ||
} else { | ||
tree[key] = wVars.values[varName]; // Reference. | ||
} | ||
} else if (tree[key].type === esprima.Syntax.EmptyStatement) { | ||
@@ -99,3 +374,3 @@ // Arrays are objects, but delete tree[key] does not | ||
} else { | ||
simplifyTree(tree[key]); | ||
simplifyTree(tree[key], wVars); | ||
} | ||
@@ -107,3 +382,3 @@ } | ||
/* | ||
* Returns whether or not the node is intended as a wildcard node, which | ||
* Returns whether the structure node is intended as a wildcard node, which | ||
* can be filled in by anything in others' code. | ||
@@ -116,2 +391,8 @@ */ | ||
/* Returns whether the structure node is intended as a wildcard variable. */ | ||
function isWildcardVar(node) { | ||
return (node.name && _.isString(node.name) && node.name.length >= 2 && | ||
node.name[0] === "$"); | ||
} | ||
/* | ||
@@ -125,3 +406,3 @@ * Returns true if currTree matches the wildcard structure toFind. | ||
*/ | ||
function checkMatchTree(currTree, toFind, peersToFind) { | ||
function checkMatchTree(currTree, toFind, peersToFind, wVars) { | ||
if (_.isArray(toFind)) { | ||
@@ -131,5 +412,6 @@ console.error("toFind should never be an array."); | ||
} | ||
if (exactMatchNode(currTree, toFind)) { | ||
if (exactMatchNode(currTree, toFind, peersToFind, wVars)) { | ||
return true; | ||
} | ||
// Check children. | ||
for (var key in currTree) { | ||
@@ -141,5 +423,5 @@ if (!currTree.hasOwnProperty(key) || !_.isObject(currTree[key])) { | ||
if ((_.isArray(currTree[key]) && | ||
checkNodeArray(currTree[key], toFind, peersToFind)) || | ||
checkNodeArray(currTree[key], toFind, peersToFind, wVars)) || | ||
(!_.isArray(currTree[key]) && | ||
checkMatchTree(currTree[key], toFind, peersToFind))) { | ||
checkMatchTree(currTree[key], toFind, peersToFind, wVars))) { | ||
return true; | ||
@@ -155,5 +437,5 @@ } | ||
*/ | ||
function checkNodeArray(nodeArr, toFind, peersToFind) { | ||
function checkNodeArray(nodeArr, toFind, peersToFind, wVars) { | ||
for (var i = 0; i < nodeArr.length; i += 1) { | ||
if (checkMatchTree(nodeArr[i], toFind, peersToFind)) { | ||
if (checkMatchTree(nodeArr[i], toFind, peersToFind, wVars)) { | ||
if (!peersToFind || peersToFind.length === 0) { | ||
@@ -164,3 +446,3 @@ return true; // Found everything needed on this level. | ||
// this level we need to match on subsequent iterations | ||
toFind = peersToFind.shift(); | ||
toFind = peersToFind.shift(); // Destructive. | ||
} | ||
@@ -185,3 +467,3 @@ } | ||
*/ | ||
function exactMatchNode(currNode, toFind) { | ||
function exactMatchNode(currNode, toFind, peersToFind, wVars) { | ||
for (var key in toFind) { | ||
@@ -205,2 +487,14 @@ // Ignore inherited properties; also, null properties can be | ||
if (subCurr === undefined || subCurr === null) { | ||
if (key === "wildcardVar") { | ||
if (wVars.leftToSkip[subFind] > 0) { | ||
wVars.leftToSkip[subFind] -= 1; | ||
return false; // Skip, this does not match our wildcard | ||
} | ||
// We have skipped the required number, so take this guess. | ||
// Copy over all of currNode's properties into | ||
// wVars.values[subFind] so the var references set up in | ||
// simplifyTree behave like currNode. Shallow copy. | ||
_.extend(wVars.values[subFind], currNode); | ||
return true; // This node is now our variable. | ||
} | ||
return false; | ||
@@ -221,4 +515,4 @@ } | ||
var newToFind = subFind[0]; | ||
var peers = subFind.length > 1 ? subFind.slice(1) : []; | ||
if (!checkNodeArray(subCurr, newToFind, peers)) { | ||
var peers = subFind.slice(1); | ||
if (!checkNodeArray(subCurr, newToFind, peers, wVars)) { | ||
return false; | ||
@@ -228,3 +522,3 @@ } | ||
// Both are objects, so do a recursive compare. | ||
if (!checkMatchTree(subCurr, subFind)) { | ||
if (!checkMatchTree(subCurr, subFind, peersToFind, wVars)) { | ||
return false; | ||
@@ -247,4 +541,101 @@ } | ||
/* | ||
* Takes in a string for a structure and returns HTML for nice styling. | ||
* The blanks (_) are enclosed in span.structuredjs_blank, and the | ||
* structured.js variables ($someVar) are enclosed in span.structuredjs_var | ||
* for special styling. | ||
* | ||
* See pretty-display/index.html for a demo and sample stylesheet. | ||
* | ||
* Only works when RainbowJS (http://craig.is/making/rainbows) is | ||
* included on the page; if RainbowJS is not available, simply | ||
* returns the code string. RainbowJS is not available as an npm | ||
* module. | ||
*/ | ||
function prettyHtml(code, callback) { | ||
if (!Rainbow) { | ||
return code; | ||
} | ||
Rainbow.color(code, "javascript", function(formattedCode) { | ||
var output = ("<pre class='rainbowjs'>" + | ||
addStyling(formattedCode) + "</pre>"); | ||
callback(output); | ||
}); | ||
} | ||
/* | ||
* Helper function for prettyHtml that takes in a string (the formatted | ||
* output of RainbowJS) and inserts special StructuredJS spans for | ||
* blanks (_) and variables ($something). | ||
* | ||
* The optional parameter maintainStyles should be set to true if the | ||
* caller wishes to keep the class assignments from the previous call | ||
* to addStyling and continue where we left off. This parameter is | ||
* valuable for visual consistency across different structures that share | ||
* variables. | ||
*/ | ||
function addStyling(code, maintainStyles) { | ||
if (!maintainStyles) { | ||
addStyling.styleMap = {}; | ||
addStyling.counter = 0; | ||
} | ||
// First replace underscores with empty structuredjs_blank spans | ||
// Regex: Match any underscore _ that is not preceded or followed by an | ||
// alphanumeric character. | ||
code = code.replace(/(^|[^A-Za-z0-9])_(?![A-Za-z0-9])/g, | ||
"$1<span class='structuredjs_blank'></span>"); | ||
// Next replace variables with empty structuredjs_var spans numbered | ||
// with classes. | ||
// This regex is in two parts: | ||
// Part 1, delimited by the non-capturing parentheses `(?: ...)`: | ||
// (^|[^\w])\$(\w+) | ||
// Match any $ that is preceded by either a 'start of line', or a | ||
// non-alphanumeric character, and is followed by at least one | ||
// alphanumeric character (the variable name). | ||
// Part 2, also delimited by the non-capturing parentheses: | ||
// ()\$<span class="function call">(\w+)<\/span> | ||
// Match any function call immediately preceded by a dollar sign, | ||
// where the Rainbow syntax highlighting separated a $foo() | ||
// function call by placing the dollar sign outside. | ||
// the function call span to create | ||
// $<span class="function call">foo</span>. | ||
// We combine the two parts with an | (an OR) so that either matches. | ||
// The reason we do this all in one go rather than in two separate | ||
// calls to replace is so that we color the string in order, | ||
// rather than coloring all non-function calls and then going back | ||
// to do all function calls (a minor point, but otherwise the | ||
// interactive pretty display becomes jarring as previous | ||
// function call colors change when new variables are introduced.) | ||
// Finally, add the /g flag for global replacement. | ||
var regexVariables = /(?:(^|[^\w])\$(\w+))|(?:\$<span class="function call">(\w+)<\/span>)/g; | ||
return code.replace(regexVariables, | ||
function(m, prev, varName, fnVarName) { | ||
// Necessary to handle the fact we are essentially performing | ||
// two regexes at once as outlined above. | ||
prev = prev || ""; | ||
varName = varName || fnVarName; | ||
var fn = addStyling; | ||
// Assign the next available class to this variable if it does | ||
// not yet exist in our style mapping. | ||
if (!(varName in fn.styleMap)) { | ||
fn.styleMap[varName] = (fn.counter < fn.styles.length ? | ||
fn.styles[fn.counter] : "extra"); | ||
fn.counter += 1; | ||
} | ||
return (prev + "<span class='structuredjs_var " + | ||
fn.styleMap[varName] + "'>" + "</span>"); | ||
} | ||
); | ||
} | ||
// Store some properties on the addStyling function to maintain the | ||
// styleMap between runs if desired. | ||
// Right now just support 7 different variables. Just add more if needed. | ||
addStyling.styles = ["one", "two", "three", "four", "five", "six", | ||
"seven"]; | ||
addStyling.styleMap = {}; | ||
addStyling.counter = 0; | ||
exports.match = match; | ||
})(exports); | ||
exports.prettify = prettyHtml; | ||
})(typeof window !== "undefined" ? window : global); |
524
tests.js
@@ -11,12 +11,32 @@ /* QUnit tests for StructuredJS. */ | ||
var basicTests = function() { | ||
QUnit.module("Basic detection"); | ||
test("Accepts string for structure", function() { | ||
ok(Structured.match("var draw = function() {}", | ||
"function() { var draw = function() {};}"), | ||
"Accepts string and matches"); | ||
equal(Structured.match("var drow = function() {}", | ||
"function() { var draw = function() {};}"), | ||
false, | ||
"Accepts string and doesn't match it"); | ||
}); | ||
test("Positive tests of syntax", function() { | ||
ok(Structured.match("", | ||
function() {}), | ||
"Empty structure matches empty string."); | ||
ok(Structured.match("if (y > 30 && x > 13) {x += y;}", | ||
function structure() { if (_) {} }), | ||
function() { if (_) {} }), | ||
"Basic if-statement structure matches."); | ||
ok(Structured.match("if (y > 30 && x > 13) {x += y;}", | ||
function foo() { if (_) {} }), | ||
"Using a named function is allowable as well."); | ||
ok(Structured.match("if (y > 30 && x > 13) {x += y;} else { y += 2;}", | ||
function structure() { if (_) {} else {}}), | ||
function() { if (_) {} else {}}), | ||
"Basic if-else statement structure matches."); | ||
@@ -26,43 +46,43 @@ | ||
else if(x <10) {y -= 20;} else { y += 2;}", | ||
function structure() { if (_) {} else if (_) {} else {}}), | ||
function() { if (_) {} else if (_) {} else {}}), | ||
"Basic if, else-if, else statement structure matches."); | ||
ok(Structured.match("for (var a = 0; a < 10; a += 1) { a -= 2;}", | ||
function structure() { for (_; _; _) {} }), | ||
function() { for (_; _; _) {} }), | ||
"Basic for-statement structure matches."); | ||
ok(Structured.match("var a = 30;", | ||
function structure() { var _ = _; }), | ||
function() { var _ = _; }), | ||
"Basic variable declaration + initialization matches."); | ||
ok(Structured.match("var test = function() {return 3+2;}", | ||
function structure() { var _ = function() {};}), | ||
function() { var _ = function() {};}), | ||
"Basic function assignment into var matches."); | ||
ok(Structured.match("function foo() {return x+2;}", | ||
function structure() { function _() {};}), | ||
function() { function _() {}}), | ||
"Basic standalone function declaration matches."); | ||
ok(Structured.match("rect();", | ||
function structure() { rect(); }), | ||
function() { rect(); }), | ||
"No-parameter call to function matches."); | ||
ok(Structured.match("rect(3);", | ||
function structure() { rect(); }), | ||
function() { rect(); }), | ||
"Parameterized call to no-param function structure matches."); | ||
ok(Structured.match("rect(30, 40, 10, 11);", | ||
function structure() { rect(30, 40, 10, 11); }), | ||
function() { rect(30, 40, 10, 11); }), | ||
"Fully specified parameter call to function matches."); | ||
ok(Structured.match("rect(30, 40, 10, 11);", | ||
function structure() { rect(30, _, _, 11); }), | ||
function() { rect(30, _, _, 11); }), | ||
"Parameters with wildcards call to function matches."); | ||
ok(Structured.match("rect(30, 40);", | ||
function structure() { rect(_, _); }), | ||
function() { rect(_, _); }), | ||
"Parameters with all wildcards call to function matches."); | ||
ok(Structured.match("rect(30, 40, 30);", | ||
function structure() { rect(_, _); }), | ||
function() { rect(_, _); }), | ||
"Extra params to function matches."); | ||
@@ -74,8 +94,8 @@ }); | ||
equal(Structured.match("if (y > 30 && x > 13) {x += y;}", | ||
function structure() { if (_) {_} else {}}), | ||
function() { if (_) {_;} else {}}), | ||
false, | ||
"If-else does not match only an if."); | ||
equal(Structured.match("if (y > 30 && x > 13) {x += y;} else {y += 2;}", | ||
function structure() { if (_) {} else if (_) {} else {}}), | ||
equal(Structured.match("if(y > 30 && x > 13) {x += y;} else {y += 2;}", | ||
function() { if (_) {} else if (_) {} else {}}), | ||
false, | ||
@@ -85,3 +105,3 @@ "If, else if, else structure does not match only if else."); | ||
equal(Structured.match("var a;", | ||
function structure() { var _ = _; }), | ||
function() { var _ = _; }), | ||
false, | ||
@@ -91,3 +111,3 @@ "Variable declaration + init does not match just declaration."); | ||
equal(Structured.match("while(true) { a -= 2;}", | ||
function structure() { for (_; _; _) {} }), | ||
function() { for (_; _; _) {} }), | ||
false, | ||
@@ -97,3 +117,3 @@ "For-statement does not match a while loop."); | ||
equal(Structured.match("var test = 3+5", | ||
function structure() { var _ = function() {};}), | ||
function() { var _ = function() {};}), | ||
false, | ||
@@ -103,8 +123,8 @@ "Basic function declaration does not match basic var declaration"); | ||
equal(Structured.match("var test = function foo() {return 3+2;}", | ||
function structure() { function _() {};}), | ||
function() { function _() {}}), | ||
false, | ||
"Function declaration does not match function assignment into var"); | ||
"Function declaration doesn't match function assignment into var"); | ||
equal(Structured.match("rect();", | ||
function structure() { ellipse(); }), | ||
function() { ellipse(); }), | ||
false, | ||
@@ -114,3 +134,3 @@ "Call to function does not match differently-named function."); | ||
equal(Structured.match("rect(300, 400, 100, 110);", | ||
function structure() { rect(30, 40, 10, 11); }), | ||
function() { rect(30, 40, 10, 11); }), | ||
false, | ||
@@ -120,3 +140,3 @@ "Fully specified parameter call to function identifies mismatch."); | ||
equal(Structured.match("rect(30);", | ||
function structure() { rect(30, 40); }), | ||
function() { rect(30, 40); }), | ||
false, | ||
@@ -126,3 +146,3 @@ "Too few parameters does not match."); | ||
equal(Structured.match("rect(60, 40, 10, 11);", | ||
function structure() { rect(30, _, _, 11); }), | ||
function() { rect(30, _, _, 11); }), | ||
false, | ||
@@ -132,3 +152,3 @@ "Parameters with wildcards call to function identifies mismatch."); | ||
equal(Structured.match("rect();", | ||
function structure() { rect(_, _); }), | ||
function() { rect(_, _); }), | ||
false, | ||
@@ -138,3 +158,3 @@ "Wildcard params do not match no-params for function call."); | ||
equal(Structured.match("rect(30, 40);", | ||
function structure() { rect(_, _, _); }), | ||
function() { rect(_, _, _); }), | ||
false, | ||
@@ -151,7 +171,7 @@ "Parameters with too few wildcards call to function mismatches."); | ||
ok(Structured.match("if (y > 30 && x > 13) {x += y;} \ | ||
else if(x <10) {y -= 20;} else { y += 2;}", | ||
function structure() { if (_) {} else {}}), | ||
else if (x <10) {y -= 20;} else { y += 2;}", | ||
function() { if (_) {} else {}}), | ||
"Extra else-if statement correctly allowed though not specified."); | ||
structure = function structure() { | ||
structure = function() { | ||
for (; _ < 10; _ += 1) { | ||
@@ -189,3 +209,3 @@ if (_) { | ||
structure = function structure() { | ||
structure = function() { | ||
if (_) { | ||
@@ -232,3 +252,3 @@ if (_) { | ||
structure = function structure() { | ||
structure = function() { | ||
var draw = function() { | ||
@@ -245,3 +265,3 @@ rect(_, _, _, _); | ||
structure = function structure() { | ||
structure = function() { | ||
var draw = function() { | ||
@@ -259,3 +279,3 @@ rect(); | ||
structure = function structure() { | ||
structure = function() { | ||
var draw = function() { | ||
@@ -308,18 +328,21 @@ var _ = 10; | ||
ok(Structured.match("rect(); ellipse();", | ||
function structure() { rect(); ellipse()}), | ||
function() { rect(); ellipse()}), | ||
"Back-to-back function calls match."); | ||
equal(Structured.match("rect();", | ||
function structure() { rect(); ellipse()}), | ||
function() { rect(); ellipse()}), | ||
false, | ||
"Back-to-back function calls do not match only the first."); | ||
structure = function structure() { | ||
structure = function() { | ||
var _ = 0; | ||
var _ = 1; | ||
var _ = _; | ||
}; | ||
code = "var a = 0; var b = 1;"; | ||
code = "var a = 0; var b = 1; var c = 30;"; | ||
ok(Structured.match(code, structure), "Back-to-back vars matched."); | ||
equal(Structured.match("var a = 0; var b = 1;", | ||
structure), false, "Partial match does not suffice."); | ||
structure = function structure() { | ||
structure = function() { | ||
var draw = function() { | ||
@@ -350,3 +373,3 @@ rect(); | ||
var structure, code; | ||
structure = function structure() { | ||
structure = function() { | ||
if (_ % 2) { | ||
@@ -366,2 +389,419 @@ _ += _; | ||
var varCallbackTests = function() { | ||
QUnit.module("User-defined variable callbacks"); | ||
test("Basic single variable callbacks", function() { | ||
var structure, code; | ||
equal(Structured.match("var x = 10; var y = 20;", | ||
function() {var _ = $a;}, { | ||
"varCallbacks": { | ||
"$a": function(obj) { | ||
return false; | ||
} | ||
} | ||
}), | ||
false, "Always false varCallback causes failure."); | ||
equal(Structured.match("var x = 10; var y = 20; var k = 42;", | ||
function() {var _ = $a; var _ = $b;}, { | ||
"varCallbacks": { | ||
"$a": function(obj) { | ||
return false; | ||
}, | ||
"$b": function(obj) { | ||
return true; | ||
} | ||
} | ||
}), | ||
false, "One always false varCallback of two causes failure."); | ||
equal(Structured.match("var x = 10; var y = 20;", | ||
function() {var _ = $a;}, { | ||
"varCallbacks": { | ||
"$a": function(obj) { | ||
return true; | ||
} | ||
} | ||
}), | ||
true, "Always true varCallback still matches."); | ||
equal(Structured.match("var x = 10; var y = 20;", | ||
function() {var z = $a;}, { | ||
"varCallbacks": { | ||
"$a": function(obj) { | ||
return true; | ||
} | ||
} | ||
}), | ||
false, "Always true varCallback with no match does not match."); | ||
equal(Structured.match("var x = 10; var y = 20; var z = 40;", | ||
function() {var _ = $a;}, { | ||
"varCallbacks": { | ||
"$a": function(obj) { | ||
return obj.type === "Literal" && obj.value === 40; | ||
} | ||
} | ||
}), | ||
true, "Basic single-matching var callback matches."); | ||
equal(Structured.match("var x = 10; var y = 20; var z = 40;", | ||
function() {var _ = $a; var _ = $b;}, { | ||
"varCallbacks": { | ||
"$a": function(obj) { | ||
return obj.type === "Literal" && obj.value > 11; | ||
}, | ||
"$b": function(obj) { | ||
return obj.type === "Literal" && obj.value > 30; | ||
} | ||
} | ||
}), | ||
true, "Two single-matching var callbacks match correctly."); | ||
equal(Structured.match("var x = 10; var y = 20; var z = 40;", | ||
function() {var _ = $a; var _ = $b;}, { | ||
"varCallbacks": { | ||
"$a": function(obj) { | ||
return obj.type === "Literal" && obj.value > 11; | ||
}, | ||
"$b": function(obj) { | ||
return obj.type === "Literal" && obj.value < 11; | ||
} | ||
} | ||
}), | ||
false, "Two single-matching var callbacks still need ordering."); | ||
var varCallbacks; | ||
varCallbacks = { | ||
"$a": function(obj) { | ||
return {"failure": "Nothing can match $a!"}; | ||
} | ||
}; | ||
var result = Structured.match("var x = 10; var y = 20", | ||
function() {var $a = _;}, {"varCallbacks": varCallbacks}); | ||
equal(result, false, "Returning failure object will be false"); | ||
equal(varCallbacks.failure, "Nothing can match $a!", | ||
"Failure message is correctly set, basic."); | ||
varCallbacks = { | ||
"$a": function(obj) { | ||
return true; | ||
}, | ||
"$b": function(obj) { | ||
if (obj.value > 30) { | ||
return true; | ||
} | ||
return {"failure": "Make sure the value is big"} | ||
} | ||
}; | ||
var result = Structured.match("var x = 10; var y = 20; var c = 0;", | ||
function() {var $a = $b;}, {"varCallbacks": varCallbacks}); | ||
equal(result, false, "Returning failure object is false"); | ||
equal(varCallbacks.failure, "Make sure the value is big", | ||
"Failure message is correctly set, basic."); | ||
varCallbacks = { | ||
"$a": function(obj) { | ||
return true; | ||
}, | ||
"$b": function(obj) { | ||
if (obj.value > 90) { | ||
return true; | ||
} | ||
return {"failure": "Make sure the value is big"}; | ||
} | ||
}; | ||
var result = Structured.match("var x = 10; var y = 20; var c = 100;", | ||
function() {var $a = $b;}, {"varCallbacks": varCallbacks}); | ||
equal(result, true, "Matches still work around failure messages"); | ||
equal(varCallbacks.failure, undefined, | ||
"Failure message is not set if no failure."); | ||
}); | ||
test("More complicated single variable callbacks", function() { | ||
var code, structure, varCallbacks; | ||
structure = function() { | ||
var $a = _, _ = $val; | ||
var $d = function() {}; | ||
var draw = function() { | ||
var $b = $a + _; | ||
$d(_, $e, $b, _); | ||
$d($a); | ||
$a = $e.length; | ||
}; | ||
}; | ||
code = " \n \ | ||
var a = 10, z = 3, b = 20, y = 1, k = foo(); \n \ | ||
var bar = function(x) {return x + 3;}; \n \ | ||
var foo = function(x) {return x + 3;}; \n \ | ||
var draw = function() { \n \ | ||
var t = z + y; \n \ | ||
foo(t); \n \ | ||
foo(3, 'falcon', t, 10); \n \ | ||
foo(3, 'eagle', t, 10); \n \ | ||
test(z); \n \ | ||
foo(z); \n \ | ||
z = 'falcon'.length; \n \ | ||
z = 'eagle'.length; \n \ | ||
} \n \ | ||
"; | ||
var varCallbacks = { | ||
"$e": function(obj) { | ||
return obj.value === "eagle"; | ||
}, | ||
"$val": function(obj) { | ||
return (obj.type === "CallExpression" && | ||
obj.callee.name === "foo"); | ||
} | ||
}; | ||
equal(Structured.match(code, structure, {varCallbacks: varCallbacks}), | ||
true, "Complex with parameters, function names, etc works."); | ||
}); | ||
test("Multiple variable callbacks", function() { | ||
var varCallbacks, code, structure, result; | ||
varCallbacks = { | ||
"$a, $b": function(a, b) { | ||
return a.value > b.value; | ||
} | ||
}; | ||
result = Structured.match("var x = 50; var y = 20;", | ||
function() {var _ = $a; var _ = $b;}, | ||
{"varCallbacks": varCallbacks}); | ||
equal(result, true, "Simple multiple variable callback works."); | ||
varCallbacks = { | ||
"$a, $b": function(a, b) { | ||
return a.value > b.value; | ||
} | ||
}; | ||
result = Structured.match("var x = 50; var y = 20;", | ||
function() {var _ = $a; var _ = $b;}, | ||
{"varCallbacks": varCallbacks}); | ||
equal(result, true, "Simple multiple variable callback works."); | ||
varCallbacks = { | ||
"$a,$b, $c": function(a, b, c) { | ||
return a.value > b.value && c.value !== 40; | ||
} | ||
}; | ||
equal(Structured.match("var a = 3; var b = 1; var c = 4;", | ||
function() {var a = $a; var b = $b; var c = $c;}), | ||
true, "Trim from left works."); | ||
// TODO test more involved var callbacks. | ||
varCallbacks = { | ||
"$c, $a, $b": function(c, a, b) { | ||
return a.value > b.value; | ||
}, | ||
"$c": function(c) { | ||
return c.type === "Identifier" && c.name !== "foo"; | ||
}, | ||
"$c, $d, $e": function(c, d, e) { | ||
return d.value === e.value; | ||
} | ||
}; | ||
structure = function() { | ||
_ += $a + $b; | ||
$c($e, $d); | ||
}; | ||
code = "tree += 30 + 50 + 10; plant(40, 20); forest(30, 30);"; | ||
result = Structured.match(code, structure, | ||
{"varCallbacks": varCallbacks}); | ||
equal(result, true, "Multiple multiple-var callbacks work."); | ||
code = ("tree += 30 + 50 + 70; plant(40, 0) + forest(30, 30);" + | ||
"tree += 30 + 50 + 10; plant(40, 0) + forest(30, 60);"); | ||
result = Structured.match(code, structure, | ||
{"varCallbacks": varCallbacks}); | ||
equal(result, false, "False multiple multiple-var callbacks work."); | ||
equal(varCallbacks.failure, undefined, | ||
"No failure message if none specified."); | ||
varCallbacks = { | ||
"$red, $green, $blue": function(red, green, blue) { | ||
if (red.value < 50) { | ||
return {"failure": "Red must be greater than 50"}; | ||
} | ||
if (green.value < blue.value) { | ||
return {"failure": "Use more green than blue"}; | ||
} | ||
return true; | ||
} | ||
}; | ||
structure = function() { | ||
fill($red, $green, $blue); | ||
}; | ||
result = Structured.match("var foo = 5; foo += 2; fill(100, 40, 200);", | ||
structure, {"varCallbacks": varCallbacks}); | ||
equal(result, false, "False RGB matching works."); | ||
equal(varCallbacks.failure, "Use more green than blue", | ||
"False RGB message works"); | ||
result = Structured.match("var foo = 5; foo += 2; fill(100, 40, 2);", | ||
structure, {"varCallbacks": varCallbacks}); | ||
equal(result, true, "True RGB matching works."); | ||
equal(varCallbacks.failure, undefined, "True RGB message works"); | ||
}); | ||
}; | ||
var constantFolding = function() { | ||
QUnit.module("Constant folding"); | ||
test("Simple constant folding", function() { | ||
ok(Structured.match("var x = -5;", | ||
function() { var x = $num; },{ | ||
"varCallbacks": { | ||
"$num": function(num) { | ||
return num && num.value && num.value < 0; | ||
} | ||
} | ||
}), | ||
"Unary - operator folded on number literals."); | ||
ok(Structured.match("var x = +5;", | ||
function() { var x = $num; },{ | ||
"varCallbacks": { | ||
"$num": function(num) { | ||
return num && num.value && num.value > -10; | ||
} | ||
} | ||
}), | ||
"Unary + operator folded on number literals."); | ||
ok(Structured.match("var y = 10; var x = +y; x = -y;", | ||
function() { var x = +$var; x = -$var; }), | ||
"Unary + - operators work on non-literals."); | ||
}); | ||
}; | ||
var wildcardVarTests = function() { | ||
QUnit.module("Wildcard variables"); | ||
test("Simple wildcard tests", function() { | ||
var structure, code; | ||
ok(Structured.match("var x = 10; x -= 1;", | ||
function() { var $a = 10; $a -= 1; }), | ||
"Basic variable match works."); | ||
equal(Structured.match("var x = 10; y -= 1;", | ||
function() { var $a = 10; $a -= 1; }), | ||
false, "Basic variable no-match works."); | ||
ok(Structured.match("var x = 10; var y = 10; var t = 3; var c = 1; c = 3; t += 2; y += 2;", | ||
function() {var $a = 10; var $b = _; $b += 2; $a += 2;}), | ||
"Basic multiple-option variable match works."); | ||
equal(Structured.match("var x = 10; var y = 10; var t = 3; var c = 1; c = 3; y += 2;", | ||
function() {var $a = _; var $b = _; $b = 3 + 2; $a += 2;}), false, | ||
"Basic multiple-option variable no-match works."); | ||
equal(Structured.match("if(true) {var x = 2;} var y = 4; z = x + y", | ||
function() {if (_) {var $a = _;} var $b = 4; _ = $a + $b;}), | ||
true, "Simple multiple var match works"); | ||
// QUnit test code | ||
structure = function() { | ||
function $k(bar) {} | ||
$k; | ||
}; | ||
code = "function foo(bar) {} bar; foo;"; | ||
equal(Structured.match(code, structure), | ||
true, "Matching declared function name works."); | ||
}); | ||
test("Involved wildcard tests", function() { | ||
var structure, code; | ||
structure = function() { | ||
var $a, $b, $c, $d; | ||
$a = 1; | ||
$b = 1; | ||
$c = 1; | ||
$d = 1; | ||
$a += 3; | ||
}; | ||
code = " \n \ | ||
var r, s, t, u, v, w, x, y, z; \n \ | ||
r = 1; s = 1; t = 1; u = 1; v = 1; w = 1; x = 1; y = 1; z = 1; \n \ | ||
u += 3; \n \ | ||
"; | ||
equal(Structured.match(code, structure), | ||
true, "More ambiguous multi-variable matching works."); | ||
code = " \n \ | ||
var r, s, t, u, v, w, x, y, z; \n \ | ||
r = 1; s = 1; t = 1; u = 1; v = 1; w = 1; x = 1; y = 1; z = 1; \n \ | ||
x += 3; \n \ | ||
"; | ||
equal(Structured.match(code, structure), | ||
false, "More ambiguous multi-variable non-matching works."); | ||
structure = function() { | ||
var $a = _; | ||
var $d = function() {} | ||
var draw = function() { | ||
var $b = $a + _; | ||
$d(_, $e, $b, _); | ||
$d($a); | ||
$a = $e.length; | ||
} | ||
}; | ||
code = " \n \ | ||
var a = 10, b = 20, z = 3, y = 1; \n \ | ||
var bar = function(x) {return x + 3;}; \n \ | ||
var foo = function(x) {return x + 3;}; \n \ | ||
var draw = function() { \n \ | ||
var t = z + y; \n \ | ||
foo(t); \n \ | ||
foo(3, 'eagle', t, 10); \n \ | ||
test(z); \n \ | ||
foo(z); \n \ | ||
z = 'eagle'.length; \n \ | ||
} \n \ | ||
"; | ||
equal(Structured.match(code, structure), | ||
true, "Complex vars with parameters, function names, etc works."); | ||
code = " \n \ | ||
var a = 10, b = 20, z = 3, y = 1; \n \ | ||
var bar = function(x) {return x + 3;}; \n \ | ||
var foo = function(x) {return x + 3;}; \n \ | ||
var draw = function() { \n \ | ||
var t = z + y; \n \ | ||
foo(t); \n \ | ||
foo(3, 'eagle', t, 10); \n \ | ||
test(z); \n \ | ||
foo(z); \n \ | ||
z = 'eaglee'.length; \n \ | ||
} \n \ | ||
"; | ||
equal(Structured.match(code, structure), | ||
false, "Complex vars with small mismatch breaks."); | ||
}); | ||
}; | ||
var nestingOrder = function() { | ||
QUnit.module("Nesting order"); | ||
/* | ||
* A structure implicitly applies a partial order constraint on | ||
* the nesting "levels" of expressions. In particular, if pattern | ||
* expression A lexically appears before pattern expression B, then | ||
* any binding of concrete expressions to A and B implies that B is | ||
* nested at the same or deeper level than A. | ||
*/ | ||
test("Simple nesting tests", function() { | ||
var structure, code; | ||
ok(Structured.match("var x = 5; while(true) { var y = 6; }", | ||
function() { var x = 5; var y = 6; }), | ||
"Downward expression ordering works.") | ||
ok(!Structured.match("while(true) { var x = 5; } var y = 6;", | ||
function() { var x = 5; var y = 6; }), | ||
"Upward expression ordering fails.") | ||
}); | ||
}; | ||
var runAll = function() { | ||
@@ -373,5 +813,9 @@ basicTests(); | ||
peerTests(); | ||
wildcardVarTests(); | ||
varCallbackTests(); | ||
constantFolding(); | ||
combinedTests(); | ||
nestingOrder(); | ||
}; | ||
runAll(); |
Sorry, the diff of this file is not supported yet
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
1136175
26
27637
81
2