diff --git a/docs/user-guide/rules/list.md b/docs/user-guide/rules/list.md index 7d6f4ea482..30b5847210 100644 --- a/docs/user-guide/rules/list.md +++ b/docs/user-guide/rules/list.md @@ -41,6 +41,7 @@ Grouped first by the following categories and then by the [_thing_](http://apps. ### Declaration block +- [`declaration-block-no-duplicate-custom-properties`](../../../lib/rules/declaration-block-no-duplicate-custom-properties/README.md): Disallow duplicate custom properties within declaration blocks. - [`declaration-block-no-duplicate-properties`](../../../lib/rules/declaration-block-no-duplicate-properties/README.md): Disallow duplicate properties within declaration blocks. - [`declaration-block-no-shorthand-property-overrides`](../../../lib/rules/declaration-block-no-shorthand-property-overrides/README.md): Disallow shorthand properties that override related longhand properties. diff --git a/lib/rules/declaration-block-no-duplicate-custom-properties/README.md b/lib/rules/declaration-block-no-duplicate-custom-properties/README.md new file mode 100644 index 0000000000..b384b0df0c --- /dev/null +++ b/lib/rules/declaration-block-no-duplicate-custom-properties/README.md @@ -0,0 +1,40 @@ +# declaration-block-no-duplicate-custom-properties + +Disallow duplicate custom properties within declaration blocks. + + +```css +a { --custom-property: pink; --custom-property: orange; } +/** ↑ ↑ + * These duplicated custom properties */ +``` + +This rule is case-sensitive. + +## Options + +### `true` + +The following patterns are considered violations: + + +```css +a { --custom-property: pink; --custom-property: orange; } +``` + + +```css +a { --custom-property: pink; background: orange; --custom-property: orange } +``` + +The following patterns are _not_ considered violations: + + +```css +a { --custom-property: pink; } +``` + + +```css +a { --custom-property: pink; --cUstOm-prOpErtY: orange; } +``` diff --git a/lib/rules/declaration-block-no-duplicate-custom-properties/__tests__/index.js b/lib/rules/declaration-block-no-duplicate-custom-properties/__tests__/index.js new file mode 100644 index 0000000000..8c428a0ea1 --- /dev/null +++ b/lib/rules/declaration-block-no-duplicate-custom-properties/__tests__/index.js @@ -0,0 +1,165 @@ +'use strict'; + +const { messages, ruleName } = require('..'); + +testRule({ + ruleName, + config: [true], + + accept: [ + { + code: 'a { --custom-property: 1 }', + }, + { + code: 'a { --custom-property: 1; --cUstOm-prOpErtY: 1 }', + }, + { + code: 'a { --custom-property: 1; color: pink; --cUstOm-prOpErtY: 1 }', + }, + { + code: 'a { color: var(--custom-property, --custom-property) }', + }, + { + code: 'a { --custom-property: pink; @media { --custom-property: orange; } }', + description: 'nested', + }, + { + code: + 'a { --custom-property: pink; @media { --custom-property: orange; &::before { --custom-property: black; } } }', + description: 'double nested', + }, + { + code: + 'a { --custom-property: pink; { &:hover { --custom-property: orange; --cUstOm-prOpErtY: black; } } }', + description: 'spec nested', + }, + { + code: + 'a { --cUstOm-prOpErtY: pink; { &:hover { --custom-property: orange; --cUstOm-prOpErtY: black; } } }', + description: 'spec nested', + }, + ], + + reject: [ + { + code: 'a { --custom-property: 1; --custom-property: 2; }', + message: messages.rejected('--custom-property'), + }, + { + code: 'a { --custom-property: 1; color: pink; --custom-property: 1; }', + message: messages.rejected('--custom-property'), + }, + { + code: 'a { --custom-property: 1; --cUstOm-prOpErtY: 1; color: pink; --cUstOm-prOpErtY: 1; }', + message: messages.rejected('--cUstOm-prOpErtY'), + }, + { + code: + 'a { --custom-property: pink; { &:hover { --custom-property: orange; --custom-property: black; } } }', + description: 'spec nested', + message: messages.rejected('--custom-property'), + }, + { + code: + 'a { --custom-property: pink; @media { --custom-property: orange; --custom-property: black; } }', + description: 'nested', + message: messages.rejected('--custom-property'), + }, + { + code: + '@media { --custom-property: orange; .foo { --custom-property: black; --custom-property: white; } }', + description: 'nested', + message: messages.rejected('--custom-property'), + }, + { + code: + 'a { --custom-property: pink; @media { --custom-property: orange; &::before { --custom-property: black; --custom-property: white; } } }', + description: 'double nested', + message: messages.rejected('--custom-property'), + }, + { + code: + 'a { --custom-property: pink; @media { --custom-property: orange; .foo { --custom-property: black; --custom-property: white; } } }', + description: 'double nested again', + message: messages.rejected('--custom-property'), + }, + ], +}); + +testRule({ + ruleName, + config: [true], + skipBasicChecks: true, + syntax: 'html', + + accept: [ + { + code: '', + }, + { + code: '', + }, + { + code: + '', + }, + { + code: '', + }, + { + code: + '', + }, + ], + + reject: [ + { + code: '', + message: messages.rejected('--custom-property'), + }, + { + code: + '', + message: messages.rejected('--custom-property'), + }, + { + code: '', + message: messages.rejected('--custom-property'), + }, + { + code: + '', + message: messages.rejected('--cUstOm-prOpErtY'), + }, + ], +}); + +testRule({ + ruleName, + config: [true], + skipBasicChecks: true, + syntax: 'css-in-js', + + accept: [ + { + code: + "import styled from 'react-emotion'\nexport default styled.div` --custom-property: pink; `;", + }, + { + code: + "import styled from 'react-emotion'\nexport default styled.div` --custom-property: pink; --cUstOm-prOpErtY: pink; `;", + }, + ], + reject: [ + { + code: + "import styled from 'styled-components';\nexport default styled.div` --custom-property: pink; --custom-property: orange; `;", + message: messages.rejected('--custom-property'), + }, + { + code: + "import styled from 'react-emotion'\nexport default styled.div` --custom-property: pink; --custom-property: orange; `;", + message: messages.rejected('--custom-property'), + }, + ], +}); diff --git a/lib/rules/declaration-block-no-duplicate-custom-properties/index.js b/lib/rules/declaration-block-no-duplicate-custom-properties/index.js new file mode 100644 index 0000000000..59712cf56d --- /dev/null +++ b/lib/rules/declaration-block-no-duplicate-custom-properties/index.js @@ -0,0 +1,61 @@ +// @ts-nocheck + +'use strict'; + +const eachDeclarationBlock = require('../../utils/eachDeclarationBlock'); +const isCustomProperty = require('../../utils/isCustomProperty'); +const isStandardSyntaxProperty = require('../../utils/isStandardSyntaxProperty'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'declaration-block-no-duplicate-custom-properties'; + +const messages = ruleMessages(ruleName, { + rejected: (property) => `Unexpected duplicate "${property}"`, +}); + +function rule(on) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { actual: on }); + + if (!validOptions) { + return; + } + + eachDeclarationBlock(root, (eachDecl) => { + const decls = new Set(); + + eachDecl((decl) => { + const prop = decl.prop; + + if (!isStandardSyntaxProperty(prop)) { + return; + } + + if (!isCustomProperty(prop)) { + return; + } + + const isDuplicate = decls.has(prop); + + if (isDuplicate) { + report({ + message: messages.rejected(prop), + node: decl, + result, + ruleName, + }); + + return; + } + + decls.add(prop); + }); + }); + }; +} + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/index.js b/lib/rules/index.js index 51ef1353a8..ebbc17de38 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -72,6 +72,9 @@ const rules = { 'custom-property-pattern': importLazy(() => require('./custom-property-pattern'))(), 'declaration-bang-space-after': importLazy(() => require('./declaration-bang-space-after'))(), 'declaration-bang-space-before': importLazy(() => require('./declaration-bang-space-before'))(), + 'declaration-block-no-duplicate-custom-properties': importLazy(() => + require('./declaration-block-no-duplicate-custom-properties'), + )(), 'declaration-block-no-duplicate-properties': importLazy(() => require('./declaration-block-no-duplicate-properties'), )(),