Skip to content

Commit 2d45503

Browse files
committed
feat(linter/plugins): support placeholders in messageIds
1 parent e70f4fa commit 2d45503

File tree

6 files changed

+173
-3
lines changed

6 files changed

+173
-3
lines changed

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface DiagnosticWithLoc extends DiagnosticBase {
2828

2929
export interface DiagnosticWithMessageId extends DiagnosticBase {
3030
messageId: string;
31+
data?: Record<string, string | number>;
3132
node?: Ranged;
3233
loc?: Location;
3334
}
@@ -161,7 +162,7 @@ export class Context {
161162
let message: string;
162163
if (hasOwn(diagnostic, 'messageId')) {
163164
const diagWithMessageId = diagnostic as DiagnosticWithMessageId;
164-
message = this.#resolveMessage(diagWithMessageId.messageId, internal);
165+
message = this.#resolveMessage(diagWithMessageId.messageId, diagWithMessageId.data, internal);
165166
} else {
166167
message = diagnostic.message;
167168
if (typeof message !== 'string') {
@@ -212,14 +213,16 @@ export class Context {
212213
}
213214

214215
/**
215-
* Resolve a messageId to its message string.
216+
* Resolve a messageId to its message string, with optional data interpolation.
216217
* @param messageId - The message ID to resolve
218+
* @param data - Optional data for placeholder interpolation
217219
* @param internal - Internal context containing messages
218220
* @returns Resolved message string
219221
* @throws {Error} If messageId is not found in messages
220222
*/
221223
#resolveMessage(
222224
messageId: string,
225+
data: Record<string, string | number> | undefined,
223226
internal: InternalContext,
224227
): string {
225228
const { messages } = internal;
@@ -236,7 +239,18 @@ export class Context {
236239
);
237240
}
238241

239-
return messages[messageId];
242+
let message = messages[messageId];
243+
244+
// Interpolate placeholders {{key}} with data values
245+
if (data) {
246+
message = message.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
247+
key = key.trim();
248+
const value = data[key];
249+
return value !== undefined ? String(value) : match;
250+
});
251+
}
252+
253+
return message;
240254
}
241255

242256
static {

apps/oxlint/test/e2e.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ describe('oxlint CLI', () => {
5353
await testFixture('message_id_plugin');
5454
});
5555

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

0 commit comments

Comments
 (0)