diff --git a/apps/oxlint/src-js/index.ts b/apps/oxlint/src-js/index.ts index e2a625bc2bb43..458921b5180df 100644 --- a/apps/oxlint/src-js/index.ts +++ b/apps/oxlint/src-js/index.ts @@ -2,7 +2,7 @@ import type { Context } from './plugins/context.ts'; import type { CreateOnceRule, Plugin, Rule } from './plugins/load.ts'; import type { BeforeHook, Visitor, VisitorWithHooks } from './plugins/types.ts'; -export type { Context, Diagnostic } from './plugins/context.ts'; +export type { Context, Diagnostic, DiagnosticBase, DiagnosticWithLoc, DiagnosticWithNode } from './plugins/context.ts'; export type { Fix, Fixer, FixFn, Range } from './plugins/fix.ts'; export type { CreateOnceRule, CreateRule, Plugin, Rule } from './plugins/load.ts'; export type { diff --git a/apps/oxlint/src-js/plugins/context.ts b/apps/oxlint/src-js/plugins/context.ts index 5d782dab4d98e..c8461c646871c 100644 --- a/apps/oxlint/src-js/plugins/context.ts +++ b/apps/oxlint/src-js/plugins/context.ts @@ -1,17 +1,28 @@ import { getFixes } from './fix.js'; -import { SOURCE_CODE } from './source_code.js'; +import { getIndexFromLoc, SOURCE_CODE } from './source_code.js'; import type { Fix, FixFn } from './fix.ts'; import type { SourceCode } from './source_code.ts'; -import type { Node } from './types.ts'; +import type { Location, Node } from './types.ts'; + +const { hasOwn } = Object; // Diagnostic in form passed by user to `Context#report()` -export interface Diagnostic { +export type Diagnostic = DiagnosticWithNode | DiagnosticWithLoc; + +export interface DiagnosticBase { message: string; - node: Node; fix?: FixFn; } +export interface DiagnosticWithNode extends DiagnosticBase { + node: Node; +} + +export interface DiagnosticWithLoc extends DiagnosticBase { + loc: Location; +} + // Diagnostic in form sent to Rust interface DiagnosticReport { message: string; @@ -127,16 +138,38 @@ export class Context { /** * Report error. * @param diagnostic - Diagnostic object + * @throws {TypeError} If `diagnostic` is invalid */ report(diagnostic: Diagnostic): void { const internal = getInternal(this, 'report errors'); // TODO: Validate `diagnostic` - const { node } = diagnostic; + let start: number, end: number, loc: Location; + if (hasOwn(diagnostic, 'loc') && (loc = (diagnostic as DiagnosticWithLoc).loc) != null) { + // `loc` + if (typeof loc !== 'object') throw new TypeError('`loc` must be an object'); + start = getIndexFromLoc(loc.start); + end = getIndexFromLoc(loc.end); + } else { + // `node` + const { node } = diagnostic as DiagnosticWithNode; + if (node == null) throw new TypeError('Either `node` or `loc` is required'); + if (typeof node !== 'object') throw new TypeError('`node` must be an object'); + ({ start, end } = node); + // Do type validation checks here, to ensure no error in serialization / deserialization. + // Range validation happens on Rust side. + if ( + typeof start !== 'number' || typeof end !== 'number' || + start < 0 || end < 0 || (start | 0) !== start || (end | 0) !== end + ) { + throw new TypeError('`node.start` and `node.end` must be non-negative integers'); + } + } + diagnostics.push({ message: diagnostic.message, - start: node.start, - end: node.end, + start, + end, ruleIndex: internal.ruleIndex, fixes: getFixes(diagnostic, internal), }); diff --git a/apps/oxlint/src-js/plugins/source_code.ts b/apps/oxlint/src-js/plugins/source_code.ts index bfdc0f131af94..d16951c32d776 100644 --- a/apps/oxlint/src-js/plugins/source_code.ts +++ b/apps/oxlint/src-js/plugins/source_code.ts @@ -608,7 +608,7 @@ function getLocFromIndex(offset: number): LineColumn { * @throws {TypeError|RangeError} If `loc` is not an object with a numeric `line` and `column`, * or if the `line` is less than or equal to zero, or the line or column is out of the expected range. */ -function getIndexFromLoc(loc: LineColumn): number { +export function getIndexFromLoc(loc: LineColumn): number { if (loc !== null && typeof loc === 'object') { const { line, column } = loc; if (typeof line === 'number' && typeof column === 'number' && (line | 0) === line && (column | 0) === column) { diff --git a/apps/oxlint/test/e2e.test.ts b/apps/oxlint/test/e2e.test.ts index fe26c30d7e400..f90cc04bd3c05 100644 --- a/apps/oxlint/test/e2e.test.ts +++ b/apps/oxlint/test/e2e.test.ts @@ -115,6 +115,10 @@ describe('oxlint CLI', () => { await testFixture('basic_custom_plugin_multiple_rules'); }); + it('should support reporting diagnostic with `loc`', async () => { + await testFixture('diagnostic_loc'); + }); + it('should receive ESTree-compatible AST', async () => { await testFixture('estree'); }); diff --git a/apps/oxlint/test/fixtures/diagnostic_loc/.oxlintrc.json b/apps/oxlint/test/fixtures/diagnostic_loc/.oxlintrc.json new file mode 100644 index 0000000000000..75cb7aee3a4cb --- /dev/null +++ b/apps/oxlint/test/fixtures/diagnostic_loc/.oxlintrc.json @@ -0,0 +1,7 @@ +{ + "jsPlugins": ["./plugin.ts"], + "categories": { "correctness": "off" }, + "rules": { + "loc-plugin/no-bugger": "error" + } +} diff --git a/apps/oxlint/test/fixtures/diagnostic_loc/files/index.js b/apps/oxlint/test/fixtures/diagnostic_loc/files/index.js new file mode 100644 index 0000000000000..0b95cd14b0f94 --- /dev/null +++ b/apps/oxlint/test/fixtures/diagnostic_loc/files/index.js @@ -0,0 +1,3 @@ +debugger; +debugger; +debugger; diff --git a/apps/oxlint/test/fixtures/diagnostic_loc/output.snap.md b/apps/oxlint/test/fixtures/diagnostic_loc/output.snap.md new file mode 100644 index 0000000000000..7c2b9101b51ec --- /dev/null +++ b/apps/oxlint/test/fixtures/diagnostic_loc/output.snap.md @@ -0,0 +1,43 @@ +# Exit code +1 + +# stdout +``` + x loc-plugin(no-bugger): Bugger! + ,-[files/index.js:1:3] + 1 | debugger; + : ^^^^^^ + 2 | debugger; + `---- + + x loc-plugin(no-bugger): Bugger debugger debug! + ,-[files/index.js:1:3] + 1 | ,-> debugger; + 2 | | debugger; + 3 | `-> debugger; + `---- + + x loc-plugin(no-bugger): Bugger! + ,-[files/index.js:2:3] + 1 | debugger; + 2 | debugger; + : ^^^^^^ + 3 | debugger; + `---- + + x loc-plugin(no-bugger): Bugger! + ,-[files/index.js:3:3] + 2 | debugger; + 3 | debugger; + : ^^^^^^ + `---- + +Found 0 warnings and 4 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. +``` diff --git a/apps/oxlint/test/fixtures/diagnostic_loc/plugin.ts b/apps/oxlint/test/fixtures/diagnostic_loc/plugin.ts new file mode 100644 index 0000000000000..6c2baf031b481 --- /dev/null +++ b/apps/oxlint/test/fixtures/diagnostic_loc/plugin.ts @@ -0,0 +1,37 @@ +import type { Plugin } from '../../../dist/index.js'; + +const plugin: Plugin = { + meta: { + name: 'loc-plugin', + }, + rules: { + 'no-bugger': { + create(context) { + let debuggerCount = 0; + return { + Program(_node) { + context.report({ + message: 'Bugger debugger debug!', + loc: { + start: { line: 1, column: 2 }, + end: { line: 3, column: 5 }, + }, + }); + }, + DebuggerStatement(_node) { + debuggerCount++; + context.report({ + message: 'Bugger!', + loc: { + start: { line: debuggerCount, column: 2 }, + end: { line: debuggerCount, column: 8 }, + }, + }); + }, + }; + }, + }, + }, +}; + +export default plugin;