/** * @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) ) } }) ) } }