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.
375 lines
11 KiB
375 lines
11 KiB
/** |
|
* @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) |
|
) |
|
} |
|
}) |
|
) |
|
} |
|
}
|
|
|