From 4dd08ea4bb22586e51dbf020bf519020367d9848 Mon Sep 17 00:00:00 2001 From: Romain Menke <11521496+romainmenke@users.noreply.github.com> Date: Fri, 5 Apr 2024 07:31:50 +0200 Subject: [PATCH] Fix `selector-max-attribute` end positions (#7592) * Fix `selector-max-attribute` end positions * fmt * Create short-cobras-join.md --- .changeset/short-cobras-join.md | 5 +++ .../__tests__/index.mjs | 41 +++++++++++++++---- lib/rules/selector-max-attribute/index.cjs | 37 +++++++++-------- lib/rules/selector-max-attribute/index.mjs | 38 ++++++++--------- 4 files changed, 76 insertions(+), 45 deletions(-) create mode 100644 .changeset/short-cobras-join.md diff --git a/.changeset/short-cobras-join.md b/.changeset/short-cobras-join.md new file mode 100644 index 0000000000..4685e40ee4 --- /dev/null +++ b/.changeset/short-cobras-join.md @@ -0,0 +1,5 @@ +--- +"stylelint": patch +--- + +Fixed: `selector-max-attribute` end positions diff --git a/lib/rules/selector-max-attribute/__tests__/index.mjs b/lib/rules/selector-max-attribute/__tests__/index.mjs index 288c31ada9..c52bc22a22 100644 --- a/lib/rules/selector-max-attribute/__tests__/index.mjs +++ b/lib/rules/selector-max-attribute/__tests__/index.mjs @@ -158,6 +158,8 @@ testRule({ message: messages.expected('[type="text"][name="message"][data-attribute="value"]', 2), line: 1, column: 1, + endLine: 1, + endColumn: 54, }, { code: '[type="text"][name="message"][disabled] {}', @@ -165,6 +167,8 @@ testRule({ message: messages.expected('[type="text"][name="message"][disabled]', 2), line: 1, column: 1, + endLine: 1, + endColumn: 40, }, { code: '[type="text"] [name="message"] [data-attribute="value"] {}', @@ -172,6 +176,8 @@ testRule({ message: messages.expected('[type="text"] [name="message"] [data-attribute="value"]', 2), line: 1, column: 1, + endLine: 1, + endColumn: 56, }, { code: '[type="text"], \n[type="number"][name="quality"][data-attribute="value"] {}', @@ -179,6 +185,8 @@ testRule({ message: messages.expected('[type="number"][name="quality"][data-attribute="value"]', 2), line: 2, column: 1, + endLine: 2, + endColumn: 56, }, { code: '[type="text"], \n[type="number"][name="quality"][disabled] {}', @@ -186,20 +194,26 @@ testRule({ message: messages.expected('[type="number"][name="quality"][disabled]', 2), line: 2, column: 1, + endLine: 2, + endColumn: 42, }, { code: ':not([type="text"][name="message"][data-attribute="value"]) {}', description: ':not(): greater than max classes, inside', - message: messages.expected('[type="text"][name="message"][data-attribute="value"]', 2), + message: messages.expected(':not([type="text"][name="message"][data-attribute="value"])', 2), line: 1, - column: 6, + column: 1, + endLine: 1, + endColumn: 60, }, { code: ':not([type="text"][name="message"][disabled]) {}', description: ':not(): greater than max classes, inside', - message: messages.expected('[type="text"][name="message"][disabled]', 2), + message: messages.expected(':not([type="text"][name="message"][disabled])', 2), line: 1, - column: 6, + column: 1, + endLine: 1, + endColumn: 46, }, { code: '[type="text"][name="message"][data-attribute="value"] :not([data-attribute-2="value"]) {}', @@ -210,6 +224,8 @@ testRule({ ), line: 1, column: 1, + endLine: 1, + endColumn: 87, }, { code: '[type="text"][name="message"][data-attribute="value"]:not([disabled]) {}', @@ -220,16 +236,17 @@ testRule({ ), line: 1, column: 1, + endLine: 1, + endColumn: 70, }, { code: '[type="text"] { &:hover > [data-attribute="value"][data-attribute-2="value"] {} }', description: 'nested selectors: greater than max classes', - message: messages.expected( - '[type="text"]:hover > [data-attribute="value"][data-attribute-2="value"]', - 2, - ), + message: messages.expected('&:hover > [data-attribute="value"][data-attribute-2="value"]', 2), line: 1, column: 17, + endLine: 1, + endColumn: 77, }, ], }); @@ -262,6 +279,8 @@ testRule({ message: messages.expected('[type="text"][name="message"]', 0), line: 1, column: 17, + endLine: 1, + endColumn: 46, }, ], }); @@ -299,12 +318,16 @@ testRule({ message: messages.expected('[foo]', 0), line: 1, column: 1, + endLine: 1, + endColumn: 6, }, { code: '[not-my-attr] {}', message: messages.expected('[not-my-attr]', 0), line: 1, column: 1, + endLine: 1, + endColumn: 14, }, ], }); @@ -325,6 +348,8 @@ testRule({ message: messages.expected('[foo]', 0), line: 1, column: 1, + endLine: 1, + endColumn: 6, }, ], }); diff --git a/lib/rules/selector-max-attribute/index.cjs b/lib/rules/selector-max-attribute/index.cjs index 67ded4b67b..27b852cf46 100644 --- a/lib/rules/selector-max-attribute/index.cjs +++ b/lib/rules/selector-max-attribute/index.cjs @@ -2,13 +2,13 @@ // please instead edit the ESM counterpart and rebuild with Rollup (npm run build). 'use strict'; -const resolvedNestedSelector = require('postcss-resolve-nested-selector'); const validateTypes = require('../../utils/validateTypes.cjs'); +const flattenNestedSelectorsForRule = require('../../utils/flattenNestedSelectorsForRule.cjs'); const isContextFunctionalPseudoClass = require('../../utils/isContextFunctionalPseudoClass.cjs'); const isNonNegativeInteger = require('../../utils/isNonNegativeInteger.cjs'); const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule.cjs'); +const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector.cjs'); const optionsMatches = require('../../utils/optionsMatches.cjs'); -const parseSelector = require('../../utils/parseSelector.cjs'); const report = require('../../utils/report.cjs'); const ruleMessages = require('../../utils/ruleMessages.cjs'); const validateOptions = require('../../utils/validateOptions.cjs'); @@ -50,14 +50,15 @@ const rule = (primary, secondaryOptions) => { } /** + * @param {import('postcss-selector-parser').Container} resolvedSelectorNode * @param {import('postcss-selector-parser').Container} selectorNode * @param {import('postcss').Rule} ruleNode */ - function checkSelector(selectorNode, ruleNode) { - const count = selectorNode.reduce((total, childNode) => { + function checkSelector(resolvedSelectorNode, selectorNode, ruleNode) { + const count = resolvedSelectorNode.reduce((total, childNode) => { // Only traverse inside actual selectors and context functional pseudo-classes if (childNode.type === 'selector' || isContextFunctionalPseudoClass(childNode)) { - checkSelector(childNode, ruleNode); + checkSelector(childNode, selectorNode, ruleNode); } if (childNode.type !== 'attribute') { @@ -76,31 +77,31 @@ const rule = (primary, secondaryOptions) => { }, 0); if (selectorNode.type !== 'root' && selectorNode.type !== 'pseudo' && count > primary) { - const selector = selectorNode.toString(); + const index = selectorNode.first?.sourceIndex ?? 0; + const selectorStr = selectorNode.toString().trim(); report({ ruleName, result, node: ruleNode, message: messages.expected, - messageArgs: [selector, primary], - word: selector, + messageArgs: [selectorStr, primary], + index, + endIndex: index + selectorStr.length, }); } } root.walkRules((ruleNode) => { - if (!isStandardSyntaxRule(ruleNode)) { - return; - } + if (!isStandardSyntaxRule(ruleNode)) return; - for (const selector of ruleNode.selectors) { - for (const resolvedSelector of resolvedNestedSelector(selector, ruleNode)) { - parseSelector(resolvedSelector, result, ruleNode, (container) => - checkSelector(container, ruleNode), - ); - } - } + if (!isStandardSyntaxSelector(ruleNode.selector)) return; + + flattenNestedSelectorsForRule(ruleNode, result).forEach(({ selector, resolvedSelectors }) => { + resolvedSelectors.forEach((resolvedSelector) => { + checkSelector(resolvedSelector, selector, ruleNode); + }); + }); }); }; }; diff --git a/lib/rules/selector-max-attribute/index.mjs b/lib/rules/selector-max-attribute/index.mjs index 70d29180cf..33d1b2d708 100644 --- a/lib/rules/selector-max-attribute/index.mjs +++ b/lib/rules/selector-max-attribute/index.mjs @@ -1,11 +1,10 @@ -import resolvedNestedSelector from 'postcss-resolve-nested-selector'; - import { isRegExp, isString } from '../../utils/validateTypes.mjs'; +import flattenNestedSelectorsForRule from '../../utils/flattenNestedSelectorsForRule.mjs'; import isContextFunctionalPseudoClass from '../../utils/isContextFunctionalPseudoClass.mjs'; import isNonNegativeInteger from '../../utils/isNonNegativeInteger.mjs'; import isStandardSyntaxRule from '../../utils/isStandardSyntaxRule.mjs'; +import isStandardSyntaxSelector from '../../utils/isStandardSyntaxSelector.mjs'; import optionsMatches from '../../utils/optionsMatches.mjs'; -import parseSelector from '../../utils/parseSelector.mjs'; import report from '../../utils/report.mjs'; import ruleMessages from '../../utils/ruleMessages.mjs'; import validateOptions from '../../utils/validateOptions.mjs'; @@ -47,14 +46,15 @@ const rule = (primary, secondaryOptions) => { } /** + * @param {import('postcss-selector-parser').Container} resolvedSelectorNode * @param {import('postcss-selector-parser').Container} selectorNode * @param {import('postcss').Rule} ruleNode */ - function checkSelector(selectorNode, ruleNode) { - const count = selectorNode.reduce((total, childNode) => { + function checkSelector(resolvedSelectorNode, selectorNode, ruleNode) { + const count = resolvedSelectorNode.reduce((total, childNode) => { // Only traverse inside actual selectors and context functional pseudo-classes if (childNode.type === 'selector' || isContextFunctionalPseudoClass(childNode)) { - checkSelector(childNode, ruleNode); + checkSelector(childNode, selectorNode, ruleNode); } if (childNode.type !== 'attribute') { @@ -73,31 +73,31 @@ const rule = (primary, secondaryOptions) => { }, 0); if (selectorNode.type !== 'root' && selectorNode.type !== 'pseudo' && count > primary) { - const selector = selectorNode.toString(); + const index = selectorNode.first?.sourceIndex ?? 0; + const selectorStr = selectorNode.toString().trim(); report({ ruleName, result, node: ruleNode, message: messages.expected, - messageArgs: [selector, primary], - word: selector, + messageArgs: [selectorStr, primary], + index, + endIndex: index + selectorStr.length, }); } } root.walkRules((ruleNode) => { - if (!isStandardSyntaxRule(ruleNode)) { - return; - } + if (!isStandardSyntaxRule(ruleNode)) return; - for (const selector of ruleNode.selectors) { - for (const resolvedSelector of resolvedNestedSelector(selector, ruleNode)) { - parseSelector(resolvedSelector, result, ruleNode, (container) => - checkSelector(container, ruleNode), - ); - } - } + if (!isStandardSyntaxSelector(ruleNode.selector)) return; + + flattenNestedSelectorsForRule(ruleNode, result).forEach(({ selector, resolvedSelectors }) => { + resolvedSelectors.forEach((resolvedSelector) => { + checkSelector(resolvedSelector, selector, ruleNode); + }); + }); }); }; };