@@ -123,9 +123,16 @@ export interface IOxlintPluginOptions {
123123 noErrorOnUnmatchedPattern ?: boolean ;
124124 /** Disable automatic loading of nested configuration files. Maps to `--disable-nested-config`. */
125125 disableNestedConfig ?: boolean ;
126- /** Enable rules that require type information. Maps to `--type-aware`. */
126+ /**
127+ * Enable rules that require type information. Maps to `--type-aware`. Requires the
128+ * `oxlint-tsgolint` package, which is resolved from the consuming project or its shared rig.
129+ */
127130 typeAware ?: boolean ;
128- /** Enable experimental type checking (includes TypeScript compiler diagnostics). Maps to `--type-check`. */
131+ /**
132+ * Enable experimental type checking (includes TypeScript compiler diagnostics). Maps to
133+ * `--type-check`. Requires the `oxlint-tsgolint` package, which is resolved from the consuming
134+ * project or its shared rig.
135+ */
129136 typeCheck ?: boolean ;
130137 /** Report unused inline disable directives. Maps to `--report-unused-disable-directives`. */
131138 reportUnusedDisableDirectives ?: boolean ;
@@ -140,8 +147,9 @@ export interface IOxlintPluginOptions {
140147
141148 /**
142149 * If specified, a Static Analysis Results Interchange Format (SARIF) log of all findings will be
143- * written to the provided path, relative to the project root. This is produced by an additional
144- * oxlint invocation using `--format=sarif`.
150+ * written to the provided path, relative to the project root. When set, oxlint is run once with
151+ * `--format=sarif` and the reported diagnostics are extracted from that same SARIF output, so no
152+ * additional oxlint invocation is incurred.
145153 */
146154 sarifLogPath ?: string ;
147155}
@@ -175,6 +183,37 @@ export interface IOxlintJsonOutput {
175183 diagnostics : IOxlintDiagnostic [ ] ;
176184}
177185
186+ // ----- SARIF types (subset needed for diagnostic extraction) -----
187+
188+ export interface IOxlintSarifRegion {
189+ startLine ?: number ;
190+ startColumn ?: number ;
191+ }
192+
193+ export interface IOxlintSarifPhysicalLocation {
194+ artifactLocation ?: { uri ?: string } ;
195+ region ?: IOxlintSarifRegion ;
196+ }
197+
198+ export interface IOxlintSarifLocation {
199+ physicalLocation ?: IOxlintSarifPhysicalLocation ;
200+ }
201+
202+ export interface IOxlintSarifResult {
203+ level ?: string ;
204+ message ?: { text ?: string } ;
205+ ruleId ?: string ;
206+ locations ?: IOxlintSarifLocation [ ] ;
207+ }
208+
209+ export interface IOxlintSarifRun {
210+ results ?: IOxlintSarifResult [ ] ;
211+ }
212+
213+ export interface IOxlintSarifLog {
214+ runs ?: IOxlintSarifRun [ ] ;
215+ }
216+
178217const DEFAULT_PATHS : ReadonlyArray < string > = [ 'src' ] ;
179218
180219// The file extensions that oxlint is able to lint. Changed files with any other extension (for
@@ -361,3 +400,138 @@ export function createFileErrorForDiagnostic(
361400 column : span ?. column
362401 } ) ;
363402}
403+
404+ /**
405+ * Extracts diagnostics from an oxlint SARIF log, converting each SARIF result into the common
406+ * {@link IOxlintDiagnostic} format used for error reporting.
407+ */
408+ export function extractDiagnosticsFromSarif ( sarifLog : IOxlintSarifLog ) : IOxlintDiagnostic [ ] {
409+ const diagnostics : IOxlintDiagnostic [ ] = [ ] ;
410+
411+ for ( const run of sarifLog . runs ?? [ ] ) {
412+ for ( const result of run . results ?? [ ] ) {
413+ const location : IOxlintSarifLocation | undefined = result . locations ?. [ 0 ] ;
414+ const physicalLocation : IOxlintSarifPhysicalLocation | undefined = location ?. physicalLocation ;
415+ const uri : string = physicalLocation ?. artifactLocation ?. uri ?? '' ;
416+ const region : IOxlintSarifRegion | undefined = physicalLocation ?. region ;
417+
418+ let severity : 'error' | 'warning' | 'advice' ;
419+ switch ( result . level ) {
420+ case 'error' :
421+ severity = 'error' ;
422+ break ;
423+ case 'note' :
424+ severity = 'advice' ;
425+ break ;
426+ default :
427+ severity = 'warning' ;
428+ break ;
429+ }
430+
431+ const diagnostic : IOxlintDiagnostic = {
432+ message : result . message ?. text ?? '' ,
433+ code : result . ruleId ,
434+ severity,
435+ filename : uri ,
436+ labels : region
437+ ? [ { span : { offset : 0 , length : 0 , line : region . startLine ?? 0 , column : region . startColumn ?? 0 } } ]
438+ : undefined
439+ } ;
440+
441+ diagnostics . push ( diagnostic ) ;
442+ }
443+ }
444+
445+ return diagnostics ;
446+ }
447+
448+ /**
449+ * The maximum length (in characters) permitted for a single oxlint command line.
450+ *
451+ * @remarks
452+ * Windows caps the command line passed to `CreateProcess` at 32767 characters. We stay well under
453+ * that limit to leave room for the Node executable path, the oxlint bin path, and any argument
454+ * quoting overhead applied by the operating system.
455+ */
456+ export const MAX_COMMAND_LINE_LENGTH : number = 30000 ;
457+
458+ /**
459+ * Splits the positional lint paths into batches such that each resulting oxlint command line stays
460+ * within {@link MAX_COMMAND_LINE_LENGTH}. This avoids exceeding the operating system's command-line
461+ * length limit (notably on Windows, where a large set of changed files could otherwise overflow the
462+ * ~32 KiB `CreateProcess` limit).
463+ *
464+ * @param prefixArgs - The command-line tokens that precede the paths on every invocation (for
465+ * example the Node executable, the oxlint bin path, and all non-path oxlint arguments).
466+ * @param lintPaths - The positional file or folder paths to lint.
467+ * @param maxCommandLineLength - The maximum permitted command-line length.
468+ * @returns One or more batches of lint paths. A single path that on its own exceeds the limit is
469+ * still returned in its own batch, since it cannot be split any further. Always returns at least
470+ * one batch so that callers can iterate uniformly.
471+ */
472+ export function batchLintPaths (
473+ prefixArgs : ReadonlyArray < string > ,
474+ lintPaths : ReadonlyArray < string > ,
475+ maxCommandLineLength : number = MAX_COMMAND_LINE_LENGTH
476+ ) : string [ ] [ ] {
477+ // Length contributed by the fixed prefix, counting a separating space before each token.
478+ let prefixLength : number = 0 ;
479+ for ( const arg of prefixArgs ) {
480+ prefixLength += arg . length + 1 ;
481+ }
482+
483+ const batches : string [ ] [ ] = [ ] ;
484+ let currentBatch : string [ ] = [ ] ;
485+ let currentLength : number = prefixLength ;
486+
487+ for ( const lintPath of lintPaths ) {
488+ const pathLength : number = lintPath . length + 1 ;
489+ if ( currentBatch . length > 0 && currentLength + pathLength > maxCommandLineLength ) {
490+ batches . push ( currentBatch ) ;
491+ currentBatch = [ ] ;
492+ currentLength = prefixLength ;
493+ }
494+ currentBatch . push ( lintPath ) ;
495+ currentLength += pathLength ;
496+ }
497+
498+ if ( currentBatch . length > 0 ) {
499+ batches . push ( currentBatch ) ;
500+ }
501+
502+ if ( batches . length === 0 ) {
503+ batches . push ( [ ] ) ;
504+ }
505+
506+ return batches ;
507+ }
508+
509+ /**
510+ * Merges the raw SARIF log output produced by one or more oxlint invocations into a single SARIF
511+ * document.
512+ *
513+ * @remarks
514+ * When linting is split into multiple batches (see {@link batchLintPaths}), each batch produces its
515+ * own SARIF log. This concatenates the `runs` from every batch into the first log so that all of the
516+ * findings are preserved in one file. When only a single log is provided, its original text is
517+ * returned unchanged so that the output is byte-for-byte identical to what oxlint emitted.
518+ *
519+ * @param rawSarifLogs - The raw (unparsed) SARIF stdout captured from each oxlint invocation.
520+ * @returns The merged SARIF log serialized as a string.
521+ */
522+ export function mergeSarifLogs ( rawSarifLogs : ReadonlyArray < string > ) : string {
523+ if ( rawSarifLogs . length === 1 ) {
524+ return rawSarifLogs [ 0 ] ;
525+ }
526+
527+ const base : { runs ?: unknown [ ] } = JSON . parse ( rawSarifLogs [ 0 ] ) ;
528+ base . runs = base . runs ?? [ ] ;
529+ for ( let i : number = 1 ; i < rawSarifLogs . length ; ++ i ) {
530+ const next : { runs ?: unknown [ ] } = JSON . parse ( rawSarifLogs [ i ] ) ;
531+ if ( next . runs ) {
532+ base . runs . push ( ...next . runs ) ;
533+ }
534+ }
535+
536+ return JSON . stringify ( base , undefined , 2 ) ;
537+ }
0 commit comments