Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add support for loose object matching

Adds support for var/let, and adds support for loose object matching.
  • Loading branch information...
commit 4c6b655cbad9ceb104304143e1112aa02201ef5a 1 parent 176437e
Ryan Patterson authored
Showing with 3,836 additions and 34 deletions.
  1. +19 −25 README.mdown
  2. +82 −9 jsgrep
  3. +3,735 −0 tests/javelin.js
View
44 README.mdown
@@ -1,51 +1,45 @@
# jsgrep: a syntactically-aware grep for JavaScript
-Jsgrep is program that allows you to search for a particular JavaScript pattern,
-using the abstract syntax tree (AST) of the program, which allows you to match
-for particular identifiers, string constants, function calls, etc., without
-having to be concerned with the whitespace of the original program.
+Jsgrep is program that searches for a particular JavaScript pattern using the
+abstract syntax tree (AST) of the program. This enables matching expressions
+based on their JavaScript meaning, rather than based on simple strings.
## Examples
-**Find all instances of a string constant**
-
- $ ./jsgrep "'image'" tests/*.js
- tests/jquery.js: return elem.nodeName.toLowerCase() === "input" && "image" === elem.type;
-
-Jsgrep parses the query as JavaScript and uses that to compare against the
-target program, so the type of quote you search for does not matter.
-
**Find calls to window.setTimeout with a 0 timeout**
$ jsgrep 'setTimeout(A, 0)' tests/*.js
tests/jquery.js: setTimeout( function() {
tests/jquery.js: setTimeout( clearFxNow, 0 );
-Because jsgrep is using the abstract syntax tree, it can identify that the
-particular call to `setTimeout` matches, even when it spans multiple lines
-and contains an inline function.
-
+Jsgrep uses metavariables as wildcards. Metavariables match any valid JavaScript
+chunk, so in this case, the first match was an entire inline function. In both
+cases, the second parameter to setTimeout was a literal value of 0.
**Find value defaulting**
- $ ./jsgrep "A = A || B;" tests/*.js
+ $ jsgrep "A = A || B;" tests/*.js
tests/jquery.js: args = args || [];
- tests/jquery.js: type = type || "fx";
- tests/jquery.js: type = type || "fx";
- tests/jquery.js: results = results || [];
- tests/jquery.js: context = context || document;
- tests/jquery.js: result = result || 1;
- tests/jquery.js: qualifier = qualifier || 0;
- tests/jquery.js: context = context || document;
tests/jquery.js: dataType = dataType || options.dataTypes[ 0 ];
When the pattern references the same metavariable multiple times, jsgrep ensures
that the value of the metavariable is the same throughout the match.
+**Find classes that have a 'path' property**
+
+ $ jsgrep -p C "JX.install(C, { properties: { path: X } })" tests/*.js
+ tests/javelin.js: 'Event'
+ tests/javelin.js: 'URI'
+
+Jsgrep allows you to search object initializations partially, which enables
+easily drilling into the structure of JavaScript classes. This example also uses
+the `-p` flag to print only a particular matched variable.
+
## TODO
* Add support for ... to the parser
-* Add support for { asdf: value } patterns
+* Consider support for --where/--eval
+* Support for most statements in patterns
## Contributors
View
91 jsgrep
@@ -12,7 +12,8 @@ var config = {
patterns: [ ],
paths: [ ],
print: false,
- dumpAst: false
+ dumpAst: false,
+ strictMatches: false
};
function usage(hasError) {
@@ -33,7 +34,10 @@ function usage(hasError) {
console.log(" matching pattern.");
console.log("");
console.log(" -p, --print=VAR Instead of printing the matching line, print the");
- console.log(" matching variable.");
+ console.log(" matching metavariable.");
+ console.log("");
+ console.log(" -S, --strict-matches Require strict matches for object initializers and");
+ console.log(" blocks of statements.");
console.log("");
console.log(" --dump-ast Instead of searching, dump the AST for the patterns.");
process.exit(0);
@@ -42,7 +46,7 @@ function usage(hasError) {
(function parseArgs() {
var getopt = require('node-getopt');
var parser = new getopt.BasicParser(
- 'e:(pattern)o(only-matching)p:(print)D(dump-ast)h(help)',
+ 'e:(pattern)o(only-matching)p:(print)S(strict-matches)D(dump-ast)h(help)',
process.argv);
while ((option = parser.getopt()) !== undefined) {
@@ -56,6 +60,9 @@ function usage(hasError) {
case 'p':
config.print = option.optarg;
break;
+ case 'S':
+ config.strictMatches = true;
+ break;
case 'D':
config.dumpAst = true;
break;
@@ -159,6 +166,32 @@ function astIsEqual(node, pattern, variables) {
return false;
}
+ if (node.type == tokens.OBJECT_INIT && !config.strictMatches) {
+ // Strict matching will be handled normally (below).
+ if (pattern.children.length > node.children.length) {
+ return false;
+ }
+
+ var keys = _.clone(pattern.children);
+ for (var i = 0; i < node.children.length; i++) {
+ for (var j = 0; j < keys.length;) {
+ if (astIsEqual(node.children[i], keys[j], variables)) {
+ keys.splice(j, 1);
+ break;
+ } else {
+ j++;
+ }
+ }
+
+ if (keys.length == 0) {
+ break;
+ }
+ }
+
+ // No keys left over -> match.
+ return keys.length == 0;
+ }
+
switch(node.type) {
// Core values
case tokens.FALSE:
@@ -218,17 +251,15 @@ function astIsEqual(node, pattern, variables) {
case tokens.ASSIGN:
//case tokens.BLOCK:
case tokens.CALL:
- //case tokens.COMMA:
+ case tokens.COMMA:
case tokens.DELETE:
case tokens.DOT:
case tokens.HOOK:
case tokens.INDEX:
- //case tokens.LET:
- //case tokens.VAR:
// Special
case tokens.ARRAY_INIT: // TODO: handle ...
case tokens.LIST: // TODO: handle ...
- case tokens.OBJECT_INIT: // TODO: handle ... and partial object matching
+ case tokens.OBJECT_INIT: // TODO: handle ...
case tokens.PROPERTY_INIT:
//case tokens.SCRIPT:
if (node.children.length == pattern.children.length) {
@@ -241,6 +272,48 @@ function astIsEqual(node, pattern, variables) {
}
break;
+ case tokens.LET:
+ case tokens.VAR:
+ // All of var's children are IDENTIFIERs with name/initializer values
+ // TODO: this does not support destructuring assignments
+ if (pattern.children.length > node.children.length) {
+ return false;
+ }
+
+ var keys = _.clone(pattern.children);
+ for (var i = 0; i < node.children.length; i++) {
+ for (var j = 0; j < keys.length;) {
+ if (astIsEqual(node.children[i], keys[j], variables)) {
+ // If the pattern has an initializer, it must be equal to the one
+ // in the source.
+ if (keys[j].initializer &&
+ (!node.children[i].initializer ||
+ !astIsEqual(node.children[i].initializer,
+ keys[j].initializer, variables))) {
+ return false;
+ }
+ // If in strict mode and the pattern has no initializer, neither can
+ // the source.
+ if (!keys[j].initializer && config.strictMatches &&
+ node.children[i].initializer) {
+ return false;
+ }
+ keys.splice(j, 1);
+ break;
+ } else {
+ j++;
+ }
+ }
+
+ if (keys.length == 0) {
+ break;
+ }
+ }
+
+ // No keys left over -> match.
+ return keys.length == 0;
+ break;
+
case tokens.SEMICOLON:
if (!node.expression && !pattern.expression) {
return true;
@@ -328,8 +401,8 @@ function astIsEqual(node, pattern, variables) {
//forEachNode(node.block);
break;
- //case tokens.THROW:
- //forEachNode(node.exception, callback);
+ case tokens.THROW:
+ return astIsEqual(node.exception, pattern.exception, variables);
break;
default:
View
3,735 tests/javelin.js
3,735 additions, 0 deletions not shown
Please sign in to comment.
Something went wrong with that request. Please try again.