Skip to content

Commit

Permalink
Add experimental support for custom message arguments (#6312)
Browse files Browse the repository at this point in the history
  • Loading branch information
ybiquitous committed Sep 12, 2022
1 parent e826690 commit dd04517
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/tiny-flowers-destroy.md
@@ -0,0 +1,5 @@
---
"stylelint": minor
---

Added: experimental support for custom message arguments
25 changes: 25 additions & 0 deletions docs/user-guide/configure.md
Expand Up @@ -109,6 +109,31 @@ For example, the following rule configuration would substitute in custom message

Alternately, you can write a [custom formatter](../developer-guide/formatters.md) for maximum control if you need serious customization.

Experimental feature: some rules support message arguments. For example, when configuring the `color-no-hex` rule, the hex color can be used in the message string:

`.stylelintrc.js`:

```js
{
'color-no-hex': [true, {
message: (hex) => `Don't use hex colors like "${hex}"`,
}]
}
```

`.stylelintrc.json`:

<!-- prettier-ignore -->
```json
{
"color-no-hex": [true, {
"message": "Don't use hex colors like \"%s\""
}]
}
```

With formats that don't support a function like JSON, you can use a `printf`-like format (e.g., `%s`). On the other hand, with JS format, you can use both a `printf`-like format and a function.

### `reportDisables`

You can set the `reportDisables` secondary option to report any `stylelint-disable` comments for this rule, effectively disallowing authors to opt out of it.
Expand Down
2 changes: 2 additions & 0 deletions lib/rules/color-no-hex/README.md
Expand Up @@ -9,6 +9,8 @@ a { color: #333 }
* This hex color */
```

The [`message` secondary option](../../../docs/user-guide/configure.md#message) can accept the arguments of this rule.

## Options

### `true`
Expand Down
3 changes: 2 additions & 1 deletion lib/rules/color-no-hex/index.js
Expand Up @@ -42,7 +42,8 @@ const rule = (primary) => {
const endIndex = index + node.value.length;

report({
message: messages.rejected(node.value),
message: messages.rejected,
messageArgs: [node.value],
node: decl,
index,
endIndex,
Expand Down
96 changes: 82 additions & 14 deletions lib/utils/__tests__/report.test.js
Expand Up @@ -2,15 +2,17 @@

const report = require('../report');

it('without disabledRanges', () => {
const defaultRangeBy = () => ({ start: { line: 2, column: 1 }, end: { line: 2, column: 2 } });

test('without disabledRanges', () => {
const v = {
ruleName: 'foo',
result: {
warn: jest.fn(),
},
message: 'bar',
node: {
rangeBy: () => ({ start: { line: 2, column: 1 }, end: { line: 2, column: 2 } }),
rangeBy: defaultRangeBy,
},
};

Expand All @@ -21,7 +23,7 @@ it('without disabledRanges', () => {
expect(spyArgs[1].node).toBe(v.node);
});

it('with irrelevant general disabledRange', () => {
test('with irrelevant general disabledRange', () => {
const v = {
ruleName: 'foo',
result: {
Expand All @@ -34,7 +36,7 @@ it('with irrelevant general disabledRange', () => {
},
message: 'bar',
node: {
rangeBy: () => ({ start: { line: 2, column: 1 }, end: { line: 2, column: 2 } }),
rangeBy: defaultRangeBy,
},
};

Expand All @@ -45,7 +47,7 @@ it('with irrelevant general disabledRange', () => {
expect(spyArgs[1].node).toBe(v.node);
});

it('with relevant general disabledRange', () => {
test('with relevant general disabledRange', () => {
const v = {
ruleName: 'foo',
result: {
Expand All @@ -66,7 +68,7 @@ it('with relevant general disabledRange', () => {
expect(v.result.warn).toHaveBeenCalledTimes(0);
});

it('with irrelevant rule-specific disabledRange', () => {
test('with irrelevant rule-specific disabledRange', () => {
const v = {
ruleName: 'foo',
result: {
Expand All @@ -91,7 +93,7 @@ it('with irrelevant rule-specific disabledRange', () => {
expect(spyArgs[1].node).toBe(v.node);
});

it('with relevant rule-specific disabledRange', () => {
test('with relevant rule-specific disabledRange', () => {
const v = {
ruleName: 'foo',
result: {
Expand All @@ -113,7 +115,7 @@ it('with relevant rule-specific disabledRange', () => {
expect(v.result.warn).toHaveBeenCalledTimes(0);
});

it('with relevant general disabledRange, among others', () => {
test('with relevant general disabledRange, among others', () => {
const v = {
ruleName: 'foo',
result: {
Expand All @@ -137,7 +139,7 @@ it('with relevant general disabledRange, among others', () => {
expect(v.result.warn).toHaveBeenCalledTimes(0);
});

it('with relevant rule-specific disabledRange, among others', () => {
test('with relevant rule-specific disabledRange, among others', () => {
const v = {
ruleName: 'foo',
result: {
Expand All @@ -162,7 +164,7 @@ it('with relevant rule-specific disabledRange, among others', () => {
expect(v.result.warn).toHaveBeenCalledTimes(0);
});

it('with relevant rule-specific disabledRange with range report', () => {
test('with relevant rule-specific disabledRange with range report', () => {
const v = {
ruleName: 'foo',
result: {
Expand All @@ -184,7 +186,7 @@ it('with relevant rule-specific disabledRange with range report', () => {
expect(v.result.warn).toHaveBeenCalledTimes(0);
});

it("with quiet mode on and rule severity of 'warning'", () => {
test("with quiet mode on and rule severity of 'warning'", () => {
const v = {
ruleName: 'foo',
result: {
Expand All @@ -198,15 +200,15 @@ it("with quiet mode on and rule severity of 'warning'", () => {
},
message: 'bar',
node: {
rangeBy: () => ({ start: { line: 6, column: 1 }, end: { line: 6, column: 2 } }),
rangeBy: defaultRangeBy,
},
};

report(v);
expect(v.result.warn).toHaveBeenCalledTimes(0);
});

it("with quiet mode on and rule severity of 'error'", () => {
test("with quiet mode on and rule severity of 'error'", () => {
const v = {
ruleName: 'foo',
result: {
Expand All @@ -220,10 +222,76 @@ it("with quiet mode on and rule severity of 'error'", () => {
},
message: 'bar',
node: {
rangeBy: () => ({ start: { line: 6, column: 1 }, end: { line: 6, column: 2 } }),
rangeBy: defaultRangeBy,
},
};

report(v);
expect(v.result.warn).toHaveBeenCalledTimes(1);
});

test('with message function', () => {
const v = {
ruleName: 'foo',
result: {
warn: jest.fn(),
},
message: (a, b, c, d) => `a=${a}, b=${b}, c=${c}, d=${d}`,
messageArgs: ['str', true, 10, /regex/],
node: {
rangeBy: defaultRangeBy,
},
};

report(v);
const spyArgs = v.result.warn.mock.calls[0];

expect(spyArgs[0]).toBe('a=str, b=true, c=10, d=/regex/');
expect(spyArgs[1].node).toBe(v.node);
});

test('with custom message', () => {
const v = {
ruleName: 'foo',
result: {
warn: jest.fn(),
stylelint: {
customMessages: { foo: 'A custom message: %s, %s, %d, %s' },
},
},
message: 'bar',
messageArgs: ['str', true, 10, /regex/],
node: {
rangeBy: defaultRangeBy,
},
};

report(v);
const spyArgs = v.result.warn.mock.calls[0];

expect(spyArgs[0]).toBe('A custom message: str, true, 10, /regex/');
expect(spyArgs[1].node).toBe(v.node);
});

test('with custom message function', () => {
const v = {
ruleName: 'foo',
result: {
warn: jest.fn(),
stylelint: {
customMessages: { foo: (a, b) => `a=${a}, b=${b}` },
},
},
message: 'bar',
messageArgs: ['str', 123],
node: {
rangeBy: defaultRangeBy,
},
};

report(v);
const spyArgs = v.result.warn.mock.calls[0];

expect(spyArgs[0]).toBe('a=str, b=123');
expect(spyArgs[1].node).toBe(v.node);
});
26 changes: 23 additions & 3 deletions lib/utils/report.js
@@ -1,5 +1,7 @@
'use strict';

const util = require('util');

/**
* Report a problem.
*
Expand All @@ -15,7 +17,7 @@
* @type {typeof import('stylelint').utils.report}
*/
module.exports = function report(problem) {
const { ruleName, result, message, line, node, index, endIndex, word } = problem;
const { ruleName, result, message, messageArgs, line, node, index, endIndex, word } = problem;

result.stylelint = result.stylelint || {
ruleSeverities: {},
Expand Down Expand Up @@ -104,8 +106,26 @@ module.exports = function report(problem) {
warningProperties.word = word;
}

const warningMessage =
(result.stylelint.customMessages && result.stylelint.customMessages[ruleName]) || message;
const { customMessages } = result.stylelint;
const warningMessage = buildWarningMessage(
(customMessages && customMessages[ruleName]) || message,
messageArgs,
);

result.warn(warningMessage, warningProperties);
};

/**
* @param {import('stylelint').RuleMessage} message
* @param {import('stylelint').Problem['messageArgs']} messageArgs
* @returns {string}
*/
function buildWarningMessage(message, messageArgs) {
const args = messageArgs || [];

if (typeof message === 'string') {
return util.format(message, ...args);
}

return message(...args);
}
9 changes: 6 additions & 3 deletions types/stylelint/index.d.ts
Expand Up @@ -89,7 +89,7 @@ declare module 'stylelint' {

export type StylelintPostcssResult = {
ruleSeverities: { [ruleName: string]: Severity };
customMessages: { [ruleName: string]: any };
customMessages: { [ruleName: string]: RuleMessage };
ruleMetadata: { [ruleName: string]: Partial<RuleMeta> };
quiet?: boolean;
disabledRanges: DisabledRangeObject;
Expand Down Expand Up @@ -154,7 +154,9 @@ declare module 'stylelint' {
bivariance(...args: (string | number | boolean | RegExp)[]): string;
}['bivariance'];

export type RuleMessages = { [message: string]: string | RuleMessageFunc };
export type RuleMessage = string | RuleMessageFunc;

export type RuleMessages = { [message: string]: RuleMessage };

export type RuleOptionsPossibleFunc = (value: unknown) => boolean;

Expand Down Expand Up @@ -350,7 +352,8 @@ declare module 'stylelint' {
export type Problem = {
ruleName: string;
result: PostcssResult;
message: string;
message: RuleMessage;
messageArgs?: Parameters<RuleMessage> | undefined;
node: PostCSS.Node;
/**
* The inclusive start index of the problem, relative to the node's
Expand Down

0 comments on commit dd04517

Please sign in to comment.