Permalink
Browse files

Working jspatch!

jspatch works and emits diff files.
  • Loading branch information...
1 parent e3ef524 commit 05b5585bdbd021d6f788651ceeede5f8554313f0 Ryan Patterson committed Jan 11, 2012
Showing with 139 additions and 25 deletions.
  1. +72 −14 jspatch
  2. +67 −11 lib/jsgrep.js
View
86 jspatch
@@ -2,6 +2,7 @@
var Narcissus = require('narcissus/main');
var _ = require('underscore');
+var child_process = require('child_process');
var fs = require('fs');
var path = require('path');
@@ -72,18 +73,55 @@ try {
}
var matchFn = eval("(function custom_matcher(jsgrep) {" + patchSource + "})");
-function doFile(filename) {
+function generateDiff(oldFilename, newFile, callback) {
+ var tempFilename, tries = 1;
+ do {
+ tempFilename = path.join(process.env.TEMP || "/tmp",
+ "jspatch." + path.basename(oldFilename, ".js") +
+ (tries == 1 ? "" : ("." + tries)) + ".js");
+ tries++;
+ } while (path.existsSync(tempFilename));
+
+ var tempFile = fs.openSync(tempFilename, 'w');
+ try {
+ fs.writeSync(tempFile, newFile);
+ } finally {
+ fs.closeSync(tempFile);
+ }
+
+ var child =
+ child_process.spawn('diff', [ '-u', oldFilename, tempFilename ]);
+
+ child.stdout.on('data', function(data) {
+ process.stdout.write(data);
+ });
+ child.stderr.on('data', function(data) {
+ process.stderr.write(data);
+ });
+
+ child.on('exit', function(code) {
+ fs.unlinkSync(tempFilename);
+ if (code !== 0 && code !== 1) {
+ // diff returns 0 for no differences, 1 for differences, 2 for error
+ console.error('jspatch: diff exited with code ' + code);
+ process.exit(1);
+ }
+ callback();
+ });
+}
+
+function doFile(filename, callback) {
var source = fs.readFileSync(filename).toString();
// If the first character is a shebang, comment it out
if (source.substr(0, 2) == '#!') {
- source = "// " + source;
+ source = "//" + source;
}
try {
var ast = Narcissus.parser.parse(source, filename, 1);
} catch(e) {
console.warn(e.name + ": " + e.message);
- return;
+ return callback();
}
try {
@@ -102,38 +140,58 @@ function doFile(filename) {
}
var modifiedSource = ast.tokenizer.source;
+
+ // Try to parse the modified source
+ try {
+ Narcissus.parser.parse(modifiedSource, 'modified source', 1);
+ } catch(e) {
+ console.log('jspatch: ' + filename +
+ ': warning: modifications resulted in an invalid JavaScript file!');
+ console.log('This is probably a bug in jspatch. ' +
+ 'Please let ry@fb.com know!');
+ }
+
if (source.substr(0, 4) == '//#!') {
- // Now remove the command form the shebang
+ // Now remove the comment from the shebang
source = source.substr(2);
modifiedSource = modifiedSource.substr(2);
}
- console.log(modifiedSource);
+ generateDiff(filename, modifiedSource, callback);
}
-function doDirectory(directory) {
+var paths = config.paths;
+
+function queueDirectory(directory) {
if (!config.recursive) {
return;
}
var files = fs.readdirSync(directory);
for (var i = 0; i < files.length; i++) {
var filePath = path.join(directory, files[i]);
var stat = fs.statSync(filePath);
- if (stat.isDirectory()) {
- doDirectory(filePath);
- } else if (filePath.substr(-3) == ".js") {
- doFile(filePath);
+ if (stat.isDirectory() ||
+ filePath.substr(-3) == ".js") {
+ paths.push(filePath);
}
}
}
-for (var i = 0; i < config.paths.length; i++) {
- var stat = fs.statSync(config.paths[i]);
+function jspatch_loop() {
+ if (paths.length == 0) {
+ return;
+ }
+
+ var nextPath = paths.shift();
+ var stat = fs.statSync(nextPath);
if (stat.isDirectory()) {
- doDirectory(config.paths[i]);
+ queueDirectory(nextPath);
+ return jspatch_loop();
} else {
- doFile(config.paths[i]);
+ doFile(nextPath, jspatch_loop);
}
}
+jspatch_loop();
+
// vim: ft=javascript
View
@@ -8,6 +8,13 @@ function Matcher(ast, options) {
this.ast = ast;
this.options = options || {};
this.value = ast.value;
+
+ // jsgrepDelta -> the offset (due to source modifications) of the parsed AST's
+ // locations.
+ // XXX: this relies on modifications happening *in source order*
+ if (typeof this.ast.tokenizer.jsgrepDelta === 'undefined') {
+ this.ast.tokenizer.jsgrepDelta = 0;
+ }
}
exports.Matcher = Matcher;
@@ -20,6 +27,13 @@ Matcher.compilePattern = function(pattern) {
if (pattern in PATTERN_CACHE) {
return PATTERN_CACHE[pattern];
}
+
+ var patternAst = Matcher.compilePatternNoCache(pattern);
+ PATTERN_CACHE[pattern] = patternAst;
+ return patternAst;
+};
+
+Matcher.compilePatternNoCache = function(pattern) {
var patternAst = Narcissus.parser.parse(pattern, pattern, 0);
if (patternAst.children.length == 1) {
@@ -33,10 +47,20 @@ Matcher.compilePattern = function(pattern) {
patternAst = patternAst.expression;
}
- PATTERN_CACHE[pattern] = patternAst;
return patternAst;
};
+Matcher.identifierIsMetavar = function(identifier) {
+ return /^[A-Z](_.*)?$/.test(identifier);
+};
+
+Matcher.getSourceForNode = function(target) {
+ var tokenizer = target.tokenizer;
+ var start = target.start + target.tokenizer.jsgrepDelta;
+ var end = target.end + target.tokenizer.jsgrepDelta;
+ return target.tokenizer.source.substring(start, end);
+};
+
Matcher.prototype.find = function(pattern, callback, options) {
var ret = [ ];
var self = this;
@@ -62,8 +86,18 @@ Matcher.prototype.matchStrict = function(pattern, callback) {
return this._match(this.ast, pattern, callback, matchOptions);
};
+Matcher.prototype.replaceAll = function(pattern, replacement) {
+ this.find(pattern, function(vars) {
+ vars.node.replaceWith(replacement);
+ });
+};
+
Matcher.prototype.replaceWith = function(replacement) {
- this.options.parent.replace(this.options.name, replacement);
+ if (this.options.parent) {
+ this.options.parent.replace(this.options.name, replacement);
+ } else {
+ this._replace(this.ast, replacement);
+ }
};
Matcher.prototype.replace = function(v, replacement) {
@@ -72,20 +106,41 @@ Matcher.prototype.replace = function(v, replacement) {
message: v + ' is not a bound metavariable'
};
}
- // replacement is ITSELF A PATTERN SO META
- //var replAst = Matcher.compilePattern(replacement);
- // Not yet, we'll do that later
var target = this.options.boundVars[v].ast;
- // jsgrepDelta -> the offset (due to source modifications) of the parsed AST's
- // locations.
- // XXX: this relies on modifications happening *in source order*
- if (typeof target.tokenizer.jsgrepDelta === 'undefined') {
+ this._replace(target, replacement);
+};
+
+Matcher.prototype._replace = function(target, replacement) {
+ const tokens = Narcissus.definitions.tokenIds;
+
+ // replacement is ITSELF A PATTERN SO META
+ // Cannot cache because we modify its source in-place.
+ var replAst = Matcher.compilePatternNoCache(replacement);
+
+ for (var i in this.options.boundVars) {
+ if (Matcher.identifierIsMetavar(i)) {
+ var self = this;
+ forEachNode(replAst, function(node) {
+ if (node.type == tokens.IDENTIFIER && node.value == i) {
+ self._replaceWithString(node,
+ Matcher.getSourceForNode(self.options.boundVars[i].ast));
+ }
+ });
+ }
+ }
+
+ this._replaceWithString(target, replAst.tokenizer.source);
+};
+
+Matcher.prototype._replaceWithString = function(target, replacement) {
+ if (!target.tokenizer.jsgrepDelta) {
target.tokenizer.jsgrepDelta = 0;
}
var start = target.start + target.tokenizer.jsgrepDelta;
var end = target.end + target.tokenizer.jsgrepDelta;
+
target.tokenizer.source =
target.tokenizer.source.substring(0, start) +
replacement +
@@ -253,10 +308,11 @@ var astIsEqual = exports.astIsEqual = function(node, pattern, config) {
}
if (pattern.type == tokens.IDENTIFIER &&
- /^[A-Z](_.*)?$/.test(pattern.value)) {
+ Matcher.identifierIsMetavar(pattern.value)) {
if (pattern.value in config.variables) {
// Variable already matched, compare this node to that value
- return astIsEqual(node, config.variables[pattern.value]);
+ return astIsEqual(node, config.variables[pattern.value],
+ { strictMatches: true });
} else {
if (config.failOnMetavar) {
throw {

0 comments on commit 05b5585

Please sign in to comment.