// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::borrow::Cow; use std::collections::HashSet; use std::sync::Arc; use deno_ast::ModuleSpecifier; use deno_config::deno_json::ConfigFile; use deno_config::deno_json::LintRulesConfig; use deno_config::workspace::WorkspaceResolver; use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_graph::ModuleGraph; use deno_lint::diagnostic::LintDiagnostic; use deno_lint::rules::LintRule; use crate::resolver::SloppyImportsResolver; mod no_sloppy_imports; mod no_slow_types; // used for publishing pub use no_slow_types::collect_no_slow_type_diagnostics; pub trait PackageLintRule: std::fmt::Debug + Send + Sync { fn code(&self) -> &'static str; fn tags(&self) -> &'static [&'static str] { &[] } fn docs(&self) -> &'static str; fn help_docs_url(&self) -> Cow<'static, str>; fn lint_package( &self, graph: &ModuleGraph, entrypoints: &[ModuleSpecifier], ) -> Vec; } pub(super) trait ExtendedLintRule: LintRule { /// If the rule supports the incremental cache. fn supports_incremental_cache(&self) -> bool; fn help_docs_url(&self) -> Cow<'static, str>; fn into_base(self: Box) -> Box; } pub enum FileOrPackageLintRule { File(Box), Package(Box), } #[derive(Debug)] enum CliLintRuleKind { DenoLint(Box), Extended(Box), Package(Box), } #[derive(Debug)] pub struct CliLintRule(CliLintRuleKind); impl CliLintRule { pub fn code(&self) -> &'static str { use CliLintRuleKind::*; match &self.0 { DenoLint(rule) => rule.code(), Extended(rule) => rule.code(), Package(rule) => rule.code(), } } pub fn tags(&self) -> &'static [&'static str] { use CliLintRuleKind::*; match &self.0 { DenoLint(rule) => rule.tags(), Extended(rule) => rule.tags(), Package(rule) => rule.tags(), } } pub fn docs(&self) -> &'static str { use CliLintRuleKind::*; match &self.0 { DenoLint(rule) => rule.docs(), Extended(rule) => rule.docs(), Package(rule) => rule.docs(), } } pub fn help_docs_url(&self) -> Cow<'static, str> { use CliLintRuleKind::*; match &self.0 { DenoLint(rule) => { Cow::Owned(format!("https://lint.deno.land/rules/{}", rule.code())) } Extended(rule) => rule.help_docs_url(), Package(rule) => rule.help_docs_url(), } } pub fn supports_incremental_cache(&self) -> bool { use CliLintRuleKind::*; match &self.0 { DenoLint(_) => true, Extended(rule) => rule.supports_incremental_cache(), // graph rules don't go through the incremental cache, so allow it Package(_) => true, } } pub fn into_file_or_pkg_rule(self) -> FileOrPackageLintRule { use CliLintRuleKind::*; match self.0 { DenoLint(rule) => FileOrPackageLintRule::File(rule), Extended(rule) => FileOrPackageLintRule::File(rule.into_base()), Package(rule) => FileOrPackageLintRule::Package(rule), } } } #[derive(Debug)] pub struct ConfiguredRules { pub all_rule_codes: HashSet<&'static str>, pub rules: Vec, } impl ConfiguredRules { pub fn incremental_cache_state(&self) -> Option { if self.rules.iter().any(|r| !r.supports_incremental_cache()) { return None; } // use a hash of the rule names in order to bust the cache let mut codes = self.rules.iter().map(|r| r.code()).collect::>(); // ensure this is stable by sorting it codes.sort_unstable(); Some(codes) } } pub struct LintRuleProvider { sloppy_imports_resolver: Option>, workspace_resolver: Option>, } impl LintRuleProvider { pub fn new( sloppy_imports_resolver: Option>, workspace_resolver: Option>, ) -> Self { Self { sloppy_imports_resolver, workspace_resolver, } } pub fn resolve_lint_rules_err_empty( &self, rules: LintRulesConfig, maybe_config_file: Option<&ConfigFile>, ) -> Result { let lint_rules = self.resolve_lint_rules(rules, maybe_config_file); if lint_rules.rules.is_empty() { bail!("No rules have been configured") } Ok(lint_rules) } pub fn resolve_lint_rules( &self, rules: LintRulesConfig, maybe_config_file: Option<&ConfigFile>, ) -> ConfiguredRules { let deno_lint_rules = deno_lint::rules::get_all_rules(); let cli_lint_rules = vec![CliLintRule(CliLintRuleKind::Extended( Box::new(no_sloppy_imports::NoSloppyImportsRule::new( self.sloppy_imports_resolver.clone(), self.workspace_resolver.clone(), )), ))]; let cli_graph_rules = vec![CliLintRule(CliLintRuleKind::Package( Box::new(no_slow_types::NoSlowTypesRule), ))]; let mut all_rule_names = HashSet::with_capacity( deno_lint_rules.len() + cli_lint_rules.len() + cli_graph_rules.len(), ); let all_rules = deno_lint_rules .into_iter() .map(|rule| CliLintRule(CliLintRuleKind::DenoLint(rule))) .chain(cli_lint_rules) .chain(cli_graph_rules) .inspect(|rule| { all_rule_names.insert(rule.code()); }); let rules = filtered_rules( all_rules, rules .tags .or_else(|| Some(get_default_tags(maybe_config_file))), rules.exclude, rules.include, ); ConfiguredRules { rules, all_rule_codes: all_rule_names, } } } fn get_default_tags(maybe_config_file: Option<&ConfigFile>) -> Vec { let mut tags = Vec::with_capacity(2); tags.push("recommended".to_string()); if maybe_config_file.map(|c| c.is_package()).unwrap_or(false) { tags.push("jsr".to_string()); } tags } fn filtered_rules( all_rules: impl Iterator, maybe_tags: Option>, maybe_exclude: Option>, maybe_include: Option>, ) -> Vec { let tags_set = maybe_tags.map(|tags| tags.into_iter().collect::>()); let mut rules = all_rules .filter(|rule| { let mut passes = if let Some(tags_set) = &tags_set { rule .tags() .iter() .any(|t| tags_set.contains(&t.to_string())) } else { true }; if let Some(includes) = &maybe_include { if includes.contains(&rule.code().to_owned()) { passes |= true; } } if let Some(excludes) = &maybe_exclude { if excludes.contains(&rule.code().to_owned()) { passes &= false; } } passes }) .collect::>(); rules.sort_by_key(|r| r.code()); rules } #[cfg(test)] mod test { use super::*; use crate::args::LintRulesConfig; #[test] fn recommended_rules_when_no_tags_in_config() { let rules_config = LintRulesConfig { exclude: Some(vec!["no-debugger".to_string()]), include: None, tags: None, }; let rules_provider = LintRuleProvider::new(None, None); let rules = rules_provider.resolve_lint_rules(rules_config, None); let mut rule_names = rules .rules .into_iter() .map(|r| r.code().to_string()) .collect::>(); rule_names.sort(); let mut recommended_rule_names = rules_provider .resolve_lint_rules(Default::default(), None) .rules .into_iter() .filter(|r| r.tags().iter().any(|t| *t == "recommended")) .map(|r| r.code().to_string()) .filter(|n| n != "no-debugger") .collect::>(); recommended_rule_names.sort(); assert_eq!(rule_names, recommended_rule_names); } }