From a3cc2c4ca34b7348135fe987cc5907bda3e9929b Mon Sep 17 00:00:00 2001 From: Richard Hallows Date: Sat, 29 May 2021 09:25:02 +0100 Subject: [PATCH] Add custom-property-no-missing-var-function rule (#5317) --- docs/user-guide/rules/list.md | 4 + .../README.md | 45 ++++++ .../__tests__/index.js | 136 ++++++++++++++++++ .../index.js | 76 ++++++++++ lib/rules/index.js | 3 + 5 files changed, 264 insertions(+) create mode 100644 lib/rules/custom-property-no-missing-var-function/README.md create mode 100644 lib/rules/custom-property-no-missing-var-function/__tests__/index.js create mode 100644 lib/rules/custom-property-no-missing-var-function/index.js diff --git a/docs/user-guide/rules/list.md b/docs/user-guide/rules/list.md index 86926c1ed9..171f9e3015 100644 --- a/docs/user-guide/rules/list.md +++ b/docs/user-guide/rules/list.md @@ -34,6 +34,10 @@ Grouped first by the following categories and then by the [_thing_](http://apps. - [`unit-no-unknown`](../../../lib/rules/unit-no-unknown/README.md): Disallow unknown units. +### Custom property + +- [`custom-property-no-missing-var-function`](../../../lib/rules/custom-property-no-missing-var-function/README.md): Disallow missing `var` function for custom properties. + ### Property - [`property-no-unknown`](../../../lib/rules/property-no-unknown/README.md): Disallow unknown properties. diff --git a/lib/rules/custom-property-no-missing-var-function/README.md b/lib/rules/custom-property-no-missing-var-function/README.md new file mode 100644 index 0000000000..e91ebb1cfa --- /dev/null +++ b/lib/rules/custom-property-no-missing-var-function/README.md @@ -0,0 +1,45 @@ +# custom-property-no-missing-var-function + +Disallow missing `var` function for custom properties. + + +```css + :root { --foo: red; } + a { color: --foo; } +/** ↑ + * This custom property */ +``` + +This rule only reports custom properties that are defined within the same source. + +## Options + +### `true` + +The following patterns are considered violations: + + +```css +:root { --foo: red; } +a { color: --foo; } +``` + + +```css +@property --foo {} +a { color: --foo; } +``` + +The following patterns are _not_ considered violations: + + +```css +:root { --foo: red; } +a { color: var(--foo); } +``` + + +```css +@property --foo {} +a { color: var(--foo); } +``` diff --git a/lib/rules/custom-property-no-missing-var-function/__tests__/index.js b/lib/rules/custom-property-no-missing-var-function/__tests__/index.js new file mode 100644 index 0000000000..1a927fc32c --- /dev/null +++ b/lib/rules/custom-property-no-missing-var-function/__tests__/index.js @@ -0,0 +1,136 @@ +'use strict'; + +const stripIndent = require('common-tags').stripIndent; + +const { messages, ruleName } = require('..'); + +testRule({ + ruleName, + config: true, + + accept: [ + { + code: 'a { color: --foo; }', + description: 'undeclared dashed-ident', + }, + { + code: 'a { color: var(--foo); }', + description: 'undeclared dashed-ident in var', + }, + { + code: 'a { color: env(--foo); }', + description: 'undeclared dashed-ident in env', + }, + { + code: 'a { color: color(--foo 0% 0% 0% 0%); }', + description: 'undeclared dashed-ident in color', + }, + { + code: 'a { color: calc(var(--foo) + var(--bar)); }', + description: 'undeclared dashed-idents in vars in calc', + }, + { + code: 'a { color: var(--foo, red); }', + description: 'undeclared dashed-idents in var with fallback', + }, + { + code: 'a { --foo: var(--bar); }', + description: 'undeclared dashed-idents in vars assigned to custom property', + }, + { + code: ':root { --foo: red; } a { color: var(--foo); }', + description: 'declared custom property in var', + }, + { + code: '@property --foo {} a { color: var(--foo); }', + description: 'declared via at-property custom property in var', + }, + { + code: ':--foo {}', + description: 'custom selector', + }, + { + code: '@media(--foo) {}', + description: 'custom media query', + }, + ], + + reject: [ + { + code: 'a { --foo: red; color: --foo; }', + message: messages.rejected('--foo'), + line: 1, + column: 24, + description: 'declared custom property', + }, + { + code: '@property --foo {} a { color: --foo; }', + message: messages.rejected('--foo'), + line: 1, + column: 31, + description: 'declared via at-property custom property', + }, + { + code: ':root { --bar: 0; } a { --foo: --bar; }', + message: messages.rejected('--bar'), + line: 1, + column: 32, + description: 'declared in :root custom property', + }, + { + code: ':root { --bar: 0px; } a { color: calc(var(--foo) + --bar)); }', + message: messages.rejected('--bar'), + line: 1, + column: 52, + description: 'declared custom property and used inside calc', + }, + { + code: ':root { --foo: pink; } a { color: --foo, red; }', + message: messages.rejected('--foo'), + line: 1, + column: 36, + description: 'declared custom property and used with fall back', + }, + { + code: ':root { --bar: 0; } a { color: --foo(--bar); }', + message: messages.rejected('--bar'), + line: 1, + column: 38, + description: 'declared custom property used inside custom function', + }, + { + code: stripIndent` + :root { + --bar: 0; + --baz: 0; + } + + a { + --foo: --bar; + color: --baz; + } + `, + warnings: [ + { message: messages.rejected('--bar'), line: 7, column: 9 }, + { message: messages.rejected('--baz'), line: 8, column: 9 }, + ], + description: 'two declared custom properties', + }, + { + code: stripIndent` + @property --bar {} + @property --baz {} + + a { + --foo: --bar; + color: --baz; + } + `, + warnings: [ + { message: messages.rejected('--bar'), line: 5, column: 9 }, + { message: messages.rejected('--baz'), line: 6, column: 9 }, + ], + description: 'two declared via at-property custom properties', + }, + ], +}); diff --git a/lib/rules/custom-property-no-missing-var-function/index.js b/lib/rules/custom-property-no-missing-var-function/index.js new file mode 100644 index 0000000000..bc2e15bb34 --- /dev/null +++ b/lib/rules/custom-property-no-missing-var-function/index.js @@ -0,0 +1,76 @@ +// @ts-nocheck + +'use strict'; + +const valueParser = require('postcss-value-parser'); + +const declarationValueIndex = require('../../utils/declarationValueIndex'); +const isCustomProperty = require('../../utils/isCustomProperty'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); + +const ruleName = 'custom-property-no-missing-var-function'; + +const messages = ruleMessages(ruleName, { + rejected: (customProperty) => `Unexpected missing var function for "${customProperty}"`, +}); + +function rule(actual) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual, + }); + + if (!validOptions) return; + + const customProperties = new Set(); + + root.walkAtRules(/^property$/i, (atRule) => { + customProperties.add(atRule.params); + }); + + root.walkDecls(({ prop }) => { + if (isCustomProperty(prop)) customProperties.add(prop); + }); + + root.walkDecls((decl) => { + const { value } = decl; + const parsedValue = valueParser(value); + + parsedValue.walk((node) => { + if (isVarFunction(node)) return false; + + if (!isDashedIdent(node)) return; + + if (!isKnownCustomProperty(node)) return; + + report({ + message: messages.rejected(node.value), + node: decl, + index: declarationValueIndex(decl) + node.sourceIndex, + result, + ruleName, + }); + + return false; + }); + }); + + function isKnownCustomProperty({ value }) { + return customProperties.has(value); + } + }; +} + +function isDashedIdent({ type, value }) { + return type === 'word' && value.startsWith('--'); +} + +function isVarFunction({ type, value }) { + return type === 'function' && value === 'var'; +} + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/index.js b/lib/rules/index.js index f7345e7a08..c140ce59c3 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -66,6 +66,9 @@ const rules = { 'custom-property-empty-line-before': importLazy(() => require('./custom-property-empty-line-before'), )(), + 'custom-property-no-missing-var-function': importLazy(() => + require('./custom-property-no-missing-var-function'), + )(), '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'))(),