From 16871985d9badd02965e33176a07342e0efd61d9 Mon Sep 17 00:00:00 2001 From: Matt Wang Date: Mon, 22 Jun 2020 07:59:30 -0700 Subject: [PATCH 1/4] Add ignoreContextFunctionalPseudoClasses to selector-max-id (#4835) Co-authored-by: Richard Hallows --- lib/rules/selector-max-id/README.md | 29 +++++++++++++ lib/rules/selector-max-id/__tests__/index.js | 25 +++++++++++ lib/rules/selector-max-id/index.js | 44 +++++++++++++++----- 3 files changed, 88 insertions(+), 10 deletions(-) diff --git a/lib/rules/selector-max-id/README.md b/lib/rules/selector-max-id/README.md index 8ad12f1a62..301fe42f45 100644 --- a/lib/rules/selector-max-id/README.md +++ b/lib/rules/selector-max-id/README.md @@ -74,3 +74,32 @@ The following patterns are _not_ considered violations: /* `#bar` is inside `:not()`, so it is evaluated separately */ #foo #bar:not(#baz) {} ``` + +### `ignoreContextFunctionalPseudoClasses: ["/regex/", /regex/, "non-regex"]` + +Ignore selectors inside of specified [functional pseudo-classes](https://drafts.csswg.org/selectors-4/#pseudo-classes) that provide [evaluation contexts](https://drafts.csswg.org/selectors-4/#specificity-rules). + +Given: + +```js +[":not", /^:(h|H)as$/]; +``` + +The following patterns are considered violations: + + +```css +a:matches(#foo) {} +``` + +While the following patters are _not_ considered violations: + + +```css +a:not(#foo) {} +``` + + +```css +a:has(#foo) {} +``` diff --git a/lib/rules/selector-max-id/__tests__/index.js b/lib/rules/selector-max-id/__tests__/index.js index cec638d00d..6647ccc659 100644 --- a/lib/rules/selector-max-id/__tests__/index.js +++ b/lib/rules/selector-max-id/__tests__/index.js @@ -459,3 +459,28 @@ testRule({ }, ], }); + +testRule({ + ruleName, + config: [0, { ignoreContextFunctionalPseudoClasses: [':not', /^:(h|H)as$/] }], + + accept: [ + { + code: 'a:not(#foo) {}', + description: 'selector within ignored pseudo-class (string input)', + }, + { + code: 'a:has(#foo) {}', + description: 'selector within ignored pseudo-class (regex input)', + }, + ], + reject: [ + { + code: 'a:matches(#foo) {}', + description: 'selector within non-ignored pseudo class', + message: messages.expected('#foo', 0), + line: 1, + column: 11, + }, + ], +}); diff --git a/lib/rules/selector-max-id/index.js b/lib/rules/selector-max-id/index.js index 0114f7b4fb..bf608bb3dc 100644 --- a/lib/rules/selector-max-id/index.js +++ b/lib/rules/selector-max-id/index.js @@ -2,8 +2,10 @@ 'use strict'; +const _ = require('lodash'); const isLogicalCombination = require('../../utils/isLogicalCombination'); const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule'); +const optionsMatches = require('../../utils/optionsMatches'); const parseSelector = require('../../utils/parseSelector'); const report = require('../../utils/report'); const resolvedNestedSelector = require('postcss-resolve-nested-selector'); @@ -17,16 +19,27 @@ const messages = ruleMessages(ruleName, { `Expected "${selector}" to have no more than ${max} ID ${max === 1 ? 'selector' : 'selectors'}`, }); -function rule(max) { +function rule(max, options) { return (root, result) => { - const validOptions = validateOptions(result, ruleName, { - actual: max, - possible: [ - function (max) { - return typeof max === 'number' && max >= 0; + const validOptions = validateOptions( + result, + ruleName, + { + actual: max, + possible: [ + function (max) { + return typeof max === 'number' && max >= 0; + }, + ], + }, + { + actual: options, + possible: { + ignoreContextFunctionalPseudoClasses: [_.isString, _.isRegExp], }, - ], - }); + optional: true, + }, + ); if (!validOptions) { return; @@ -34,8 +47,12 @@ function rule(max) { function checkSelector(selectorNode, ruleNode) { const count = selectorNode.reduce((total, childNode) => { - // Only traverse inside actual selectors and logical combinations - if (childNode.type === 'selector' || isLogicalCombination(childNode)) { + // Only traverse inside actual selectors and logical combinations that are not part of ignored functional pseudo-classes + if ( + childNode.type === 'selector' || + (isLogicalCombination(childNode) && + !isIgnoredContextFunctionalPseudoClass(childNode, options)) + ) { checkSelector(childNode, ruleNode); } @@ -53,6 +70,13 @@ function rule(max) { } } + function isIgnoredContextFunctionalPseudoClass(node, options) { + return ( + node.type === 'pseudo' && + optionsMatches(options, 'ignoreContextFunctionalPseudoClasses', node.value) + ); + } + root.walkRules((ruleNode) => { if (!isStandardSyntaxRule(ruleNode)) { return; From 32abe3b8ce7655397f12281f636371bb67020b81 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Mon, 22 Jun 2020 16:01:02 +0100 Subject: [PATCH 2/4] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e7cb94090..06e05a7479 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project are documented in this file. +## Head + +- Added: `ignoreContextFunctionalPseudoClasses` to `selector-max-id` ([#4835](https://github.com/stylelint/stylelint/pull/4835)) + ## 13.6.1 - Fixed: `max-empty-lines` TypeError from inline comment with autofix and sugarss syntax ([#4821](https://github.com/stylelint/stylelint/pull/4821)). From 56a01089aece4608263be8013250c16a6236a82c Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Mon, 22 Jun 2020 16:02:18 +0100 Subject: [PATCH 3/4] A small refactor (#4837) --- lib/getPostcssResult.js | 56 +++++++++++++++++++++++--------------- lib/standalone.js | 44 ++++++++++++++++++++---------- types/stylelint/index.d.ts | 8 ++++-- 3 files changed, 69 insertions(+), 39 deletions(-) diff --git a/lib/getPostcssResult.js b/lib/getPostcssResult.js index c8926b9f0c..5228f862bd 100644 --- a/lib/getPostcssResult.js +++ b/lib/getPostcssResult.js @@ -5,16 +5,19 @@ const LazyResult = require('postcss/lib/lazy-result'); const postcss = require('postcss'); const syntaxes = require('./syntaxes'); +/** @typedef {import('postcss').Result} Result */ +/** @typedef {import('postcss').Syntax} Syntax */ +/** @typedef {import('stylelint').CustomSyntax} CustomSyntax */ +/** @typedef {import('stylelint').GetPostcssOptions} GetPostcssOptions */ /** @typedef {import('stylelint').StylelintInternalApi} StylelintInternalApi */ -/** @typedef {{parse: any, stringify: any}} Syntax */ const postcssProcessor = postcss(); /** * @param {StylelintInternalApi} stylelint - * @param {import('stylelint').GetPostcssOptions} options + * @param {GetPostcssOptions} options * - * @returns {Promise} + * @returns {Promise} */ module.exports = function (stylelint, options = {}) { const cached = options.filePath ? stylelint._postcssResultCache.get(options.filePath) : undefined; @@ -40,25 +43,7 @@ module.exports = function (stylelint, options = {}) { let syntax = null; if (stylelint._options.customSyntax) { - try { - // TODO TYPES determine which type has customSyntax - const customSyntax = /** @type {any} */ require(stylelint._options.customSyntax); - - /* - * PostCSS allows for syntaxes that only contain a parser, however, - * it then expects the syntax to be set as the `parser` option rather than `syntax`. - */ - if (!customSyntax.parse) { - syntax = { - parse: customSyntax, - stringify: postcss.stringify, - }; - } else { - syntax = customSyntax; - } - } catch (e) { - throw new Error(`Cannot resolve custom syntax module ${stylelint._options.customSyntax}`); - } + syntax = getCustomSyntax(stylelint._options.customSyntax); } else if (stylelint._options.syntax) { if (stylelint._options.syntax === 'css') { syntax = cssSyntax(stylelint); @@ -125,6 +110,33 @@ module.exports = function (stylelint, options = {}) { }); }; +/** + * @param {CustomSyntax} customSyntax + * @returns {Syntax} + */ +function getCustomSyntax(customSyntax) { + let resolved; + + try { + resolved = require(customSyntax); + } catch (error) { + throw new Error(`Cannot resolve custom syntax module ${customSyntax}`); + } + + /* + * PostCSS allows for syntaxes that only contain a parser, however, + * it then expects the syntax to be set as the `parse` option. + */ + if (!resolved.parse) { + resolved = { + parse: resolved, + stringify: postcss.stringify, + }; + } + + return resolved; +} + /** * @param {string} filePath * @returns {Promise} diff --git a/lib/standalone.js b/lib/standalone.js index b427e00995..34190d312b 100644 --- a/lib/standalone.js +++ b/lib/standalone.js @@ -25,6 +25,7 @@ const writeFileAtomic = require('write-file-atomic'); /** @typedef {import('stylelint').StylelintStandaloneReturnValue} StylelintStandaloneReturnValue */ /** @typedef {import('stylelint').StylelintResult} StylelintResult */ /** @typedef {import('stylelint').Formatter} Formatter */ +/** @typedef {import('stylelint').FormatterIdentifier} FormatterIdentifier */ /** * @param {StylelintStandaloneOptions} options @@ -79,20 +80,10 @@ module.exports = function (options) { /** @type {Formatter} */ let formatterFunction; - if (typeof formatter === 'string') { - formatterFunction = formatters[formatter]; - - if (formatterFunction === undefined) { - return Promise.reject( - new Error( - `You must use a valid formatter option: ${getFormatterOptionsText()} or a function`, - ), - ); - } - } else if (typeof formatter === 'function') { - formatterFunction = formatter; - } else { - formatterFunction = formatters.json; + try { + formatterFunction = getFormatterFunction(formatter); + } catch (error) { + return Promise.reject(error); } const stylelint = createStylelint({ @@ -288,6 +279,31 @@ module.exports = function (options) { }); }; +/** + * @param {FormatterIdentifier | undefined} selected + * @returns {Formatter} + */ +function getFormatterFunction(selected) { + /** @type {Formatter} */ + let formatterFunction; + + if (typeof selected === 'string') { + formatterFunction = formatters[selected]; + + if (formatterFunction === undefined) { + throw new Error( + `You must use a valid formatter option: ${getFormatterOptionsText()} or a function`, + ); + } + } else if (typeof selected === 'function') { + formatterFunction = selected; + } else { + formatterFunction = formatters.json; + } + + return formatterFunction; +} + /** * @param {import('stylelint').StylelintInternalApi} stylelint * @param {any} error diff --git a/types/stylelint/index.d.ts b/types/stylelint/index.d.ts index 39a61017b3..f3f68f9836 100644 --- a/types/stylelint/index.d.ts +++ b/types/stylelint/index.d.ts @@ -90,6 +90,8 @@ declare module 'stylelint' { export type FormatterIdentifier = 'compact' | 'json' | 'string' | 'unix' | 'verbose' | Formatter; + export type CustomSyntax = string; + export type StylelintOptions = { config?: StylelintConfig; configFile?: string; @@ -100,7 +102,7 @@ declare module 'stylelint' { reportInvalidScopeDisables?: boolean; reportNeedlessDisables?: boolean; syntax?: string; - customSyntax?: string; + customSyntax?: CustomSyntax; fix?: boolean; }; @@ -110,7 +112,7 @@ declare module 'stylelint' { filePath?: string; codeProcessors?: Array; syntax?: string; - customSyntax?: string; + customSyntax?: CustomSyntax; }; export type GetLintSourceOptions = GetPostcssOptions & { existingPostcssResult?: Result }; @@ -158,7 +160,7 @@ declare module 'stylelint' { reportInvalidScopeDisables?: boolean; maxWarnings?: number; syntax?: string; - customSyntax?: string; + customSyntax?: CustomSyntax; formatter?: FormatterIdentifier; disableDefaultIgnores?: boolean; fix?: boolean; From e652b3e1ca96237b04420a730882ca065c92a896 Mon Sep 17 00:00:00 2001 From: m-allanson Date: Mon, 22 Jun 2020 18:55:32 +0100 Subject: [PATCH 4/4] Fix changelog formatting --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06e05a7479..45869e889e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,9 @@ All notable changes to this project are documented in this file. -## Head +## Head -- Added: `ignoreContextFunctionalPseudoClasses` to `selector-max-id` ([#4835](https://github.com/stylelint/stylelint/pull/4835)) +- Added: `ignoreContextFunctionalPseudoClasses` to `selector-max-id` ([#4835](https://github.com/stylelint/stylelint/pull/4835)). ## 13.6.1