Skip to content

add --rename option to rename declarations and references #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Sep 15, 2014
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ Use the `--single_quotes` option to output `'$scope'` instead of `"$scope"`.
Use the `--regexp` option to restrict matching further or to expand matching.
See description further down.

Use the `--rename` option to rename providers (services, factories, controllers, etc.) with
a new name when declared and referenced through annotation.

Use the `--plugin` option to load a user plugin with the provided path (*experimental*,
0.9.x may change API). See [plugin-example.js](plugin-example.js) for more info.

Expand Down
51 changes: 41 additions & 10 deletions ng-annotate-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const ngInject = require("./nginject");
const generateSourcemap = require("./generate-sourcemap");
const Lut = require("./lut");
const scopeTools = require("./scopetools");
const stringmap = require("stringmap");

const chainedRouteProvider = 1;
const chainedUrlRouterProvider = 2;
Expand Down Expand Up @@ -233,7 +234,7 @@ function matchRegular(node, ctx) {
const args = node.arguments;
const target = (is.someof(method.name, ["config", "run"]) ?
args.length === 1 && args[0] :
args.length === 2 && args[0].type === "Literal" && is.string(args[0].value) && args[1]);
args.length === 2 && args[0].type === "Literal" && is.string(args[0].value) && [args[0], args[1]]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main difference is here, where I return another target, besides the defined function, that is the given provider name.


if (target) {
target.$always = true;
Expand Down Expand Up @@ -281,16 +282,23 @@ function matchResolve(props) {
return [];
};

function stringify(arr, quot) {
function getReplaceString(ctx, originalString) {
if (ctx.rename) {
return ctx.rename.get(originalString) || originalString;
}
return originalString;
}

function stringify(ctx, arr, quot) {
return "[" + arr.map(function(arg) {
return quot + arg.name + quot;
return quot + getReplaceString(ctx, arg.name) + quot;
}).join(", ") + "]";
}

function insertArray(functionExpression, fragments, quot) {
function insertArray(ctx, functionExpression, fragments, quot) {
const range = functionExpression.range;

const args = stringify(functionExpression.params, quot);
const args = stringify(ctx, functionExpression.params, quot);
fragments.push({
start: range[0],
end: range[0],
Expand All @@ -303,13 +311,14 @@ function insertArray(functionExpression, fragments, quot) {
});
}

function replaceArray(array, fragments, quot) {
function replaceArray(ctx, array, fragments, quot) {
const functionExpression = last(array.elements);

if (functionExpression.params.length === 0) {
return removeArray(array, fragments);
}
const args = stringify(functionExpression.params, quot);

const args = stringify(ctx, functionExpression.params, quot);
fragments.push({
start: array.range[0],
end: functionExpression.range[0],
Expand All @@ -332,6 +341,16 @@ function removeArray(array, fragments) {
});
}

function replaceString(ctx, string, fragments, quot) {
const customReplace = getReplaceString(ctx, string.value);
const originalQuotes = string.raw.substr(0,1);
fragments.push({
start: string.range[0],
end: string.range[1],
str: originalQuotes + customReplace + originalQuotes
});
}

function judgeSuspects(ctx) {
const suspects = ctx.suspects;
const mode = ctx.mode;
Expand Down Expand Up @@ -366,11 +385,13 @@ function judgeSuspects(ctx) {
}

if (mode === "rebuild" && isAnnotatedArray(target)) {
replaceArray(target, fragments, quot);
replaceArray(ctx, target, fragments, quot);
} else if (mode === "remove" && isAnnotatedArray(target)) {
removeArray(target, fragments);
} else if (is.someof(mode, ["add", "rebuild"]) && isFunctionExpressionWithArgs(target)) {
insertArray(target, fragments, quot);
insertArray(ctx, target, fragments, quot);
} else if (isGenericProviderName(target)) {
replaceString(ctx, target, fragments, quot);
} else {
// if it's not array or function-expression, then it's a candidate for foo.$inject = [..]
judgeInjectArraySuspect(target, ctx);
Expand Down Expand Up @@ -466,7 +487,7 @@ function judgeInjectArraySuspect(node, ctx) {
const start = hasArrayBefore ? prevNode.range[0]: posAfterFunctionDeclaration;
const end = hasArrayBefore ? skipNewline(prevNode.range[1]) : nextNode.range[1];

const str = fmt("{0}{1}{2}.$inject = {3};", EOL, indent, name, ctx.stringify(params, ctx.quot));
const str = fmt("{0}{1}{2}.$inject = {3};", EOL, indent, name, ctx.stringify(ctx, params, ctx.quot));

if (ctx.mode === "rebuild" && hasArray) {
ctx.fragments.push({
Expand Down Expand Up @@ -520,6 +541,9 @@ function isFunctionExpressionWithArgs(node) {
function isFunctionDeclarationWithArgs(node) {
return node.type === "FunctionDeclaration" && node.params.length >= 1;
}
function isGenericProviderName(node) {
return node.type === "Literal" && is.string(node.value);
}

module.exports = function ngAnnotate(src, options) {
const mode = (options.add && options.remove ? "rebuild" :
Expand All @@ -532,6 +556,12 @@ module.exports = function ngAnnotate(src, options) {

const quot = options.single_quotes ? "'" : '"';
const re = (options.regexp ? new RegExp(options.regexp) : /^[a-zA-Z0-9_\$\.\s]+$/);
const rename = new stringmap();
if (options.rename) {
options.rename.forEach(function(value) {
rename.set(value.from, value.to);
});
}
let ast;
const stats = {};
try {
Expand Down Expand Up @@ -589,6 +619,7 @@ module.exports = function ngAnnotate(src, options) {
return src.slice(range[0], range[1]);
},
re: re,
rename: rename,
comments: comments,
fragments: fragments,
suspects: suspects,
Expand Down
17 changes: 16 additions & 1 deletion ng-annotate.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const tryor = require("tryor");
const ngAnnotate = require("./ng-annotate-main");
const version = require("./package.json").version;
const convertSourceMap = require("convert-source-map");
const stringmap = require("stringmap");
const optimist = require("optimist")
.usage("ng-annotate v" + version + "\n\nUsage: ng-annotate OPTIONS <file>\n\n" +
"provide - instead of <file> to read from stdin\n" +
Expand Down Expand Up @@ -43,6 +44,11 @@ const optimist = require("optimist")
.options("regexp", {
describe: "detect short form myMod.controller(...) iff myMod matches regexp",
})
.options("rename", {
describe: "rename declarations and annotated refernces\n" +
"originalName newName anotherOriginalName anotherNewName ...",
default: ""
})
.options("plugin", {
describe: "use plugin with path (experimental)",
})
Expand Down Expand Up @@ -118,7 +124,7 @@ function runAnnotate(err, src) {
config.inFile = filename;
}

["add", "remove", "o", "sourcemap", "sourceroot", "regexp", "single_quotes", "plugin", "stats"].forEach(function(opt) {
["add", "remove", "o", "sourcemap", "sourceroot", "regexp", "rename", "single_quotes", "plugin", "stats"].forEach(function(opt) {
if (opt in argv) {
config[opt] = argv[opt];
}
Expand All @@ -143,6 +149,15 @@ function runAnnotate(err, src) {
});
}

if (config.rename) {
const flattenRename = config.rename.split(" ");
const renameArray = [];
for (let i = 0; i < flattenRename.length; i = i + 2) {
renameArray.push({"from": flattenRename[i], "to": flattenRename[i+1]});
}
config.rename = renameArray;
}

const run_t0 = Date.now();
const ret = ngAnnotate(src, config);
const run_t1 = Date.now();
Expand Down
124 changes: 124 additions & 0 deletions nginject-comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"use strict";

const os = require("os");
const is = require("simple-is");
const fmt = require("simple-fmt");

module.exports = {
init: ngInjectCommentsInit,
};

function ngInjectCommentsInit(ctx) {
const comments = ctx.comments;
const triggers = [];
for (let i = 0; i < comments.length; i++) {
const comment = comments[i];
const pos = comment.value.indexOf("@ngInject");
if (pos >= 0) {
triggers.push({
pos: comment.range[1],
fn: visitNodeFollowingNgInjectComment,
ctx: ctx,
});
}
}

ctx.triggers.addMany(triggers);
}

function nestedObjectValues(node, res) {
res = res || [];

node.properties.forEach(function(prop) {
const v = prop.value;
if (is.someof(v.type, ["FunctionExpression", "ArrayExpression"])) {
res.push(v);
} else if (v.type === "ObjectExpression") {
nestedObjectValues(v, res);
}
});

return res;
}

function visitNodeFollowingNgInjectComment(node, ctx) {
// handle most common case: /*@ngInject*/ prepended to an array or function expression
// (or call expression, in case of IIFE jumping)
if (node.type === "ArrayExpression" || node.type === "FunctionExpression" || node.type === "CallExpression") {
ctx.addModuleContextIndependentSuspect(node, ctx);
return;
}

if (node.type === "ObjectExpression") {
nestedObjectValues(node).forEach(function(n) {
ctx.addModuleContextIndependentSuspect(n, ctx);
});
return;
}

// /*@ngInject*/ var foo = function($scope) {} and
// /*@ngInject*/ function foo($scope) {}
let d0 = null;
const nr1 = node.range[1];
if (node.type === "VariableDeclaration" && node.declarations.length === 1 &&
(d0 = node.declarations[0]).init && ctx.isFunctionExpressionWithArgs(d0.init)) {
const isSemicolonTerminated = (ctx.src[nr1 - 1] === ";");
addRemoveInjectArray(d0.init.params, isSemicolonTerminated ? nr1 : d0.init.range[1], d0.id.name);
} else if (ctx.isFunctionDeclarationWithArgs(node)) {
addRemoveInjectArray(node.params, nr1, node.id.name);
} else if (node.type === "ExpressionStatement" && node.expression.type === "AssignmentExpression" &&
ctx.isFunctionExpressionWithArgs(node.expression.right)) {
const isSemicolonTerminated = (ctx.src[nr1 - 1] === ";");
const name = ctx.srcForRange(node.expression.left.range);
addRemoveInjectArray(node.expression.right.params, isSemicolonTerminated ? nr1 : node.expression.right.range[1], name);
}

function getIndent(pos) {
const src = ctx.src;
const lineStart = src.lastIndexOf("\n", pos - 1) + 1;
let i = lineStart;
for (; src[i] === " " || src[i] === "\t"; i++) {
}
return src.slice(lineStart, i);
}

function addRemoveInjectArray(params, posAfterFunctionDeclaration, name) {
const indent = getIndent(posAfterFunctionDeclaration);
const str = fmt("{0}{1}{2}.$inject = {3};", os.EOL, indent, name, ctx.stringify(ctx, params, ctx.quot));

ctx.triggers.add({
pos: posAfterFunctionDeclaration,
fn: visitNodeFollowingFunctionDeclaration,
});

function visitNodeFollowingFunctionDeclaration(nextNode) {
const assignment = nextNode.expression;
let lvalue;
const hasInjectArray = (nextNode.type === "ExpressionStatement" && assignment.type === "AssignmentExpression" &&
assignment.operator === "=" &&
(lvalue = assignment.left).type === "MemberExpression" &&
lvalue.computed === false && ctx.srcForRange(lvalue.object.range) === name && lvalue.property.name === "$inject");

if (ctx.mode === "rebuild" && hasInjectArray) {
ctx.fragments.push({
start: posAfterFunctionDeclaration,
end: nextNode.range[1],
str: str,
});
} else if (ctx.mode === "remove" && hasInjectArray) {
ctx.fragments.push({
start: posAfterFunctionDeclaration,
end: nextNode.range[1],
str: "",
});
} else if (is.someof(ctx.mode, ["add", "rebuild"]) && !hasInjectArray) {
ctx.fragments.push({
start: posAfterFunctionDeclaration,
end: posAfterFunctionDeclaration,
str: str,
});
}
}
}
}

22 changes: 22 additions & 0 deletions run-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const diff = require("diff");
const findLineColumn = require("find-line-column");
const fmt = require("simple-fmt");
const SourceMapConsumer = require("source-map").SourceMapConsumer;
const stringmap = require("stringmap");

function slurp(filename) {
return String(fs.readFileSync(filename));
Expand All @@ -23,6 +24,18 @@ function test(correct, got, name) {
}
}

const renameOptions = [
{"from":"$a", "to": "$aRenamed"},
{"from":"$b", "to": "$bRenamed"},
{"from":"$c", "to": "$cRenamed"},
{"from":"$d", "to": "$dRenamed"},
{"from":"$e", "to": "$eRenamed"},
{"from":"$f", "to": "$fRenamed"},
{"from":"$g", "to": "$gRenamed"},
{"from":"$h", "to": "$hRenamed"},
{"from":"$i", "to": "$iRenamed"}
];

function testSourcemap(original, got, sourcemap) {
const smc = new SourceMapConsumer(sourcemap);

Expand Down Expand Up @@ -59,6 +72,15 @@ console.log("testing adding annotations using single quotes");
const annotatedSingleQuotes = ngAnnotate(original, {add: true, single_quotes: true}).src;
test(slurp("tests/with_annotations_single.js"), annotatedSingleQuotes, "with_annotations_single.js");

const rename = slurp("tests/rename.js");

console.log("testing adding annotations and renaming");
const annotatedRenamed = ngAnnotate(rename, {
add: true,
rename: renameOptions
}).src;
test(slurp("tests/rename.annotated.js"), annotatedRenamed, "rename.annotated.js");

console.log("testing removing annotations");
test(original, ngAnnotate(annotated, {remove: true}).src, "original.js");

Expand Down
Loading