diff --git a/docs/user-guide/rules/list.md b/docs/user-guide/rules/list.md index 2fb3483afa..a0a41341ae 100644 --- a/docs/user-guide/rules/list.md +++ b/docs/user-guide/rules/list.md @@ -88,6 +88,7 @@ Grouped first by the following categories and then by the [_thing_](http://apps. ### Color - [`color-function-notation`](../../../lib/rules/color-function-notation/README.md): Specify modern or legacy notation for applicable color-functions (Autofixable). +- [`color-hex-alpha`](../../../lib/rules/color-hex-alpha/README.md): Require or disallow alpha channel for hex colors. - [`color-named`](../../../lib/rules/color-named/README.md): Require (where possible) or disallow named colors. - [`color-no-hex`](../../../lib/rules/color-no-hex/README.md): Disallow hex colors. diff --git a/lib/rules/color-hex-alpha/README.md b/lib/rules/color-hex-alpha/README.md new file mode 100644 index 0000000000..079e6725ab --- /dev/null +++ b/lib/rules/color-hex-alpha/README.md @@ -0,0 +1,66 @@ +# color-hex-alpha + +Require or disallow alpha channel for hex colors. + + +```css +a { color: #ffff } +/** ↑ + * This hex alpha */ +``` + +## Options + +`string`: `"always"|"never"` + +### `"always"` + +The following patterns are considered violations: + + +```css +a { color: #fff; } +``` + + +```css +a { color: #ffffff; } +``` + +The following patterns are _not_ considered violations: + + +```css +a { color: #fffa; } +``` + + +```css +a { color: #ffffffaa; } +``` + +### `"never"` + +The following patterns are considered violations: + + +```css +a { color: #fffa; } +``` + + +```css +a { color: #ffffffaa; } +``` + +The following patterns are _not_ considered violations: + + +```css +a { color: #fff; } +``` + + +```css +a { color: #ffffff; } +``` diff --git a/lib/rules/color-hex-alpha/__tests__/index.js b/lib/rules/color-hex-alpha/__tests__/index.js new file mode 100644 index 0000000000..892fcb0673 --- /dev/null +++ b/lib/rules/color-hex-alpha/__tests__/index.js @@ -0,0 +1,87 @@ +'use strict'; + +const { messages, ruleName } = require('..'); + +testRule({ + ruleName, + config: 'never', + accept: [ + { + code: 'a { color: #fff; }', + }, + { + code: 'a { color: #ffffff; }', + }, + ], + reject: [ + { + code: 'a { color: #ffff; }', + message: messages.unexpected('#ffff'), + line: 1, + column: 11, + }, + ], +}); + +testRule({ + ruleName, + config: 'always', + accept: [ + { + code: 'a { color: #ffff; }', + }, + { + code: 'a { color: #ffffffff; }', + }, + { + code: 'a { background: linear-gradient(to left, #fffa, red 100%); }', + }, + { + code: 'a { background: url(#fff); }', + }, + ], + reject: [ + { + code: 'a { color:#fff; }', + message: messages.expected('#fff'), + line: 1, + column: 11, + }, + { + code: 'a { color:#ffffff; }', + message: messages.expected('#ffffff'), + line: 1, + column: 11, + }, + { + code: 'a { background: linear-gradient(to left, #fff, red 100%); }', + message: messages.expected('#fff'), + line: 1, + column: 42, + }, + ], +}); +testRule({ + ruleName, + config: ['never'], + syntax: 'scss', + + accept: [ + { + code: 'a { color: #{ff}; }', + description: 'scss interpolation', + }, + { + code: 'a { $var: #fff;}', + description: 'scss interpolation', + }, + ], + reject: [ + { + code: 'a { $var: #fffa;}', + message: messages.expected('#fff'), + line: 1, + column: 8, + }, + ], +}); diff --git a/lib/rules/color-hex-alpha/index.js b/lib/rules/color-hex-alpha/index.js new file mode 100644 index 0000000000..0cbff6e6b8 --- /dev/null +++ b/lib/rules/color-hex-alpha/index.js @@ -0,0 +1,65 @@ +// @ts-nocheck +'use strict'; + +const declarationValueIndex = require('../../utils/declarationValueIndex'); +const isStandardSyntaxDeclaration = require('../../utils/isStandardSyntaxDeclaration'); +const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); +const valueParser = require('postcss-value-parser'); + +const ruleName = 'color-hex-alpha'; + +const messages = ruleMessages(ruleName, { + expected: (hex) => `Expected alpha channel in "${hex}"`, + unexpected: (hex) => `Unexpected alpha channel in "${hex}"`, +}); + +function rule(primary) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: primary, + possible: ['always', 'never'], + }); + + if (!validOptions) return; + + root.walkDecls((decl) => { + if (!isStandardSyntaxDeclaration(decl)) return; + + const parsedValue = valueParser(decl.value); + + parsedValue.walk((node) => { + if (node.type !== 'word') return; + + if (node.value.startsWith('#')) return; + + let hexValue = node.value; + + if (!isStandardSyntaxValue(hexValue)) return; + + if (primary === 'always' && (hexValue.length === 5 || hexValue.length === 9)) { + return; + } + + if (primary === 'never' && (hexValue.length === 4 || hexValue.length === 7)) { + return; + } + + report({ + message: + primary === 'never' ? messages.unexpected(hexValue) : messages.expected(hexValue), + node: decl, + index: declarationValueIndex(decl) + node.sourceIndex, + result, + ruleName, + }); + }); + }); + }; +} + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/index.js b/lib/rules/index.js index 4846a04021..004b168fbc 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -55,6 +55,7 @@ const rules = { require('./block-opening-brace-space-before'), )(), 'color-function-notation': importLazy(() => require('./color-function-notation'))(), + 'color-hex-alpha': importLazy(() => require('./color-hex-alpha'))(), 'color-hex-case': importLazy(() => require('./color-hex-case'))(), 'color-hex-length': importLazy(() => require('./color-hex-length'))(), 'color-named': importLazy(() => require('./color-named'))(),