@@ -11,17 +11,24 @@ use std::{
1111use cow_utils:: CowUtils ;
1212use ignore:: { gitignore:: Gitignore , overrides:: OverrideBuilder } ;
1313use oxc_allocator:: AllocatorPool ;
14- use oxc_diagnostics:: { DiagnosticSender , DiagnosticService , GraphicalReportHandler , OxcDiagnostic } ;
14+ use oxc_diagnostics:: {
15+ DiagnosticSender , DiagnosticService , GraphicalReportHandler , OxcDiagnostic , Severity ,
16+ } ;
1517use oxc_linter:: {
1618 AllowWarnDeny , Config , ConfigStore , ConfigStoreBuilder , ExternalLinter , ExternalPluginStore ,
1719 InvalidFilterKind , LintFilter , LintOptions , LintService , LintServiceOptions , Linter , Oxlintrc ,
20+ read_to_string,
1821} ;
1922use rustc_hash:: { FxHashMap , FxHashSet } ;
2023use serde_json:: Value ;
2124
2225use crate :: {
2326 cli:: { CliRunResult , LintCommand , MiscOptions , ReportUnusedDirectives , Runner , WarningOptions } ,
2427 output_formatter:: { LintCommandInfo , OutputFormatter } ,
28+ tsgolint:: {
29+ MessageType , TsGoLintInput , TsGoLintInputFile , TsGoLintState , parse_tsgolint_output,
30+ try_find_tsgolint_executable,
31+ } ,
2532 walk:: Walk ,
2633} ;
2734
@@ -280,19 +287,43 @@ impl Runner for LintRunner {
280287
281288 let lint_config = config_builder. build ( ) ;
282289
290+ // Enable type-aware linting if `--tsconfig` is passed
291+ // TODO: Add a warning message if `tsgolint` cannot be found, but type-aware rules are enabled
292+ let tsgolint_path =
293+ try_find_tsgolint_executable ( options. cwd ( ) ) . unwrap_or ( PathBuf :: from ( "tsgolint" ) ) ;
294+ let type_aware = basic_options. tsconfig . is_some ( ) ;
295+
296+ let tsgolint_state = if type_aware {
297+ Some ( TsGoLintState {
298+ cwd : options. cwd ( ) . to_path_buf ( ) ,
299+ paths : paths. clone ( ) ,
300+ rules : lint_config
301+ . rules ( )
302+ . iter ( )
303+ . filter_map ( |( rule, status) | {
304+ if status. is_warn_deny ( ) && rule. is_tsgolint_rule ( ) {
305+ Some ( ( * status, rule. clone ( ) ) )
306+ } else {
307+ None
308+ }
309+ } )
310+ . collect ( ) ,
311+ } )
312+ } else {
313+ None
314+ } ;
315+
283316 let report_unused_directives = match inline_config_options. report_unused_directives {
284317 ReportUnusedDirectives :: WithoutSeverity ( true ) => Some ( AllowWarnDeny :: Warn ) ,
285318 ReportUnusedDirectives :: WithSeverity ( Some ( severity) ) => Some ( severity) ,
286319 _ => None ,
287320 } ;
288321
289- let linter = Linter :: new (
290- LintOptions :: default ( ) ,
291- ConfigStore :: new ( lint_config, nested_configs, external_plugin_store) ,
292- self . external_linter ,
293- )
294- . with_fix ( fix_options. fix_kind ( ) )
295- . with_report_unused_directives ( report_unused_directives) ;
322+ let config_store = ConfigStore :: new ( lint_config, nested_configs, external_plugin_store) ;
323+ let tsgolint_config_store = if type_aware { Some ( config_store. clone ( ) ) } else { None } ;
324+ let linter = Linter :: new ( LintOptions :: default ( ) , config_store, self . external_linter )
325+ . with_fix ( fix_options. fix_kind ( ) )
326+ . with_report_unused_directives ( report_unused_directives) ;
296327
297328 let tsconfig = basic_options. tsconfig ;
298329 if let Some ( path) = tsconfig. as_ref ( ) {
@@ -315,11 +346,171 @@ impl Runner for LintRunner {
315346
316347 let ( mut diagnostic_service, tx_error) =
317348 Self :: get_diagnostic_service ( & output_formatter, & warning_options, & misc_options) ;
349+ let tsgolint_error_sender = if type_aware { Some ( tx_error. clone ( ) ) } else { None } ;
318350
319351 let number_of_rules = linter. number_of_rules ( ) ;
320352
321353 let allocator_pool = AllocatorPool :: new ( rayon:: current_num_threads ( ) ) ;
322354
355+ if type_aware
356+ && let Some ( tsgolint_state) = tsgolint_state
357+ && let Some ( tsgolint_config_store) = tsgolint_config_store
358+ && let Some ( tsconfig) = tsconfig
359+ && let Some ( tsgolint_error_sender) = tsgolint_error_sender
360+ && tsgolint_state. rules . len ( ) > 0
361+ && tsgolint_state. paths . len ( ) > 0
362+ {
363+ // Feed JSON into STDIN of tsgolint in this format:
364+ // ```
365+ // {
366+ // "files": [
367+ // {
368+ // "file_path": "/absolute/path/to/file.ts",
369+ // "rules": ["rule-1", "another-rule"]
370+ // }
371+ // ]
372+ // }
373+ // ```
374+ let json_input = TsGoLintInput {
375+ files : tsgolint_state
376+ . paths
377+ . iter ( )
378+ . map ( |path| TsGoLintInputFile {
379+ file_path : path. to_string_lossy ( ) . to_string ( ) ,
380+ rules : tsgolint_state
381+ . rules
382+ . iter ( )
383+ . map ( |( _, r) | r. name ( ) . to_owned ( ) )
384+ . collect ( ) ,
385+ } )
386+ . collect ( ) ,
387+ } ;
388+
389+ let handler = std:: thread:: spawn ( move || {
390+ // `./tsgolint headless --tsconfig /path/to/project/tsconfig.json --cwd /path/to/project
391+ let child = std:: process:: Command :: new ( tsgolint_path)
392+ . arg ( "headless" )
393+ . arg ( "--tsconfig" )
394+ . arg ( tsconfig)
395+ . arg ( "--cwd" )
396+ . arg ( & tsgolint_state. cwd )
397+ . stdin ( std:: process:: Stdio :: piped ( ) )
398+ . stdout ( std:: process:: Stdio :: piped ( ) )
399+ . spawn ( ) ;
400+
401+ let Ok ( mut child) = child else {
402+ // For now, silently ignore errors if `tsgolint` does not appear to be installed, or cannot
403+ // be spawned correctly.
404+ return Ok ( ( ) ) ;
405+ } ;
406+
407+ let mut stdin = child. stdin . take ( ) . expect ( "Failed to open tsgolint stdin" ) ;
408+
409+ std:: thread:: spawn ( move || {
410+ let json =
411+ serde_json:: to_string ( & json_input) . expect ( "Failed to serialize JSON" ) ;
412+
413+ stdin. write_all ( json. as_bytes ( ) ) . expect ( "Failed to write to tsgolint stdin" ) ;
414+ } ) ;
415+
416+ // TODO: Stream diagnostics as they are emitted, rather than waiting
417+ let output = child. wait_with_output ( ) . expect ( "Failed to wait for tsgolint process" ) ;
418+
419+ match parse_tsgolint_output ( & output. stdout ) {
420+ Ok ( parsed) => {
421+ let oxc_diagnostics = parsed
422+ . into_iter ( )
423+ . filter_map ( |tsgolint_diagnostic| {
424+ // For now, ignore any `tsgolint` errors.
425+ if tsgolint_diagnostic. r#type == MessageType :: Error {
426+ return None ;
427+ }
428+ // Resolve this config if not already stored
429+ // TODO(perf): do not re-resolve the same file path multiple times, instead cache the config
430+ let path = tsgolint_diagnostic. file_path . clone ( ) ;
431+ let resolved_config = tsgolint_config_store. resolve ( & path) ;
432+ // TODO(perf): build a map of rule->severity, rather than doing a linear search
433+ let severity =
434+ resolved_config. rules . iter ( ) . find_map ( |( rule, status) | {
435+ if rule. name ( ) == tsgolint_diagnostic. rule {
436+ Some ( ( * status, rule) )
437+ } else {
438+ None
439+ }
440+ } ) ;
441+
442+ let oxc_diagnostic: OxcDiagnostic = tsgolint_diagnostic. into ( ) ;
443+ if severity. is_none ( ) {
444+ // If the severity is not found, we should not report the diagnostic
445+ return None ;
446+ }
447+ let oxc_diagnostic = oxc_diagnostic. with_severity ( severity. map_or (
448+ Severity :: Warning ,
449+ |( severity, _) | {
450+ if severity == AllowWarnDeny :: Deny {
451+ Severity :: Error
452+ } else {
453+ Severity :: Warning
454+ }
455+ } ,
456+ ) ) ;
457+
458+ Some ( ( path, oxc_diagnostic) )
459+ } )
460+ // group diagnostics by file path
461+ . fold (
462+ FxHashMap :: default ( ) ,
463+ |mut acc : FxHashMap < PathBuf , Vec < OxcDiagnostic > > ,
464+ ( file_path, diagnostic) | {
465+ acc. entry ( file_path) . or_default ( ) . push ( diagnostic) ;
466+
467+ acc
468+ } ,
469+ )
470+ . into_iter ( )
471+ . map ( |( file_path, diagnostics) | {
472+ (
473+ file_path. clone ( ) ,
474+ DiagnosticService :: wrap_diagnostics (
475+ tsgolint_state. cwd . clone ( ) ,
476+ file_path. clone ( ) ,
477+ & read_to_string ( & file_path)
478+ . unwrap_or_else ( |_| String :: new ( ) ) ,
479+ 0 ,
480+ diagnostics,
481+ ) ,
482+ )
483+ } )
484+ . collect :: < FxHashMap < _ , _ > > ( ) ;
485+
486+ for ( file_path, diagnostics) in oxc_diagnostics {
487+ tsgolint_error_sender
488+ . send ( ( file_path, diagnostics) )
489+ . expect ( "Failed to send diagnostic" ) ;
490+ }
491+
492+ Ok ( ( ) )
493+ }
494+
495+ Err ( err) => Err ( format ! ( "Failed to parse tsgolint output: {err}" ) ) ,
496+ }
497+ } ) ;
498+
499+ match handler. join ( ) {
500+ Ok ( Ok ( ( ) ) ) => {
501+ // Successfully ran tsgolint
502+ }
503+ Ok ( Err ( err) ) => {
504+ print_and_flush_stdout ( stdout, & format ! ( "Error running tsgolint: {err:?}" ) ) ;
505+ return CliRunResult :: TsGoLintError ;
506+ }
507+ Err ( err) => {
508+ print_and_flush_stdout ( stdout, & format ! ( "Error running tsgolint: {err:?}" ) ) ;
509+ return CliRunResult :: TsGoLintError ;
510+ }
511+ }
512+ }
513+
323514 // Spawn linting in another thread so diagnostics can be printed immediately from diagnostic_service.run.
324515 rayon:: spawn ( move || {
325516 let mut lint_service = LintService :: new ( linter, allocator_pool, options) ;
@@ -1195,4 +1386,10 @@ mod test {
11951386 let args = & [ "-c" , ".oxlintrc.json" ] ;
11961387 Tester :: new ( ) . with_cwd ( "fixtures/issue_11644" . into ( ) ) . test_and_snapshot ( args) ;
11971388 }
1389+
1390+ #[ test]
1391+ fn test_tsgolint ( ) {
1392+ let args = & [ "fixtures/tsgolint" , "--tsconfig" , "fixtures/tsgolint/tsconfig.json" ] ;
1393+ Tester :: new ( ) . test_and_snapshot ( args) ;
1394+ }
11981395}
0 commit comments