From 5e0bd2c7a629ba557ab0e9584c9d63e7526800ea Mon Sep 17 00:00:00 2001 From: Magomed Chemurziev <51162910+ItMaga@users.noreply.github.com> Date: Wed, 11 Jan 2023 16:11:53 +0300 Subject: [PATCH] Add `padding-lines-in-component-definition` rule (#2052) --- docs/rules/index.md | 1 + .../padding-lines-in-component-definition.md | 163 +++ lib/configs/no-layout-rules.js | 1 + lib/index.js | 1 + .../padding-lines-in-component-definition.js | 359 +++++ .../padding-lines-in-component-definition.js | 1240 +++++++++++++++++ 6 files changed, 1765 insertions(+) create mode 100644 docs/rules/padding-lines-in-component-definition.md create mode 100644 lib/rules/padding-lines-in-component-definition.js create mode 100644 tests/lib/rules/padding-lines-in-component-definition.js diff --git a/docs/rules/index.md b/docs/rules/index.md index 2e21edc56..b23f68427 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -253,6 +253,7 @@ For example: | [vue/no-v-text](./no-v-text.md) | disallow use of v-text | | :hammer: | | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | :lipstick: | | [vue/padding-line-between-tags](./padding-line-between-tags.md) | require or disallow newlines between sibling tags in template | :wrench: | :lipstick: | +| [vue/padding-lines-in-component-definition](./padding-lines-in-component-definition.md) | require or disallow padding lines in component definition | :wrench: | :lipstick: | | [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: | | [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | :hammer: | | [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: | diff --git a/docs/rules/padding-lines-in-component-definition.md b/docs/rules/padding-lines-in-component-definition.md new file mode 100644 index 000000000..ab46a532e --- /dev/null +++ b/docs/rules/padding-lines-in-component-definition.md @@ -0,0 +1,163 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/padding-lines-in-component-definition +description: require or disallow padding lines in component definition +--- +# vue/padding-lines-in-component-definition + +> require or disallow padding lines in component definition + +- :exclamation: ***This rule has not been released yet.*** +- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule. + +## :book: Rule Details + +This rule requires or disallows blank lines in the component definition. Properly blank lines help developers improve code readability and code style flexibility. + + + +```vue + +``` + + + + + +```vue + +``` + + + +## :wrench: Options + +```json +{ + "vue/padding-lines-in-component-definition": ["error", { + "betweenOptions": "always" | "never", + + "withinOption": { + "props": { + "betweenItems": "always" | "never" | "ignore", + "withinEach": "always" | "never" | "ignore", + } | "always" | "never" | "ignore", // shortcut to set both + + "data": { + "betweenItems": "always" | "never" | "ignore", + "withinEach": "always" | "never" | "ignore", + } | "always" | "never" | "ignore" // shortcut to set both + + // ... all options + } | "always" | "never" | "ignore", + + "groupSingleLineProperties": true | false + }] +} +``` + +- `betweenOptions` ... Setting padding lines between options. default `always` +- `withinOption` ... Setting padding lines within option + - `emits` ... Setting padding between lines between `emits` and `defineEmits`. default `always` + - `props` ... Setting padding between lines between `props` and `defineProps`. default `always` + - ... +- `groupSingleLineProperties` ... Setting groupings of multiple consecutive single-line properties (e.g. `name`, `inheritAttrs`), default `true` + +### Group single-line properties + + + +```vue + +``` + + + +### With custom options + + + +```vue + +``` + + + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/padding-lines-in-component-definition.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/padding-lines-in-component-definition.js) diff --git a/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js index b1486f157..59880b846 100644 --- a/lib/configs/no-layout-rules.js +++ b/lib/configs/no-layout-rules.js @@ -44,6 +44,7 @@ module.exports = { 'vue/operator-linebreak': 'off', 'vue/padding-line-between-blocks': 'off', 'vue/padding-line-between-tags': 'off', + 'vue/padding-lines-in-component-definition': 'off', 'vue/script-indent': 'off', 'vue/singleline-html-element-content-newline': 'off', 'vue/space-in-parens': 'off', diff --git a/lib/index.js b/lib/index.js index f5da9a706..aef60a604 100644 --- a/lib/index.js +++ b/lib/index.js @@ -163,6 +163,7 @@ module.exports = { 'order-in-components': require('./rules/order-in-components'), 'padding-line-between-blocks': require('./rules/padding-line-between-blocks'), 'padding-line-between-tags': require('./rules/padding-line-between-tags'), + 'padding-lines-in-component-definition': require('./rules/padding-lines-in-component-definition'), 'prefer-import-from-vue': require('./rules/prefer-import-from-vue'), 'prefer-prop-type-boolean-first': require('./rules/prefer-prop-type-boolean-first'), 'prefer-separate-static-class': require('./rules/prefer-separate-static-class'), diff --git a/lib/rules/padding-lines-in-component-definition.js b/lib/rules/padding-lines-in-component-definition.js new file mode 100644 index 000000000..c15762b34 --- /dev/null +++ b/lib/rules/padding-lines-in-component-definition.js @@ -0,0 +1,359 @@ +/** + * @author ItMaga + * See LICENSE file in root directory for full license. + */ +'use strict' + +/** + * @typedef {import('../utils').ComponentProp} ComponentProp + * @typedef {import('../utils').GroupName} GroupName + */ + +const utils = require('../utils') +const { isCommentToken } = require('eslint-utils') + +const AvailablePaddingOptions = { + Never: 'never', + Always: 'always', + Ignore: 'ignore' +} +const OptionKeys = { + BetweenOptions: 'betweenOptions', + WithinOption: 'withinOption', + BetweenItems: 'betweenItems', + WithinEach: 'withinEach', + GroupSingleLineProperties: 'groupSingleLineProperties' +} + +/** + * @param {Token} node + */ +function isComma(node) { + return node.type === 'Punctuator' && node.value === ',' +} + +/** + * @param {string} nodeType + */ +function isValidProperties(nodeType) { + return ['Property', 'SpreadElement'].includes(nodeType) +} + +/** + * Split the source code into multiple lines based on the line delimiters. + * @param {string} text Source code as a string. + * @returns {string[]} Array of source code lines. + */ +function splitLines(text) { + return text.split(/\r\n|[\r\n\u2028\u2029]/gu) +} + +/** + * @param {any} initialOption + * @param {string} optionKey + * @private + * */ +function parseOption(initialOption, optionKey) { + return typeof initialOption === 'string' + ? initialOption + : initialOption[optionKey] +} + +/** + * @param {any} initialOption + * @param {string} optionKey + * @private + * */ +function parseBooleanOption(initialOption, optionKey) { + if (typeof initialOption === 'string') { + if (initialOption === AvailablePaddingOptions.Always) return true + if (initialOption === AvailablePaddingOptions.Never) return false + } + return initialOption[optionKey] +} + +/** + * @param {(Property | SpreadElement)} currentProperty + * @param {(Property | SpreadElement)} nextProperty + * @param {boolean} option + * @returns {boolean} + * @private + * */ +function needGroupSingleLineProperties(currentProperty, nextProperty, option) { + const isSingleCurrentProperty = + currentProperty.loc.start.line === currentProperty.loc.end.line + const isSingleNextProperty = + nextProperty.loc.start.line === nextProperty.loc.end.line + + return isSingleCurrentProperty && isSingleNextProperty && option +} + +module.exports = { + meta: { + type: 'layout', + docs: { + description: 'require or disallow padding lines in component definition', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/padding-lines-in-component-definition.html' + }, + fixable: 'whitespace', + schema: [ + { + oneOf: [ + { + enum: [ + AvailablePaddingOptions.Always, + AvailablePaddingOptions.Never + ] + }, + { + type: 'object', + additionalProperties: false, + properties: { + [OptionKeys.BetweenOptions]: { + enum: Object.values(AvailablePaddingOptions) + }, + [OptionKeys.WithinOption]: { + oneOf: [ + { + enum: Object.values(AvailablePaddingOptions) + }, + { + type: 'object', + patternProperties: { + '^[a-zA-Z]*$': { + oneOf: [ + { + enum: Object.values(AvailablePaddingOptions) + }, + { + type: 'object', + properties: { + [OptionKeys.BetweenItems]: { + enum: Object.values(AvailablePaddingOptions) + }, + [OptionKeys.WithinEach]: { + enum: Object.values(AvailablePaddingOptions) + } + }, + additionalProperties: false + } + ] + } + }, + minProperties: 1, + additionalProperties: false + } + ] + }, + [OptionKeys.GroupSingleLineProperties]: { + type: 'boolean' + } + } + } + ] + } + ], + messages: { + never: 'Unexpected blank line before this definition.', + always: 'Expected blank line before this definition.', + groupSingleLineProperties: + 'Unexpected blank line between single line properties.' + } + }, + /** @param {RuleContext} context */ + create(context) { + const options = context.options[0] || AvailablePaddingOptions.Always + const sourceCode = context.getSourceCode() + + /** + * @param {(Property | SpreadElement)} currentProperty + * @param {(Property | SpreadElement | Token)} nextProperty + * @param {RuleFixer} fixer + * */ + function replaceLines(currentProperty, nextProperty, fixer) { + const commaToken = sourceCode.getTokenAfter(currentProperty, isComma) + + const start = commaToken ? commaToken.range[1] : currentProperty.range[1] + const end = nextProperty.range[0] + + const paddingText = sourceCode.text.slice(start, end) + const newText = `\n${splitLines(paddingText).pop()}` + + return fixer.replaceTextRange([start, end], newText) + } + + /** + * @param {(Property | SpreadElement)} currentProperty + * @param {(Property | SpreadElement | Token)} nextProperty + * @param {RuleFixer} fixer + * @param {number} betweenLinesRange + * */ + function insertLines( + currentProperty, + nextProperty, + fixer, + betweenLinesRange + ) { + const commaToken = sourceCode.getTokenAfter(currentProperty, isComma) + + const lineBeforeNextProperty = + sourceCode.lines[nextProperty.loc.start.line - 1] + const lastSpaces = /** @type {RegExpExecArray} */ ( + /^\s*/.exec(lineBeforeNextProperty) + )[0] + + const newText = betweenLinesRange === 0 ? `\n\n${lastSpaces}` : '\n' + return fixer.insertTextAfter(commaToken || currentProperty, newText) + } + + /** + * @param {(Property | SpreadElement)[]} properties + * @param {any} option + * @param {any} nextOption + * */ + function verify(properties, option, nextOption) { + const groupSingleLineProperties = parseBooleanOption( + options, + OptionKeys.GroupSingleLineProperties + ) + + for (const [i, currentProperty] of properties.entries()) { + const nextProperty = properties[i + 1] + + if (nextProperty && option !== AvailablePaddingOptions.Ignore) { + const tokenBeforeNext = sourceCode.getTokenBefore(nextProperty, { + includeComments: true + }) + const isCommentBefore = isCommentToken(tokenBeforeNext) + const reportNode = isCommentBefore ? tokenBeforeNext : nextProperty + + const betweenLinesRange = + reportNode.loc.start.line - currentProperty.loc.end.line + + if ( + needGroupSingleLineProperties( + currentProperty, + nextProperty, + groupSingleLineProperties + ) + ) { + if (betweenLinesRange > 1) { + context.report({ + node: reportNode, + messageId: 'groupSingleLineProperties', + loc: reportNode.loc, + fix(fixer) { + return replaceLines(currentProperty, reportNode, fixer) + } + }) + } + continue + } + + if ( + betweenLinesRange <= 1 && + option === AvailablePaddingOptions.Always + ) { + context.report({ + node: reportNode, + messageId: 'always', + loc: reportNode.loc, + fix(fixer) { + return insertLines( + currentProperty, + reportNode, + fixer, + betweenLinesRange + ) + } + }) + } else if ( + betweenLinesRange > 1 && + option === AvailablePaddingOptions.Never + ) { + context.report({ + node: reportNode, + messageId: 'never', + loc: reportNode.loc, + fix(fixer) { + return replaceLines(currentProperty, reportNode, fixer) + } + }) + } + } + + if (!nextOption) return + + const name = /** @type {GroupName | null} */ ( + currentProperty.type === 'Property' && + utils.getStaticPropertyName(currentProperty) + ) + if (!name) continue + + const propertyOption = parseOption(nextOption, name) + if (!propertyOption) continue + + const nestedProperties = + currentProperty.type === 'Property' && + currentProperty.value.type === 'ObjectExpression' && + currentProperty.value.properties + if (!nestedProperties) continue + + verify( + nestedProperties, + parseOption(propertyOption, OptionKeys.BetweenItems), + parseOption(propertyOption, OptionKeys.WithinEach) + ) + } + } + + return utils.compositingVisitors( + utils.defineVueVisitor(context, { + onVueObjectEnter(node) { + verify( + node.properties, + parseOption(options, OptionKeys.BetweenOptions), + parseOption(options, OptionKeys.WithinOption) + ) + } + }), + utils.defineScriptSetupVisitor(context, { + onDefinePropsEnter(_, props) { + const propNodes = /** @type {(Property | SpreadElement)[]} */ ( + props + .filter((prop) => prop.node && isValidProperties(prop.node.type)) + .map((prop) => prop.node) + ) + + const withinOption = parseOption(options, OptionKeys.WithinOption) + const propsOption = withinOption && parseOption(withinOption, 'props') + if (!propsOption) return + + verify( + propNodes, + parseOption(propsOption, OptionKeys.BetweenItems), + parseOption(propsOption, OptionKeys.WithinEach) + ) + }, + onDefineEmitsEnter(_, emits) { + const emitNodes = /** @type {(Property | SpreadElement)[]} */ ( + emits + .filter((emit) => emit.node && isValidProperties(emit.node.type)) + .map((emit) => emit.node) + ) + + const withinOption = parseOption(options, OptionKeys.WithinOption) + const emitsOption = withinOption && parseOption(withinOption, 'emits') + if (!emitsOption) return + + verify( + emitNodes, + parseOption(emitsOption, OptionKeys.BetweenItems), + parseOption(emitsOption, OptionKeys.WithinEach) + ) + } + }) + ) + } +} diff --git a/tests/lib/rules/padding-lines-in-component-definition.js b/tests/lib/rules/padding-lines-in-component-definition.js new file mode 100644 index 000000000..a4ed4d3c3 --- /dev/null +++ b/tests/lib/rules/padding-lines-in-component-definition.js @@ -0,0 +1,1240 @@ +/** + * @author ItMaga + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/padding-lines-in-component-definition') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('padding-lines-in-component-definition', rule, { + valid: [ + { + filename: 'Never.vue', + code: ` + + `, + options: ['never'] + }, + { + filename: 'Setup.vue', + code: ` + import { ref, defineComponent } from 'vue'; + + `, + options: ['never'] + }, + { + filename: 'BetweenOptionsNever.vue', + code: ` + import { defineComponent } from 'vue'; + + `, + options: [ + { + betweenOptions: 'never', + withinOption: 'never' + } + ] + }, + { + filename: 'GroupSingleLineProperties.vue', + code: ` + import { defineComponent } from 'vue'; + + `, + options: [ + { + betweenOptions: 'always', + withinOption: 'never', + groupSingleLineProperties: true + } + ] + }, + { + filename: 'WithinOption.vue', + code: ` + import { defineComponent } from 'vue'; + + `, + options: [ + { + betweenOptions: 'always', + withinOption: { + props: { + betweenItems: 'never', + withinEach: 'always' + }, + data: { + betweenItems: 'always', + withinEach: 'never' + } + } + } + ] + }, + { + filename: 'CustomOptions.vue', + code: ` + + `, + options: [ + { + betweenOptions: 'always', + withinOption: { + props: { + betweenItems: 'always', + withinEach: 'never' + }, + customOption: { + betweenItems: 'always' + } + } + } + ] + }, + { + filename: 'NewVue.js', + code: ` + new Vue({ + name: 'NewVue', + inheritAttrs: false, + props: { + foo: { + type: String, + required: true, + }, + bar: { + type: String, + required: true, + } + }, + customOption: { + getString() { return '1' }, + getNumber: () => 1, + }, + }) + `, + options: ['never'] + }, + { + filename: 'Mixin.js', + code: ` + Vue.mixin({ + name: 'Mixin', + inheritAttrs: false, + props: { + foo: { + type: String, + required: true, + }, + bar: { + type: String, + required: true, + } + }, + customOption: { + getString() { return '1' }, + getNumber: () => 1, + }, + }) + `, + options: [ + { + betweenOptions: 'never', + withinOption: 'ignore', + groupSingleLineProperties: true + } + ] + }, + { + filename: 'DefineProps.vue', + code: ` + import { defineProps } from 'vue' + + `, + options: ['never'] + }, + { + filename: 'DefineEmits.vue', + code: ` + import { defineEmits } from 'vue' + + `, + options: [ + { + betweenOptions: 'always', + withinOption: { + emits: { + betweenItems: 'always' + } + }, + groupSingleLineProperties: false + } + ] + }, + { + filename: 'Comment.vue', + code: ` + + `, + options: ['always'] + }, + { + filename: 'Spread.vue', + code: ` + + `, + options: ['never'] + }, + { + filename: 'SpreadWithComment.vue', + code: ` + + `, + options: ['always'] + } + ], + invalid: [ + { + filename: 'Always.vue', + code: ` + + `, + output: ` + + `, + options: ['always'], + errors: [ + { + message: 'Expected blank line before this definition.', + line: 5 + }, + { + message: 'Expected blank line before this definition.', + line: 10 + }, + { + message: 'Expected blank line before this definition.', + line: 15 + } + ] + }, + { + filename: 'Setup.vue', + code: ` + import { ref, defineComponent } from 'vue'; + + `, + output: ` + import { ref, defineComponent } from 'vue'; + + `, + options: ['never'], + errors: [ + { + message: 'Unexpected blank line before this definition.', + line: 7 + } + ] + }, + { + filename: 'BetweenOptionsAlways.vue', + code: ` + import { defineComponent } from 'vue'; + + `, + output: ` + import { defineComponent } from 'vue'; + + `, + options: [ + { + betweenOptions: 'always', + withinOption: 'never' + } + ], + errors: [ + { + message: 'Expected blank line before this definition.', + line: 6 + } + ] + }, + { + filename: 'GroupSingleLineProperties.vue', + code: ` + import { defineComponent } from 'vue'; + + `, + output: ` + import { defineComponent } from 'vue'; + + `, + options: [ + { + betweenOptions: 'always', + withinOption: 'never', + groupSingleLineProperties: true + } + ], + errors: [ + { + message: 'Unexpected blank line between single line properties.', + line: 7 + } + ] + }, + { + filename: 'WithinOption.vue', + code: ` + import { defineComponent } from 'vue'; + + `, + output: ` + import { defineComponent } from 'vue'; + + `, + options: [ + { + betweenOptions: 'always', + withinOption: { + props: { + betweenItems: 'always', + withinEach: 'always' + }, + data: { + betweenItems: 'always', + withinEach: 'never' + } + } + } + ], + errors: [ + { + message: 'Expected blank line before this definition.', + line: 10 + }, + { + message: 'Expected blank line before this definition.', + line: 12 + }, + { + message: 'Expected blank line before this definition.', + line: 14 + } + ] + }, + { + filename: 'CustomOptions.vue', + code: ` + + `, + output: ` + + `, + options: [ + { + betweenOptions: 'always', + withinOption: { + props: { + betweenItems: 'always', + withinEach: 'never' + }, + customOption: { + betweenItems: 'never' + } + } + } + ], + errors: [ + { + message: 'Unexpected blank line before this definition.', + line: 23 + } + ] + }, + { + filename: 'NewVue.js', + code: ` + new Vue({ + name: 'NewVue', + + inheritAttrs: false, + + props: { + foo: { + type: String, + required: true, + }, + bar: { + type: String, + required: true, + } + }, + customOption: { + getString() { return '1' }, + getNumber: () => 1, + }, + }) + `, + output: ` + new Vue({ + name: 'NewVue', + inheritAttrs: false, + + props: { + foo: { + type: String, + required: true, + }, + bar: { + type: String, + required: true, + } + }, + + customOption: { + getString() { return '1' }, + getNumber: () => 1, + }, + }) + `, + options: [ + { + betweenOptions: 'always', + withinOption: 'ignore', + groupSingleLineProperties: true + } + ], + errors: [ + { + message: 'Unexpected blank line between single line properties.', + line: 5 + }, + { + message: 'Expected blank line before this definition.', + line: 17 + } + ] + }, + { + filename: 'Mixin.js', + code: ` + Vue.mixin({ + name: 'NewVue', + inheritAttrs: false, + props: { + foo: { + type: String, + required: true, + }, + bar: { + type: String, + required: true, + } + }, + customOption: { + getString() { return '1' }, + getNumber: () => 1, + }, + }) + `, + output: ` + Vue.mixin({ + name: 'NewVue', + + inheritAttrs: false, + + props: { + foo: { + type: String, + required: true, + }, + bar: { + type: String, + required: true, + } + }, + + customOption: { + getString() { return '1' }, + getNumber: () => 1, + }, + }) + `, + options: [ + { + betweenOptions: 'always', + withinOption: 'ignore', + groupSingleLineProperties: false + } + ], + errors: [ + { + message: 'Expected blank line before this definition.', + line: 4 + }, + { + message: 'Expected blank line before this definition.', + line: 5 + }, + { + message: 'Expected blank line before this definition.', + line: 15 + } + ] + }, + { + filename: 'DefineProps.vue', + code: ` + import { defineProps } from 'vue' + + `, + output: ` + import { defineProps } from 'vue' + + `, + options: [ + { + betweenOptions: 'always', + withinOption: { + props: { + betweenItems: 'always', + withinEach: 'never' + } + } + } + ], + errors: [ + { + message: 'Unexpected blank line before this definition.', + line: 8 + }, + { + message: 'Expected blank line before this definition.', + line: 10 + } + ] + }, + { + filename: 'DefineEmits.vue', + code: ` + import { defineEmits } from 'vue' + + `, + output: ` + import { defineEmits } from 'vue' + + `, + options: ['never'], + errors: [ + { + message: 'Unexpected blank line before this definition.', + line: 8 + } + ] + }, + { + filename: 'WithinOption.vue', + code: ` + import { defineComponent } from 'vue'; + + `, + output: ` + import { defineComponent } from 'vue'; + + `, + options: [ + { + betweenOptions: 'always', + withinOption: { + props: { + betweenItems: 'always', + withinEach: 'always' + } + }, + groupSingleLineProperties: true + } + ], + errors: [ + { + message: 'Expected blank line before this definition.', + line: 5 + }, + { + message: 'Expected blank line before this definition.', + line: 9 + } + ] + }, + { + filename: 'Comment.vue', + code: ` + + `, + output: ` + + `, + options: ['always'], + errors: [ + { + message: 'Expected blank line before this definition.', + line: 5 + }, + { + message: 'Expected blank line before this definition.', + line: 9 + }, + { + message: 'Expected blank line before this definition.', + line: 13 + }, + { + message: 'Unexpected blank line between single line properties.', + line: 21 + } + ] + }, + { + filename: 'Spread.vue', + code: ` + + `, + output: ` + + `, + options: ['always'], + errors: [ + { + message: 'Expected blank line before this definition.', + line: 6 + }, + { + message: 'Expected blank line before this definition.', + line: 10 + }, + { + message: 'Expected blank line before this definition.', + line: 11 + } + ] + }, + { + filename: 'DefineWithSpreadAndComment.vue', + code: ` + import { defineEmits, defineProps } from 'vue' + + `, + output: ` + import { defineEmits, defineProps } from 'vue' + + `, + options: [ + { + betweenOptions: 'always', + withinOption: { + emits: 'always', + props: 'never' + }, + groupSingleLineProperties: false + } + ], + errors: [ + { + message: 'Expected blank line before this definition.', + line: 7 + }, + { + message: 'Expected blank line before this definition.', + line: 8 + }, + { + message: 'Unexpected blank line before this definition.', + line: 16 + } + ] + } + ] +})