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 declaration-property-max-values #5920

Merged
merged 13 commits into from Mar 7, 2022
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
1 change: 1 addition & 0 deletions docs/user-guide/rules/list.md
Expand Up @@ -165,6 +165,7 @@ Within each cateogory, the rules are grouped by the [_thing_](http://apps.workfl

- [`declaration-block-no-redundant-longhand-properties`](../../../lib/rules/declaration-block-no-redundant-longhand-properties/README.md): Disallow longhand properties that can be combined into one shorthand property.
- [`declaration-no-important`](../../../lib/rules/declaration-no-important/README.md): Disallow `!important` within declarations.
- [`declaration-property-max-values`](../../../lib/rules/declaration-property-max-values/README.md): Limit the number of values for a list of properties within declarations.
- [`declaration-property-unit-allowed-list`](../../../lib/rules/declaration-property-unit-allowed-list/README.md): Specify a list of allowed property and unit pairs within declarations.
- [`declaration-property-unit-disallowed-list`](../../../lib/rules/declaration-property-unit-disallowed-list/README.md): Specify a list of disallowed property and unit pairs within declarations.
- [`declaration-property-value-allowed-list`](../../../lib/rules/declaration-property-value-allowed-list/README.md): Specify a list of allowed property and value pairs within declarations.
Expand Down
52 changes: 52 additions & 0 deletions lib/rules/declaration-property-max-values/README.md
@@ -0,0 +1,52 @@
# declaration-property-max-values

Limit the number of values for a list of properties within declarations.

## Options

`object`: `{ "unprefixed-property-name": int }`

If a property name is surrounded with `"/"` (e.g. `"/^margin/"`), it is interpreted as a regular expression. This allows, for example, easy targeting of shorthands: `/^margin/` will match `margin`, `margin-top`, `margin-inline`, etc.

Given:

```json
{
"border": 2,
"/^margin/": 1,
},
```

The following patterns are considered problems:

<!-- prettier-ignore -->
```css
a { border: 1px solid blue; }
```

<!-- prettier-ignore -->
```css
a { margin: 1px 2px; }
```

<!-- prettier-ignore -->
```css
a { margin-inline: 1px 2px; }
```

The following patterns are _not_ considered problems:

<!-- prettier-ignore -->
```css
a { border: 1px solid; }
```

<!-- prettier-ignore -->
```css
a { margin: 1px; }
```

<!-- prettier-ignore -->
```css
a { margin-inline: 1px; }
```
mattxwang marked this conversation as resolved.
Show resolved Hide resolved
87 changes: 87 additions & 0 deletions lib/rules/declaration-property-max-values/__tests__/index.js
@@ -0,0 +1,87 @@
'use strict';

const { messages, ruleName } = require('..');

testRule({
ruleName,

config: [
{
border: 2,
'/^margin/': 1,
},
],

accept: [
{
code: 'a { margin: 0; }',
},
{
code: 'a { margin: 1px; }',
},
{
code: 'a { margin: var(--foo); }',
description: 'deals with CSS variables',
},
{
code: 'a { margin: 1px /* 3px */; }',
description: 'ignore values in comments',
},
{
code: 'a { margin-inline: 1px; }',
},
{
code: 'a { margin: ; }',
},
{
code: 'a { border: 1px; }',
},
{
code: 'a { border: 1px solid; }',
},
{
code: 'a { transition: margin-right 2s ease-in-out; }',
description: 'irrelevant shorthand',
},
],

reject: [
{
code: 'a { margin: 1px 2px; }',
message: messages.rejected('margin', 1),
line: 1,
column: 5,
},
{
code: 'a { margin-inline: 1px 2px; }',
message: messages.rejected('margin-inline', 1),
line: 1,
column: 5,
},
{
code: 'a { margin: var(--foo) var(--bar); }',
message: messages.rejected('margin', 1),
line: 1,
column: 5,
description: 'deals with CSS variables',
},
{
code: 'a { margin: 1px 2px 3px 4px; }',
message: messages.rejected('margin', 1),
line: 1,
column: 5,
},
{
code: 'a { margin: 0 0 0 0; }',
message: messages.rejected('margin', 1),
line: 1,
column: 5,
},
{
code: 'a { border: 1px solid blue; }',
message: messages.rejected('border', 2),
line: 1,
column: 5,
},
],
mattxwang marked this conversation as resolved.
Show resolved Hide resolved
});
75 changes: 75 additions & 0 deletions lib/rules/declaration-property-max-values/index.js
@@ -0,0 +1,75 @@
'use strict';

const valueParser = require('postcss-value-parser');

const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const vendor = require('../../utils/vendor');
const validateOptions = require('../../utils/validateOptions');
const { isNumber } = require('../../utils/validateTypes');
const validateObjectWithProps = require('../../utils/validateObjectWithProps');

const ruleName = 'declaration-property-max-values';

const messages = ruleMessages(ruleName, {
rejected: (property, max) =>
`Expected "${property}" to have no more than ${max} ${max === 1 ? 'value' : 'values'}`,
});

const meta = {
url: 'https://stylelint.io/user-guide/rules/list/declaration-property-max-values',
};

/**
* @param {valueParser.Node} node
*/
const isValueNode = (node) => {
return node.type === 'word' || node.type === 'function' || node.type === 'string';
};

/** @type {import('stylelint').Rule<Record<string, number>>} */
const rule = (primary) => {
return (root, result) => {
mattxwang marked this conversation as resolved.
Show resolved Hide resolved
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [validateObjectWithProps(isNumber)],
});

if (!validOptions) {
return;
}

root.walkDecls((decl) => {
const { prop, value } = decl;
const propLength = valueParser(value).nodes.filter(isValueNode).length;

const unprefixedProp = vendor.unprefixed(prop);
const propKey = Object.keys(primary).find((propIdentifier) =>
matchesStringOrRegExp(unprefixedProp, propIdentifier),
);

if (!propKey) {
return;
}

const max = primary[propKey];

if (propLength <= max) {
return;
}

report({
message: messages.rejected(prop, max),
node: decl,
ybiquitous marked this conversation as resolved.
Show resolved Hide resolved
result,
ruleName,
});
});
};
};

rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;
1 change: 1 addition & 0 deletions lib/rules/index.js
Expand Up @@ -81,6 +81,7 @@ const rules = {
'declaration-colon-space-before': importLazy('./declaration-colon-space-before'),
'declaration-empty-line-before': importLazy('./declaration-empty-line-before'),
'declaration-no-important': importLazy('./declaration-no-important'),
'declaration-property-max-values': importLazy('./declaration-property-max-values'),
'declaration-property-unit-allowed-list': importLazy('./declaration-property-unit-allowed-list'),
'declaration-property-unit-disallowed-list': importLazy(
'./declaration-property-unit-disallowed-list',
Expand Down
36 changes: 36 additions & 0 deletions lib/utils/__tests__/validateObjectWithProps.test.js
@@ -0,0 +1,36 @@
'use strict';

const validateObjectWithProps = require('../validateObjectWithProps');
const { isNumber } = require('../validateTypes');

describe('validateObjectWithProps', () => {
it('should return a function', () => {
expect(validateObjectWithProps((x) => x)).toBeInstanceOf(Function);
});

it('should reject non-objects', () => {
expect(validateObjectWithProps((x) => x)(42)).toBeFalsy();
});

describe('simple isNumber validator', () => {
const validator = validateObjectWithProps(isNumber);

it('should accept an object where the validators are true for any value', () => {
expect(
validator({
value1: 1,
value2: 2,
}),
).toBeTruthy();
});

it('should be false if the validator is false for at least one value', () => {
expect(
validator({
value1: 1,
value2: '2',
}),
).toBeFalsy();
});
});
});
28 changes: 28 additions & 0 deletions lib/utils/validateObjectWithProps.js
@@ -0,0 +1,28 @@
'use strict';

const { isPlainObject } = require('./validateTypes');

/**
* Check whether the variable is an object and all its properties agree with the provided validator.
*
* @example
* config = {
* value1: 1,
* value2: 2,
* value3: 3,
* };
* validateObjectWithProps(isNumber)(config);
* //=> true
*
* @param {(value: unknown) => boolean} validator
* @returns {(value: unknown) => boolean}
*/
module.exports = (validator) => (value) => {
if (!isPlainObject(value)) {
return false;
}

return Object.values(value).every((item) => {
return validator(item);
});
};