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.

186 lines
5.4 KiB

/**
* @author Yosuke Ota
* issue https://github.com/vuejs/eslint-plugin-vue/issues/140
*/
'use strict'
const utils = require('../utils')
const { parseSelector } = require('../utils/selector')
/**
* @typedef {import('../utils/selector').VElementSelector} VElementSelector
*/
const DEFAULT_ORDER = Object.freeze([['script', 'template'], 'style'])
/**
* @param {VElement} element
* @return {string}
*/
function getAttributeString(element) {
return element.startTag.attributes
.map((attribute) => {
if (attribute.value && attribute.value.type !== 'VLiteral') {
return ''
}
return `${attribute.key.name}${
attribute.value && attribute.value.value
? `=${attribute.value.value}`
: ''
}`
})
.join(' ')
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce order of component top-level elements',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/block-order.html'
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
order: {
type: 'array',
items: {
anyOf: [
{ type: 'string' },
{ type: 'array', items: { type: 'string' }, uniqueItems: true }
]
},
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
],
messages: {
unexpected:
"'<{{elementName}}{{elementAttributes}}>' should be above '<{{firstUnorderedName}}{{firstUnorderedAttributes}}>' on line {{line}}."
}
},
/**
* @param {RuleContext} context - The rule context.
* @returns {RuleListener} AST event handlers.
*/
create(context) {
/**
* @typedef {object} OrderElement
* @property {string} selectorText
* @property {VElementSelector} selector
* @property {number} index
*/
/** @type {OrderElement[]} */
const orders = []
/** @type {(string|string[])[]} */
const orderOptions =
(context.options[0] && context.options[0].order) || DEFAULT_ORDER
for (const [index, selectorOrSelectors] of orderOptions.entries()) {
if (Array.isArray(selectorOrSelectors)) {
for (const selector of selectorOrSelectors) {
orders.push({
selectorText: selector,
selector: parseSelector(selector, context),
index
})
}
} else {
orders.push({
selectorText: selectorOrSelectors,
selector: parseSelector(selectorOrSelectors, context),
index
})
}
}
/**
* @param {VElement} element
*/
function getOrderElement(element) {
return orders.find((o) => o.selector.test(element))
}
const sourceCode = context.getSourceCode()
const documentFragment =
sourceCode.parserServices.getDocumentFragment &&
sourceCode.parserServices.getDocumentFragment()
function getTopLevelHTMLElements() {
if (documentFragment) {
return documentFragment.children.filter(utils.isVElement)
}
return []
}
return {
Program(node) {
if (utils.hasInvalidEOF(node)) {
return
}
const elements = getTopLevelHTMLElements()
const elementsWithOrder = elements.flatMap((element) => {
const order = getOrderElement(element)
return order ? [{ order, element }] : []
})
const sourceCode = context.getSourceCode()
for (const [index, elementWithOrders] of elementsWithOrder.entries()) {
const { order: expected, element } = elementWithOrders
const firstUnordered = elementsWithOrder
.slice(0, index)
.filter(({ order }) => expected.index < order.index)
.sort((e1, e2) => e1.order.index - e2.order.index)[0]
if (firstUnordered) {
const firstUnorderedAttributes = getAttributeString(
firstUnordered.element
)
const elementAttributes = getAttributeString(element)
context.report({
node: element,
loc: element.loc,
messageId: 'unexpected',
data: {
elementName: element.name,
elementAttributes: elementAttributes
? ` ${elementAttributes}`
: '',
firstUnorderedName: firstUnordered.element.name,
firstUnorderedAttributes: firstUnorderedAttributes
? ` ${firstUnorderedAttributes}`
: '',
line: firstUnordered.element.loc.start.line
},
*fix(fixer) {
// insert element before firstUnordered
const fixedElements = elements.flatMap((it) => {
if (it === firstUnordered.element) {
return [element, it]
} else if (it === element) {
return []
}
return [it]
})
for (let i = elements.length - 1; i >= 0; i--) {
if (elements[i] !== fixedElements[i]) {
yield fixer.replaceTextRange(
elements[i].range,
sourceCode.text.slice(...fixedElements[i].range)
)
}
}
}
})
}
}
}
}
}
}