From 86593d1119bcdd434b5c8eb6ae6dbfc4c6f75618 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Wed, 26 Oct 2022 22:17:22 -0700 Subject: [PATCH 1/4] Initial stylelint-polaris/coverage rule --- stylelint-polaris/plugins/coverage/index.js | 53 +++++++++++++++++++ .../plugins/coverage/index.test.js | 29 ++++++++++ 2 files changed, 82 insertions(+) create mode 100644 stylelint-polaris/plugins/coverage/index.js create mode 100644 stylelint-polaris/plugins/coverage/index.test.js diff --git a/stylelint-polaris/plugins/coverage/index.js b/stylelint-polaris/plugins/coverage/index.js new file mode 100644 index 00000000000..6334206ca28 --- /dev/null +++ b/stylelint-polaris/plugins/coverage/index.js @@ -0,0 +1,53 @@ +const stylelint = require('stylelint'); + +const ruleName = 'stylelint-polaris/coverage'; + +/** + * @typedef {{ + * [category: string]: import('stylelint').ConfigRules + * }} PrimaryOptions + */ + +module.exports = stylelint.createPlugin( + ruleName, + /** @param {PrimaryOptions} primaryOptions */ + (primaryOptions) => { + return (root, result) => { + if (typeof primaryOptions !== 'object' && primaryOptions !== null) return; + + for (const [categoryName, categoryConfigRules] of Object.entries( + primaryOptions, + )) { + for (const [categoryRuleName, categoryRuleSettings] of Object.entries( + categoryConfigRules, + )) { + stylelint.utils.checkAgainstRule( + { + ruleName: categoryRuleName, + ruleSettings: categoryRuleSettings, + root, + }, + (warning) => { + const coverageRuleName = `${ruleName}/${categoryName}`; + const coverageMessage = warning.text.replace( + categoryRuleName, + coverageRuleName, + ); + + stylelint.utils.report({ + message: coverageMessage, + ruleName: coverageRuleName, + result, + node: warning.node, + line: warning.line, + column: warning.column, + endLine: warning.endLine, + endColumn: warning.endColumn, + }); + }, + ); + } + } + }; + }, +); diff --git a/stylelint-polaris/plugins/coverage/index.test.js b/stylelint-polaris/plugins/coverage/index.test.js new file mode 100644 index 00000000000..4f8973f0d35 --- /dev/null +++ b/stylelint-polaris/plugins/coverage/index.test.js @@ -0,0 +1,29 @@ +const {ruleName} = require('.'); + +const config = { + motion: { + 'at-rule-disallowed-list': [['keyframes'], {severity: 'warning'}], + }, +}; + +testRule({ + ruleName, + plugins: [__dirname], + config, + customSyntax: 'postcss-scss', + accept: [ + { + code: '@media (min-width: 320px) {}', + description: 'Uses allowed at-rule', + }, + ], + + reject: [ + { + code: '@keyframes foo {}', + description: 'Uses disallowed at-rule', + message: + 'Unexpected at-rule "keyframes" (stylelint-polaris/coverage/motion)', + }, + ], +}); From 897f5ea2fb198ae29c8cdbd0caedf8e0c4390920 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Thu, 27 Oct 2022 09:42:35 -0700 Subject: [PATCH 2/4] Add changeset entry --- .changeset/weak-islands-wait.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/weak-islands-wait.md diff --git a/.changeset/weak-islands-wait.md b/.changeset/weak-islands-wait.md new file mode 100644 index 00000000000..041545e55c9 --- /dev/null +++ b/.changeset/weak-islands-wait.md @@ -0,0 +1,5 @@ +--- +'@shopify/stylelint-polaris': minor +--- + +Add `stylelint-polaris/coverage` rule From b36cd5aa2c315eb851419366eb94091ba32f2115 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Mon, 31 Oct 2022 09:08:21 -0700 Subject: [PATCH 3/4] Move initialization logic out of the file processor callback --- stylelint-polaris/plugins/coverage/index.js | 66 +++++++++++---------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/stylelint-polaris/plugins/coverage/index.js b/stylelint-polaris/plugins/coverage/index.js index 6334206ca28..02479234484 100644 --- a/stylelint-polaris/plugins/coverage/index.js +++ b/stylelint-polaris/plugins/coverage/index.js @@ -12,41 +12,43 @@ module.exports = stylelint.createPlugin( ruleName, /** @param {PrimaryOptions} primaryOptions */ (primaryOptions) => { - return (root, result) => { - if (typeof primaryOptions !== 'object' && primaryOptions !== null) return; + if (typeof primaryOptions !== 'object' && primaryOptions !== null) return; + + const rules = []; - for (const [categoryName, categoryConfigRules] of Object.entries( - primaryOptions, + for (const [categoryName, categoryConfigRules] of Object.entries( + primaryOptions, + )) { + for (const [categoryRuleName, categoryRuleSettings] of Object.entries( + categoryConfigRules, )) { - for (const [categoryRuleName, categoryRuleSettings] of Object.entries( - categoryConfigRules, - )) { - stylelint.utils.checkAgainstRule( - { - ruleName: categoryRuleName, - ruleSettings: categoryRuleSettings, - root, - }, - (warning) => { - const coverageRuleName = `${ruleName}/${categoryName}`; - const coverageMessage = warning.text.replace( - categoryRuleName, - coverageRuleName, - ); + rules.push({ + categoryRuleName, + categoryRuleSettings, + coverageRuleName: `${ruleName}/${categoryName}`, + }); + } + } + + return (root, result) => { + for (const rule of rules) { + const {categoryRuleName, categoryRuleSettings, coverageRuleName} = rule; - stylelint.utils.report({ - message: coverageMessage, - ruleName: coverageRuleName, - result, - node: warning.node, - line: warning.line, - column: warning.column, - endLine: warning.endLine, - endColumn: warning.endColumn, - }); - }, - ); - } + stylelint.utils.checkAgainstRule( + { + ruleName: categoryRuleName, + ruleSettings: categoryRuleSettings, + root, + }, + (warning) => { + stylelint.utils.report({ + result, + node: warning.node, + ruleName: coverageRuleName, + message: warning.text.replace(categoryRuleName, coverageRuleName), + }); + }, + ); } }; }, From 669e830ccb311cd93a86a3e1dd3f076b99968486 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Mon, 31 Oct 2022 11:23:05 -0700 Subject: [PATCH 4/4] Add primaryOptions validation --- stylelint-polaris/plugins/coverage/index.js | 49 ++++++++++++++------- stylelint-polaris/utils/index.js | 12 +++++ 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/stylelint-polaris/plugins/coverage/index.js b/stylelint-polaris/plugins/coverage/index.js index 02479234484..e755dc125a5 100644 --- a/stylelint-polaris/plugins/coverage/index.js +++ b/stylelint-polaris/plugins/coverage/index.js @@ -1,5 +1,7 @@ const stylelint = require('stylelint'); +const {isObject} = require('../../utils'); + const ruleName = 'stylelint-polaris/coverage'; /** @@ -12,25 +14,28 @@ module.exports = stylelint.createPlugin( ruleName, /** @param {PrimaryOptions} primaryOptions */ (primaryOptions) => { - if (typeof primaryOptions !== 'object' && primaryOptions !== null) return; - - const rules = []; - - for (const [categoryName, categoryConfigRules] of Object.entries( - primaryOptions, - )) { - for (const [categoryRuleName, categoryRuleSettings] of Object.entries( - categoryConfigRules, - )) { - rules.push({ - categoryRuleName, - categoryRuleSettings, - coverageRuleName: `${ruleName}/${categoryName}`, - }); - } - } + const isPrimaryOptionsValid = validatePrimaryOptions(primaryOptions); + + const rules = !isPrimaryOptionsValid + ? [] + : Object.entries(primaryOptions).flatMap( + ([categoryName, categoryConfigRules]) => + Object.entries(categoryConfigRules).map( + ([categoryRuleName, categoryRuleSettings]) => ({ + categoryRuleName, + categoryRuleSettings, + coverageRuleName: `${ruleName}/${categoryName}`, + }), + ), + ); return (root, result) => { + const validOptions = stylelint.utils.validateOptions(result, ruleName, { + actual: isPrimaryOptionsValid, + }); + + if (!validOptions) return; + for (const rule of rules) { const {categoryRuleName, categoryRuleSettings, coverageRuleName} = rule; @@ -53,3 +58,13 @@ module.exports = stylelint.createPlugin( }; }, ); + +function validatePrimaryOptions(primaryOptions) { + if (!isObject(primaryOptions)) return false; + + for (const categoryConfigRules of Object.values(primaryOptions)) { + if (!isObject(categoryConfigRules)) return false; + } + + return true; +} diff --git a/stylelint-polaris/utils/index.js b/stylelint-polaris/utils/index.js index de77d65c40d..25eb90bccfd 100644 --- a/stylelint-polaris/utils/index.js +++ b/stylelint-polaris/utils/index.js @@ -181,6 +181,17 @@ function isNumber(value) { return typeof value === 'number' || value instanceof Number; } +/** + * Checks if the value is an object and not an array or null. + * https://github.com/jonschlinkert/isobject/blob/15d5d58ea9fbc632dffd52917ac6791cd92251ab/index.js#L9 + * @param {unknown} value + */ +function isObject(value) { + return ( + value != null && typeof value === 'object' && Array.isArray(value) === false + ); +} + /** * Checks if the value is a RegExp object. * @param {unknown} value @@ -212,6 +223,7 @@ module.exports.hasScssInterpolation = hasScssInterpolation; module.exports.isBoolean = isBoolean; module.exports.isCustomProperty = isCustomProperty; module.exports.isNumber = isNumber; +module.exports.isObject = isObject; module.exports.isRegExp = isRegExp; module.exports.isScssInterpolation = isScssInterpolation; module.exports.isString = isString;