From 0534735c2ff23618cccaf4c5d462ced34651063d Mon Sep 17 00:00:00 2001 From: Ika Date: Thu, 29 Nov 2018 09:28:35 +0800 Subject: [PATCH] fix(vue): tweak semicolon for single expression in event bindings (#5519) --- src/common/internal-plugins.js | 4 + src/language-html/printer-html.js | 20 +- src/language-html/syntax-vue.js | 14 + src/language-js/html-binding.js | 19 + src/language-js/parser-babylon.js | 86 +- src/language-js/printer-estree.js | 20 +- .../html_vue/__snapshots__/jsfmt.spec.js.snap | 886 +++++++++++++++++- tests/html_vue/attributes.vue | 2 + tests/html_vue/jsfmt.spec.js | 1 + .../__snapshots__/jsfmt.spec.js.snap | 2 +- website/static/worker.js | 4 + 11 files changed, 987 insertions(+), 71 deletions(-) diff --git a/src/common/internal-plugins.js b/src/common/internal-plugins.js index d071a32d79f5..9be225ece530 100644 --- a/src/common/internal-plugins.js +++ b/src/common/internal-plugins.js @@ -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; diff --git a/src/language-html/printer-html.js b/src/language-html/printer-html.js index 34c70c63e8db..b8611902b6e4 100644 --- a/src/language-html/printer-html.js +++ b/src/language-html/printer-html.js @@ -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) { @@ -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" }) + ) ); } diff --git a/src/language-html/syntax-vue.js b/src/language-html/syntax-vue.js index 234bacaf0cb9..eb8698545633 100644 --- a/src/language-html/syntax-vue.js +++ b/src/language-html/syntax-vue.js @@ -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 }; diff --git a/src/language-js/html-binding.js b/src/language-js/html-binding.js index 411a2e9988ad..0bcb25c173f9 100644 --- a/src/language-js/html-binding.js +++ b/src/language-js/html-binding.js @@ -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 }; diff --git a/src/language-js/parser-babylon.js b/src/language-js/parser-babylon.js index 00ad22e73e55..01402e55169b 100644 --- a/src/language-js/parser-babylon.js +++ b/src/language-js/parser-babylon.js @@ -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++) { @@ -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); @@ -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, @@ -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 } }; diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js index 124fa0d77f3f..6a25d125eaa6 100644 --- a/src/language-js/printer-estree.js +++ b/src/language-js/printer-estree.js @@ -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, @@ -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"), diff --git a/tests/html_vue/__snapshots__/jsfmt.spec.js.snap b/tests/html_vue/__snapshots__/jsfmt.spec.js.snap index 0aac6713767a..d3fd4f7a21f5 100644 --- a/tests/html_vue/__snapshots__/jsfmt.spec.js.snap +++ b/tests/html_vue/__snapshots__/jsfmt.spec.js.snap @@ -37,6 +37,8 @@ printWidth: 80 console.log(test); } " + @click="doSomething()" + @click="doSomething;" > @@ -59,9 +61,9 @@ printWidth: 80 return x; })" @click="/* hello */" - @click="/* 1 */ $emit(/* 2 */ 'click' /* 3 */) /* 4 */; /* 5 */" - @click="$emit('click');" - @click="$emit('click');" + @click="/* 1 */ $emit(/* 2 */ 'click' /* 3 */) /* 4 */ /* 5 */" + @click="$emit('click')" + @click="$emit('click')" @click=" $emit('click'); if (something) { @@ -96,6 +98,8 @@ printWidth: 80 console.log(test); } " + @click="doSomething()" + @click="doSomething;" > @@ -140,6 +144,8 @@ trailingComma: "es5" console.log(test); } " + @click="doSomething()" + @click="doSomething;" > @@ -162,9 +168,9 @@ trailingComma: "es5" return x; })" @click="/* hello */" - @click="/* 1 */ $emit(/* 2 */ 'click' /* 3 */) /* 4 */; /* 5 */" - @click="$emit('click');" - @click="$emit('click');" + @click="/* 1 */ $emit(/* 2 */ 'click' /* 3 */) /* 4 */ /* 5 */" + @click="$emit('click')" + @click="$emit('click')" @click=" $emit('click'); if (something) { @@ -199,6 +205,115 @@ trailingComma: "es5" console.log(test); } " + @click="doSomething()" + @click="doSomething;" + > + + +================================================================================ +`; + +exports[`attributes.vue 3`] = ` +====================================options===================================== +parsers: ["vue"] +printWidth: 80 +semi: false + | printWidth +=====================================input====================================== + + +=====================================output===================================== + @@ -355,7 +470,7 @@ export default { :data-issue-id="issue.id" @mousedown="mouseDown" @mousemove="mouseMove" - @mouseup="showIssue($event);" + @mouseup="showIssue($event)" > + + + + +================================================================================ +`; + +exports[`board_card.vue 3`] = ` +====================================options===================================== +parsers: ["vue"] +printWidth: 80 +semi: false + | printWidth +=====================================input====================================== + + + + +=====================================output===================================== + + +