Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom-property-no-missing-var-function rule #5317

Merged
merged 2 commits into from May 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -65,6 +65,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