Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions apps/oxlint/src-js/plugins/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface DiagnosticWithLoc extends DiagnosticBase {

export interface DiagnosticWithMessageId extends DiagnosticBase {
messageId: string;
data?: Record<string, string | number>;
node?: Ranged;
loc?: Location;
}
Expand Down Expand Up @@ -161,7 +162,7 @@ export class Context {
let message: string;
if (hasOwn(diagnostic, 'messageId')) {
const diagWithMessageId = diagnostic as DiagnosticWithMessageId;
message = this.#resolveMessage(diagWithMessageId.messageId, internal);
message = this.#resolveMessage(diagWithMessageId.messageId, diagWithMessageId.data, internal);
} else {
message = diagnostic.message;
if (typeof message !== 'string') {
Expand Down Expand Up @@ -212,14 +213,16 @@ export class Context {
}

/**
* Resolve a messageId to its message string.
* Resolve a messageId to its message string, with optional data interpolation.
* @param messageId - The message ID to resolve
* @param data - Optional data for placeholder interpolation
* @param internal - Internal context containing messages
* @returns Resolved message string
* @throws {Error} If messageId is not found in messages
*/
#resolveMessage(
messageId: string,
data: Record<string, string | number> | undefined,
internal: InternalContext,
): string {
const { messages } = internal;
Expand All @@ -236,7 +239,18 @@ export class Context {
);
}

return messages[messageId];
let message = messages[messageId];

// Interpolate placeholders {{key}} with data values
if (data) {
message = message.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
key = key.trim();
const value = data[key];
return value !== undefined ? String(value) : match;
});
}

return message;
}

static {
Expand Down
4 changes: 4 additions & 0 deletions apps/oxlint/test/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ describe('oxlint CLI', () => {
await testFixture('message_id_plugin');
});

it('should support messageId placeholder interpolation', async () => {
await testFixture('message_id_interpolation');
});

it('should report an error for unknown messageId', async () => {
await testFixture('message_id_error');
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"jsPlugins": ["./plugin.ts"],
"categories": {
"correctness": "off"
},
"rules": {
"interpolation-test/no-var": "error"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
var testWithNoData = {};
var testWithName = {};
var testWithMultiple = {};
var testWithMissingData = {};
var testWithSpaces = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Exit code
1

# stdout
```
x interpolation-test(no-var): Variable {{name}} should not use var
,-[files/index.js:1:1]
1 | var testWithNoData = {};
: ^^^^^^^^^^^^^^^^^^^^^^^^
2 | var testWithName = {};
`----

x interpolation-test(no-var): Variable testWithName of type string should not use var
,-[files/index.js:2:1]
1 | var testWithNoData = {};
2 | var testWithName = {};
: ^^^^^^^^^^^^^^^^^^^^^^
3 | var testWithMultiple = {};
`----

x interpolation-test(no-var): Variable testWithMultiple of type number should not use var
,-[files/index.js:3:1]
2 | var testWithName = {};
3 | var testWithMultiple = {};
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
4 | var testWithMissingData = {};
`----

x interpolation-test(no-var): Value is example and name is {{name}}
,-[files/index.js:4:1]
3 | var testWithMultiple = {};
4 | var testWithMissingData = {};
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5 | var testWithSpaces = {};
`----

x interpolation-test(no-var): Value with spaces: hello and name: world
,-[files/index.js:5:1]
4 | var testWithMissingData = {};
5 | var testWithSpaces = {};
: ^^^^^^^^^^^^^^^^^^^^^^^^
`----

Found 0 warnings and 5 errors.
Finished in Xms on 1 file using X threads.
```

# stderr
```
WARNING: JS plugins are experimental and not subject to semver.
Breaking changes are possible while JS plugins support is under development.
```
86 changes: 86 additions & 0 deletions apps/oxlint/test/fixtures/message_id_interpolation/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { Plugin } from '../../../dist/index.js';

const plugin: Plugin = {
meta: {
name: 'interpolation-test',
},
rules: {
'no-var': {
meta: {
messages: {
noData: 'Variables should not use var',
withName: 'Variable {{name}} should not use var',
withMultiple: 'Variable {{name}} of type {{type}} should not use var',
// edge cases
missingData: 'Value is {{value}} and name is {{name}}',
withSpaces: 'Value with spaces: {{ value }} and name: {{ name }}',
},
},
create(context) {
return {
VariableDeclaration(node) {
if (node.kind === 'var') {
const declarations = node.declarations;
if (declarations.length > 0) {
const firstDeclaration = declarations[0];
if (firstDeclaration.id.type === 'Identifier') {
const name = firstDeclaration.id.name;

// Test with single placeholder
if (name === 'testWithNoData') {
context.report({
messageId: 'withName',
node,
});
} // Test with multiple placeholders
else if (name === 'testWithName') {
context.report({
messageId: 'withMultiple',
node,
data: {
name,
type: 'string',
},
});
} // Test without data
else if (name === 'testWithMultiple') {
context.report({
messageId: 'withMultiple',
node,
data: {
name,
type: 'number',
},
});
} else if (name === 'testWithMissingData') {
// Test missing data - placeholder should remain
context.report({
messageId: 'missingData',
node,
data: {
value: 'example',
// name is missing
},
});
} else if (name === 'testWithSpaces') {
// Test whitespace in placeholders
context.report({
messageId: 'withSpaces',
node,
data: {
value: 'hello',
name: 'world',
},
});
}
}
}
}
},
};
},
},
},
};

export default plugin;
Loading