Merge pull request #5679 from loganfsmyth/concat-static-analysis

Add static analysis for "".concat(obj, "str")
This commit is contained in:
Tobias Koppers 2017-09-19 10:22:10 +02:00 committed by GitHub
commit f0dcde4800
8 changed files with 167 additions and 33 deletions

View File

@ -321,45 +321,76 @@ class Parser extends Tapable {
}
return new BasicEvaluatedExpression().setString(result).setRange(expr.range);
});
});
/**
* @param {string} kind "cooked" | "raw"
* @param {any[]} quasis quasis
* @param {any[]} expressions expressions
* @return {BasicEvaluatedExpression[]} Simplified template
*/
function getSimplifiedTemplateResult(kind, quasis, expressions) {
const parts = [];
/**
* @param {string} kind "cooked" | "raw"
* @param {any[]} quasis quasis
* @param {any[]} expressions expressions
* @return {BasicEvaluatedExpression[]} Simplified template
*/
function getSimplifiedTemplateResult(kind, quasis, expressions) {
const parts = [];
for(let i = 0; i < quasis.length; i++) {
parts.push(new BasicEvaluatedExpression().setString(quasis[i].value[kind]).setRange(quasis[i].range));
for(let i = 0; i < quasis.length; i++) {
parts.push(new BasicEvaluatedExpression().setString(quasis[i].value[kind]).setRange(quasis[i].range));
if(i > 0) {
const prevExpr = parts[parts.length - 2],
lastExpr = parts[parts.length - 1];
const expr = this.evaluateExpression(expressions[i - 1]);
if(!(expr.isString() || expr.isNumber())) continue;
if(i > 0) {
const prevExpr = parts[parts.length - 2],
lastExpr = parts[parts.length - 1];
const expr = this.evaluateExpression(expressions[i - 1]);
if(!(expr.isString() || expr.isNumber())) continue;
prevExpr.setString(prevExpr.string + (expr.isString() ? expr.string : expr.number) + lastExpr.string);
prevExpr.setRange([prevExpr.range[0], lastExpr.range[1]]);
parts.pop();
}
prevExpr.setString(prevExpr.string + (expr.isString() ? expr.string : expr.number) + lastExpr.string);
prevExpr.setRange([prevExpr.range[0], lastExpr.range[1]]);
parts.pop();
}
return parts;
}
return parts;
}
this.plugin("evaluate TemplateLiteral", function(node) {
const parts = getSimplifiedTemplateResult.call(this, "cooked", node.quasis, node.expressions);
if(parts.length === 1) {
return parts[0].setRange(node.range);
}
return new BasicEvaluatedExpression().setTemplateString(parts).setRange(node.range);
});
this.plugin("evaluate TaggedTemplateExpression", function(node) {
if(this.evaluateExpression(node.tag).identifier !== "String.raw") return;
const parts = getSimplifiedTemplateResult.call(this, "raw", node.quasi.quasis, node.quasi.expressions);
return new BasicEvaluatedExpression().setTemplateString(parts).setRange(node.range);
});
this.plugin("evaluate CallExpression .concat", function(expr, param) {
if(!param.isString() && !param.isWrapped()) return;
let stringSuffix = null;
let hasUnknownParams = false;
for(let i = expr.arguments.length - 1; i >= 0; i--) {
const argExpr = this.evaluateExpression(expr.arguments[i]);
if(!argExpr.isString() && !argExpr.isNumber()) {
hasUnknownParams = true;
break;
}
const value = argExpr.isString() ? argExpr.string : "" + argExpr.number;
const newString = value + (stringSuffix ? stringSuffix.string : "");
const newRange = [argExpr.range[0], (stringSuffix || argExpr).range[1]];
stringSuffix = new BasicEvaluatedExpression().setString(newString).setRange(newRange);
}
this.plugin("evaluate TemplateLiteral", function(node) {
const parts = getSimplifiedTemplateResult.call(this, "cooked", node.quasis, node.expressions);
if(parts.length === 1) {
return parts[0].setRange(node.range);
}
return new BasicEvaluatedExpression().setTemplateString(parts).setRange(node.range);
});
this.plugin("evaluate TaggedTemplateExpression", function(node) {
if(this.evaluateExpression(node.tag).identifier !== "String.raw") return;
const parts = getSimplifiedTemplateResult.call(this, "raw", node.quasi.quasis, node.quasi.expressions);
return new BasicEvaluatedExpression().setTemplateString(parts).setRange(node.range);
});
if(hasUnknownParams) {
const prefix = param.isString() ? param : param.prefix;
return new BasicEvaluatedExpression().setWrapped(prefix, stringSuffix).setRange(expr.range);
} else if(param.isWrapped()) {
const postfix = stringSuffix || param.postfix;
return new BasicEvaluatedExpression().setWrapped(param.prefix, postfix).setRange(expr.range);
} else {
const newString = param.string + (stringSuffix ? stringSuffix.string : "");
return new BasicEvaluatedExpression().setString(newString).setRange(expr.range);
}
});
this.plugin("evaluate CallExpression .split", function(expr, param) {
if(!param.isString()) return;

View File

@ -331,6 +331,39 @@ describe("Parser", () => {
"b.Number": "number=123",
"b['Number']": "number=123",
"b[Number]": "",
"'str'.concat()": "string=str",
"'str'.concat('one')": "string=strone",
"'str'.concat('one').concat('two')": "string=stronetwo",
"'str'.concat('one').concat('two', 'three')": "string=stronetwothree",
"'str'.concat('one', 'two')": "string=stronetwo",
"'str'.concat('one', 'two').concat('three')": "string=stronetwothree",
"'str'.concat('one', 'two').concat('three', 'four')": "string=stronetwothreefour",
"'str'.concat('one', obj)": "wrapped=['str' string=str]+[null]",
"'str'.concat('one', obj).concat()": "wrapped=['str' string=str]+[null]",
"'str'.concat('one', obj, 'two')": "wrapped=['str' string=str]+['two' string=two]",
"'str'.concat('one', obj, 'two').concat()": "wrapped=['str' string=str]+['two' string=two]",
"'str'.concat('one', obj, 'two').concat('three')": "wrapped=['str' string=str]+['three' string=three]",
"'str'.concat(obj)": "wrapped=['str' string=str]+[null]",
"'str'.concat(obj).concat()": "wrapped=['str' string=str]+[null]",
"'str'.concat(obj).concat('one', 'two')": "wrapped=['str' string=str]+['one', 'two' string=onetwo]",
"'str'.concat(obj).concat(obj, 'one')": "wrapped=['str' string=str]+['one' string=one]",
"'str'.concat(obj).concat(obj, 'one', 'two')": "wrapped=['str' string=str]+['one', 'two' string=onetwo]",
"'str'.concat(obj).concat('one', obj, 'one')": "wrapped=['str' string=str]+['one' string=one]",
"'str'.concat(obj).concat('one', obj, 'two', 'three')": "wrapped=['str' string=str]+['two', 'three' string=twothree]",
"'str'.concat(obj, 'one')": "wrapped=['str' string=str]+['one' string=one]",
"'str'.concat(obj, 'one').concat()": "wrapped=['str' string=str]+['one' string=one]",
"'str'.concat(obj, 'one').concat('two', 'three')": "wrapped=['str' string=str]+['two', 'three' string=twothree]",
"'str'.concat(obj, 'one').concat(obj, 'two', 'three')": "wrapped=['str' string=str]+['two', 'three' string=twothree]",
"'str'.concat(obj, 'one').concat('two', obj, 'three')": "wrapped=['str' string=str]+['three' string=three]",
"'str'.concat(obj, 'one').concat('two', obj, 'three', 'four')": "wrapped=['str' string=str]+['three', 'four' string=threefour]",
"'str'.concat(obj, 'one', 'two')": "wrapped=['str' string=str]+['one', 'two' string=onetwo]",
"'str'.concat(obj, 'one', 'two').concat()": "wrapped=['str' string=str]+['one', 'two' string=onetwo]",
"'str'.concat(obj, 'one', 'two').concat('three', 'four')": "wrapped=['str' string=str]+['three', 'four' string=threefour]",
"'str'.concat(obj, 'one', 'two').concat(obj, 'three', 'four')": "wrapped=['str' string=str]+['three', 'four' string=threefour]",
"'str'.concat(obj, 'one', 'two').concat('three', obj, 'four')": "wrapped=['str' string=str]+['four' string=four]",
"'str'.concat(obj, 'one', 'two').concat('three', obj, 'four', 'five')": "wrapped=['str' string=str]+['four', 'five' string=fourfive]",
"`start${obj}mid${obj2}end`": "template=[start string=start],[mid string=mid],[end string=end]", // eslint-disable-line no-template-curly-in-string
"`start${'str'}mid${obj2}end`": "template=[start${'str'}mid string=startstrmid],[end string=end]", // eslint-disable-line no-template-curly-in-string
"'abc'.substr(1)": "string=bc",
"'abcdef'.substr(2, 3)": "string=cde",
"'abcdef'.substring(2, 3)": "string=c",
@ -359,6 +392,7 @@ describe("Parser", () => {
if(evalExpr.isConditional()) result.push("options=[" + evalExpr.options.map(evalExprToString).join("],[") + "]");
if(evalExpr.isArray()) result.push("items=[" + evalExpr.items.map(evalExprToString).join("],[") + "]");
if(evalExpr.isConstArray()) result.push("array=[" + evalExpr.array.join("],[") + "]");
if(evalExpr.isTemplateString()) result.push("template=[" + evalExpr.quasis.map(evalExprToString).join("],[") + "]");
if(evalExpr.isWrapped()) result.push("wrapped=[" + evalExprToString(evalExpr.prefix) + "]+[" + evalExprToString(evalExpr.postfix) + "]");
if(evalExpr.range) {
const start = evalExpr.range[0] - 5;

View File

@ -18,3 +18,22 @@ it("should parse template strings in amd requires", function(done) {
}
}
})
it("should parse .concat strings in amd requires", function(done) {
var name = "abc";
var suffix = "Test";
var pending = [
require(["./abc/abcTest"], test),
require(["./abc/".concat(name, "Test")], test),
require(["./".concat(name, "/").concat(name, "Test")], test),
require(["./abc/".concat(name).concat(suffix)], test)
].length;
function test (result) {
result.default.should.eql("ok")
if (--pending <= 0) {
done()
}
}
})

View File

@ -5,7 +5,6 @@ it("should parse template strings in require.ensure requires", function(done) {
require.ensure([], function(require) {
var imports = [
require(`./abc/${name}Test`),
require(`./abc/${name}Test`),
require(`./${name}/${name}Test`),
require(`./abc/${name}${suffix}`),
@ -43,3 +42,44 @@ it("should parse template strings in require.resolve", function() {
// can't use typeof as that depends on webpack config.
require.resolve(`./sync/${name}Test`).should.not.be.undefined();
})
it("should parse .concat strings in require.ensure requires", function(done) {
var name = "abc";
var suffix = "Test";
require.ensure([], function(require) {
var imports = [
require("./abc/".concat(name, "Test")),
require("./".concat(name, "/").concat(name, "Test")),
require("./abc/".concat(name).concat(suffix))
];
for (var i = 0; i < imports.length; i++) {
imports[i].default.should.eql("ok");
}
done()
})
})
it("should parse .concat strings in sync requires", function() {
var name = "sync";
var suffix = "Test";
var imports = [
require("./sync/".concat(name, "Test")),
require("./sync/".concat(name).concat(suffix)),
require("./sync/sync".concat("Test"))
];
for (var i = 0; i < imports.length; i++) {
imports[i].default.should.eql("sync");
}
})
it("should parse .concat strings in require.resolve", function() {
var name = "sync";
// Arbitrary assertion; can't use .ok() as it could be 0,
// can't use typeof as that depends on webpack config.
require.resolve("./sync/".concat(name, "Test")).should.not.be.undefined();
})

View File

@ -16,5 +16,15 @@ it("should parse template strings in import", function(done) {
.then(function () { done(); }, done)
});
it("should parse .concat strings in import", function(done) {
var name = "abc".split("");
var suffix = "Test";
import("./abc/".concat(name[0]).concat(name[1]).concat(name[2], "Test"))
.then(function (imported) {
imported.default.should.eql("ok");
})
.then(function () { done(); }, done)
});
require("./cjs")
require("./amd")