@@ -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