From dc767f7f98b2246c4b9fe440a4abab0fb3c44474 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Thu, 13 Apr 2023 16:52:07 -0700 Subject: [PATCH 1/4] Initial custom-property-disallowed-list plugin --- .../custom-property-allowed-list/index.js | 50 ++--- .../index.test.js | 11 + .../custom-property-disallowed-list/README.md | 36 ++++ .../custom-property-disallowed-list/index.js | 189 ++++++++++++++++++ .../index.test.js | 108 ++++++++++ 5 files changed, 365 insertions(+), 29 deletions(-) create mode 100644 stylelint-polaris/plugins/custom-property-disallowed-list/README.md create mode 100644 stylelint-polaris/plugins/custom-property-disallowed-list/index.js create mode 100644 stylelint-polaris/plugins/custom-property-disallowed-list/index.test.js diff --git a/stylelint-polaris/plugins/custom-property-allowed-list/index.js b/stylelint-polaris/plugins/custom-property-allowed-list/index.js index b42adc4ee79..fc4a8da026f 100644 --- a/stylelint-polaris/plugins/custom-property-allowed-list/index.js +++ b/stylelint-polaris/plugins/custom-property-allowed-list/index.js @@ -16,18 +16,23 @@ const messages = stylelint.utils.ruleMessages(ruleName, { * @type {stylelint.RuleMessageFunc} */ rejected: (prop, value, prefix, isInvalidProp, invalidValues) => { - if (isInvalidProp) { - return `Unexpected prefix "${prefix}" for defined custom property "${prop}" - Properties with prefixes "--p-" or "--pc-" cannot be defined outside of Polaris"`; - } - - if (invalidValues) { - const plural = invalidValues.length > 1; - return `Unexpected value "${value}" for property "${prop}" - Token${ - plural ? 's' : '' - } ${invalidValues.map((token) => `"${token}"`).join(', ')} ${ - plural ? 'are' : 'is' - } either private or ${plural ? 'do' : 'does'} not exist`; - } + const invalidPropertyMessage = isInvalidProp + ? `Unexpected prefix "${prefix}" for defined custom property "${prop}" - Properties with prefixes "--p-" or "--pc-" cannot be defined outside of Polaris"` + : null; + + const plural = invalidValues?.length > 1; + + const invalidValueMessage = invalidValues + ? `Unexpected value "${value}" for property "${prop}" - Token${ + plural ? 's' : '' + } ${invalidValues.map((token) => `"${token}"`).join(', ')} ${ + plural ? 'are' : 'is' + } either private or ${plural ? 'do' : 'does'} not exist` + : null; + + return [invalidPropertyMessage, invalidValueMessage] + .filter(Boolean) + .join('. '); }, }); @@ -82,7 +87,7 @@ const {rule} = stylelint.createPlugin( value, ); - if (isInvalidProperty) { + if (isInvalidProperty || invalidValues) { stylelint.utils.report({ message: messages.rejected( prop, @@ -96,21 +101,6 @@ const {rule} = stylelint.createPlugin( ruleName, }); } - - if (invalidValues) { - stylelint.utils.report({ - message: messages.rejected( - prop, - value, - undefined, - isInvalidProperty, - invalidValues, - ), - node: decl, - result, - ruleName, - }); - } }); }; }, @@ -122,7 +112,9 @@ const {rule} = stylelint.createPlugin( * @returns {string} */ function getCustomPropertyPrefix(property) { - return `--${property.split('-')[2]}-`; + return isCustomProperty(property) + ? `--${property.split('-')[2]}-` + : undefined; } /** diff --git a/stylelint-polaris/plugins/custom-property-allowed-list/index.test.js b/stylelint-polaris/plugins/custom-property-allowed-list/index.test.js index 8256e93d81e..0b1eb40fc93 100644 --- a/stylelint-polaris/plugins/custom-property-allowed-list/index.test.js +++ b/stylelint-polaris/plugins/custom-property-allowed-list/index.test.js @@ -96,5 +96,16 @@ testRule({ endLine: 1, endColumn: 28, }, + { + code: '.a { --pc-foo: var(--pc-bar); }', + description: 'Using --pc- prefixed tokens is disallowed', + message: messages.rejected('--pc-foo', 'var(--pc-bar)', '--pc-', true, [ + '--pc-bar', + ]), + line: 1, + column: 6, + endLine: 1, + endColumn: 30, + }, ], }); diff --git a/stylelint-polaris/plugins/custom-property-disallowed-list/README.md b/stylelint-polaris/plugins/custom-property-disallowed-list/README.md new file mode 100644 index 00000000000..79391769ee9 --- /dev/null +++ b/stylelint-polaris/plugins/custom-property-disallowed-list/README.md @@ -0,0 +1,36 @@ +## Custom property disallowed list plugin + +### Options: + +```ts +interface PrimaryOptions { + /** + * A list of regular expressions or string literals that match disallowed custom properties. + */ + disallowedProperties?: (string | RegExp)[]; + /** + * A map of properties and their disallowed custom properties represented as a list + * of regular expressions or string literals. + */ + disallowedValues?: {[property: string]: (string | RegExp)[]}; +} +``` + +### Configuration + +```js +const stylelintConfig = { + rules: { + 'polaris/custom-property-disallowed-list': { + disallowedProperties: ['--p-foo'], + disallowedValues: { + width: ['--p-foo', /--p-bar/ /* etc... */], + '/.+/': ['--p-foo', /--p-bar/ /* etc... */], + }, + }, + }, +}; +``` + +> Note: Property keys for `disallowedValues` are evaluated in order. Please ensure that you +> order your property keys from most specific to least specific. diff --git a/stylelint-polaris/plugins/custom-property-disallowed-list/index.js b/stylelint-polaris/plugins/custom-property-disallowed-list/index.js new file mode 100644 index 00000000000..5b52d898c06 --- /dev/null +++ b/stylelint-polaris/plugins/custom-property-disallowed-list/index.js @@ -0,0 +1,189 @@ +const stylelint = require('stylelint'); +const valueParser = require('postcss-value-parser'); + +const { + vendorUnprefixed, + matchesStringOrRegExp, + isCustomProperty, + isRegExp, + isString, +} = require('../../utils'); + +const ruleName = 'polaris/custom-property-disallowed-list'; + +const messages = stylelint.utils.ruleMessages(ruleName, { + /** + * @type {stylelint.RuleMessageFunc} + */ + rejected: (prop, value, isInvalidProp, invalidValues) => { + const invalidPropertyMessage = isInvalidProp + ? `Unexpected custom property definition "${prop}"` + : null; + + const plural = (invalidValues?.length ?? 0) > 1; + + const invalidValueMessage = invalidValues + ? `Unexpected value${plural ? 's' : ''} "${invalidValues.join( + ', ', + )}" for property "${prop}"` + : null; + + const message = [invalidPropertyMessage, invalidValueMessage] + .filter(Boolean) + .join('. '); + + return message; + }, +}); + +/** @typedef {(string | RegExp)[]} DisallowedPatterns */ + +/** + * @typedef {Object} PrimaryOptions + * @property {DisallowedPatterns} [disallowedProperties] + * @property {{[property: string]: DisallowedPatterns}} [disallowedValues] + */ + +const {rule} = stylelint.createPlugin( + ruleName, + /** @param {PrimaryOptions} primary */ + (primary) => { + return (root, result) => { + const validOptions = stylelint.utils.validateOptions( + result, + ruleName, + { + actual: primary.disallowedProperties, + possible: isDisallowedPatterns, + optional: true, + }, + { + actual: primary.disallowedValues, + possible: validateDisallowedValuesOption, + optional: true, + }, + ); + + if (!validOptions) { + return; + } + + const {disallowedProperties = [], disallowedValues = {}} = primary; + + root.walkDecls((decl) => { + const prop = decl.prop; + const value = decl.value; + + const isInvalidProperty = isInvalidCustomProperty( + disallowedProperties, + prop, + ); + + const invalidValues = getInvalidCustomPropertyValues( + disallowedValues, + prop, + value, + ); + + if (isInvalidProperty || invalidValues) { + stylelint.utils.report({ + message: messages.rejected( + prop, + value, + isInvalidProperty, + invalidValues, + ), + node: decl, + result, + ruleName, + }); + } + }); + }; + }, +); + +/** + * @param {NonNullable} disallowedProperties + * @param {string} property + */ +function isInvalidCustomProperty(disallowedProperties, property) { + if (!isCustomProperty(property)) return false; + + const isInvalid = disallowedProperties.some((disallowedProperty) => { + return matchesStringOrRegExp(property, disallowedProperty); + }); + + return isInvalid; +} + +/** + * @param {NonNullable} disallowedValues + * @param {string} prop + * @param {string} value + * @returns {string[] | undefined} + */ +function getInvalidCustomPropertyValues(disallowedValues, prop, value) { + const invalidValues = []; + + const unprefixedProp = vendorUnprefixed(prop); + + /** Property key for the disallowed values option */ + const propKey = Object.keys(disallowedValues).find((propIdentifier) => + matchesStringOrRegExp(unprefixedProp, propIdentifier), + ); + + if (!propKey) return; + + const disallowedPatterns = disallowedValues[propKey]; + + if (!disallowedPatterns.length) return; + + valueParser(value).walk((node) => { + if ( + node.type === 'word' && + isCustomProperty(node.value) && + matchesStringOrRegExp(node.value, disallowedPatterns) + ) { + invalidValues.push(node.value); + } + }); + + if (invalidValues.length > 0) return invalidValues; +} + +module.exports = { + rule, + ruleName, + messages, +}; + +/** + * Validates the input is an array of String or RegExp. + * @param {unknown} disallowedPatterns + * @returns {disallowedPatterns is DisallowedPatterns} + */ +function isDisallowedPatterns(disallowedPatterns) { + if (!Array.isArray(disallowedPatterns)) return false; + + for (const pattern of disallowedPatterns) { + if (!(isString(pattern) || isRegExp(pattern))) return false; + } + + return true; +} + +/** + * @param {unknown} option - `primary.disallowedValues` option. + */ +function validateDisallowedValuesOption(option) { + if (typeof option !== 'object' || option === null) return false; + + for (const [property, disallowedPatterns] of Object.entries(option)) { + if (!(isString(property) && isDisallowedPatterns(disallowedPatterns))) { + return false; + } + } + + return true; +} diff --git a/stylelint-polaris/plugins/custom-property-disallowed-list/index.test.js b/stylelint-polaris/plugins/custom-property-disallowed-list/index.test.js new file mode 100644 index 00000000000..9cf1fb2bbed --- /dev/null +++ b/stylelint-polaris/plugins/custom-property-disallowed-list/index.test.js @@ -0,0 +1,108 @@ +const {messages, ruleName} = require('.'); + +const config = [ + { + disallowedProperties: ['--p-foo', /--p-bar/], + disallowedValues: { + '/.+/': ['--p-foo', /--p-bar/], + }, + }, +]; + +testRule({ + ruleName, + plugins: [__dirname], + config, + accept: [ + { + code: '.a { --p-foo-bar: red; }', + description: 'Defining allowed custom property', + }, + { + code: '.a { color: var(--p-foo-bar); }', + description: 'Using allowed custom property value', + }, + { + code: '.a { --p-foo-bar: var(--p-foo-bar); }', + description: 'Using allowed custom property and value', + }, + ], + + reject: [ + { + code: '.a { --p-foo: red; }', + description: 'Defining disallowed custom property (literal)', + message: messages.rejected('--p-foo', 'red', true, undefined), + line: 1, + column: 6, + endLine: 1, + endColumn: 19, + }, + { + code: '.a { --p-bar-foo: red; }', + description: 'Defining disallowed custom property (regex)', + message: messages.rejected('--p-bar-foo', 'red', true, undefined), + line: 1, + column: 6, + endLine: 1, + endColumn: 23, + }, + { + code: '.a { color: var(--p-foo); }', + description: 'Defining disallowed custom property value (literal)', + message: messages.rejected('color', 'var(--p-foo)', false, ['--p-foo']), + line: 1, + column: 6, + endLine: 1, + endColumn: 26, + }, + { + code: '.a { color: var(--p-bar-foo); }', + description: 'Defining disallowed custom property value (regex)', + message: messages.rejected('color', 'var(--p-bar-foo)', false, [ + '--p-bar-foo', + ]), + line: 1, + column: 6, + endLine: 1, + endColumn: 30, + }, + { + code: '.a { --p-foo: var(--p-bar); }', + description: 'Defining disallowed custom property and value', + message: messages.rejected('--p-foo', 'var(--p-bar)', true, ['--p-bar']), + line: 1, + column: 6, + endLine: 1, + endColumn: 28, + }, + { + code: '.a { border: var(--p-foo) solid var(--p-bar); }', + description: 'Defining multiple disallowed custom property values', + message: messages.rejected( + 'border', + 'var(--p-foo) solid var(--p-bar)', + false, + ['--p-foo', '--p-bar'], + ), + line: 1, + column: 6, + endLine: 1, + endColumn: 46, + }, + { + code: '.a { --p-foo: var(--p-foo) solid var(--p-bar); }', + description: 'Defining multiple disallowed custom properties and values', + message: messages.rejected( + '--p-foo', + 'var(--p-foo) solid var(--p-bar)', + true, + ['--p-foo', '--p-bar'], + ), + line: 1, + column: 6, + endLine: 1, + endColumn: 47, + }, + ], +}); From 9b8101ccff43c5ad3c91292ca8d1f5629b634a73 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Fri, 14 Apr 2023 10:15:21 -0700 Subject: [PATCH 2/4] Update error messages and test descriptions --- .../custom-property-allowed-list/index.js | 4 ++- .../index.test.js | 25 +++++++++++++++---- .../custom-property-disallowed-list/index.js | 6 ++--- .../index.test.js | 10 ++++---- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/stylelint-polaris/plugins/custom-property-allowed-list/index.js b/stylelint-polaris/plugins/custom-property-allowed-list/index.js index fc4a8da026f..99a778e385a 100644 --- a/stylelint-polaris/plugins/custom-property-allowed-list/index.js +++ b/stylelint-polaris/plugins/custom-property-allowed-list/index.js @@ -30,9 +30,11 @@ const messages = stylelint.utils.ruleMessages(ruleName, { } either private or ${plural ? 'do' : 'does'} not exist` : null; - return [invalidPropertyMessage, invalidValueMessage] + const message = [invalidPropertyMessage, invalidValueMessage] .filter(Boolean) .join('. '); + + return message; }, }); diff --git a/stylelint-polaris/plugins/custom-property-allowed-list/index.test.js b/stylelint-polaris/plugins/custom-property-allowed-list/index.test.js index 0b1eb40fc93..1748c827a86 100644 --- a/stylelint-polaris/plugins/custom-property-allowed-list/index.test.js +++ b/stylelint-polaris/plugins/custom-property-allowed-list/index.test.js @@ -97,15 +97,30 @@ testRule({ endColumn: 28, }, { - code: '.a { --pc-foo: var(--pc-bar); }', - description: 'Using --pc- prefixed tokens is disallowed', - message: messages.rejected('--pc-foo', 'var(--pc-bar)', '--pc-', true, [ - '--pc-bar', + code: '.a { --p-foo: var(--p-bar); }', + description: 'Using disallowed --p- prefixed custom property and value', + message: messages.rejected('--p-foo', 'var(--p-bar)', '--p-', true, [ + '--p-bar', ]), line: 1, column: 6, endLine: 1, - endColumn: 30, + endColumn: 28, + }, + { + code: '.a { --p-foo: var(--p-bar) solid var(--p-baz); }', + description: 'Using disallowed --p- prefixed custom property and values', + message: messages.rejected( + '--p-foo', + 'var(--p-bar) solid var(--p-baz)', + '--p-', + true, + ['--p-bar', '--p-baz'], + ), + line: 1, + column: 6, + endLine: 1, + endColumn: 47, }, ], }); diff --git a/stylelint-polaris/plugins/custom-property-disallowed-list/index.js b/stylelint-polaris/plugins/custom-property-disallowed-list/index.js index 5b52d898c06..110b008b88b 100644 --- a/stylelint-polaris/plugins/custom-property-disallowed-list/index.js +++ b/stylelint-polaris/plugins/custom-property-disallowed-list/index.js @@ -23,9 +23,9 @@ const messages = stylelint.utils.ruleMessages(ruleName, { const plural = (invalidValues?.length ?? 0) > 1; const invalidValueMessage = invalidValues - ? `Unexpected value${plural ? 's' : ''} "${invalidValues.join( - ', ', - )}" for property "${prop}"` + ? `Unexpected value${plural ? 's' : ''} ${invalidValues + .map((token) => `"${token}"`) + .join(', ')} for property "${prop}"` : null; const message = [invalidPropertyMessage, invalidValueMessage] diff --git a/stylelint-polaris/plugins/custom-property-disallowed-list/index.test.js b/stylelint-polaris/plugins/custom-property-disallowed-list/index.test.js index 9cf1fb2bbed..c909b677318 100644 --- a/stylelint-polaris/plugins/custom-property-disallowed-list/index.test.js +++ b/stylelint-polaris/plugins/custom-property-disallowed-list/index.test.js @@ -39,9 +39,9 @@ testRule({ endColumn: 19, }, { - code: '.a { --p-bar-foo: red; }', + code: '.a { --p-bar-baz: red; }', description: 'Defining disallowed custom property (regex)', - message: messages.rejected('--p-bar-foo', 'red', true, undefined), + message: messages.rejected('--p-bar-baz', 'red', true, undefined), line: 1, column: 6, endLine: 1, @@ -57,10 +57,10 @@ testRule({ endColumn: 26, }, { - code: '.a { color: var(--p-bar-foo); }', + code: '.a { color: var(--p-bar-baz); }', description: 'Defining disallowed custom property value (regex)', - message: messages.rejected('color', 'var(--p-bar-foo)', false, [ - '--p-bar-foo', + message: messages.rejected('color', 'var(--p-bar-baz)', false, [ + '--p-bar-baz', ]), line: 1, column: 6, From 2453271e5546ae52c72be41599d0313e90a4ef8c Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Fri, 14 Apr 2023 10:41:25 -0700 Subject: [PATCH 3/4] Improve test descriptions --- .../custom-property-disallowed-list/index.js | 2 +- .../custom-property-disallowed-list/index.test.js | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/stylelint-polaris/plugins/custom-property-disallowed-list/index.js b/stylelint-polaris/plugins/custom-property-disallowed-list/index.js index 110b008b88b..24cd916c618 100644 --- a/stylelint-polaris/plugins/custom-property-disallowed-list/index.js +++ b/stylelint-polaris/plugins/custom-property-disallowed-list/index.js @@ -20,7 +20,7 @@ const messages = stylelint.utils.ruleMessages(ruleName, { ? `Unexpected custom property definition "${prop}"` : null; - const plural = (invalidValues?.length ?? 0) > 1; + const plural = invalidValues?.length > 1; const invalidValueMessage = invalidValues ? `Unexpected value${plural ? 's' : ''} ${invalidValues diff --git a/stylelint-polaris/plugins/custom-property-disallowed-list/index.test.js b/stylelint-polaris/plugins/custom-property-disallowed-list/index.test.js index c909b677318..e9d647382e8 100644 --- a/stylelint-polaris/plugins/custom-property-disallowed-list/index.test.js +++ b/stylelint-polaris/plugins/custom-property-disallowed-list/index.test.js @@ -20,18 +20,18 @@ testRule({ }, { code: '.a { color: var(--p-foo-bar); }', - description: 'Using allowed custom property value', + description: 'Defining allowed custom property value', }, { code: '.a { --p-foo-bar: var(--p-foo-bar); }', - description: 'Using allowed custom property and value', + description: 'Defining allowed custom property and value', }, ], reject: [ { code: '.a { --p-foo: red; }', - description: 'Defining disallowed custom property (literal)', + description: 'Defining disallowed custom property (literal match)', message: messages.rejected('--p-foo', 'red', true, undefined), line: 1, column: 6, @@ -40,7 +40,7 @@ testRule({ }, { code: '.a { --p-bar-baz: red; }', - description: 'Defining disallowed custom property (regex)', + description: 'Defining disallowed custom property (regex match)', message: messages.rejected('--p-bar-baz', 'red', true, undefined), line: 1, column: 6, @@ -49,7 +49,7 @@ testRule({ }, { code: '.a { color: var(--p-foo); }', - description: 'Defining disallowed custom property value (literal)', + description: 'Defining disallowed custom property value (literal match)', message: messages.rejected('color', 'var(--p-foo)', false, ['--p-foo']), line: 1, column: 6, @@ -58,7 +58,7 @@ testRule({ }, { code: '.a { color: var(--p-bar-baz); }', - description: 'Defining disallowed custom property value (regex)', + description: 'Defining disallowed custom property value (regex match)', message: messages.rejected('color', 'var(--p-bar-baz)', false, [ '--p-bar-baz', ]), @@ -92,7 +92,7 @@ testRule({ }, { code: '.a { --p-foo: var(--p-foo) solid var(--p-bar); }', - description: 'Defining multiple disallowed custom properties and values', + description: 'Defining multiple disallowed custom property and values', message: messages.rejected( '--p-foo', 'var(--p-foo) solid var(--p-bar)', From 1c933f1c2736c79e014084012c77e31fa8b3502d Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Fri, 14 Apr 2023 11:02:59 -0700 Subject: [PATCH 4/4] Add changeset entry --- .changeset/sour-parrots-cry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sour-parrots-cry.md diff --git a/.changeset/sour-parrots-cry.md b/.changeset/sour-parrots-cry.md new file mode 100644 index 00000000000..5a13faa7633 --- /dev/null +++ b/.changeset/sour-parrots-cry.md @@ -0,0 +1,5 @@ +--- +'@shopify/stylelint-polaris': minor +--- + +Added `custom-property-disallowed-list` rule