diff --git a/docs/user-guide/rules/list.md b/docs/user-guide/rules/list.md index 8a841475b9..86926c1ed9 100644 --- a/docs/user-guide/rules/list.md +++ b/docs/user-guide/rules/list.md @@ -93,6 +93,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..0f0499a6ac --- /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: #fffa } +/** ↑ + * This alpha channel */ +``` + +## 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..9dfae00e37 --- /dev/null +++ b/lib/rules/color-hex-alpha/__tests__/index.js @@ -0,0 +1,92 @@ +'use strict'; + +const postcssScss = require('postcss-scss'); + +const { messages, ruleName } = require('..'); + +testRule({ + ruleName, + config: 'always', + accept: [ + { + code: 'a { color: #ffff; }', + }, + { + code: 'a { color: #ffffffff; }', + }, + { + code: 'a { background: linear-gradient(to left, #fffa, #000000aa 100%); }', + }, + { + code: 'a { background: url(#fff); }', + }, + ], + reject: [ + { + code: 'a { color: #fff; }', + message: messages.expected('#fff'), + line: 1, + column: 12, + }, + { + code: 'a { color: #ffffff; }', + message: messages.expected('#ffffff'), + line: 1, + column: 12, + }, + { + code: 'a { background: linear-gradient(to left, #fff, #000000 100%); }', + warnings: [ + { message: messages.expected('#fff'), line: 1, column: 42 }, + { message: messages.expected('#000000'), line: 1, column: 48 }, + ], + }, + ], +}); + +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: 12, + }, + ], +}); + +testRule({ + ruleName, + config: 'always', + customSyntax: postcssScss, + + accept: [ + { + code: 'a { color: #{f}; }', + description: 'scss interpolation of 3 characters', + }, + { + code: '$var: #ffff;', + description: 'alpha channel scss variable', + }, + ], + reject: [ + { + code: '$var: #fff', + message: messages.expected('#fff'), + line: 1, + column: 7, + description: 'no alpha channel scss variable', + }, + ], +}); diff --git a/lib/rules/color-hex-alpha/index.js b/lib/rules/color-hex-alpha/index.js new file mode 100644 index 0000000000..99d917e69b --- /dev/null +++ b/lib/rules/color-hex-alpha/index.js @@ -0,0 +1,68 @@ +// @ts-nocheck +'use strict'; + +const declarationValueIndex = require('../../utils/declarationValueIndex'); +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}"`, +}); + +const HEX = /^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i; + +function rule(primary) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: primary, + possible: ['always', 'never'], + }); + + if (!validOptions) return; + + root.walkDecls((decl) => { + const parsedValue = valueParser(decl.value); + + parsedValue.walk((node) => { + if (isUrlFunction(node)) return false; + + if (!isHexColor(node)) return; + + const { value } = node; + + if (primary === 'always' && hasAlphaChannel(value)) return; + + if (primary === 'never' && !hasAlphaChannel(value)) return; + + report({ + message: primary === 'never' ? messages.unexpected(value) : messages.expected(value), + node: decl, + index: declarationValueIndex(decl) + node.sourceIndex, + result, + ruleName, + }); + }); + }); + }; +} + +function isUrlFunction({ type, value }) { + return type === 'function' && value === 'url'; +} + +function isHexColor({ type, value }) { + return type === 'word' && HEX.test(value); +} + +function hasAlphaChannel(hex) { + return hex.length === 5 || hex.length === 9; +} + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/index.js b/lib/rules/index.js index a18a2d4fcb..f7345e7a08 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -51,6 +51,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'))(),