Skip to content

Commit

Permalink
Add custom-property-no-missing-var-function rule (#5317)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeddy3 committed May 29, 2021
1 parent e7c655f commit a3cc2c4
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 0 deletions.
4 changes: 4 additions & 0 deletions docs/user-guide/rules/list.md
Expand Up @@ -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.
Expand Down
45 changes: 45 additions & 0 deletions 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.

<!-- prettier-ignore -->
```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:

<!-- prettier-ignore -->
```css
:root { --foo: red; }
a { color: --foo; }
```

<!-- prettier-ignore -->
```css
@property --foo {}
a { color: --foo; }
```

The following patterns are _not_ considered violations:

<!-- prettier-ignore -->
```css
:root { --foo: red; }
a { color: var(--foo); }
```

<!-- prettier-ignore -->
```css
@property --foo {}
a { color: var(--foo); }
```
136 changes: 136 additions & 0 deletions 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',
},
],
});
76 changes: 76 additions & 0 deletions 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;
3 changes: 3 additions & 0 deletions lib/rules/index.js
Expand Up @@ -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'))(),
Expand Down

0 comments on commit a3cc2c4

Please sign in to comment.