diff options
Diffstat (limited to 'cli/tools/lint/rules')
-rw-r--r-- | cli/tools/lint/rules/mod.rs | 296 | ||||
-rw-r--r-- | cli/tools/lint/rules/no_sloppy_imports.md | 20 | ||||
-rw-r--r-- | cli/tools/lint/rules/no_sloppy_imports.rs | 214 | ||||
-rw-r--r-- | cli/tools/lint/rules/no_slow_types.md | 3 | ||||
-rw-r--r-- | cli/tools/lint/rules/no_slow_types.rs | 98 |
5 files changed, 631 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); + } +} diff --git a/cli/tools/lint/rules/no_sloppy_imports.md b/cli/tools/lint/rules/no_sloppy_imports.md new file mode 100644 index 000000000..08547c9da --- /dev/null +++ b/cli/tools/lint/rules/no_sloppy_imports.md @@ -0,0 +1,20 @@ +Enforces specifying explicit references to paths in module specifiers. + +Non-explicit specifiers are ambiguous and require probing for the correct file +path on every run, which has a performance overhead. + +Note: This lint rule is only active when using `--unstable-sloppy-imports`. + +### Invalid: + +```typescript +import { add } from "./math/add"; +import { ConsoleLogger } from "./loggers"; +``` + +### Valid: + +```typescript +import { add } from "./math/add.ts"; +import { ConsoleLogger } from "./loggers/index.ts"; +``` diff --git a/cli/tools/lint/rules/no_sloppy_imports.rs b/cli/tools/lint/rules/no_sloppy_imports.rs new file mode 100644 index 000000000..b5e057bfc --- /dev/null +++ b/cli/tools/lint/rules/no_sloppy_imports.rs @@ -0,0 +1,214 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::HashMap; +use std::sync::Arc; + +use deno_ast::SourceRange; +use deno_config::workspace::WorkspaceResolver; +use deno_core::anyhow::anyhow; +use deno_graph::source::ResolutionMode; +use deno_graph::source::ResolveError; +use deno_graph::Range; +use deno_lint::diagnostic::LintDiagnosticDetails; +use deno_lint::diagnostic::LintDiagnosticRange; +use deno_lint::diagnostic::LintFix; +use deno_lint::diagnostic::LintFixChange; +use deno_lint::rules::LintRule; +use text_lines::LineAndColumnIndex; + +use crate::graph_util::CliJsrUrlProvider; +use crate::resolver::SloppyImportsResolution; +use crate::resolver::SloppyImportsResolver; + +use super::ExtendedLintRule; + +#[derive(Debug)] +pub struct NoSloppyImportsRule { + sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>, + // None for making printing out the lint rules easy + workspace_resolver: Option<Arc<WorkspaceResolver>>, +} + +impl NoSloppyImportsRule { + pub fn new( + sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>, + workspace_resolver: Option<Arc<WorkspaceResolver>>, + ) -> Self { + NoSloppyImportsRule { + sloppy_imports_resolver, + workspace_resolver, + } + } +} + +const CODE: &str = "no-sloppy-imports"; +const DOCS_URL: &str = "https://docs.deno.com/runtime/manual/tools/unstable_flags/#--unstable-sloppy-imports"; + +impl ExtendedLintRule for NoSloppyImportsRule { + fn supports_incremental_cache(&self) -> bool { + // only allow the incremental cache when we don't + // do sloppy import resolution because sloppy import + // resolution requires knowing about the surrounding files + // in addition to the current one + self.sloppy_imports_resolver.is_none() || self.workspace_resolver.is_none() + } + + fn help_docs_url(&self) -> Cow<'static, str> { + Cow::Borrowed(DOCS_URL) + } + + fn into_base(self: Box<Self>) -> Box<dyn LintRule> { + self + } +} + +impl LintRule for NoSloppyImportsRule { + fn lint_program_with_ast_view<'view>( + &self, + context: &mut deno_lint::context::Context<'view>, + _program: deno_lint::Program<'view>, + ) { + let Some(workspace_resolver) = &self.workspace_resolver else { + return; + }; + let Some(sloppy_imports_resolver) = &self.sloppy_imports_resolver else { + return; + }; + if context.specifier().scheme() != "file" { + return; + } + + let resolver = SloppyImportCaptureResolver { + workspace_resolver, + sloppy_imports_resolver, + captures: Default::default(), + }; + + deno_graph::parse_module_from_ast(deno_graph::ParseModuleFromAstOptions { + graph_kind: deno_graph::GraphKind::All, + specifier: context.specifier().clone(), + maybe_headers: None, + parsed_source: context.parsed_source(), + // ignore resolving dynamic imports like import(`./dir/${something}`) + file_system: &deno_graph::source::NullFileSystem, + jsr_url_provider: &CliJsrUrlProvider, + maybe_resolver: Some(&resolver), + // don't bother resolving npm specifiers + maybe_npm_resolver: None, + }); + + for (range, sloppy_import) in resolver.captures.borrow_mut().drain() { + let start_range = + context.text_info().loc_to_source_pos(LineAndColumnIndex { + line_index: range.start.line, + column_index: range.start.character, + }); + let end_range = + context.text_info().loc_to_source_pos(LineAndColumnIndex { + line_index: range.end.line, + column_index: range.end.character, + }); + let source_range = SourceRange::new(start_range, end_range); + context.add_diagnostic_details( + Some(LintDiagnosticRange { + range: source_range, + description: None, + text_info: context.text_info().clone(), + }), + LintDiagnosticDetails { + message: "Sloppy imports are not allowed.".to_string(), + code: CODE.to_string(), + custom_docs_url: Some(DOCS_URL.to_string()), + fixes: context + .specifier() + .make_relative(sloppy_import.as_specifier()) + .map(|relative| { + vec![LintFix { + description: Cow::Owned(sloppy_import.as_quick_fix_message()), + changes: vec![LintFixChange { + new_text: Cow::Owned({ + let relative = if relative.starts_with("../") { + relative + } else { + format!("./{}", relative) + }; + let current_text = + context.text_info().range_text(&source_range); + if current_text.starts_with('"') { + format!("\"{}\"", relative) + } else if current_text.starts_with('\'') { + format!("'{}'", relative) + } else { + relative + } + }), + range: source_range, + }], + }] + }) + .unwrap_or_default(), + hint: None, + info: vec![], + }, + ); + } + } + + fn code(&self) -> &'static str { + CODE + } + + fn docs(&self) -> &'static str { + include_str!("no_sloppy_imports.md") + } + + fn tags(&self) -> &'static [&'static str] { + &["recommended"] + } +} + +#[derive(Debug)] +struct SloppyImportCaptureResolver<'a> { + workspace_resolver: &'a WorkspaceResolver, + sloppy_imports_resolver: &'a SloppyImportsResolver, + captures: RefCell<HashMap<Range, SloppyImportsResolution>>, +} + +impl<'a> deno_graph::source::Resolver for SloppyImportCaptureResolver<'a> { + fn resolve( + &self, + specifier_text: &str, + referrer_range: &Range, + mode: ResolutionMode, + ) -> Result<deno_ast::ModuleSpecifier, deno_graph::source::ResolveError> { + let resolution = self + .workspace_resolver + .resolve(specifier_text, &referrer_range.specifier) + .map_err(|err| ResolveError::Other(err.into()))?; + + match resolution { + deno_config::workspace::MappedResolution::Normal(specifier) + | deno_config::workspace::MappedResolution::ImportMap(specifier) => { + match self.sloppy_imports_resolver.resolve(&specifier, mode) { + Some(res) => { + self + .captures + .borrow_mut() + .entry(referrer_range.clone()) + .or_insert_with(|| res.clone()); + Ok(res.into_specifier()) + } + None => Ok(specifier), + } + } + deno_config::workspace::MappedResolution::WorkspaceNpmPackage { + .. + } + | deno_config::workspace::MappedResolution::PackageJson { .. } => { + Err(ResolveError::Other(anyhow!(""))) + } + } + } +} diff --git a/cli/tools/lint/rules/no_slow_types.md b/cli/tools/lint/rules/no_slow_types.md new file mode 100644 index 000000000..d0764d865 --- /dev/null +++ b/cli/tools/lint/rules/no_slow_types.md @@ -0,0 +1,3 @@ +Enforces using types that are explicit or can be simply inferred. + +Read more: https://jsr.io/docs/about-slow-types diff --git a/cli/tools/lint/rules/no_slow_types.rs b/cli/tools/lint/rules/no_slow_types.rs new file mode 100644 index 000000000..bc3f835b1 --- /dev/null +++ b/cli/tools/lint/rules/no_slow_types.rs @@ -0,0 +1,98 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::borrow::Cow; + +use deno_ast::diagnostics::Diagnostic; +use deno_ast::ModuleSpecifier; +use deno_graph::FastCheckDiagnostic; +use deno_graph::ModuleGraph; +use deno_lint::diagnostic::LintDiagnostic; +use deno_lint::diagnostic::LintDiagnosticDetails; +use deno_lint::diagnostic::LintDiagnosticRange; + +use super::PackageLintRule; + +const CODE: &str = "no-slow-types"; + +#[derive(Debug)] +pub struct NoSlowTypesRule; + +impl PackageLintRule for NoSlowTypesRule { + fn code(&self) -> &'static str { + CODE + } + + fn tags(&self) -> &'static [&'static str] { + &["jsr"] + } + + fn docs(&self) -> &'static str { + include_str!("no_slow_types.md") + } + + fn help_docs_url(&self) -> Cow<'static, str> { + Cow::Borrowed("https://jsr.io/docs/about-slow-types") + } + + fn lint_package( + &self, + graph: &ModuleGraph, + entrypoints: &[ModuleSpecifier], + ) -> Vec<LintDiagnostic> { + collect_no_slow_type_diagnostics(graph, entrypoints) + .into_iter() + .map(|d| LintDiagnostic { + specifier: d.specifier().clone(), + range: d.range().map(|range| LintDiagnosticRange { + text_info: range.text_info.clone(), + range: range.range, + description: d.range_description().map(|r| r.to_string()), + }), + details: LintDiagnosticDetails { + message: d.message().to_string(), + code: CODE.to_string(), + hint: d.hint().map(|h| h.to_string()), + info: d + .info() + .iter() + .map(|info| Cow::Owned(info.to_string())) + .collect(), + fixes: vec![], + custom_docs_url: d.docs_url().map(|u| u.into_owned()), + }, + }) + .collect() + } +} + +/// Collects diagnostics from the module graph for the +/// given package's export URLs. +pub fn collect_no_slow_type_diagnostics( + graph: &ModuleGraph, + package_export_urls: &[ModuleSpecifier], +) -> Vec<FastCheckDiagnostic> { + let mut js_exports = package_export_urls + .iter() + .filter_map(|url| graph.get(url).and_then(|m| m.js())); + // fast check puts the same diagnostics in each entrypoint for the + // package (since it's all or nothing), so we only need to check + // the first one JS entrypoint + let Some(module) = js_exports.next() else { + // could happen if all the exports are JSON + return vec![]; + }; + + if let Some(diagnostics) = module.fast_check_diagnostics() { + let mut diagnostics = diagnostics.clone(); + diagnostics.sort_by_cached_key(|d| { + ( + d.specifier().clone(), + d.range().map(|r| r.range), + d.code().to_string(), + ) + }); + diagnostics + } else { + Vec::new() + } +} |