You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
376 lines
11 KiB
376 lines
11 KiB
1 month ago
|
/**
|
||
|
* @author ItMaga <https://github.com/ItMaga>
|
||
|
* See LICENSE file in root directory for full license.
|
||
|
*/
|
||
|
'use strict'
|
||
|
|
||
|
/**
|
||
|
* @typedef {import('../utils').ComponentProp} ComponentProp
|
||
|
* @typedef {import('../utils').ComponentEmit} ComponentEmit
|
||
|
* @typedef {import('../utils').GroupName} GroupName
|
||
|
*/
|
||
|
|
||
|
const utils = require('../utils')
|
||
|
const { isCommentToken } = require('@eslint-community/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 === ','
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @typedef {Exclude<ComponentProp | ComponentEmit, {type:'infer-type'}> & { node: {type: 'Property' | 'SpreadElement'} }} ValidComponentPropOrEmit
|
||
|
*/
|
||
|
/**
|
||
|
* @template {ComponentProp | ComponentEmit} T
|
||
|
* @param {T} propOrEmit
|
||
|
* @returns {propOrEmit is ValidComponentPropOrEmit & T}
|
||
|
*/
|
||
|
function isValidProperties(propOrEmit) {
|
||
|
return Boolean(
|
||
|
propOrEmit.type !== 'infer-type' &&
|
||
|
propOrEmit.node &&
|
||
|
['Property', 'SpreadElement'].includes(propOrEmit.node.type)
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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 = props
|
||
|
.filter(isValidProperties)
|
||
|
.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 = emits
|
||
|
.filter(isValidProperties)
|
||
|
.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)
|
||
|
)
|
||
|
},
|
||
|
onDefineOptionsEnter(node) {
|
||
|
if (node.arguments.length === 0) return
|
||
|
const define = node.arguments[0]
|
||
|
if (define.type !== 'ObjectExpression') return
|
||
|
verify(
|
||
|
define.properties,
|
||
|
parseOption(options, OptionKeys.BetweenOptions),
|
||
|
parseOption(options, OptionKeys.WithinOption)
|
||
|
)
|
||
|
}
|
||
|
})
|
||
|
)
|
||
|
}
|
||
|
}
|