Skip to content

Commit f0dcde4

Browse files
authored
Merge pull request webpack#5679 from loganfsmyth/concat-static-analysis
Add static analysis for "".concat(obj, "str")
2 parents 1772beb + 08179b3 commit f0dcde4

8 files changed

Lines changed: 169 additions & 35 deletions

File tree

lib/Parser.js

Lines changed: 65 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -321,45 +321,76 @@ class Parser extends Tapable {
321321
}
322322
return new BasicEvaluatedExpression().setString(result).setRange(expr.range);
323323
});
324+
});
324325

325-
/**
326-
* @param {string} kind "cooked" | "raw"
327-
* @param {any[]} quasis quasis
328-
* @param {any[]} expressions expressions
329-
* @return {BasicEvaluatedExpression[]} Simplified template
330-
*/
331-
function getSimplifiedTemplateResult(kind, quasis, expressions) {
332-
const parts = [];
333-
334-
for(let i = 0; i < quasis.length; i++) {
335-
parts.push(new BasicEvaluatedExpression().setString(quasis[i].value[kind]).setRange(quasis[i].range));
336-
337-
if(i > 0) {
338-
const prevExpr = parts[parts.length - 2],
339-
lastExpr = parts[parts.length - 1];
340-
const expr = this.evaluateExpression(expressions[i - 1]);
341-
if(!(expr.isString() || expr.isNumber())) continue;
342-
343-
prevExpr.setString(prevExpr.string + (expr.isString() ? expr.string : expr.number) + lastExpr.string);
344-
prevExpr.setRange([prevExpr.range[0], lastExpr.range[1]]);
345-
parts.pop();
346-
}
326+
/**
327+
* @param {string} kind "cooked" | "raw"
328+
* @param {any[]} quasis quasis
329+
* @param {any[]} expressions expressions
330+
* @return {BasicEvaluatedExpression[]} Simplified template
331+
*/
332+
function getSimplifiedTemplateResult(kind, quasis, expressions) {
333+
const parts = [];
334+
335+
for(let i = 0; i < quasis.length; i++) {
336+
parts.push(new BasicEvaluatedExpression().setString(quasis[i].value[kind]).setRange(quasis[i].range));
337+
338+
if(i > 0) {
339+
const prevExpr = parts[parts.length - 2],
340+
lastExpr = parts[parts.length - 1];
341+
const expr = this.evaluateExpression(expressions[i - 1]);
342+
if(!(expr.isString() || expr.isNumber())) continue;
343+
344+
prevExpr.setString(prevExpr.string + (expr.isString() ? expr.string : expr.number) + lastExpr.string);
345+
prevExpr.setRange([prevExpr.range[0], lastExpr.range[1]]);
346+
parts.pop();
347347
}
348-
return parts;
349348
}
349+
return parts;
350+
}
350351

351-
this.plugin("evaluate TemplateLiteral", function(node) {
352-
const parts = getSimplifiedTemplateResult.call(this, "cooked", node.quasis, node.expressions);
353-
if(parts.length === 1) {
354-
return parts[0].setRange(node.range);
352+
this.plugin("evaluate TemplateLiteral", function(node) {
353+
const parts = getSimplifiedTemplateResult.call(this, "cooked", node.quasis, node.expressions);
354+
if(parts.length === 1) {
355+
return parts[0].setRange(node.range);
356+
}
357+
return new BasicEvaluatedExpression().setTemplateString(parts).setRange(node.range);
358+
});
359+
this.plugin("evaluate TaggedTemplateExpression", function(node) {
360+
if(this.evaluateExpression(node.tag).identifier !== "String.raw") return;
361+
const parts = getSimplifiedTemplateResult.call(this, "raw", node.quasi.quasis, node.quasi.expressions);
362+
return new BasicEvaluatedExpression().setTemplateString(parts).setRange(node.range);
363+
});
364+
365+
this.plugin("evaluate CallExpression .concat", function(expr, param) {
366+
if(!param.isString() && !param.isWrapped()) return;
367+
368+
let stringSuffix = null;
369+
let hasUnknownParams = false;
370+
for(let i = expr.arguments.length - 1; i >= 0; i--) {
371+
const argExpr = this.evaluateExpression(expr.arguments[i]);
372+
if(!argExpr.isString() && !argExpr.isNumber()) {
373+
hasUnknownParams = true;
374+
break;
355375
}
356-
return new BasicEvaluatedExpression().setTemplateString(parts).setRange(node.range);
357-
});
358-
this.plugin("evaluate TaggedTemplateExpression", function(node) {
359-
if(this.evaluateExpression(node.tag).identifier !== "String.raw") return;
360-
const parts = getSimplifiedTemplateResult.call(this, "raw", node.quasi.quasis, node.quasi.expressions);
361-
return new BasicEvaluatedExpression().setTemplateString(parts).setRange(node.range);
362-
});
376+
377+
const value = argExpr.isString() ? argExpr.string : "" + argExpr.number;
378+
379+
const newString = value + (stringSuffix ? stringSuffix.string : "");
380+
const newRange = [argExpr.range[0], (stringSuffix || argExpr).range[1]];
381+
stringSuffix = new BasicEvaluatedExpression().setString(newString).setRange(newRange);
382+
}
383+
384+
if(hasUnknownParams) {
385+
const prefix = param.isString() ? param : param.prefix;
386+
return new BasicEvaluatedExpression().setWrapped(prefix, stringSuffix).setRange(expr.range);
387+
} else if(param.isWrapped()) {
388+
const postfix = stringSuffix || param.postfix;
389+
return new BasicEvaluatedExpression().setWrapped(param.prefix, postfix).setRange(expr.range);
390+
} else {
391+
const newString = param.string + (stringSuffix ? stringSuffix.string : "");
392+
return new BasicEvaluatedExpression().setString(newString).setRange(expr.range);
393+
}
363394
});
364395
this.plugin("evaluate CallExpression .split", function(expr, param) {
365396
if(!param.isString()) return;

test/Parser.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,39 @@ describe("Parser", () => {
331331
"b.Number": "number=123",
332332
"b['Number']": "number=123",
333333
"b[Number]": "",
334+
"'str'.concat()": "string=str",
335+
"'str'.concat('one')": "string=strone",
336+
"'str'.concat('one').concat('two')": "string=stronetwo",
337+
"'str'.concat('one').concat('two', 'three')": "string=stronetwothree",
338+
"'str'.concat('one', 'two')": "string=stronetwo",
339+
"'str'.concat('one', 'two').concat('three')": "string=stronetwothree",
340+
"'str'.concat('one', 'two').concat('three', 'four')": "string=stronetwothreefour",
341+
"'str'.concat('one', obj)": "wrapped=['str' string=str]+[null]",
342+
"'str'.concat('one', obj).concat()": "wrapped=['str' string=str]+[null]",
343+
"'str'.concat('one', obj, 'two')": "wrapped=['str' string=str]+['two' string=two]",
344+
"'str'.concat('one', obj, 'two').concat()": "wrapped=['str' string=str]+['two' string=two]",
345+
"'str'.concat('one', obj, 'two').concat('three')": "wrapped=['str' string=str]+['three' string=three]",
346+
"'str'.concat(obj)": "wrapped=['str' string=str]+[null]",
347+
"'str'.concat(obj).concat()": "wrapped=['str' string=str]+[null]",
348+
"'str'.concat(obj).concat('one', 'two')": "wrapped=['str' string=str]+['one', 'two' string=onetwo]",
349+
"'str'.concat(obj).concat(obj, 'one')": "wrapped=['str' string=str]+['one' string=one]",
350+
"'str'.concat(obj).concat(obj, 'one', 'two')": "wrapped=['str' string=str]+['one', 'two' string=onetwo]",
351+
"'str'.concat(obj).concat('one', obj, 'one')": "wrapped=['str' string=str]+['one' string=one]",
352+
"'str'.concat(obj).concat('one', obj, 'two', 'three')": "wrapped=['str' string=str]+['two', 'three' string=twothree]",
353+
"'str'.concat(obj, 'one')": "wrapped=['str' string=str]+['one' string=one]",
354+
"'str'.concat(obj, 'one').concat()": "wrapped=['str' string=str]+['one' string=one]",
355+
"'str'.concat(obj, 'one').concat('two', 'three')": "wrapped=['str' string=str]+['two', 'three' string=twothree]",
356+
"'str'.concat(obj, 'one').concat(obj, 'two', 'three')": "wrapped=['str' string=str]+['two', 'three' string=twothree]",
357+
"'str'.concat(obj, 'one').concat('two', obj, 'three')": "wrapped=['str' string=str]+['three' string=three]",
358+
"'str'.concat(obj, 'one').concat('two', obj, 'three', 'four')": "wrapped=['str' string=str]+['three', 'four' string=threefour]",
359+
"'str'.concat(obj, 'one', 'two')": "wrapped=['str' string=str]+['one', 'two' string=onetwo]",
360+
"'str'.concat(obj, 'one', 'two').concat()": "wrapped=['str' string=str]+['one', 'two' string=onetwo]",
361+
"'str'.concat(obj, 'one', 'two').concat('three', 'four')": "wrapped=['str' string=str]+['three', 'four' string=threefour]",
362+
"'str'.concat(obj, 'one', 'two').concat(obj, 'three', 'four')": "wrapped=['str' string=str]+['three', 'four' string=threefour]",
363+
"'str'.concat(obj, 'one', 'two').concat('three', obj, 'four')": "wrapped=['str' string=str]+['four' string=four]",
364+
"'str'.concat(obj, 'one', 'two').concat('three', obj, 'four', 'five')": "wrapped=['str' string=str]+['four', 'five' string=fourfive]",
365+
"`start${obj}mid${obj2}end`": "template=[start string=start],[mid string=mid],[end string=end]", // eslint-disable-line no-template-curly-in-string
366+
"`start${'str'}mid${obj2}end`": "template=[start${'str'}mid string=startstrmid],[end string=end]", // eslint-disable-line no-template-curly-in-string
334367
"'abc'.substr(1)": "string=bc",
335368
"'abcdef'.substr(2, 3)": "string=cde",
336369
"'abcdef'.substring(2, 3)": "string=c",
@@ -359,6 +392,7 @@ describe("Parser", () => {
359392
if(evalExpr.isConditional()) result.push("options=[" + evalExpr.options.map(evalExprToString).join("],[") + "]");
360393
if(evalExpr.isArray()) result.push("items=[" + evalExpr.items.map(evalExprToString).join("],[") + "]");
361394
if(evalExpr.isConstArray()) result.push("array=[" + evalExpr.array.join("],[") + "]");
395+
if(evalExpr.isTemplateString()) result.push("template=[" + evalExpr.quasis.map(evalExprToString).join("],[") + "]");
362396
if(evalExpr.isWrapped()) result.push("wrapped=[" + evalExprToString(evalExpr.prefix) + "]+[" + evalExprToString(evalExpr.postfix) + "]");
363397
if(evalExpr.range) {
364398
const start = evalExpr.range[0] - 5;
File renamed without changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,22 @@ it("should parse template strings in amd requires", function(done) {
1818
}
1919
}
2020
})
21+
22+
it("should parse .concat strings in amd requires", function(done) {
23+
var name = "abc";
24+
var suffix = "Test";
25+
26+
var pending = [
27+
require(["./abc/abcTest"], test),
28+
require(["./abc/".concat(name, "Test")], test),
29+
require(["./".concat(name, "/").concat(name, "Test")], test),
30+
require(["./abc/".concat(name).concat(suffix)], test)
31+
].length;
32+
33+
function test (result) {
34+
result.default.should.eql("ok")
35+
if (--pending <= 0) {
36+
done()
37+
}
38+
}
39+
})
Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ it("should parse template strings in require.ensure requires", function(done) {
55

66
require.ensure([], function(require) {
77
var imports = [
8-
require(`./abc/${name}Test`),
98
require(`./abc/${name}Test`),
109
require(`./${name}/${name}Test`),
1110
require(`./abc/${name}${suffix}`),
@@ -43,3 +42,44 @@ it("should parse template strings in require.resolve", function() {
4342
// can't use typeof as that depends on webpack config.
4443
require.resolve(`./sync/${name}Test`).should.not.be.undefined();
4544
})
45+
46+
it("should parse .concat strings in require.ensure requires", function(done) {
47+
var name = "abc";
48+
var suffix = "Test";
49+
50+
require.ensure([], function(require) {
51+
var imports = [
52+
require("./abc/".concat(name, "Test")),
53+
require("./".concat(name, "/").concat(name, "Test")),
54+
require("./abc/".concat(name).concat(suffix))
55+
];
56+
57+
for (var i = 0; i < imports.length; i++) {
58+
imports[i].default.should.eql("ok");
59+
}
60+
done()
61+
})
62+
})
63+
64+
it("should parse .concat strings in sync requires", function() {
65+
var name = "sync";
66+
var suffix = "Test";
67+
68+
var imports = [
69+
require("./sync/".concat(name, "Test")),
70+
require("./sync/".concat(name).concat(suffix)),
71+
require("./sync/sync".concat("Test"))
72+
];
73+
74+
for (var i = 0; i < imports.length; i++) {
75+
imports[i].default.should.eql("sync");
76+
}
77+
})
78+
79+
it("should parse .concat strings in require.resolve", function() {
80+
var name = "sync";
81+
82+
// Arbitrary assertion; can't use .ok() as it could be 0,
83+
// can't use typeof as that depends on webpack config.
84+
require.resolve("./sync/".concat(name, "Test")).should.not.be.undefined();
85+
})

test/cases/parsing/template-string/index.js renamed to test/cases/parsing/complex-require/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,15 @@ it("should parse template strings in import", function(done) {
1616
.then(function () { done(); }, done)
1717
});
1818

19+
it("should parse .concat strings in import", function(done) {
20+
var name = "abc".split("");
21+
var suffix = "Test";
22+
import("./abc/".concat(name[0]).concat(name[1]).concat(name[2], "Test"))
23+
.then(function (imported) {
24+
imported.default.should.eql("ok");
25+
})
26+
.then(function () { done(); }, done)
27+
});
28+
1929
require("./cjs")
2030
require("./amd")
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)