@@ -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+ ResolvedLinterState , 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,47 @@ 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 type_aware = basic_options. tsconfig . is_some ( ) ;
293+
283294 let report_unused_directives = match inline_config_options. report_unused_directives {
284295 ReportUnusedDirectives :: WithoutSeverity ( true ) => Some ( AllowWarnDeny :: Warn ) ,
285296 ReportUnusedDirectives :: WithSeverity ( Some ( severity) ) => Some ( severity) ,
286297 _ => None ,
287298 } ;
299+ let ( mut diagnostic_service, tx_error) =
300+ Self :: get_diagnostic_service ( & output_formatter, & warning_options, & misc_options) ;
288301
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) ;
302+ let config_store = ConfigStore :: new ( lint_config, nested_configs, external_plugin_store) ;
303+
304+ let tsgolint_state = if type_aware {
305+ Some ( TsGoLintState {
306+ error_sender : tx_error. clone ( ) ,
307+ config_store : config_store. clone ( ) ,
308+ executable_path : try_find_tsgolint_executable ( options. cwd ( ) )
309+ . unwrap_or ( PathBuf :: from ( "tsgolint" ) ) ,
310+ cwd : options. cwd ( ) . to_path_buf ( ) ,
311+ paths : paths. clone ( ) ,
312+ rules : config_store
313+ . rules ( )
314+ . iter ( )
315+ . filter_map ( |( rule, status) | {
316+ if status. is_warn_deny ( ) && rule. is_tsgolint_rule ( ) {
317+ Some ( ( * status, rule. clone ( ) ) )
318+ } else {
319+ None
320+ }
321+ } )
322+ . collect ( ) ,
323+ } )
324+ } else {
325+ None
326+ } ;
327+
328+ let linter = Linter :: new ( LintOptions :: default ( ) , config_store, self . external_linter )
329+ . with_fix ( fix_options. fix_kind ( ) )
330+ . with_report_unused_directives ( report_unused_directives) ;
296331
297332 let tsconfig = basic_options. tsconfig ;
298333 if let Some ( path) = tsconfig. as_ref ( ) {
@@ -313,13 +348,154 @@ impl Runner for LintRunner {
313348 }
314349 }
315350
316- let ( mut diagnostic_service, tx_error) =
317- Self :: get_diagnostic_service ( & output_formatter, & warning_options, & misc_options) ;
318-
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 ( tsconfig) = tsconfig
358+ && !tsgolint_state. rules . is_empty ( )
359+ && !tsgolint_state. paths . is_empty ( )
360+ {
361+ // Feed JSON into STDIN of tsgolint in this format:
362+ // ```
363+ // {
364+ // "files": [
365+ // {
366+ // "file_path": "/absolute/path/to/file.ts",
367+ // "rules": ["rule-1", "another-rule"]
368+ // }
369+ // ]
370+ // }
371+ // ```
372+ let json_input = TsGoLintInput {
373+ files : tsgolint_state
374+ . paths
375+ . iter ( )
376+ . map ( |path| TsGoLintInputFile {
377+ file_path : path. to_string_lossy ( ) . to_string ( ) ,
378+ rules : tsgolint_state
379+ . rules
380+ . iter ( )
381+ . map ( |( _, r) | r. name ( ) . to_owned ( ) )
382+ . collect ( ) ,
383+ } )
384+ . collect ( ) ,
385+ } ;
386+
387+ let handler = std:: thread:: spawn ( move || {
388+ // `./tsgolint headless --tsconfig /path/to/project/tsconfig.json --cwd /path/to/project
389+ let child = std:: process:: Command :: new ( tsgolint_state. executable_path )
390+ . arg ( "headless" )
391+ . arg ( "--tsconfig" )
392+ . arg ( tsconfig)
393+ . arg ( "--cwd" )
394+ . arg ( & tsgolint_state. cwd )
395+ . stdin ( std:: process:: Stdio :: piped ( ) )
396+ . stdout ( std:: process:: Stdio :: piped ( ) )
397+ . spawn ( ) ;
398+
399+ let Ok ( mut child) = child else {
400+ // For now, silently ignore errors if `tsgolint` does not appear to be installed, or cannot
401+ // be spawned correctly.
402+ return Ok ( ( ) ) ;
403+ } ;
404+
405+ let mut stdin = child. stdin . take ( ) . expect ( "Failed to open tsgolint stdin" ) ;
406+
407+ std:: thread:: spawn ( move || {
408+ let json =
409+ serde_json:: to_string ( & json_input) . expect ( "Failed to serialize JSON" ) ;
410+
411+ stdin. write_all ( json. as_bytes ( ) ) . expect ( "Failed to write to tsgolint stdin" ) ;
412+ } ) ;
413+
414+ // TODO: Stream diagnostics as they are emitted, rather than waiting
415+ let output = child. wait_with_output ( ) . expect ( "Failed to wait for tsgolint process" ) ;
416+
417+ match parse_tsgolint_output ( & output. stdout ) {
418+ Ok ( parsed) => {
419+ let mut resolved_configs: FxHashMap < PathBuf , ResolvedLinterState > =
420+ FxHashMap :: default ( ) ;
421+ let mut severities: FxHashMap < String , Option < AllowWarnDeny > > =
422+ FxHashMap :: default ( ) ;
423+
424+ let mut oxc_diagnostics: FxHashMap < PathBuf , Vec < OxcDiagnostic > > =
425+ FxHashMap :: default ( ) ;
426+ for tsgolint_diagnostic in parsed {
427+ // For now, ignore any `tsgolint` errors.
428+ if tsgolint_diagnostic. r#type == MessageType :: Error {
429+ continue ;
430+ }
431+
432+ let path = tsgolint_diagnostic. file_path . clone ( ) ;
433+ let resolved_config = resolved_configs
434+ . entry ( path. clone ( ) )
435+ . or_insert_with ( || tsgolint_state. config_store . resolve ( & path) ) ;
436+
437+ let rule = tsgolint_diagnostic. rule . clone ( ) ;
438+ let severity = severities. entry ( rule) . or_insert_with ( || {
439+ resolved_config. rules . iter ( ) . find_map ( |( rule, status) | {
440+ if rule. name ( ) == tsgolint_diagnostic. rule {
441+ Some ( * status)
442+ } else {
443+ None
444+ }
445+ } )
446+ } ) ;
447+
448+ let oxc_diagnostic: OxcDiagnostic = tsgolint_diagnostic. into ( ) ;
449+ let Some ( severity) = severity else {
450+ // If the severity is not found, we should not report the diagnostic
451+ continue ;
452+ } ;
453+ let oxc_diagnostic =
454+ oxc_diagnostic. with_severity ( if * severity == AllowWarnDeny :: Deny {
455+ Severity :: Error
456+ } else {
457+ Severity :: Warning
458+ } ) ;
459+
460+ oxc_diagnostics. entry ( path. clone ( ) ) . or_default ( ) . push ( oxc_diagnostic) ;
461+ }
462+
463+ for ( file_path, diagnostics) in oxc_diagnostics {
464+ let diagnostics = DiagnosticService :: wrap_diagnostics (
465+ tsgolint_state. cwd . clone ( ) ,
466+ file_path. clone ( ) ,
467+ & read_to_string ( & file_path) . unwrap_or_else ( |_| String :: new ( ) ) ,
468+ 0 ,
469+ diagnostics,
470+ ) ;
471+ tsgolint_state
472+ . error_sender
473+ . send ( ( file_path. clone ( ) , diagnostics) )
474+ . expect ( "Failed to send diagnostic" ) ;
475+ }
476+
477+ Ok ( ( ) )
478+ }
479+
480+ Err ( err) => Err ( format ! ( "Failed to parse tsgolint output: {err}" ) ) ,
481+ }
482+ } ) ;
483+
484+ match handler. join ( ) {
485+ Ok ( Ok ( ( ) ) ) => {
486+ // Successfully ran tsgolint
487+ }
488+ Ok ( Err ( err) ) => {
489+ print_and_flush_stdout ( stdout, & format ! ( "Error running tsgolint: {err:?}" ) ) ;
490+ return CliRunResult :: TsGoLintError ;
491+ }
492+ Err ( err) => {
493+ print_and_flush_stdout ( stdout, & format ! ( "Error running tsgolint: {err:?}" ) ) ;
494+ return CliRunResult :: TsGoLintError ;
495+ }
496+ }
497+ }
498+
323499 // Spawn linting in another thread so diagnostics can be printed immediately from diagnostic_service.run.
324500 rayon:: spawn ( move || {
325501 let mut lint_service = LintService :: new ( linter, allocator_pool, options) ;
@@ -1195,4 +1371,10 @@ mod test {
11951371 let args = & [ "-c" , ".oxlintrc.json" ] ;
11961372 Tester :: new ( ) . with_cwd ( "fixtures/issue_11644" . into ( ) ) . test_and_snapshot ( args) ;
11971373 }
1374+
1375+ #[ test]
1376+ fn test_tsgolint ( ) {
1377+ let args = & [ "fixtures/tsgolint" , "--tsconfig" , "fixtures/tsgolint/tsconfig.json" ] ;
1378+ Tester :: new ( ) . test_and_snapshot ( args) ;
1379+ }
11981380}
0 commit comments