perf(linter): share one source per file across type-aware diagnostics (#27462)

`DiagnosticHandler::send_diagnostic` in
`crates/oxc_linter/src/tsgolint.rs` copied the file's whole source text
into every type-aware diagnostic it sent:
`get_source_text(path).to_string()`, then `NamedSource::new(..,
source_text.to_owned())` in `wrap_diagnostics`. Each copy lives as long
as its diagnostic, so the oxlint process grows with diagnostics × file
size.

This builds one `Arc<NamedSource>` per file (new
`DiagnosticService::named_source`, which `wrap_diagnostics` now uses
too) and shares it across that file's diagnostics. Output is unchanged.

vscode `src` (10,201 files) with every type-aware rule enabled, `oxlint
--type-aware` with oxlint-tsgolint 7.0.2003, 316k diagnostics, release
build on an 18-core Mac, two runs each:

| | before | after |
|---|---:|---:|
| oxlint process, peak RSS | 30.5 / 31.8 GiB | 0.7 / 0.7 GiB |
| oxlint + tsgolint, peak RSS | 39.5 / 41.0 GiB | 10.0 / 10.0 GiB |
| wall | 24.0 / 18.8 s | 18.1 / 15.8 s |

The build measured here also carries the in-process tsrs backend from
the draft below, which is inactive when oxlint runs the Go tsgolint. On
a 27k-file project with 21 diagnostics, time and memory do not change.

Checked: the type-aware fixture tests (`cargo test -p oxlint --
tsgolint`, 27) pass unchanged, as do `oxc_diagnostics` and
`oxc_linter`'s unit tests; clippy is clean with warnings denied.

Also the second commit of:

- https://github.com/oxc-project/oxc/pull/27461

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Max SchwenkandClaude Opus 5.5 authored and GitHub committed 2026-10-08 16:56:10 +00:00
1 parent 316bf847f8
commit 8c57c5ecce
2 files changed
+34 -14

No files matched your search

+16 -5
View File
@@ -116,6 +116,21 @@ impl DiagnosticService {
source_text: &str,
diagnostics: Vec<OxcDiagnostic>,
) -> Vec<Error> {
let source = Self::named_source(cwd, path, source_text);
diagnostics
.into_iter()
.map(|diagnostic| diagnostic.with_source_code(Arc::clone(&source)))
.collect()
}
/// The source code and display path [`wrap_diagnostics`](DiagnosticService::wrap_diagnostics) attaches. Each
/// call copies `source_text`, so a caller that wraps one file's diagnostics a few at a time should build this once
/// per file and share it with [`OxcDiagnostic::with_source_code`].
pub fn named_source<C: AsRef<Path>, P: AsRef<Path>>(
cwd: C,
path: P,
source_text: &str,
) -> Arc<NamedSource<String>> {
// TODO: This causes snapshots to fail when running tests through a JetBrains terminal.
let is_jetbrains =
std::env::var("TERMINAL_EMULATOR").is_ok_and(|x| x.eq("JetBrains-JediTerm"));
@@ -129,11 +144,7 @@ impl DiagnosticService {
normalized_path.to_string()
});
let source = Arc::new(NamedSource::new(path_display, source_text.to_owned()));
diagnostics
.into_iter()
.map(|diagnostic| diagnostic.with_source_code(Arc::clone(&source)))
.collect()
Arc::new(NamedSource::new(path_display, source_text.to_owned()))
}
/// # Panics
+18 -9
View File
@@ -13,7 +13,7 @@ use oxc_allocator::Allocator;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use oxc_diagnostics::{DiagnosticSender, DiagnosticService, OxcDiagnostic, Severity};
use oxc_diagnostics::{DiagnosticSender, DiagnosticService, NamedSource, OxcDiagnostic, Severity};
use oxc_span::{SourceType, Span};
use super::{AllowWarnDeny, ConfigStore, DisableDirectives, ResolvedLinterState, read_to_string};
@@ -1063,6 +1063,8 @@ struct DiagnosticHandler {
silent: bool,
should_fix: bool,
source_text_cache: SourceTextCache,
/// One source per file, shared by every diagnostic of that file that is sent as it arrives.
named_sources: FxHashMap<PathBuf, Arc<NamedSource<String>>>,
error_sender: DiagnosticSender,
/// Messages requiring fixes, grouped by file path: messages.
messages_requiring_fixes: FxHashMap<PathBuf, Vec<Message>>,
@@ -1076,6 +1078,7 @@ impl DiagnosticHandler {
silent,
should_fix,
source_text_cache: SourceTextCache::default(),
named_sources: FxHashMap::default(),
error_sender,
messages_requiring_fixes: FxHashMap::default(),
messages_not_requiring_fixes: FxHashMap::default(),
@@ -1145,19 +1148,25 @@ impl DiagnosticHandler {
oxc_diagnostic: OxcDiagnostic,
severity: AllowWarnDeny,
) {
let source_text = self.get_source_text(path).to_string();
let oxc_diagnostic = oxc_diagnostic.with_severity(if severity == AllowWarnDeny::Deny {
Severity::Error
} else {
Severity::Warning
});
let diagnostics = DiagnosticService::wrap_diagnostics(
&self.cwd,
path,
&source_text,
vec![oxc_diagnostic],
);
self.error_sender.send(diagnostics).expect("Failed to send diagnostics");
let source = self.named_source(path);
self.error_sender
.send(vec![oxc_diagnostic.with_source_code(source)])
.expect("Failed to send diagnostics");
}
fn named_source(&mut self, path: &Path) -> Arc<NamedSource<String>> {
if let Some(source) = self.named_sources.get(path) {
return Arc::clone(source);
}
let cwd = self.cwd.clone();
let source = DiagnosticService::named_source(cwd, path, self.get_source_text(path));
self.named_sources.insert(path.to_path_buf(), Arc::clone(&source));
source
}
/// Consume the handler and return collected messages requiring fixes.