diff options
Diffstat (limited to 'cli/tools/lint/rules/mod.rs')
-rw-r--r-- | cli/tools/lint/rules/mod.rs | 296 |
1 files changed, 296 insertions, 0 deletions
diff --git a/cli/tools/lint/rules/mod.rs b/cli/tools/lint/rules/mod.rs new file mode 100644 index 000000000..2669ffda1 --- /dev/null +++ b/cli/tools/lint/rules/mod.rs @@ -0,0 +1,296 @@ +// 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<LintDiagnostic>; +} + +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<Self>) -> Box<dyn LintRule>; +} + +pub enum FileOrPackageLintRule { + File(Box<dyn LintRule>), + Package(Box<dyn PackageLintRule>), +} + +#[derive(Debug)] +enum CliLintRuleKind { + DenoLint(Box<dyn LintRule>), + Extended(Box<dyn ExtendedLintRule>), + Package(Box<dyn PackageLintRule>), +} + +#[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<CliLintRule>, +} + +impl ConfiguredRules { + pub fn incremental_cache_state(&self) -> Option<impl std::hash::Hash> { + 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::<Vec<_>>(); + // ensure this is stable by sorting it + codes.sort_unstable(); + Some(codes) + } +} + +pub struct LintRuleProvider { + sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>, + workspace_resolver: Option<Arc<WorkspaceResolver>>, +} + +impl LintRuleProvider { + pub fn new( + sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>, + workspace_resolver: Option<Arc<WorkspaceResolver>>, + ) -> Self { + Self { + sloppy_imports_resolver, + workspace_resolver, + } + } + + pub fn resolve_lint_rules_err_empty( + &self, + rules: LintRulesConfig, + maybe_config_file: Option<&ConfigFile>, + ) -> Result<ConfiguredRules, AnyError> { + 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<String> { + 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<Item = CliLintRule>, + maybe_tags: Option<Vec<String>>, + maybe_exclude: Option<Vec<String>>, + maybe_include: Option<Vec<String>>, +) -> Vec<CliLintRule> { + let tags_set = + maybe_tags.map(|tags| tags.into_iter().collect::<HashSet<_>>()); + + 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::<Vec<_>>(); + + 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::<Vec<_>>(); + 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::<Vec<_>>(); + recommended_rule_names.sort(); + assert_eq!(rule_names, recommended_rule_names); + } +} |