diff --git a/Cargo.lock b/Cargo.lock index 647806a1e..92cb4f628 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1434,18 +1434,18 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "2.0.3" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +checksum = "a3ac7f54ca534db81081ef1c1e7f6ea8a3ef428d2fc069097c079443d24124d3" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.3" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +checksum = "9e9465d30713b56a37ede7185763c3492a91be2f5fa68d958c44e41ab9248beb" dependencies = [ "proc-macro2", "quote", diff --git a/examples/dlint/main.rs b/examples/dlint/main.rs index 3dbd75ce2..19136a1b1 100644 --- a/examples/dlint/main.rs +++ b/examples/dlint/main.rs @@ -16,6 +16,7 @@ use deno_lint::rules::get_all_rules; use deno_lint::rules::{filtered_rules, recommended_rules}; use log::debug; use rayon::prelude::*; +use std::borrow::Cow; use std::collections::BTreeMap; use std::collections::HashSet; use std::path::PathBuf; @@ -91,6 +92,7 @@ fn run_linter( let all_rule_codes = all_rules .iter() .map(|rule| rule.code()) + .map(Cow::from) .collect::>(); let rules = if let Some(config) = maybe_config { config.get_rules() @@ -133,6 +135,7 @@ fn run_linter( default_jsx_factory: Some("React.createElement".to_string()), default_jsx_fragment_factory: Some("React.Fragment".to_string()), }, + external_linter: None, })?; let mut number_of_errors = diagnostics.len(); diff --git a/src/context.rs b/src/context.rs index 23812967f..c3a13ab85 100644 --- a/src/context.rs +++ b/src/context.rs @@ -9,7 +9,7 @@ use crate::ignore_directives::{ LineIgnoreDirective, }; use crate::linter::LinterContext; -use crate::rules::{self, LintRule}; +use crate::rules; use deno_ast::swc::ast::Expr; use deno_ast::swc::common::comments::Comment; use deno_ast::swc::common::util::take::Take; @@ -20,6 +20,7 @@ use deno_ast::{ }; use deno_ast::{MediaType, ModuleSpecifier}; use deno_ast::{MultiThreadedComments, Scope}; +use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::sync::Arc; @@ -33,7 +34,6 @@ pub struct Context<'a> { scope: Scope, control_flow: ControlFlow, traverse_flow: TraverseFlow, - all_rule_codes: &'a HashSet<&'static str>, check_unknown_rules: bool, #[allow(clippy::redundant_allocation)] // This type comes from SWC. jsx_factory: Option>>, @@ -112,7 +112,6 @@ impl<'a> Context<'a> { diagnostics: Vec::new(), traverse_flow: TraverseFlow::default(), check_unknown_rules: linter_ctx.check_unknown_rules, - all_rule_codes: &linter_ctx.all_rule_codes, jsx_factory, jsx_fragment_factory, } @@ -266,7 +265,7 @@ impl<'a> Context<'a> { /// works for diagnostics reported by other rules. pub(crate) fn ban_unused_ignore( &self, - specified_rules: &[Box], + known_rules_codes: &HashSet>, ) -> Vec { const CODE: &str = "ban-unused-ignore"; @@ -280,10 +279,8 @@ impl<'a> Context<'a> { return vec![]; } - let executed_builtin_codes: HashSet<&'static str> = - specified_rules.iter().map(|r| r.code()).collect(); let is_unused_code = |&(code, status): &(&String, &CodeStatus)| { - let is_unknown = !executed_builtin_codes.contains(code.as_str()); + let is_unknown = !known_rules_codes.contains(code.as_str()); !status.used && !is_unknown }; @@ -334,15 +331,17 @@ impl<'a> Context<'a> { // struct. /// Lint rule implementation for `ban-unknown-rule-code`. /// This should be run after all normal rules. - pub(crate) fn ban_unknown_rule_code(&mut self) -> Vec { - let is_unknown_rule = - |code: &&String| !self.all_rule_codes.contains(code.as_str()); - + pub(crate) fn ban_unknown_rule_code( + &mut self, + enabled_rules: &HashSet>, + ) -> Vec { let mut diagnostics = Vec::new(); if let Some(file_ignore) = self.file_ignore_directive.as_ref() { - for unknown_rule_code in - file_ignore.codes().keys().filter(is_unknown_rule) + for unknown_rule_code in file_ignore + .codes() + .keys() + .filter(|code| !enabled_rules.contains(code.as_str())) { let d = self.create_diagnostic( Some(self.create_diagnostic_range(file_ignore.range())), @@ -358,8 +357,10 @@ impl<'a> Context<'a> { } for line_ignore in self.line_ignore_directives.values() { - for unknown_rule_code in - line_ignore.codes().keys().filter(is_unknown_rule) + for unknown_rule_code in line_ignore + .codes() + .keys() + .filter(|code| !enabled_rules.contains(code.as_str())) { let d = self.create_diagnostic( Some(self.create_diagnostic_range(line_ignore.range())), @@ -451,6 +452,14 @@ impl<'a> Context<'a> { .push(self.create_diagnostic(maybe_range, details)); } + /// Add fully constructed diagnostics. + /// + /// This function can be used by the "external linter" to provide its own + /// diagnostics. + pub fn add_external_diagnostics(&mut self, diagnostics: &[LintDiagnostic]) { + self.diagnostics.extend_from_slice(diagnostics); + } + pub(crate) fn create_diagnostic( &self, maybe_range: Option, diff --git a/src/lib.rs b/src/lib.rs index aa3e87010..6c6ff18d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,7 @@ pub use deno_ast::view::ProgramRef; #[cfg(test)] mod lint_tests { + use std::borrow::Cow; use std::collections::HashSet; use crate::diagnostic::LintDiagnostic; @@ -41,7 +42,7 @@ mod lint_tests { fn lint( source: &str, rules: Vec>, - all_rule_codes: HashSet<&'static str>, + all_rule_codes: HashSet>, ) -> Vec { let linter = Linter::new(LinterOptions { rules, @@ -59,6 +60,7 @@ mod lint_tests { default_jsx_factory: None, default_jsx_fragment_factory: None, }, + external_linter: None, }) .expect("Failed to lint"); diagnostics @@ -67,7 +69,7 @@ mod lint_tests { fn lint_with_ast( parsed_source: &ParsedSource, rules: Vec>, - all_rule_codes: HashSet<&'static str>, + all_rule_codes: HashSet>, ) -> Vec { let linter = Linter::new(LinterOptions { rules, @@ -81,17 +83,23 @@ mod lint_tests { default_jsx_factory: None, default_jsx_fragment_factory: None, }, + None, ) } + fn get_all_rules_codes() -> HashSet> { + get_all_rules() + .into_iter() + .map(|rule| rule.code()) + .map(Cow::from) + .collect() + } + fn lint_recommended_rules(source: &str) -> Vec { lint( source, recommended_rules(get_all_rules()), - get_all_rules() - .into_iter() - .map(|rule| rule.code()) - .collect(), + get_all_rules_codes(), ) } @@ -101,10 +109,7 @@ mod lint_tests { lint_with_ast( parsed_source, recommended_rules(get_all_rules()), - get_all_rules() - .into_iter() - .map(|rule| rule.code()) - .collect(), + get_all_rules_codes(), ) } @@ -112,14 +117,7 @@ mod lint_tests { rule: Box, source: &str, ) -> Vec { - lint( - source, - vec![rule], - get_all_rules() - .into_iter() - .map(|rule| rule.code()) - .collect(), - ) + lint(source, vec![rule], get_all_rules_codes()) } #[test] diff --git a/src/linter.rs b/src/linter.rs index 2c5ebb79c..441e0169a 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -10,13 +10,15 @@ use deno_ast::diagnostics::Diagnostic; use deno_ast::MediaType; use deno_ast::ParsedSource; use deno_ast::{ModuleSpecifier, ParseDiagnostic}; +use std::borrow::Cow; use std::collections::HashSet; +use std::sync::Arc; pub struct LinterOptions { /// Rules to lint with. pub rules: Vec>, /// Collection of all the lint rule codes. - pub all_rule_codes: HashSet<&'static str>, + pub all_rule_codes: HashSet>, /// Defaults to "deno-lint-ignore-file" pub custom_ignore_file_directive: Option<&'static str>, /// Defaults to "deno-lint-ignore" @@ -40,7 +42,7 @@ pub(crate) struct LinterContext { pub check_unknown_rules: bool, /// Rules are sorted by priority pub rules: Vec>, - pub all_rule_codes: HashSet<&'static str>, + pub all_rule_codes: HashSet>, } impl LinterContext { @@ -65,11 +67,27 @@ impl LinterContext { } } +#[derive(Default)] +pub struct ExternalLinterResult { + pub diagnostics: Vec, + pub rules: Vec>, +} + +/// Perform a run of "external linter" on a parsed source file. +/// +/// Since we are working on an already parsed file, this callback +/// is infallible. If an error handling needs to be performed by the +/// external linter, it should be handled externally bt that linter, +/// and an empty [`ExternalLinterResult`] should be returned. +pub type ExternalLinterCb = + Arc Option>; + pub struct LintFileOptions { pub specifier: ModuleSpecifier, pub source_code: String, pub media_type: MediaType, pub config: LintConfig, + pub external_linter: Option, } #[derive(Debug, Clone)] @@ -107,6 +125,7 @@ impl Linter { &parsed_source, options.config.default_jsx_factory, options.config.default_jsx_fragment_factory, + options.external_linter, ); Ok((parsed_source, diagnostics)) @@ -120,26 +139,40 @@ impl Linter { &self, parsed_source: &ParsedSource, config: LintConfig, + maybe_external_linter: Option, ) -> Vec { let _mark = PerformanceMark::new("Linter::lint_with_ast"); self.lint_inner( parsed_source, config.default_jsx_factory, config.default_jsx_fragment_factory, + maybe_external_linter, ) } // TODO(bartlomieju): this struct does too much - not only it checks for ignored // lint rules, it also runs 2 additional rules. These rules should be rewritten // to use a regular way of writing a rule and not live on the `Context` struct. - fn collect_diagnostics(&self, mut context: Context) -> Vec { + fn collect_diagnostics( + &self, + mut context: Context, + external_rule_codes: Vec>, + ) -> Vec { let _mark = PerformanceMark::new("Linter::collect_diagnostics"); let mut diagnostics = context.check_ignore_directive_usage(); + + let mut all_rules = self.ctx.all_rule_codes.clone(); + all_rules.extend(external_rule_codes.iter().cloned()); + let enabled_rules: HashSet> = external_rule_codes + .into_iter() + .chain(self.ctx.rules.iter().map(|r| r.code().into())) + .collect(); + // Run `ban-unknown-rule-code` - diagnostics.extend(context.ban_unknown_rule_code()); + diagnostics.extend(context.ban_unknown_rule_code(&all_rules)); // Run `ban-unused-ignore` - diagnostics.extend(context.ban_unused_ignore(&self.ctx.rules)); + diagnostics.extend(context.ban_unused_ignore(&enabled_rules)); // Finally sort by position the diagnostics originates on then by code diagnostics.sort_by(|a, b| { @@ -159,6 +192,7 @@ impl Linter { parsed_source: &ParsedSource, default_jsx_factory: Option, default_jsx_fragment_factory: Option, + maybe_external_linter: Option, ) -> Vec { let _mark = PerformanceMark::new("Linter::lint_inner"); @@ -200,7 +234,15 @@ impl Linter { rule.lint_program_with_ast_view(&mut context, pg); } - self.collect_diagnostics(context) + let mut external_rule_codes = vec![]; + if let Some(cb) = maybe_external_linter { + if let Some(external_linter_result) = cb(parsed_source.clone()) { + context.add_external_diagnostics(&external_linter_result.diagnostics); + external_rule_codes = external_linter_result.rules; + } + } + + self.collect_diagnostics(context, external_rule_codes) }); diagnostics diff --git a/src/test_util.rs b/src/test_util.rs index 9fed7ef7b..3746354b7 100644 --- a/src/test_util.rs +++ b/src/test_util.rs @@ -1,5 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::borrow::Cow; + use crate::ast_parser; use crate::diagnostic::LintDiagnostic; use crate::linter::LintConfig; @@ -326,6 +328,7 @@ fn lint( all_rule_codes: get_all_rules() .into_iter() .map(|rule| rule.code()) + .map(Cow::from) .collect(), custom_ignore_diagnostic_directive: None, custom_ignore_file_directive: None, @@ -341,6 +344,7 @@ fn lint( default_jsx_factory: Some("React.createElement".to_owned()), default_jsx_fragment_factory: Some("React.Fragment".to_owned()), }, + external_linter: None, }); match lint_result { Ok((source, diagnostics)) => (source, diagnostics),