UNPKG

eslint-plugin-svelte

Version:
289 lines (288 loc) 11.2 kB
import { createRule } from '../utils/index.js'; import { getAttributeKeyText } from '../utils/ast-utils.js'; import { toRegExp } from '../utils/regexp.js'; const DEFAULT_ORDER = [ // `this` property. 'this', // `bind:this` directive. 'bind:this', // `id` attribute. 'id', // `name` attribute. 'name', // `slot` attribute. 'slot', // `--style-props` (Alphabetical order within the same group.) { match: '/^--/u', sort: 'alphabetical' }, // `style` attribute, and `style:` directives. ['style', '/^style:/u'], // `class` attribute. 'class', // `class:` directives. (Alphabetical order within the same group.) { match: '/^class:/u', sort: 'alphabetical' }, // other attributes. (Alphabetical order within the same group.) { match: ['!/:/u', '!/^(?:this|id|name|style|class)$/u', '!/^--/u'], sort: 'alphabetical' }, // `bind:` directives (other then `bind:this`), and `on:` directives. ['/^bind:/u', '!bind:this', '/^on:/u'], // `use:` directives. (Alphabetical order within the same group.) { match: '/^use:/u', sort: 'alphabetical' }, // `transition:` directives. (Alphabetical order within the same group.) { match: '/^transition:/u', sort: 'alphabetical' }, // `in:` directives. (Alphabetical order within the same group.) { match: '/^in:/u', sort: 'alphabetical' }, // `out:` directives. (Alphabetical order within the same group.) { match: '/^out:/u', sort: 'alphabetical' }, // `animate:` directives. (Alphabetical order within the same group.) { match: '/^animate:/u', sort: 'alphabetical' }, // `let:` directives. (Alphabetical order within the same group.) { match: '/^let:/u', sort: 'alphabetical' } ]; /** Parse options */ function parseOption(option) { const order = option?.order ?? DEFAULT_ORDER; const compiled = order.map(compileOption); return { ignore: (key) => { return !compiled.some((c) => c.match(key)); }, compare: (a, b) => { for (const c of compiled) { const matchA = c.match(a); const matchB = c.match(b); if (matchA && matchB) { if (c.sort === 'alphabetical') { return a === b ? 0 : a < b ? -1 : 1; } return 0; } if (matchA) { return -1; } if (matchB) { return 1; } } throw new Error('Illegal state'); } }; } /** Compile order option */ function compileOption(option) { const cache = {}; const compiled = compileOptionWithoutCache(option); return { match: (str) => { if (cache[str] != null) return cache[str]; return (cache[str] = compiled.match(str)); }, sort: compiled.sort }; /** Compile order option without cache */ function compileOptionWithoutCache(option) { if (typeof option === 'string') { const match = compileMatcher([option]); return { match, sort: 'ignore' }; } if (Array.isArray(option)) { const match = compileMatcher(option); return { match, sort: 'ignore' }; } const { match } = compileOptionWithoutCache(option.match); return { match, sort: option.sort || 'ignore' }; } } /** Compile matcher */ function compileMatcher(pattern) { const rules = []; for (const p of pattern) { let negative, patternStr; if (p.startsWith('!')) { // If there is `!` at the beginning, it will be parsed with a negative pattern. negative = true; patternStr = p.substring(1); } else { negative = false; patternStr = p; } const regex = toRegExp(patternStr); rules.push({ negative, match: (str) => regex.test(str) }); } return (str) => { // If the first rule is a negative pattern, they are considered to match if they do not match that pattern. let result = Boolean(rules[0]?.negative); for (const { negative, match } of rules) { if (result === !negative) { // Even if it matches, the result does not change, so skip it. continue; } if (match(str)) { result = !negative; } } return result; }; } export default createRule('sort-attributes', { meta: { docs: { description: 'enforce order of attributes', category: 'Stylistic Issues', recommended: false, conflictWithPrettier: false }, schema: [ { type: 'object', properties: { order: { type: 'array', items: { anyOf: [ { type: 'string' }, { type: 'array', items: { type: 'string' }, uniqueItems: true, minItems: 1 }, { type: 'object', properties: { match: { anyOf: [ { type: 'string' }, { type: 'array', items: { type: 'string' }, uniqueItems: true, minItems: 1 } ] }, sort: { enum: ['alphabetical', 'ignore'] } }, required: ['match', 'sort'], additionalProperties: false } ] }, uniqueItems: true, additionalItems: false }, alphabetical: { type: 'boolean' } }, additionalProperties: false } ], messages: { shouldBefore: "Attribute '{{currentKey}}' should go before '{{prevKey}}'." }, type: 'layout', fixable: 'code' }, create(context) { const option = parseOption(context.options[0]); const cacheKeyText = new Map(); /** Get key text */ function getKeyText(node) { const k = cacheKeyText.get(node); if (k != null) return k; const result = getAttributeKeyText(node, context); cacheKeyText.set(node, result); return result; } /** Report */ function report(node, previousNode) { const currentKey = getKeyText(node); const prevKey = getKeyText(previousNode); context.report({ node, messageId: 'shouldBefore', data: { currentKey, prevKey }, fix(fixer) { const attributes = node.parent.attributes; const previousNodes = attributes.slice(attributes.indexOf(previousNode), attributes.indexOf(node)); const moveNodes = [node, ...previousNodes]; const sourceCode = context.sourceCode; return moveNodes.map((moveNode, index) => { const text = sourceCode.getText(moveNode); return fixer.replaceText(previousNodes[index] || node, text); }); } }); } /** * Checks whether have a spread attribute between the node and the previous node. */ function hasSpreadAttribute(node, previousNode) { const attributes = node.parent.attributes; const previousNodes = attributes.slice(attributes.indexOf(previousNode), attributes.indexOf(node)); return previousNodes.some((a) => a.type === 'SvelteSpreadAttribute'); } /** * Verify the order for which the spread attribute exists. */ function verifyForSpreadAttributeExist(node) { const previousNodes = []; const attributes = node.parent.attributes; for (const previousNode of attributes.slice(0, attributes.indexOf(node)).reverse()) { if (previousNode.type === 'SvelteSpreadAttribute') { break; } previousNodes.unshift(previousNode); } const key = getKeyText(node); const invalidPreviousNode = previousNodes.find((previousNode) => { const prevKey = getKeyText(previousNode); if (option.ignore(prevKey)) { return false; } return option.compare(prevKey, key) > 0; }); if (invalidPreviousNode) { report(node, invalidPreviousNode); } } return { SvelteStartTag(node) { const validPreviousNodes = []; for (const attr of node.attributes) { if (attr.type === 'SvelteSpreadAttribute') { continue; } const key = getKeyText(attr); if (option.ignore(key)) { continue; } const invalidPreviousNode = validPreviousNodes.find((previousNode) => option.compare(getKeyText(previousNode), key) > 0); if (invalidPreviousNode) { if (attr.type !== 'SvelteAttribute' || !hasSpreadAttribute(attr, invalidPreviousNode)) { report(attr, invalidPreviousNode); } else { // Verify the order for which the spread attribute exists. verifyForSpreadAttributeExist(attr); } continue; } validPreviousNodes.push(attr); } } }; } });