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