Skip to content
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

fix(vue): tweak semicolon for single expression in event bindings #5519

Merged
merged 6 commits into from Nov 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/common/internal-plugins.js
Expand Up @@ -33,6 +33,10 @@ module.exports = [
return eval("require")("../language-js/parser-babylon").parsers
.__vue_expression;
},
get __vue_event_binding() {
return eval("require")("../language-js/parser-babylon").parsers
.__vue_event_binding;
},
// JS - Flow
get flow() {
return eval("require")("../language-js/parser-flow").parsers.flow;
Expand Down
20 changes: 10 additions & 10 deletions src/language-html/printer-html.js
Expand Up @@ -41,7 +41,11 @@ const {
const preprocess = require("./preprocess");
const assert = require("assert");
const { insertPragma } = require("./pragma");
const { printVueFor, printVueSlotScope } = require("./syntax-vue");
const {
printVueFor,
printVueSlotScope,
isVueEventBindingExpression
} = require("./syntax-vue");
const { printImgSrcset } = require("./syntax-attribute");

function concat(parts) {
Expand Down Expand Up @@ -942,17 +946,13 @@ function printEmbeddedAttributeValue(node, originalTextToDoc, options) {
const jsExpressionBindingPatterns = ["^v-"];

if (isKeyMatched(vueEventBindingPatterns)) {
// copied from https://github.com/vuejs/vue/blob/v2.5.17/src/compiler/codegen/events.js#L3-L4
const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function\s*\(/;
const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/;

const value = getValue()
// https://github.com/vuejs/vue/blob/v2.5.17/src/compiler/helpers.js#L104
.trim();
const value = getValue();
return printMaybeHug(
simplePathRE.test(value) || fnExpRE.test(value)
isVueEventBindingExpression(value)
? textToDoc(value, { parser: "__js_expression" })
: stripTrailingHardline(textToDoc(value, { parser: "babylon" }))
: stripTrailingHardline(
textToDoc(value, { parser: "__vue_event_binding" })
)
);
}

Expand Down
14 changes: 14 additions & 0 deletions src/language-html/syntax-vue.js
Expand Up @@ -66,7 +66,21 @@ function printVueSlotScope(value, textToDoc) {
});
}

function isVueEventBindingExpression(eventBindingValue) {
// https://github.com/vuejs/vue/blob/v2.5.17/src/compiler/codegen/events.js#L3-L4
// arrow function or anonymous function
const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function\s*\(/;
// simple member expression chain (a, a.b, a['b'], a["b"], a[0], a[b])
const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/;

// https://github.com/vuejs/vue/blob/v2.5.17/src/compiler/helpers.js#L104
const value = eventBindingValue.trim();

return fnExpRE.test(value) || simplePathRE.test(value);
}

module.exports = {
isVueEventBindingExpression,
printVueFor,
printVueSlotScope
};
19 changes: 19 additions & 0 deletions src/language-js/html-binding.js
Expand Up @@ -45,6 +45,25 @@ function printHtmlBinding(path, options, print) {
}
}

// based on https://github.com/prettier/prettier/blob/master/src/language-html/syntax-vue.js isVueEventBindingExpression()
function isVueEventBindingExpression(node) {
switch (node.type) {
case "MemberExpression":
switch (node.property.type) {
case "Identifier":
case "NumericLiteral":
case "StringLiteral":
return isVueEventBindingExpression(node.object);
}
return false;
case "Identifier":
return true;
default:
return false;
}
}

module.exports = {
isVueEventBindingExpression,
printHtmlBinding
};
86 changes: 48 additions & 38 deletions src/language-js/parser-babylon.js
Expand Up @@ -41,44 +41,49 @@ function babylonOptions(extraOptions, extraPlugins) {
);
}

function parse(text, parsers, opts) {
// Inline the require to avoid loading all the JS if we don't use it
const babylon = require("@babel/parser");
function createParse(parseMethod) {
return (text, parsers, opts) => {
// Inline the require to avoid loading all the JS if we don't use it
const babylon = require("@babel/parser");

const combinations = [
babylonOptions({ strictMode: true }, ["decorators-legacy"]),
babylonOptions({ strictMode: false }, ["decorators-legacy"]),
babylonOptions({ strictMode: true }, [
["decorators", { decoratorsBeforeExport: false }]
]),
babylonOptions({ strictMode: false }, [
["decorators", { decoratorsBeforeExport: false }]
])
];
const combinations = [
babylonOptions({ strictMode: true }, ["decorators-legacy"]),
babylonOptions({ strictMode: false }, ["decorators-legacy"]),
babylonOptions({ strictMode: true }, [
["decorators", { decoratorsBeforeExport: false }]
]),
babylonOptions({ strictMode: false }, [
["decorators", { decoratorsBeforeExport: false }]
])
];

const parseMethod =
!opts || opts.parser === "babylon" ? "parse" : "parseExpression";

let ast;
try {
ast = tryCombinations(babylon[parseMethod].bind(null, text), combinations);
} catch (error) {
throw createError(
// babel error prints (l:c) with cols that are zero indexed
// so we need our custom error
error.message.replace(/ \(.*\)/, ""),
{
start: {
line: error.loc.line,
column: error.loc.column + 1
let ast;
try {
ast = tryCombinations(
babylon[parseMethod].bind(null, text),
combinations
);
} catch (error) {
throw createError(
// babel error prints (l:c) with cols that are zero indexed
// so we need our custom error
error.message.replace(/ \(.*\)/, ""),
{
start: {
line: error.loc.line,
column: error.loc.column + 1
}
}
}
);
}
delete ast.tokens;
return postprocess(ast, Object.assign({}, opts, { originalText: text }));
);
}
delete ast.tokens;
return postprocess(ast, Object.assign({}, opts, { originalText: text }));
};
}

const parse = createParse("parse");
const parseExpression = createParse("parseExpression");

function tryCombinations(fn, combinations) {
let error;
for (let i = 0; i < combinations.length; i++) {
Expand All @@ -94,7 +99,7 @@ function tryCombinations(fn, combinations) {
}

function parseJson(text, parsers, opts) {
const ast = parse(text, parsers, Object.assign({}, opts, { parser: "json" }));
const ast = parseExpression(text, parsers, opts);

ast.comments.forEach(assertJsonNode);
assertJsonNode(ast);
Expand Down Expand Up @@ -164,17 +169,20 @@ const babylon = Object.assign(
{ parse, astFormat: "estree", hasPragma },
locFns
);
const babylonExpression = Object.assign({}, babylon, {
parse: parseExpression
});

// Export as a plugin so we can reuse the same bundle for UMD loading
module.exports = {
parsers: {
babylon,
json: Object.assign({}, babylon, {
json: Object.assign({}, babylonExpression, {
hasPragma() {
return true;
}
}),
json5: babylon,
json5: babylonExpression,
"json-stringify": Object.assign(
{
parse: parseJson,
Expand All @@ -183,8 +191,10 @@ module.exports = {
locFns
),
/** @internal */
__js_expression: babylon,
__js_expression: babylonExpression,
/** for vue filter */
__vue_expression: babylon
__vue_expression: babylonExpression,
/** for vue event binding to handle semicolon */
__vue_event_binding: babylon
}
};
20 changes: 19 additions & 1 deletion src/language-js/printer-estree.js
Expand Up @@ -34,7 +34,10 @@ const clean = require("./clean");
const insertPragma = require("./pragma").insertPragma;
const handleComments = require("./comments");
const pathNeedsParens = require("./needs-parens");
const { printHtmlBinding } = require("./html-binding");
const {
printHtmlBinding,
isVueEventBindingExpression
} = require("./html-binding");
const preprocess = require("./preprocess");
const {
hasNode,
Expand Down Expand Up @@ -494,6 +497,21 @@ function printPathNoParens(path, options, print, args) {
if (n.directive) {
return concat([nodeStr(n.expression, options, true), semi]);
}

if (options.parser === "__vue_event_binding") {
const parent = path.getParentNode();
if (
parent.type === "Program" &&
parent.body.length === 1 &&
parent.body[0] === n
) {
return concat([
path.call(print, "expression"),
isVueEventBindingExpression(n.expression) ? ";" : ""
]);
}
}

// Do not append semicolon after the only JSX element in a program
return concat([
path.call(print, "expression"),
Expand Down