From 31666df09f5bf1704946d7dfad36e4a9014139c5 Mon Sep 17 00:00:00 2001 From: kingwl Date: Fri, 16 Sep 2022 15:06:39 +0800 Subject: [PATCH 1/8] Add block-attributes-order rule --- .gitignore | 1 + lib/rules/attributes-order.js | 8 +- lib/rules/block-attributes-order.js | 317 ++++++++++++++++++++++ tests/lib/rules/block-attributes-order.js | 234 ++++++++++++++++ 4 files changed, 556 insertions(+), 4 deletions(-) create mode 100644 lib/rules/block-attributes-order.js create mode 100644 tests/lib/rules/block-attributes-order.js diff --git a/.gitignore b/.gitignore index e1401b951..1333c6c46 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ yarn.lock yarn-error.log docs/.vuepress/dist typings/eslint/lib/rules +.DS_Store diff --git a/lib/rules/attributes-order.js b/lib/rules/attributes-order.js index d88e9c934..46b634bba 100644 --- a/lib/rules/attributes-order.js +++ b/lib/rules/attributes-order.js @@ -382,10 +382,10 @@ module.exports = { { type: 'array', items: { - enum: Object.values(ATTRS), - uniqueItems: true, - additionalItems: false - } + enum: Object.values(ATTRS) + }, + uniqueItems: true, + additionalItems: false } ] }, diff --git a/lib/rules/block-attributes-order.js b/lib/rules/block-attributes-order.js new file mode 100644 index 000000000..43d9a1e96 --- /dev/null +++ b/lib/rules/block-attributes-order.js @@ -0,0 +1,317 @@ +/** + * @fileoverview enforce ordering of block attributes + * @author Wenlu Wang + */ +'use strict' +const utils = require('../utils') + +/** + * @enum {string} + */ +const WELL_KNOWN_TEMPLATE_ATTRS = { + functional: 'functional', + lang: 'lang', + src: 'src' +} + +/** + * @enum {string} + */ +const WELL_KNOWN_SCRIPT_ATTRS = { + lang: 'lang', + setup: 'setup', + src: 'src' +} + +/** + * @enum {string} + */ +const WELL_KNOWN_STYLE_ATTRS = { + scoped: 'scoped', + module: 'module', + lang: 'lang', + src: 'src' +} + +/** + * @template {string} T + * @typedef {T | T[]} OrderItem + */ + +/** + * @typedef WellKnownOrders + * @property { OrderItem[] } [template] + * @property { OrderItem[] } [script] + * @property { OrderItem[] } [style] + */ + +/** + * @typedef UserOptions + * @property {WellKnownOrders & Record[]>} [order] + */ + +/** + * Normalizes a given options. + * @param {UserOptions?} options An option to parse. + * @return {WellKnownOrders & Record[]>} + */ +function normalizeOptions(options) { + if (!options || !options.order) { + return { + template: [ + WELL_KNOWN_TEMPLATE_ATTRS.functional, + WELL_KNOWN_TEMPLATE_ATTRS.lang, + WELL_KNOWN_TEMPLATE_ATTRS.src + ], + script: [ + WELL_KNOWN_SCRIPT_ATTRS.lang, + WELL_KNOWN_SCRIPT_ATTRS.setup, + WELL_KNOWN_SCRIPT_ATTRS.src + ], + style: [ + WELL_KNOWN_STYLE_ATTRS.lang, + WELL_KNOWN_STYLE_ATTRS.module, + WELL_KNOWN_STYLE_ATTRS.scoped, + WELL_KNOWN_STYLE_ATTRS.src + ] + } + } + return options.order +} + +/** + * @param {WellKnownOrders & Record[]>} order + */ +function normalizeAttributePositions(order) { + /** + * @type { Record> } + */ + const attributePositions = {} + for (const [blockName, blockOrder] of Object.entries(order)) { + /** + * @type { Record } + */ + const attributePosition = {} + for (const [i, o] of blockOrder.entries()) { + if (Array.isArray(o)) { + for (const attr of o) { + attributePosition[attr] = i + } + } else { + attributePosition[o] = i + } + } + attributePositions[blockName] = attributePosition + } + return attributePositions +} + +/** + * @param {VAttribute | VDirective} attribute + * @param { Record } attributePosition + * @returns {number | null} + */ +function getPosition(attribute, attributePosition) { + if (attribute.directive) { + return null + } + + return attributePosition[attribute.key.name] +} + +/** + * @param {VStartTag} node + * @param {Record} attributePosition + */ +function getAttributeAndPositionList(node, attributePosition) { + /** + * @type {{ attr: (VAttribute | VDirective), position: number }[]} + */ + const results = [] + for (const attr of node.attributes) { + const position = getPosition(attr, attributePosition) + if (position == null) { + continue + } + results.push({ attr, position }) + } + return results +} + +/** + * @param {RuleContext} context - The rule context. + * @returns {RuleListener} AST event handlers. + */ +function create(context) { + const sourceCode = context.getSourceCode() + const order = normalizeOptions(context.options[0]) + const attributeAndPositions = normalizeAttributePositions(order) + + /** + * @param {VAttribute | VDirective} node + * @param {VAttribute | VDirective} previousNode + */ + function reportIssue(node, previousNode) { + const currentNodeText = sourceCode.getText(node.key) + const prevNode = sourceCode.getText(previousNode.key) + + /** + * @param {RuleFixer} fixer + */ + function fix(fixer) { + const attributes = node.parent.attributes + + const previousNodes = attributes.slice( + attributes.indexOf(previousNode), + attributes.indexOf(node) + ) + const moveNodes = [node] + for (const n of previousNodes) { + moveNodes.push(n) + } + + return moveNodes.map((moveNode, index) => { + const text = sourceCode.getText(moveNode) + return fixer.replaceText(previousNodes[index] || node, text) + }) + } + + context.report({ + node, + message: `Attribute "${currentNodeText}" should go before "${prevNode}".`, + data: { + currentNodeText + }, + fix + }) + } + + /** + * @param {VElement} element + * @returns {void} + */ + function verify(element) { + const tag = element.name + const attributePosition = attributeAndPositions[tag] + if (!attributePosition) { + return + } + + const attributeAndPositionList = getAttributeAndPositionList( + element.startTag, + attributePosition + ) + if (attributeAndPositionList.length <= 1) { + return + } + + let { attr: previousNode, position: previousPosition } = + attributeAndPositionList[0] + for (let index = 1; index < attributeAndPositionList.length; index++) { + const { attr, position } = attributeAndPositionList[index] + if (previousPosition <= position) { + previousNode = attr + previousPosition = position + } else { + reportIssue(attr, previousNode) + } + } + } + + return utils.defineDocumentVisitor(context, { + 'VDocumentFragment > VElement': verify + }) +} + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'enforce order of block attributes', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/block-attributes-order.html' + }, + fixable: 'code', + schema: [ + { + type: 'object', + properties: { + order: { + type: 'object', + properties: { + template: { + type: 'array', + items: { + anyOf: [ + { enum: Object.values(WELL_KNOWN_TEMPLATE_ATTRS) }, + { type: 'string' }, + { + type: 'array', + items: { + anyOf: [ + { enum: Object.values(WELL_KNOWN_TEMPLATE_ATTRS) }, + { type: 'string' } + ] + }, + uniqueItems: true + } + ] + }, + uniqueItems: true + }, + script: { + type: 'array', + items: { + anyOf: [ + { enum: Object.values(WELL_KNOWN_SCRIPT_ATTRS) }, + { type: 'string' }, + { + type: 'array', + items: { + anyOf: [ + { enum: Object.values(WELL_KNOWN_SCRIPT_ATTRS) }, + { type: 'string' } + ] + }, + uniqueItems: true + } + ] + }, + uniqueItems: true + }, + style: { + type: 'array', + items: { + anyOf: [ + { enum: Object.values(WELL_KNOWN_STYLE_ATTRS) }, + { type: 'string' }, + { + type: 'array', + items: { + anyOf: [ + { enum: Object.values(WELL_KNOWN_STYLE_ATTRS) }, + { type: 'string' } + ] + }, + uniqueItems: true + } + ] + }, + uniqueItems: true + } + }, + additionalProperties: { + type: 'array', + items: { + type: 'string' + }, + uniqueItems: true + } + } + }, + additionalProperties: false + } + ] + }, + create +} diff --git a/tests/lib/rules/block-attributes-order.js b/tests/lib/rules/block-attributes-order.js new file mode 100644 index 000000000..0aa80d6dd --- /dev/null +++ b/tests/lib/rules/block-attributes-order.js @@ -0,0 +1,234 @@ +/** + * @fileoverview enforce ordering of block attributes + * @author Wenlu Wang + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/block-attributes-order') +const RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2015 } +}) +tester.run('block-attributes-order', rule, { + valid: [ + { + filename: 'test-template-without-attributes.vue', + code: `