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 experimental support for custom message arguments #6312

Merged
merged 10 commits into from Sep 12, 2022
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 message string:

`.stylelintrc.js`:

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

`.stylelintrc.json`:

<!-- prettier-ignore -->
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note] Unless the disabling comment, Prettier would format the snippet like this:

{
  "color-no-hex": [
    true,
    {
      "message": "Don't use hex colors like \"%s\""
    }
  ]
}

Compared to the snippet above for .stylelintrc.js, it would not be easier to read.

```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`).
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note] I think a description of %s is necessary. Please let me know if you have a better expression.

See also the util.format on the Node.js documentation.


### `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],
Copy link
Contributor

@Mouvedia Mouvedia Sep 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remark

If I understand correctly, the order will be arbitrary.
e.g. if you don't use --fix but you want to use the fixed value in the message (this rule is not a good example because it's not fixable)

i.e.

  • we would generate the value but not edit the file in place
  • the user will need to know that the bad value is the first element and the good value is the second element

related

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Mouvedia Thanks for the share. Other problems might be found than you pointed out, but this feature is still experimental. We would try considering it based on reactions or feedback later.

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