238 lines
7 KiB
JavaScript
238 lines
7 KiB
JavaScript
|
/**
|
||
|
* @fileoverview Keep order of properties in components
|
||
|
* @author Michał Sajnóg
|
||
|
*/
|
||
|
'use strict'
|
||
|
|
||
|
const utils = require('../utils')
|
||
|
const traverseNodes = require('vue-eslint-parser').AST.traverseNodes
|
||
|
|
||
|
const defaultOrder = [
|
||
|
'el',
|
||
|
'name',
|
||
|
'parent',
|
||
|
'functional',
|
||
|
['delimiters', 'comments'],
|
||
|
['components', 'directives', 'filters'],
|
||
|
'extends',
|
||
|
'mixins',
|
||
|
'inheritAttrs',
|
||
|
'model',
|
||
|
['props', 'propsData'],
|
||
|
'data',
|
||
|
'computed',
|
||
|
'watch',
|
||
|
'asyncData',
|
||
|
'fetch',
|
||
|
'LIFECYCLE_HOOKS',
|
||
|
'methods',
|
||
|
['template', 'render'],
|
||
|
'renderError'
|
||
|
]
|
||
|
|
||
|
const groups = {
|
||
|
LIFECYCLE_HOOKS: [
|
||
|
'beforeCreate',
|
||
|
'created',
|
||
|
'beforeMount',
|
||
|
'mounted',
|
||
|
'beforeUpdate',
|
||
|
'updated',
|
||
|
'activated',
|
||
|
'deactivated',
|
||
|
'beforeDestroy',
|
||
|
'destroyed'
|
||
|
]
|
||
|
}
|
||
|
|
||
|
function getOrderMap (order) {
|
||
|
const orderMap = new Map()
|
||
|
|
||
|
order.forEach((property, i) => {
|
||
|
if (Array.isArray(property)) {
|
||
|
property.forEach(p => orderMap.set(p, i))
|
||
|
} else {
|
||
|
orderMap.set(property, i)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
return orderMap
|
||
|
}
|
||
|
|
||
|
function isComma (node) {
|
||
|
return node.type === 'Punctuator' && node.value === ','
|
||
|
}
|
||
|
|
||
|
const ARITHMETIC_OPERATORS = ['+', '-', '*', '/', '%', '**']
|
||
|
const BITWISE_OPERATORS = ['&', '|', '^', '~', '<<', '>>', '>>>']
|
||
|
const COMPARISON_OPERATORS = ['==', '!=', '===', '!==', '>', '>=', '<', '<=']
|
||
|
const RELATIONAL_OPERATORS = ['in', 'instanceof']
|
||
|
const ALL_BINARY_OPERATORS = [].concat(
|
||
|
ARITHMETIC_OPERATORS,
|
||
|
BITWISE_OPERATORS,
|
||
|
COMPARISON_OPERATORS,
|
||
|
RELATIONAL_OPERATORS
|
||
|
)
|
||
|
const LOGICAL_OPERATORS = ['&&', '||']
|
||
|
|
||
|
/*
|
||
|
* Result `true` if the node is sure that there are no side effects
|
||
|
*
|
||
|
* Currently known side effects types
|
||
|
*
|
||
|
* node.type === 'CallExpression'
|
||
|
* node.type === 'NewExpression'
|
||
|
* node.type === 'UpdateExpression'
|
||
|
* node.type === 'AssignmentExpression'
|
||
|
* node.type === 'TaggedTemplateExpression'
|
||
|
* node.type === 'UnaryExpression' && node.operator === 'delete'
|
||
|
*
|
||
|
* @param {ASTNode} node target node
|
||
|
* @param {Object} visitorKeys sourceCode.visitorKey
|
||
|
* @returns {Boolean} no side effects
|
||
|
*/
|
||
|
function isNotSideEffectsNode (node, visitorKeys) {
|
||
|
let result = true
|
||
|
const noSideEffectsNodes = new Set()
|
||
|
traverseNodes(node, {
|
||
|
visitorKeys,
|
||
|
enterNode (node, parent) {
|
||
|
if (!result || noSideEffectsNodes.has(node)) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
node.type === 'FunctionExpression' ||
|
||
|
node.type === 'Identifier' ||
|
||
|
node.type === 'Literal' ||
|
||
|
// es2015
|
||
|
node.type === 'ArrowFunctionExpression' ||
|
||
|
node.type === 'TemplateElement'
|
||
|
) {
|
||
|
// no side effects node
|
||
|
noSideEffectsNodes.add(node)
|
||
|
traverseNodes(node, {
|
||
|
visitorKeys,
|
||
|
enterNode (node) {
|
||
|
noSideEffectsNodes.add(node)
|
||
|
},
|
||
|
leaveNode () {}
|
||
|
})
|
||
|
} else if (
|
||
|
node.type !== 'Property' &&
|
||
|
node.type !== 'ObjectExpression' &&
|
||
|
node.type !== 'ArrayExpression' &&
|
||
|
(node.type !== 'UnaryExpression' || ['!', '~', '+', '-', 'typeof'].indexOf(node.operator) < 0) &&
|
||
|
(node.type !== 'BinaryExpression' || ALL_BINARY_OPERATORS.indexOf(node.operator) < 0) &&
|
||
|
(node.type !== 'LogicalExpression' || LOGICAL_OPERATORS.indexOf(node.operator) < 0) &&
|
||
|
node.type !== 'MemberExpression' &&
|
||
|
node.type !== 'ConditionalExpression' &&
|
||
|
// es2015
|
||
|
node.type !== 'SpreadElement' &&
|
||
|
node.type !== 'TemplateLiteral'
|
||
|
) {
|
||
|
// Can not be sure that a node has no side effects
|
||
|
result = false
|
||
|
}
|
||
|
},
|
||
|
leaveNode () {}
|
||
|
})
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
// ------------------------------------------------------------------------------
|
||
|
// Rule Definition
|
||
|
// ------------------------------------------------------------------------------
|
||
|
|
||
|
module.exports = {
|
||
|
meta: {
|
||
|
type: 'suggestion',
|
||
|
docs: {
|
||
|
description: 'enforce order of properties in components',
|
||
|
category: 'recommended',
|
||
|
url: 'https://eslint.vuejs.org/rules/order-in-components.html'
|
||
|
},
|
||
|
fixable: 'code', // null or "code" or "whitespace"
|
||
|
schema: [
|
||
|
{
|
||
|
type: 'object',
|
||
|
properties: {
|
||
|
order: {
|
||
|
type: 'array'
|
||
|
}
|
||
|
},
|
||
|
additionalProperties: false
|
||
|
}
|
||
|
]
|
||
|
},
|
||
|
|
||
|
create (context) {
|
||
|
const options = context.options[0] || {}
|
||
|
const order = options.order || defaultOrder
|
||
|
const extendedOrder = order.map(property => groups[property] || property)
|
||
|
const orderMap = getOrderMap(extendedOrder)
|
||
|
const sourceCode = context.getSourceCode()
|
||
|
|
||
|
function checkOrder (propertiesNodes, orderMap) {
|
||
|
const properties = propertiesNodes
|
||
|
.filter(property => property.type === 'Property')
|
||
|
.map(property => property.key)
|
||
|
|
||
|
properties.forEach((property, i) => {
|
||
|
const propertiesAbove = properties.slice(0, i)
|
||
|
const unorderedProperties = propertiesAbove
|
||
|
.filter(p => orderMap.get(p.name) > orderMap.get(property.name))
|
||
|
.sort((p1, p2) => orderMap.get(p1.name) > orderMap.get(p2.name) ? 1 : -1)
|
||
|
|
||
|
const firstUnorderedProperty = unorderedProperties[0]
|
||
|
|
||
|
if (firstUnorderedProperty) {
|
||
|
const line = firstUnorderedProperty.loc.start.line
|
||
|
context.report({
|
||
|
node: property,
|
||
|
message: `The "{{name}}" property should be above the "{{firstUnorderedPropertyName}}" property on line {{line}}.`,
|
||
|
data: {
|
||
|
name: property.name,
|
||
|
firstUnorderedPropertyName: firstUnorderedProperty.name,
|
||
|
line
|
||
|
},
|
||
|
fix (fixer) {
|
||
|
const propertyNode = property.parent
|
||
|
const firstUnorderedPropertyNode = firstUnorderedProperty.parent
|
||
|
const hasSideEffectsPossibility = propertiesNodes
|
||
|
.slice(
|
||
|
propertiesNodes.indexOf(firstUnorderedPropertyNode),
|
||
|
propertiesNodes.indexOf(propertyNode) + 1
|
||
|
)
|
||
|
.some((property) => !isNotSideEffectsNode(property, sourceCode.visitorKeys))
|
||
|
if (hasSideEffectsPossibility) {
|
||
|
return undefined
|
||
|
}
|
||
|
const afterComma = sourceCode.getTokenAfter(propertyNode)
|
||
|
const hasAfterComma = isComma(afterComma)
|
||
|
|
||
|
const beforeComma = sourceCode.getTokenBefore(propertyNode)
|
||
|
const codeStart = beforeComma.range[1] // to include comments
|
||
|
const codeEnd = hasAfterComma ? afterComma.range[1] : propertyNode.range[1]
|
||
|
|
||
|
const propertyCode = sourceCode.text.slice(codeStart, codeEnd) + (hasAfterComma ? '' : ',')
|
||
|
const insertTarget = sourceCode.getTokenBefore(firstUnorderedPropertyNode)
|
||
|
|
||
|
const removeStart = hasAfterComma ? codeStart : beforeComma.range[0]
|
||
|
|
||
|
return [
|
||
|
fixer.removeRange([removeStart, codeEnd]),
|
||
|
fixer.insertTextAfter(insertTarget, propertyCode)
|
||
|
]
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
return utils.executeOnVue(context, (obj) => {
|
||
|
checkOrder(obj.properties, orderMap)
|
||
|
})
|
||
|
}
|
||
|
}
|