Skip to content

Commit 123c57f

Browse files
committed
feat(linter/plugins): support interpolation in normal diagnostic message
1 parent aaa34e1 commit 123c57f

File tree

6 files changed

+189
-20
lines changed

6 files changed

+189
-20
lines changed

apps/oxlint/src-js/plugins/context.ts

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type Diagnostic = DiagnosticWithNode | DiagnosticWithLoc | DiagnosticWith
1313

1414
export interface DiagnosticBase {
1515
message?: string;
16+
data?: Record<string, string | number> | null | undefined;
1617
fix?: FixFn;
1718
}
1819

@@ -28,7 +29,6 @@ export interface DiagnosticWithLoc extends DiagnosticBase {
2829

2930
export interface DiagnosticWithMessageId extends DiagnosticBase {
3031
messageId: string;
31-
data?: Record<string, string | number>;
3232
node?: Ranged;
3333
loc?: Location;
3434
}
@@ -162,14 +162,26 @@ export class Context {
162162
let message: string;
163163
if (hasOwn(diagnostic, 'messageId')) {
164164
const diagWithMessageId = diagnostic as DiagnosticWithMessageId;
165-
message = resolveMessage(diagWithMessageId.messageId, diagWithMessageId.data, internal);
165+
message = resolveMessage(diagWithMessageId.messageId, internal);
166166
} else {
167167
message = diagnostic.message;
168168
if (typeof message !== 'string') {
169169
throw new TypeError('Either `message` or `messageId` is required');
170170
}
171171
}
172172

173+
// Interpolate placeholders {{key}} with data values
174+
if (hasOwn(diagnostic, 'data')) {
175+
const { data } = diagnostic;
176+
if (data != null) {
177+
message = message.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
178+
key = key.trim();
179+
const value = data[key];
180+
return value !== undefined ? String(value) : match;
181+
});
182+
}
183+
}
184+
173185
// TODO: Validate `diagnostic`
174186
let start: number, end: number, loc: Location;
175187

@@ -231,16 +243,11 @@ export class Context {
231243
/**
232244
* Resolve a message ID to its message string, with optional data interpolation.
233245
* @param messageId - The message ID to resolve
234-
* @param data - Optional data for placeholder interpolation
235246
* @param internal - Internal context containing messages
236247
* @returns Resolved message string
237248
* @throws {Error} If `messageId` is not found in `messages`
238249
*/
239-
function resolveMessage(
240-
messageId: string,
241-
data: Record<string, string | number> | undefined,
242-
internal: InternalContext,
243-
): string {
250+
function resolveMessage(messageId: string, internal: InternalContext): string {
244251
const { messages } = internal;
245252

246253
if (!messages) {
@@ -255,16 +262,5 @@ function resolveMessage(
255262
);
256263
}
257264

258-
let message = messages[messageId];
259-
260-
// Interpolate placeholders {{key}} with data values
261-
if (data) {
262-
message = message.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
263-
key = key.trim();
264-
const value = data[key];
265-
return value !== undefined ? String(value) : match;
266-
});
267-
}
268-
269-
return message;
265+
return messages[messageId];
270266
}

apps/oxlint/test/e2e.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ describe('oxlint CLI', () => {
4949
await testFixture('basic_custom_plugin');
5050
});
5151

52+
it('should support message placeholder interpolation', async () => {
53+
await testFixture('message_interpolation');
54+
});
55+
5256
it('should support messageId', async () => {
5357
await testFixture('message_id_plugin');
5458
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"jsPlugins": ["./plugin.ts"],
3+
"categories": {
4+
"correctness": "off"
5+
},
6+
"rules": {
7+
"interpolation-test/no-var": "error"
8+
}
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
var testWithNoData = {};
2+
var testWithName = {};
3+
var testWithNameNoData = {};
4+
var testWithMultiple = {};
5+
var testWithMultipleNoData = {};
6+
var testWithMissingData = {};
7+
var testWithSpaces = {};
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Exit code
2+
1
3+
4+
# stdout
5+
```
6+
x interpolation-test(no-var): Variables should not use var
7+
,-[files/index.js:1:1]
8+
1 | var testWithNoData = {};
9+
: ^^^^^^^^^^^^^^^^^^^^^^^^
10+
2 | var testWithName = {};
11+
`----
12+
13+
x interpolation-test(no-var): Variable `testWithName` should not use var
14+
,-[files/index.js:2:1]
15+
1 | var testWithNoData = {};
16+
2 | var testWithName = {};
17+
: ^^^^^^^^^^^^^^^^^^^^^^
18+
3 | var testWithNameNoData = {};
19+
`----
20+
21+
x interpolation-test(no-var): Variable `{{name}}` should not use var
22+
,-[files/index.js:3:1]
23+
2 | var testWithName = {};
24+
3 | var testWithNameNoData = {};
25+
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
26+
4 | var testWithMultiple = {};
27+
`----
28+
29+
x interpolation-test(no-var): Variable `testWithMultiple` of type `string` should not use var
30+
,-[files/index.js:4:1]
31+
3 | var testWithNameNoData = {};
32+
4 | var testWithMultiple = {};
33+
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
34+
5 | var testWithMultipleNoData = {};
35+
`----
36+
37+
x interpolation-test(no-var): Variable `{{name}}` of type `{{type}}` should not use var
38+
,-[files/index.js:5:1]
39+
4 | var testWithMultiple = {};
40+
5 | var testWithMultipleNoData = {};
41+
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42+
6 | var testWithMissingData = {};
43+
`----
44+
45+
x interpolation-test(no-var): Value is `example` and name is `{{name}}`
46+
,-[files/index.js:6:1]
47+
5 | var testWithMultipleNoData = {};
48+
6 | var testWithMissingData = {};
49+
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50+
7 | var testWithSpaces = {};
51+
`----
52+
53+
x interpolation-test(no-var): Value with spaces is `hello` and name is `world`
54+
,-[files/index.js:7:1]
55+
6 | var testWithMissingData = {};
56+
7 | var testWithSpaces = {};
57+
: ^^^^^^^^^^^^^^^^^^^^^^^^
58+
`----
59+
60+
Found 0 warnings and 7 errors.
61+
Finished in Xms on 1 file using X threads.
62+
```
63+
64+
# stderr
65+
```
66+
WARNING: JS plugins are experimental and not subject to semver.
67+
Breaking changes are possible while JS plugins support is under development.
68+
```
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { Plugin } from '../../../dist/index.js';
2+
3+
const plugin: Plugin = {
4+
meta: {
5+
name: 'interpolation-test',
6+
},
7+
rules: {
8+
'no-var': {
9+
create(context) {
10+
return {
11+
VariableDeclaration(node) {
12+
if (node.kind === 'var') {
13+
const declarations = node.declarations;
14+
if (declarations.length > 0) {
15+
const firstDeclaration = declarations[0];
16+
if (firstDeclaration.id.type === 'Identifier') {
17+
const name = firstDeclaration.id.name;
18+
19+
if (name === 'testWithNoData') {
20+
// Test with no placeholders, no data
21+
context.report({
22+
message: 'Variables should not use var',
23+
node,
24+
});
25+
} else if (name === 'testWithName') {
26+
// Test with single placeholder
27+
context.report({
28+
message: 'Variable `{{name}}` should not use var',
29+
node,
30+
data: { name },
31+
});
32+
} else if (name === 'testWithNameNoData') {
33+
// Test with single placeholder, but no data
34+
context.report({
35+
message: 'Variable `{{name}}` should not use var',
36+
node,
37+
});
38+
} else if (name === 'testWithMultiple') {
39+
// Test with multiple placeholders
40+
context.report({
41+
message: 'Variable `{{name}}` of type `{{type}}` should not use var',
42+
node,
43+
data: {
44+
name,
45+
type: 'string',
46+
},
47+
});
48+
} else if (name === 'testWithMultipleNoData') {
49+
// Test with multiple placeholders, but no data
50+
context.report({
51+
message: 'Variable `{{name}}` of type `{{type}}` should not use var',
52+
node,
53+
});
54+
} else if (name === 'testWithMissingData') {
55+
// Test missing data - placeholder should remain
56+
context.report({
57+
message: 'Value is `{{value}}` and name is `{{name}}`',
58+
node,
59+
data: {
60+
value: 'example',
61+
// name is missing
62+
},
63+
});
64+
} else if (name === 'testWithSpaces') {
65+
// Test whitespace in placeholders
66+
context.report({
67+
message: 'Value with spaces is `{{ value }}` and name is `{{ name }}`',
68+
node,
69+
data: {
70+
value: 'hello',
71+
name: 'world',
72+
},
73+
});
74+
}
75+
}
76+
}
77+
}
78+
},
79+
};
80+
},
81+
},
82+
},
83+
};
84+
85+
export default plugin;

0 commit comments

Comments
 (0)