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
36 changes: 16 additions & 20 deletions apps/oxlint/src-js/plugins/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type Diagnostic = DiagnosticWithNode | DiagnosticWithLoc | DiagnosticWith

export interface DiagnosticBase {
message?: string;
data?: Record<string, string | number> | null | undefined;
fix?: FixFn;
}

Expand All @@ -28,7 +29,6 @@ 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 @@ -162,14 +162,26 @@ export class Context {
let message: string;
if (hasOwn(diagnostic, 'messageId')) {
const diagWithMessageId = diagnostic as DiagnosticWithMessageId;
message = resolveMessage(diagWithMessageId.messageId, diagWithMessageId.data, internal);
message = resolveMessage(diagWithMessageId.messageId, internal);
} else {
message = diagnostic.message;
if (typeof message !== 'string') {
throw new TypeError('Either `message` or `messageId` is required');
}
}

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

// TODO: Validate `diagnostic`
let start: number, end: number, loc: Location;

Expand Down Expand Up @@ -231,16 +243,11 @@ export class Context {
/**
* Resolve a message ID 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`
*/
function resolveMessage(
messageId: string,
data: Record<string, string | number> | undefined,
internal: InternalContext,
): string {
function resolveMessage(messageId: string, internal: InternalContext): string {
const { messages } = internal;

if (!messages) {
Expand All @@ -255,16 +262,5 @@ function resolveMessage(
);
}

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;
return messages[messageId];
}
4 changes: 4 additions & 0 deletions apps/oxlint/test/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ describe('oxlint CLI', () => {
await testFixture('basic_custom_plugin');
});

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

it('should support messageId', async () => {
await testFixture('message_id_plugin');
});
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,7 @@
var testWithNoData = {};
var testWithName = {};
var testWithNameNoData = {};
var testWithMultiple = {};
var testWithMultipleNoData = {};
var testWithMissingData = {};
var testWithSpaces = {};
68 changes: 68 additions & 0 deletions apps/oxlint/test/fixtures/message_interpolation/output.snap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Exit code
1

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

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

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

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

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

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

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

Found 0 warnings and 7 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.
```
85 changes: 85 additions & 0 deletions apps/oxlint/test/fixtures/message_interpolation/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { Plugin } from '../../../dist/index.js';

const plugin: Plugin = {
meta: {
name: 'interpolation-test',
},
rules: {
'no-var': {
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;

if (name === 'testWithNoData') {
// Test with no placeholders, no data
context.report({
message: 'Variables should not use var',
node,
});
} else if (name === 'testWithName') {
// Test with single placeholder
context.report({
message: 'Variable `{{name}}` should not use var',
node,
data: { name },
});
} else if (name === 'testWithNameNoData') {
// Test with single placeholder, but no data
context.report({
message: 'Variable `{{name}}` should not use var',
node,
});
} else if (name === 'testWithMultiple') {
// Test with multiple placeholders
context.report({
message: 'Variable `{{name}}` of type `{{type}}` should not use var',
node,
data: {
name,
type: 'string',
},
});
} else if (name === 'testWithMultipleNoData') {
// Test with multiple placeholders, but no data
context.report({
message: 'Variable `{{name}}` of type `{{type}}` should not use var',
node,
});
} else if (name === 'testWithMissingData') {
// Test missing data - placeholder should remain
context.report({
message: 'Value is `{{value}}` and name is `{{name}}`',
node,
data: {
value: 'example',
// name is missing
},
});
} else if (name === 'testWithSpaces') {
// Test whitespace in placeholders
context.report({
message: 'Value with spaces is `{{ value }}` and name is `{{ name }}`',
node,
data: {
value: 'hello',
name: 'world',
},
});
}
}
}
}
},
};
},
},
},
};

export default plugin;
Loading