From caec13d18460a44b3ea31501ffd0c629862a493d Mon Sep 17 00:00:00 2001 From: ItMaga Date: Thu, 1 Dec 2022 23:38:47 +0300 Subject: [PATCH 1/6] feat: add padding-lines-in-component-definition rule --- 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 | 350 +++++++ .../padding-lines-in-component-definition.js | 956 ++++++++++++++++++ 6 files changed, 1472 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 224b40313..64f84ebfc 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 58e62b601..2212ea031 100644 --- a/lib/configs/no-layout-rules.js +++ b/lib/configs/no-layout-rules.js @@ -43,6 +43,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 2d14cd404..050f4755d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -162,6 +162,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..1d65363b2 --- /dev/null +++ b/lib/rules/padding-lines-in-component-definition.js @@ -0,0 +1,350 @@ +/** + * @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 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 === ',' +} + +/** + * 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} currentProperty + * @param {Property} 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', + 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} currentProperty + * @param {Property} 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} currentProperty + * @param {Property} 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[]} properties + * @param {any} option + * @param {any} nextOption + * */ + function check(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 betweenLinesRange = + nextProperty.loc.start.line - currentProperty.loc.end.line + + if ( + needGroupSingleLineProperties( + currentProperty, + nextProperty, + groupSingleLineProperties + ) + ) { + if (betweenLinesRange > 1) { + context.report({ + node: nextProperty.key, + messageId: 'groupSingleLineProperties', + loc: nextProperty.loc, + fix(fixer) { + return replaceLines(currentProperty, nextProperty, fixer) + } + }) + } + continue + } + + if ( + betweenLinesRange <= 1 && + option === AvailablePaddingOptions.Always + ) { + context.report({ + node: nextProperty.key, + messageId: 'always', + loc: nextProperty.loc, + fix(fixer) { + return insertLines( + currentProperty, + nextProperty, + fixer, + betweenLinesRange + ) + } + }) + } else if ( + betweenLinesRange > 1 && + option === AvailablePaddingOptions.Never + ) { + context.report({ + node: nextProperty.key, + messageId: 'never', + loc: nextProperty.loc, + fix(fixer) { + return replaceLines(currentProperty, nextProperty, fixer) + } + }) + } + } + + if (!nextOption) return + + const name = /** @type {GroupName | null} */ ( + utils.getStaticPropertyName(currentProperty) + ) + if (!name) continue + + const nestedProperties = [] + for (const property of utils.iterateProperties( + currentProperty.parent, + new Set([name]) + )) { + if (property.type === 'object' && property.property) { + nestedProperties.push(property.property) + } + } + + const propertyOption = parseOption(nextOption, name) + if (!propertyOption) continue + + check( + nestedProperties, + parseOption(propertyOption, OptionKeys.BetweenItems), + parseOption(propertyOption, OptionKeys.WithinEach) + ) + } + } + + return utils.compositingVisitors( + utils.defineVueVisitor(context, { + onVueObjectEnter(node) { + const propertiesFiltered = /** @type {Property[]} */ ( + node.properties.filter((property) => property.type === 'Property') + ) + + check( + propertiesFiltered, + parseOption(options, OptionKeys.BetweenOptions), + parseOption(options, OptionKeys.WithinOption) + ) + } + }), + utils.defineScriptSetupVisitor(context, { + onDefinePropsEnter(_, props) { + const propNodes = /** @type {Property[]} */ ( + props + .filter((prop) => prop.node && prop.node.type === 'Property') + .map((prop) => prop.node) + ) + + const withinOption = parseOption(options, OptionKeys.WithinOption) + const propsOption = parseOption(withinOption, 'props') + if (!propsOption) return + + check( + propNodes, + parseOption(propsOption, OptionKeys.BetweenItems), + parseOption(propsOption, OptionKeys.WithinEach) + ) + }, + onDefineEmitsEnter(_, emits) { + const emitNodes = /** @type {Property[]} */ ( + emits + .filter((emit) => emit.node && emit.node.type === 'Property') + .map((emit) => emit.node) + ) + + const withinOption = parseOption(options, OptionKeys.WithinOption) + const emitsOption = parseOption(withinOption, 'emits') + if (!emitsOption) return + + check( + 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..952608da8 --- /dev/null +++ b/tests/lib/rules/padding-lines-in-component-definition.js @@ -0,0 +1,956 @@ +/** + * @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: 'NewVue', + 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 + } + ] + } + ], + 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 + } + ] + } + ] +}) From b34bf66841f97ab24bdd62131e400d08acf7ffcf Mon Sep 17 00:00:00 2001 From: ItMaga Date: Thu, 1 Dec 2022 23:45:22 +0300 Subject: [PATCH 2/6] refactor: rename check to verify --- lib/rules/padding-lines-in-component-definition.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/rules/padding-lines-in-component-definition.js b/lib/rules/padding-lines-in-component-definition.js index 1d65363b2..4e5b4881b 100644 --- a/lib/rules/padding-lines-in-component-definition.js +++ b/lib/rules/padding-lines-in-component-definition.js @@ -202,7 +202,7 @@ module.exports = { * @param {any} option * @param {any} nextOption * */ - function check(properties, option, nextOption) { + function verify(properties, option, nextOption) { const groupSingleLineProperties = parseBooleanOption( options, OptionKeys.GroupSingleLineProperties @@ -287,7 +287,7 @@ module.exports = { const propertyOption = parseOption(nextOption, name) if (!propertyOption) continue - check( + verify( nestedProperties, parseOption(propertyOption, OptionKeys.BetweenItems), parseOption(propertyOption, OptionKeys.WithinEach) @@ -302,7 +302,7 @@ module.exports = { node.properties.filter((property) => property.type === 'Property') ) - check( + verify( propertiesFiltered, parseOption(options, OptionKeys.BetweenOptions), parseOption(options, OptionKeys.WithinOption) @@ -321,7 +321,7 @@ module.exports = { const propsOption = parseOption(withinOption, 'props') if (!propsOption) return - check( + verify( propNodes, parseOption(propsOption, OptionKeys.BetweenItems), parseOption(propsOption, OptionKeys.WithinEach) @@ -338,7 +338,7 @@ module.exports = { const emitsOption = parseOption(withinOption, 'emits') if (!emitsOption) return - check( + verify( emitNodes, parseOption(emitsOption, OptionKeys.BetweenItems), parseOption(emitsOption, OptionKeys.WithinEach) From 23354ca6ce1679db6a42e33e74ccefe6e891822e Mon Sep 17 00:00:00 2001 From: ItMaga Date: Sun, 8 Jan 2023 02:22:16 +0300 Subject: [PATCH 3/6] fix: add withinOption guard in define* --- lib/rules/padding-lines-in-component-definition.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rules/padding-lines-in-component-definition.js b/lib/rules/padding-lines-in-component-definition.js index 4e5b4881b..001eb7b10 100644 --- a/lib/rules/padding-lines-in-component-definition.js +++ b/lib/rules/padding-lines-in-component-definition.js @@ -318,7 +318,7 @@ module.exports = { ) const withinOption = parseOption(options, OptionKeys.WithinOption) - const propsOption = parseOption(withinOption, 'props') + const propsOption = withinOption && parseOption(withinOption, 'props') if (!propsOption) return verify( @@ -335,7 +335,7 @@ module.exports = { ) const withinOption = parseOption(options, OptionKeys.WithinOption) - const emitsOption = parseOption(withinOption, 'emits') + const emitsOption = withinOption && parseOption(withinOption, 'emits') if (!emitsOption) return verify( From 02dbdbe84b830efce77feff130f6dee0017ab3b2 Mon Sep 17 00:00:00 2001 From: ItMaga Date: Sun, 8 Jan 2023 02:23:14 +0300 Subject: [PATCH 4/6] feat: add non-property nodes support --- .../padding-lines-in-component-definition.js | 81 ++--- .../padding-lines-in-component-definition.js | 288 +++++++++++++++++- 2 files changed, 330 insertions(+), 39 deletions(-) diff --git a/lib/rules/padding-lines-in-component-definition.js b/lib/rules/padding-lines-in-component-definition.js index 001eb7b10..bd3c9f259 100644 --- a/lib/rules/padding-lines-in-component-definition.js +++ b/lib/rules/padding-lines-in-component-definition.js @@ -10,6 +10,7 @@ */ const utils = require('../utils') +const { isComment } = require('../utils/indent-utils') const AvailablePaddingOptions = { Never: 'never', @@ -31,6 +32,13 @@ 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. @@ -64,8 +72,8 @@ function parseBooleanOption(initialOption, optionKey) { } /** - * @param {Property} currentProperty - * @param {Property} nextProperty + * @param {(Property | SpreadElement)} currentProperty + * @param {(Property | SpreadElement)} nextProperty * @param {boolean} option * @returns {boolean} * @private/ @@ -157,8 +165,8 @@ module.exports = { const sourceCode = context.getSourceCode() /** - * @param {Property} currentProperty - * @param {Property} nextProperty + * @param {(Property | SpreadElement)} currentProperty + * @param {(Property | SpreadElement | Token)} nextProperty * @param {RuleFixer} fixer * */ function replaceLines(currentProperty, nextProperty, fixer) { @@ -174,8 +182,8 @@ module.exports = { } /** - * @param {Property} currentProperty - * @param {Property} nextProperty + * @param {(Property | SpreadElement)} currentProperty + * @param {(Property | SpreadElement | Token)} nextProperty * @param {RuleFixer} fixer * @param {number} betweenLinesRange * */ @@ -198,7 +206,7 @@ module.exports = { } /** - * @param {Property[]} properties + * @param {(Property | SpreadElement)[]} properties * @param {any} option * @param {any} nextOption * */ @@ -212,8 +220,14 @@ module.exports = { const nextProperty = properties[i + 1] if (nextProperty && option !== AvailablePaddingOptions.Ignore) { + const tokenBeforeNext = sourceCode.getTokenBefore(nextProperty, { + includeComments: true + }) + const isCommentBefore = isComment(tokenBeforeNext) + const reportNode = isCommentBefore ? tokenBeforeNext : nextProperty + const betweenLinesRange = - nextProperty.loc.start.line - currentProperty.loc.end.line + reportNode.loc.start.line - currentProperty.loc.end.line if ( needGroupSingleLineProperties( @@ -224,11 +238,11 @@ module.exports = { ) { if (betweenLinesRange > 1) { context.report({ - node: nextProperty.key, + node: reportNode, messageId: 'groupSingleLineProperties', - loc: nextProperty.loc, + loc: reportNode.loc, fix(fixer) { - return replaceLines(currentProperty, nextProperty, fixer) + return replaceLines(currentProperty, reportNode, fixer) } }) } @@ -240,13 +254,13 @@ module.exports = { option === AvailablePaddingOptions.Always ) { context.report({ - node: nextProperty.key, + node: reportNode, messageId: 'always', - loc: nextProperty.loc, + loc: reportNode.loc, fix(fixer) { return insertLines( currentProperty, - nextProperty, + reportNode, fixer, betweenLinesRange ) @@ -257,11 +271,11 @@ module.exports = { option === AvailablePaddingOptions.Never ) { context.report({ - node: nextProperty.key, + node: reportNode, messageId: 'never', - loc: nextProperty.loc, + loc: reportNode.loc, fix(fixer) { - return replaceLines(currentProperty, nextProperty, fixer) + return replaceLines(currentProperty, reportNode, fixer) } }) } @@ -270,23 +284,20 @@ module.exports = { if (!nextOption) return const name = /** @type {GroupName | null} */ ( - utils.getStaticPropertyName(currentProperty) + currentProperty.type === 'Property' && + utils.getStaticPropertyName(currentProperty) ) if (!name) continue - const nestedProperties = [] - for (const property of utils.iterateProperties( - currentProperty.parent, - new Set([name]) - )) { - if (property.type === 'object' && property.property) { - nestedProperties.push(property.property) - } - } - 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), @@ -298,12 +309,8 @@ module.exports = { return utils.compositingVisitors( utils.defineVueVisitor(context, { onVueObjectEnter(node) { - const propertiesFiltered = /** @type {Property[]} */ ( - node.properties.filter((property) => property.type === 'Property') - ) - verify( - propertiesFiltered, + node.properties, parseOption(options, OptionKeys.BetweenOptions), parseOption(options, OptionKeys.WithinOption) ) @@ -311,9 +318,9 @@ module.exports = { }), utils.defineScriptSetupVisitor(context, { onDefinePropsEnter(_, props) { - const propNodes = /** @type {Property[]} */ ( + const propNodes = /** @type {(Property | SpreadElement)[]} */ ( props - .filter((prop) => prop.node && prop.node.type === 'Property') + .filter((prop) => prop.node && isValidProperties(prop.node.type)) .map((prop) => prop.node) ) @@ -328,9 +335,9 @@ module.exports = { ) }, onDefineEmitsEnter(_, emits) { - const emitNodes = /** @type {Property[]} */ ( + const emitNodes = /** @type {(Property | SpreadElement)[]} */ ( emits - .filter((emit) => emit.node && emit.node.type === 'Property') + .filter((emit) => emit.node && isValidProperties(emit.node.type)) .map((emit) => emit.node) ) diff --git a/tests/lib/rules/padding-lines-in-component-definition.js b/tests/lib/rules/padding-lines-in-component-definition.js index 952608da8..a4ed4d3c3 100644 --- a/tests/lib/rules/padding-lines-in-component-definition.js +++ b/tests/lib/rules/padding-lines-in-component-definition.js @@ -164,7 +164,7 @@ tester.run('padding-lines-in-component-definition', rule, { code: ` + `, + options: ['always'] + }, + { + filename: 'Spread.vue', + code: ` + + `, + options: ['never'] + }, + { + filename: 'SpreadWithComment.vue', + code: ` + + `, + options: ['always'] } ], invalid: [ @@ -951,6 +1030,211 @@ tester.run('padding-lines-in-component-definition', rule, { 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 + } + ] } ] }) From 3d00c8d232c36d45e3dc7b0513a61cd05485918f Mon Sep 17 00:00:00 2001 From: ItMaga Date: Tue, 10 Jan 2023 17:53:19 +0300 Subject: [PATCH 5/6] fix: use eslint-utils --- lib/rules/padding-lines-in-component-definition.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rules/padding-lines-in-component-definition.js b/lib/rules/padding-lines-in-component-definition.js index bd3c9f259..ffd43ccac 100644 --- a/lib/rules/padding-lines-in-component-definition.js +++ b/lib/rules/padding-lines-in-component-definition.js @@ -10,7 +10,7 @@ */ const utils = require('../utils') -const { isComment } = require('../utils/indent-utils') +const { isCommentToken } = require('eslint-utils') const AvailablePaddingOptions = { Never: 'never', @@ -223,7 +223,7 @@ module.exports = { const tokenBeforeNext = sourceCode.getTokenBefore(nextProperty, { includeComments: true }) - const isCommentBefore = isComment(tokenBeforeNext) + const isCommentBefore = isCommentToken(tokenBeforeNext) const reportNode = isCommentBefore ? tokenBeforeNext : nextProperty const betweenLinesRange = From 11a6d8940166def5cf3ea58cc4404797f9f30cee Mon Sep 17 00:00:00 2001 From: ItMaga Date: Wed, 11 Jan 2023 15:29:49 +0300 Subject: [PATCH 6/6] chore: clean up code --- lib/rules/padding-lines-in-component-definition.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/rules/padding-lines-in-component-definition.js b/lib/rules/padding-lines-in-component-definition.js index ffd43ccac..c15762b34 100644 --- a/lib/rules/padding-lines-in-component-definition.js +++ b/lib/rules/padding-lines-in-component-definition.js @@ -51,17 +51,18 @@ function splitLines(text) { /** * @param {any} initialOption * @param {string} optionKey - * @private/ + * @private * */ function parseOption(initialOption, optionKey) { return typeof initialOption === 'string' ? initialOption : initialOption[optionKey] } + /** * @param {any} initialOption * @param {string} optionKey - * @private/ + * @private * */ function parseBooleanOption(initialOption, optionKey) { if (typeof initialOption === 'string') { @@ -76,7 +77,7 @@ function parseBooleanOption(initialOption, optionKey) { * @param {(Property | SpreadElement)} nextProperty * @param {boolean} option * @returns {boolean} - * @private/ + * @private * */ function needGroupSingleLineProperties(currentProperty, nextProperty, option) { const isSingleCurrentProperty = @@ -107,6 +108,7 @@ module.exports = { }, { type: 'object', + additionalProperties: false, properties: { [OptionKeys.BetweenOptions]: { enum: Object.values(AvailablePaddingOptions)