Skip to content

Commit 8be432a

Browse files
committed
refactor(tsgolint): use an iterator for tsgolint message parsing (#14297)
1 parent 2e57351 commit 8be432a

File tree

1 file changed

+117
-96
lines changed

1 file changed

+117
-96
lines changed

crates/oxc_linter/src/tsgolint.rs

Lines changed: 117 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -127,114 +127,81 @@ impl TsGoLintState {
127127
drop(stdin);
128128

129129
// Stream diagnostics as they are emitted, rather than waiting for all output
130-
let mut stdout = child.stdout.take().expect("Failed to open tsgolint stdout");
130+
let stdout = child.stdout.take().expect("Failed to open tsgolint stdout");
131131

132132
// Process stdout stream in a separate thread to send diagnostics as they arrive
133133
let cwd_clone = self.cwd.clone();
134134

135135
let stdout_handler = std::thread::spawn(move || -> Result<(), String> {
136-
let mut buffer = Vec::with_capacity(8192);
137-
let mut read_buf = [0u8; 8192];
136+
let msg_iter = TsGoLintMessageStream::new(stdout);
138137

139138
let mut source_text_map: FxHashMap<PathBuf, String> = FxHashMap::default();
140139

141-
loop {
142-
match stdout.read(&mut read_buf) {
143-
Ok(0) => break, // EOF
144-
Ok(n) => {
145-
buffer.extend_from_slice(&read_buf[..n]);
146-
147-
// Try to parse complete messages from buffer
148-
let mut cursor = std::io::Cursor::new(buffer.as_slice());
149-
let mut processed_up_to: u64 = 0;
150-
151-
while cursor.position() < buffer.len() as u64 {
152-
let start_pos = cursor.position();
153-
match parse_single_message(&mut cursor) {
154-
Ok(TsGoLintMessage::Error(err)) => {
155-
return Err(err.error);
156-
}
157-
Ok(TsGoLintMessage::Diagnostic(tsgolint_diagnostic)) => {
158-
processed_up_to = cursor.position();
159-
160-
let path = tsgolint_diagnostic.file_path.clone();
161-
let Some(resolved_config) = resolved_configs.get(&path)
162-
else {
163-
// If we don't have a resolved config for this path, skip it. We should always
164-
// have a resolved config though, since we processed them already above.
165-
continue;
166-
};
167-
168-
let severity = resolved_config.rules.iter().find_map(
169-
|(rule, status)| {
170-
if rule.name() == tsgolint_diagnostic.rule {
171-
Some(*status)
172-
} else {
173-
None
174-
}
175-
},
176-
);
177-
let Some(severity) = severity else {
178-
// If the severity is not found, we should not report the diagnostic
179-
continue;
180-
};
181-
182-
let oxc_diagnostic: OxcDiagnostic =
183-
OxcDiagnostic::from(tsgolint_diagnostic);
184-
185-
let oxc_diagnostic = oxc_diagnostic.with_severity(
186-
if severity == AllowWarnDeny::Deny {
187-
Severity::Error
188-
} else {
189-
Severity::Warning
190-
},
191-
);
192-
193-
let source_text: &str = if self.silent {
194-
// The source text is not needed in silent mode.
195-
// The source text is only here to wrap the line before and after into a nice `oxc_diagnostic` Error
196-
""
197-
} else if let Some(source_text) = source_text_map.get(&path)
198-
{
199-
source_text.as_str()
200-
} else {
201-
let source_text = read_to_string(&path)
202-
.unwrap_or_else(|_| String::new());
203-
// Insert and get a reference to the inserted string
204-
let entry = source_text_map
205-
.entry(path.clone())
206-
.or_insert(source_text);
207-
entry.as_str()
208-
};
209-
210-
let diagnostics = DiagnosticService::wrap_diagnostics(
211-
cwd_clone.clone(),
212-
path.clone(),
213-
source_text,
214-
vec![oxc_diagnostic],
215-
);
216-
217-
if error_sender.send((path, diagnostics)).is_err() {
218-
// Receiver has been dropped, stop processing
219-
return Ok(());
220-
}
221-
}
222-
Err(_) => {
223-
// Could not parse a complete message, break and keep remaining data
224-
cursor.set_position(start_pos);
225-
break;
140+
for msg in msg_iter {
141+
match msg {
142+
Ok(TsGoLintMessage::Error(err)) => {
143+
return Err(err.error);
144+
}
145+
Ok(TsGoLintMessage::Diagnostic(tsgolint_diagnostic)) => {
146+
let path = tsgolint_diagnostic.file_path.clone();
147+
let Some(resolved_config) = resolved_configs.get(&path) else {
148+
// If we don't have a resolved config for this path, skip it. We should always
149+
// have a resolved config though, since we processed them already above.
150+
continue;
151+
};
152+
153+
let severity =
154+
resolved_config.rules.iter().find_map(|(rule, status)| {
155+
if rule.name() == tsgolint_diagnostic.rule {
156+
Some(*status)
157+
} else {
158+
None
226159
}
227-
}
228-
}
229-
230-
// Keep unprocessed data for next iteration
231-
if processed_up_to > 0 {
232-
#[expect(clippy::cast_possible_truncation)]
233-
buffer.drain(..processed_up_to as usize);
160+
});
161+
let Some(severity) = severity else {
162+
// If the severity is not found, we should not report the diagnostic
163+
continue;
164+
};
165+
166+
let oxc_diagnostic: OxcDiagnostic =
167+
OxcDiagnostic::from(tsgolint_diagnostic);
168+
169+
let oxc_diagnostic =
170+
oxc_diagnostic.with_severity(if severity == AllowWarnDeny::Deny {
171+
Severity::Error
172+
} else {
173+
Severity::Warning
174+
});
175+
176+
let source_text: &str = if self.silent {
177+
// The source text is not needed in silent mode.
178+
// The source text is only here to wrap the line before and after into a nice `oxc_diagnostic` Error
179+
""
180+
} else if let Some(source_text) = source_text_map.get(&path) {
181+
source_text.as_str()
182+
} else {
183+
let source_text =
184+
read_to_string(&path).unwrap_or_else(|_| String::new());
185+
// Insert and get a reference to the inserted string
186+
let entry =
187+
source_text_map.entry(path.clone()).or_insert(source_text);
188+
entry.as_str()
189+
};
190+
191+
let diagnostics = DiagnosticService::wrap_diagnostics(
192+
cwd_clone.clone(),
193+
path.clone(),
194+
source_text,
195+
vec![oxc_diagnostic],
196+
);
197+
198+
if error_sender.send((path, diagnostics)).is_err() {
199+
// Receiver has been dropped, stop processing
200+
return Ok(());
234201
}
235202
}
236203
Err(e) => {
237-
return Err(format!("Failed to read from tsgolint stdout: {e}"));
204+
return Err(e);
238205
}
239206
}
240207
}
@@ -688,6 +655,60 @@ impl MessageType {
688655
}
689656
}
690657

658+
/// Iterator that streams messages from tsgolint stdout.
659+
struct TsGoLintMessageStream {
660+
stdout: std::process::ChildStdout,
661+
buffer: Vec<u8>,
662+
}
663+
664+
impl TsGoLintMessageStream {
665+
fn new(stdout: std::process::ChildStdout) -> TsGoLintMessageStream {
666+
TsGoLintMessageStream { stdout, buffer: Vec::with_capacity(8192) }
667+
}
668+
}
669+
670+
impl Iterator for TsGoLintMessageStream {
671+
type Item = Result<TsGoLintMessage, String>;
672+
673+
fn next(&mut self) -> Option<Self::Item> {
674+
let mut read_buf = [0u8; 8192];
675+
676+
loop {
677+
// Try to parse a complete message from the existing buffer
678+
let mut cursor = std::io::Cursor::new(self.buffer.as_slice());
679+
680+
if cursor.position() < self.buffer.len() as u64 {
681+
let start_pos = cursor.position();
682+
match parse_single_message(&mut cursor) {
683+
Ok(message) => {
684+
// Successfully parsed a message, remove it from buffer
685+
#[expect(clippy::cast_possible_truncation)]
686+
self.buffer.drain(..cursor.position() as usize);
687+
return Some(Ok(message));
688+
}
689+
Err(_) => {
690+
// Could not parse a complete message, need more data
691+
cursor.set_position(start_pos);
692+
}
693+
}
694+
}
695+
696+
// Read more data from stdout
697+
match self.stdout.read(&mut read_buf) {
698+
Ok(0) => {
699+
return None;
700+
}
701+
Ok(n) => {
702+
self.buffer.extend_from_slice(&read_buf[..n]);
703+
}
704+
Err(e) => {
705+
return Some(Err(format!("Failed to read from tsgolint stdout: {e}")));
706+
}
707+
}
708+
}
709+
}
710+
}
711+
691712
/// Parses a single message from the binary tsgolint output.
692713
// Messages are encoded as follows:
693714
// | Payload Size (uint32 LE) - 4 bytes | Message Type (uint8) - 1 byte | Payload |

0 commit comments

Comments
 (0)