diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2024-07-25 09:07:59 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-25 09:07:59 -0400 |
commit | 763f05e74dfd0032b238603f625893a52e363591 (patch) | |
tree | c6a71559472755919358afa53eecac206cad80a9 /cli/tools | |
parent | ef78d317f084ffe633253acd138a48a425113fa7 (diff) |
fix(unstable): move sloppy-import warnings to lint rule (#24710)
Adds a new `no-sloppy-imports` lint rule and cleans up the lint code.
Closes #22844
Closes https://github.com/denoland/deno_lint/issues/1293
Diffstat (limited to 'cli/tools')
-rw-r--r-- | cli/tools/lint/linter.rs | 242 | ||||
-rw-r--r-- | cli/tools/lint/mod.rs | 584 | ||||
-rw-r--r-- | cli/tools/lint/no_slow_types.rs | 38 | ||||
-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 | ||||
-rw-r--r-- | cli/tools/registry/mod.rs | 4 | ||||
-rw-r--r-- | cli/tools/registry/unfurl.rs | 4 | ||||
-rw-r--r-- | cli/tools/run/mod.rs | 7 |
11 files changed, 1002 insertions, 508 deletions
diff --git a/cli/tools/lint/linter.rs b/cli/tools/lint/linter.rs new file mode 100644 index 000000000..f6ea76c14 --- /dev/null +++ b/cli/tools/lint/linter.rs @@ -0,0 +1,242 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::path::Path; + +use deno_ast::MediaType; +use deno_ast::ModuleSpecifier; +use deno_ast::ParsedSource; +use deno_ast::SourceTextInfo; +use deno_core::anyhow::Context; +use deno_core::error::AnyError; +use deno_graph::ModuleGraph; +use deno_lint::diagnostic::LintDiagnostic; +use deno_lint::linter::LintConfig as DenoLintConfig; +use deno_lint::linter::LintFileOptions; +use deno_lint::linter::Linter as DenoLintLinter; +use deno_lint::linter::LinterOptions; + +use crate::util::fs::atomic_write_file_with_retries; +use crate::util::fs::specifier_from_file_path; + +use super::rules::FileOrPackageLintRule; +use super::rules::PackageLintRule; +use super::ConfiguredRules; + +pub struct CliLinterOptions { + pub configured_rules: ConfiguredRules, + pub fix: bool, + pub deno_lint_config: DenoLintConfig, +} + +#[derive(Debug)] +pub struct CliLinter { + fix: bool, + package_rules: Vec<Box<dyn PackageLintRule>>, + linter: DenoLintLinter, + deno_lint_config: DenoLintConfig, +} + +impl CliLinter { + pub fn new(options: CliLinterOptions) -> Self { + let rules = options.configured_rules.rules; + let mut deno_lint_rules = Vec::with_capacity(rules.len()); + let mut package_rules = Vec::with_capacity(rules.len()); + for rule in rules { + match rule.into_file_or_pkg_rule() { + FileOrPackageLintRule::File(rule) => { + deno_lint_rules.push(rule); + } + FileOrPackageLintRule::Package(rule) => { + package_rules.push(rule); + } + } + } + Self { + fix: options.fix, + package_rules, + linter: DenoLintLinter::new(LinterOptions { + rules: deno_lint_rules, + all_rule_codes: options.configured_rules.all_rule_codes, + custom_ignore_file_directive: None, + custom_ignore_diagnostic_directive: None, + }), + deno_lint_config: options.deno_lint_config, + } + } + + pub fn has_package_rules(&self) -> bool { + !self.package_rules.is_empty() + } + + pub fn lint_package( + &self, + graph: &ModuleGraph, + entrypoints: &[ModuleSpecifier], + ) -> Vec<LintDiagnostic> { + let mut diagnostics = Vec::new(); + for rule in &self.package_rules { + diagnostics.extend(rule.lint_package(graph, entrypoints)); + } + diagnostics + } + + pub fn lint_with_ast( + &self, + parsed_source: &ParsedSource, + ) -> Vec<LintDiagnostic> { + self + .linter + .lint_with_ast(parsed_source, self.deno_lint_config.clone()) + } + + pub fn lint_file( + &self, + file_path: &Path, + source_code: String, + ) -> Result<(ParsedSource, Vec<LintDiagnostic>), AnyError> { + let specifier = specifier_from_file_path(file_path)?; + let media_type = MediaType::from_specifier(&specifier); + + if self.fix { + self.lint_file_and_fix(&specifier, media_type, source_code, file_path) + } else { + self + .linter + .lint_file(LintFileOptions { + specifier, + media_type, + source_code, + config: self.deno_lint_config.clone(), + }) + .map_err(AnyError::from) + } + } + + fn lint_file_and_fix( + &self, + specifier: &ModuleSpecifier, + media_type: MediaType, + source_code: String, + file_path: &Path, + ) -> Result<(ParsedSource, Vec<LintDiagnostic>), deno_core::anyhow::Error> { + // initial lint + let (source, diagnostics) = self.linter.lint_file(LintFileOptions { + specifier: specifier.clone(), + media_type, + source_code, + config: self.deno_lint_config.clone(), + })?; + + // Try applying fixes repeatedly until the file has none left or + // a maximum number of iterations is reached. This is necessary + // because lint fixes may overlap and so we can't always apply + // them in one pass. + let mut source = source; + let mut diagnostics = diagnostics; + let mut fix_iterations = 0; + loop { + let change = apply_lint_fixes_and_relint( + specifier, + media_type, + &self.linter, + self.deno_lint_config.clone(), + source.text_info_lazy(), + &diagnostics, + )?; + match change { + Some(change) => { + source = change.0; + diagnostics = change.1; + } + None => { + break; + } + } + fix_iterations += 1; + if fix_iterations > 5 { + log::warn!( + concat!( + "Reached maximum number of fix iterations for '{}'. There's ", + "probably a bug in Deno. Please fix this file manually.", + ), + specifier, + ); + break; + } + } + + if fix_iterations > 0 { + // everything looks good and the file still parses, so write it out + atomic_write_file_with_retries( + file_path, + source.text().as_ref(), + crate::cache::CACHE_PERM, + ) + .context("Failed writing fix to file.")?; + } + + Ok((source, diagnostics)) + } +} + +fn apply_lint_fixes_and_relint( + specifier: &ModuleSpecifier, + media_type: MediaType, + linter: &DenoLintLinter, + config: DenoLintConfig, + text_info: &SourceTextInfo, + diagnostics: &[LintDiagnostic], +) -> Result<Option<(ParsedSource, Vec<LintDiagnostic>)>, AnyError> { + let Some(new_text) = apply_lint_fixes(text_info, diagnostics) else { + return Ok(None); + }; + linter + .lint_file(LintFileOptions { + specifier: specifier.clone(), + source_code: new_text, + media_type, + config, + }) + .map(Some) + .context( + "An applied lint fix caused a syntax error. Please report this bug.", + ) +} + +fn apply_lint_fixes( + text_info: &SourceTextInfo, + diagnostics: &[LintDiagnostic], +) -> Option<String> { + if diagnostics.is_empty() { + return None; + } + + let file_start = text_info.range().start; + let mut quick_fixes = diagnostics + .iter() + // use the first quick fix + .filter_map(|d| d.details.fixes.first()) + .flat_map(|fix| fix.changes.iter()) + .map(|change| deno_ast::TextChange { + range: change.range.as_byte_range(file_start), + new_text: change.new_text.to_string(), + }) + .collect::<Vec<_>>(); + if quick_fixes.is_empty() { + return None; + } + // remove any overlapping text changes, we'll circle + // back for another pass to fix the remaining + quick_fixes.sort_by_key(|change| change.range.start); + for i in (1..quick_fixes.len()).rev() { + let cur = &quick_fixes[i]; + let previous = &quick_fixes[i - 1]; + let is_overlapping = cur.range.start < previous.range.end; + if is_overlapping { + quick_fixes.remove(i); + } + } + let new_text = + deno_ast::apply_text_changes(text_info.text_str(), quick_fixes); + Some(new_text) +} diff --git a/cli/tools/lint/mod.rs b/cli/tools/lint/mod.rs index 942129120..e01b38430 100644 --- a/cli/tools/lint/mod.rs +++ b/cli/tools/lint/mod.rs @@ -3,18 +3,13 @@ //! This module provides file linting utilities using //! [`deno_lint`](https://github.com/denoland/deno_lint). use deno_ast::diagnostics::Diagnostic; -use deno_ast::MediaType; use deno_ast::ModuleSpecifier; use deno_ast::ParsedSource; -use deno_ast::SourceRange; -use deno_ast::SourceTextInfo; -use deno_config::deno_json::ConfigFile; +use deno_config::deno_json::LintRulesConfig; use deno_config::glob::FileCollector; use deno_config::glob::FilePatterns; use deno_config::workspace::WorkspaceDirectory; use deno_core::anyhow::anyhow; -use deno_core::anyhow::bail; -use deno_core::anyhow::Context; use deno_core::error::generic_error; use deno_core::error::AnyError; use deno_core::futures::future::LocalBoxFuture; @@ -23,19 +18,12 @@ use deno_core::parking_lot::Mutex; use deno_core::serde_json; use deno_core::unsync::future::LocalFutureExt; use deno_core::unsync::future::SharedLocal; -use deno_graph::FastCheckDiagnostic; use deno_graph::ModuleGraph; use deno_lint::diagnostic::LintDiagnostic; use deno_lint::linter::LintConfig; -use deno_lint::linter::LintFileOptions; -use deno_lint::linter::Linter; -use deno_lint::linter::LinterBuilder; -use deno_lint::rules; -use deno_lint::rules::LintRule; use log::debug; use log::info; use serde::Serialize; -use std::borrow::Cow; use std::collections::HashSet; use std::fs; use std::io::stdin; @@ -50,7 +38,6 @@ use crate::args::Flags; use crate::args::LintFlags; use crate::args::LintOptions; use crate::args::LintReporterKind; -use crate::args::LintRulesConfig; use crate::args::WorkspaceLintOptions; use crate::cache::Caches; use crate::cache::IncrementalCache; @@ -60,11 +47,17 @@ use crate::graph_util::ModuleGraphCreator; use crate::tools::fmt::run_parallelized; use crate::util::file_watcher; use crate::util::fs::canonicalize_path; -use crate::util::fs::specifier_from_file_path; use crate::util::path::is_script_ext; use crate::util::sync::AtomicFlag; -pub mod no_slow_types; +mod linter; +mod rules; + +pub use linter::CliLinter; +pub use linter::CliLinterOptions; +pub use rules::collect_no_slow_type_diagnostics; +pub use rules::ConfiguredRules; +pub use rules::LintRuleProvider; static STDIN_FILE_NAME: &str = "$deno$stdin.ts"; @@ -120,6 +113,7 @@ pub async fn lint( let mut linter = WorkspaceLinter::new( factory.caches()?.clone(), + factory.lint_rule_provider().await?, factory.module_graph_creator().await?.clone(), cli_options.start_dir.clone(), &cli_options.resolve_workspace_lint_options(&lint_flags)?, @@ -157,12 +151,15 @@ pub async fn lint( let lint_config = start_dir .to_lint_config(FilePatterns::new_with_base(start_dir.dir_path()))?; let lint_options = LintOptions::resolve(lint_config, &lint_flags); - let lint_rules = get_config_rules_err_empty( - lint_options.rules, - start_dir.maybe_deno_json().map(|c| c.as_ref()), - )?; + let lint_rules = factory + .lint_rule_provider() + .await? + .resolve_lint_rules_err_empty( + lint_options.rules, + start_dir.maybe_deno_json().map(|c| c.as_ref()), + )?; let file_path = cli_options.initial_cwd().join(STDIN_FILE_NAME); - let r = lint_stdin(&file_path, lint_rules.rules, deno_lint_config); + let r = lint_stdin(&file_path, lint_rules, deno_lint_config); let success = handle_lint_result( &file_path.to_string_lossy(), r, @@ -173,6 +170,7 @@ pub async fn lint( } else { let mut linter = WorkspaceLinter::new( factory.caches()?.clone(), + factory.lint_rule_provider().await?, factory.module_graph_creator().await?.clone(), cli_options.start_dir.clone(), &workspace_lint_options, @@ -234,6 +232,7 @@ type WorkspaceModuleGraphFuture = struct WorkspaceLinter { caches: Arc<Caches>, + lint_rule_provider: LintRuleProvider, module_graph_creator: Arc<ModuleGraphCreator>, workspace_dir: Arc<WorkspaceDirectory>, reporter_lock: Arc<Mutex<Box<dyn LintReporter + Send>>>, @@ -245,6 +244,7 @@ struct WorkspaceLinter { impl WorkspaceLinter { pub fn new( caches: Arc<Caches>, + lint_rule_provider: LintRuleProvider, module_graph_creator: Arc<ModuleGraphCreator>, workspace_dir: Arc<WorkspaceDirectory>, workspace_options: &WorkspaceLintOptions, @@ -253,6 +253,7 @@ impl WorkspaceLinter { Arc::new(Mutex::new(create_reporter(workspace_options.reporter_kind))); Self { caches, + lint_rule_provider, module_graph_creator, workspace_dir, reporter_lock, @@ -271,18 +272,27 @@ impl WorkspaceLinter { ) -> Result<(), AnyError> { self.file_count += paths.len(); - let lint_rules = get_config_rules_err_empty( + let lint_rules = self.lint_rule_provider.resolve_lint_rules_err_empty( lint_options.rules, member_dir.maybe_deno_json().map(|c| c.as_ref()), )?; - let incremental_cache = Arc::new(IncrementalCache::new( - self.caches.lint_incremental_cache_db(), - &lint_rules.incremental_cache_state(), - &paths, - )); + let maybe_incremental_cache = + lint_rules.incremental_cache_state().map(|state| { + Arc::new(IncrementalCache::new( + self.caches.lint_incremental_cache_db(), + &state, + &paths, + )) + }); + + let linter = Arc::new(CliLinter::new(CliLinterOptions { + configured_rules: lint_rules, + fix: lint_options.fix, + deno_lint_config: lint_config, + })); let mut futures = Vec::with_capacity(2); - if lint_rules.no_slow_types { + if linter.has_package_rules() { if self.workspace_module_graph.is_none() { let module_graph_creator = self.module_graph_creator.clone(); let packages = self.workspace_dir.jsr_packages_for_publish(); @@ -304,6 +314,7 @@ impl WorkspaceLinter { if let Some(publish_config) = publish_config { let has_error = self.has_error.clone(); let reporter_lock = self.reporter_lock.clone(); + let linter = linter.clone(); let path_urls = paths .iter() .filter_map(|p| ModuleSpecifier::from_file_path(p).ok()) @@ -318,16 +329,12 @@ impl WorkspaceLinter { if !export_urls.iter().any(|url| path_urls.contains(url)) { return Ok(()); // entrypoint is not specified, so skip } - let diagnostics = no_slow_types::collect_no_slow_type_diagnostics( - &export_urls, - &graph, - ); + let diagnostics = linter.lint_package(&graph, &export_urls); if !diagnostics.is_empty() { has_error.raise(); let mut reporter = reporter_lock.lock(); for diagnostic in &diagnostics { - reporter - .visit_diagnostic(LintOrCliDiagnostic::FastCheck(diagnostic)); + reporter.visit_diagnostic(diagnostic); } } Ok(()) @@ -339,11 +346,9 @@ impl WorkspaceLinter { futures.push({ let has_error = self.has_error.clone(); - let linter = create_linter(lint_rules.rules); let reporter_lock = self.reporter_lock.clone(); - let incremental_cache = incremental_cache.clone(); - let lint_config = lint_config.clone(); - let fix = lint_options.fix; + let maybe_incremental_cache = maybe_incremental_cache.clone(); + let linter = linter.clone(); async move { run_parallelized(paths, { move |file_path| { @@ -351,19 +356,23 @@ impl WorkspaceLinter { deno_ast::strip_bom(fs::read_to_string(&file_path)?); // don't bother rechecking this file if it didn't have any diagnostics before - if incremental_cache.is_file_same(&file_path, &file_text) { - return Ok(()); + if let Some(incremental_cache) = &maybe_incremental_cache { + if incremental_cache.is_file_same(&file_path, &file_text) { + return Ok(()); + } } - let r = lint_file(&linter, &file_path, file_text, lint_config, fix); + let r = linter.lint_file(&file_path, file_text); if let Ok((file_source, file_diagnostics)) = &r { - if file_diagnostics.is_empty() { - // update the incremental cache if there were no diagnostics - incremental_cache.update_file( - &file_path, - // ensure the returned text is used here as it may have been modified via --fix - file_source.text(), - ) + if let Some(incremental_cache) = &maybe_incremental_cache { + if file_diagnostics.is_empty() { + // update the incremental cache if there were no diagnostics + incremental_cache.update_file( + &file_path, + // ensure the returned text is used here as it may have been modified via --fix + file_source.text(), + ) + } } } @@ -384,9 +393,21 @@ impl WorkspaceLinter { .boxed_local() }); - deno_core::futures::future::try_join_all(futures).await?; + if lint_options.fix { + // run sequentially when using `--fix` to lower the chances of weird + // bugs where a file level fix affects a package level diagnostic though + // it probably will happen anyway + for future in futures { + future.await?; + } + } else { + deno_core::futures::future::try_join_all(futures).await?; + } + + if let Some(incremental_cache) = &maybe_incremental_cache { + incremental_cache.wait_completion().await; + } - incremental_cache.wait_completion().await; Ok(()) } @@ -410,11 +431,17 @@ fn collect_lint_files( #[allow(clippy::print_stdout)] pub fn print_rules_list(json: bool, maybe_rules_tags: Option<Vec<String>>) { - let lint_rules = if maybe_rules_tags.is_none() { - rules::get_all_rules() - } else { - rules::get_filtered_rules(maybe_rules_tags, None, None) - }; + let rule_provider = LintRuleProvider::new(None, None); + let lint_rules = rule_provider + .resolve_lint_rules( + LintRulesConfig { + tags: maybe_rules_tags.clone(), + include: None, + exclude: None, + }, + None, + ) + .rules; if json { let json_rules: Vec<serde_json::Value> = lint_rules @@ -442,186 +469,19 @@ pub fn print_rules_list(json: bool, maybe_rules_tags: Option<Vec<String>>) { } println!( "{}", - colors::gray(format!( - " help: https://lint.deno.land/#{}", - rule.code() - )) + colors::gray(format!(" help: {}", rule.help_docs_url())) ); println!(); } } } -pub fn create_linter(rules: Vec<&'static dyn LintRule>) -> Linter { - LinterBuilder::default() - .ignore_file_directive("deno-lint-ignore-file") - .ignore_diagnostic_directive("deno-lint-ignore") - .rules(rules) - .build() -} - -fn lint_file( - linter: &Linter, - file_path: &Path, - source_code: String, - config: LintConfig, - fix: bool, -) -> Result<(ParsedSource, Vec<LintDiagnostic>), AnyError> { - let specifier = specifier_from_file_path(file_path)?; - let media_type = MediaType::from_specifier(&specifier); - - if fix { - lint_file_and_fix( - linter, - &specifier, - media_type, - source_code, - file_path, - config, - ) - } else { - linter - .lint_file(LintFileOptions { - specifier, - media_type, - source_code, - config, - }) - .map_err(AnyError::from) - } -} - -fn lint_file_and_fix( - linter: &Linter, - specifier: &ModuleSpecifier, - media_type: MediaType, - source_code: String, - file_path: &Path, - config: LintConfig, -) -> Result<(ParsedSource, Vec<LintDiagnostic>), deno_core::anyhow::Error> { - // initial lint - let (source, diagnostics) = linter.lint_file(LintFileOptions { - specifier: specifier.clone(), - media_type, - source_code, - config: config.clone(), - })?; - - // Try applying fixes repeatedly until the file has none left or - // a maximum number of iterations is reached. This is necessary - // because lint fixes may overlap and so we can't always apply - // them in one pass. - let mut source = source; - let mut diagnostics = diagnostics; - let mut fix_iterations = 0; - loop { - let change = apply_lint_fixes_and_relint( - specifier, - media_type, - linter, - config.clone(), - source.text_info_lazy(), - &diagnostics, - )?; - match change { - Some(change) => { - source = change.0; - diagnostics = change.1; - } - None => { - break; - } - } - fix_iterations += 1; - if fix_iterations > 5 { - log::warn!( - concat!( - "Reached maximum number of fix iterations for '{}'. There's ", - "probably a bug in Deno. Please fix this file manually.", - ), - specifier, - ); - break; - } - } - - if fix_iterations > 0 { - // everything looks good and the file still parses, so write it out - fs::write(file_path, source.text().as_ref()) - .context("Failed writing fix to file.")?; - } - - Ok((source, diagnostics)) -} - -fn apply_lint_fixes_and_relint( - specifier: &ModuleSpecifier, - media_type: MediaType, - linter: &Linter, - config: LintConfig, - text_info: &SourceTextInfo, - diagnostics: &[LintDiagnostic], -) -> Result<Option<(ParsedSource, Vec<LintDiagnostic>)>, AnyError> { - let Some(new_text) = apply_lint_fixes(text_info, diagnostics) else { - return Ok(None); - }; - linter - .lint_file(LintFileOptions { - specifier: specifier.clone(), - source_code: new_text, - media_type, - config, - }) - .map(Some) - .context( - "An applied lint fix caused a syntax error. Please report this bug.", - ) -} - -fn apply_lint_fixes( - text_info: &SourceTextInfo, - diagnostics: &[LintDiagnostic], -) -> Option<String> { - if diagnostics.is_empty() { - return None; - } - - let file_start = text_info.range().start; - let mut quick_fixes = diagnostics - .iter() - // use the first quick fix - .filter_map(|d| d.fixes.first()) - .flat_map(|fix| fix.changes.iter()) - .map(|change| deno_ast::TextChange { - range: change.range.as_byte_range(file_start), - new_text: change.new_text.to_string(), - }) - .collect::<Vec<_>>(); - if quick_fixes.is_empty() { - return None; - } - // remove any overlapping text changes, we'll circle - // back for another pass to fix the remaining - quick_fixes.sort_by_key(|change| change.range.start); - for i in (1..quick_fixes.len()).rev() { - let cur = &quick_fixes[i]; - let previous = &quick_fixes[i - 1]; - let is_overlapping = cur.range.start < previous.range.end; - if is_overlapping { - quick_fixes.remove(i); - } - } - let new_text = - deno_ast::apply_text_changes(text_info.text_str(), quick_fixes); - Some(new_text) -} - /// Lint stdin and write result to stdout. /// Treats input as TypeScript. /// Compatible with `--json` flag. fn lint_stdin( file_path: &Path, - lint_rules: Vec<&'static dyn LintRule>, + configured_rules: ConfiguredRules, deno_lint_config: LintConfig, ) -> Result<(ParsedSource, Vec<LintDiagnostic>), AnyError> { let mut source_code = String::new(); @@ -629,15 +489,14 @@ fn lint_stdin( return Err(generic_error("Failed to read from stdin")); } - let linter = create_linter(lint_rules); + let linter = CliLinter::new(CliLinterOptions { + fix: false, + configured_rules, + deno_lint_config, + }); linter - .lint_file(LintFileOptions { - specifier: specifier_from_file_path(file_path)?, - source_code: deno_ast::strip_bom(source_code), - media_type: MediaType::TypeScript, - config: deno_lint_config, - }) + .lint_file(file_path, deno_ast::strip_bom(source_code)) .map_err(AnyError::from) } @@ -656,11 +515,18 @@ fn handle_lint_result( } } file_diagnostics.sort_by(|a, b| match a.specifier.cmp(&b.specifier) { - std::cmp::Ordering::Equal => a.range.start.cmp(&b.range.start), + std::cmp::Ordering::Equal => { + let a_start = a.range.as_ref().map(|r| r.range.start); + let b_start = b.range.as_ref().map(|r| r.range.start); + match a_start.cmp(&b_start) { + std::cmp::Ordering::Equal => a.details.code.cmp(&b.details.code), + other => other, + } + } file_order => file_order, }); for d in &file_diagnostics { - reporter.visit_diagnostic(LintOrCliDiagnostic::Lint(d)); + reporter.visit_diagnostic(d); } file_diagnostics.is_empty() } @@ -671,99 +537,8 @@ fn handle_lint_result( } } -#[derive(Clone, Copy)] -pub enum LintOrCliDiagnostic<'a> { - Lint(&'a LintDiagnostic), - FastCheck(&'a FastCheckDiagnostic), -} - -impl<'a> LintOrCliDiagnostic<'a> { - pub fn specifier(&self) -> &ModuleSpecifier { - match self { - LintOrCliDiagnostic::Lint(d) => &d.specifier, - LintOrCliDiagnostic::FastCheck(d) => d.specifier(), - } - } - - pub fn range(&self) -> Option<(&SourceTextInfo, SourceRange)> { - match self { - LintOrCliDiagnostic::Lint(d) => Some((&d.text_info, d.range)), - LintOrCliDiagnostic::FastCheck(d) => { - d.range().map(|r| (&r.text_info, r.range)) - } - } - } -} - -impl<'a> deno_ast::diagnostics::Diagnostic for LintOrCliDiagnostic<'a> { - fn level(&self) -> deno_ast::diagnostics::DiagnosticLevel { - match self { - LintOrCliDiagnostic::Lint(d) => d.level(), - LintOrCliDiagnostic::FastCheck(d) => d.level(), - } - } - - fn code(&self) -> Cow<'_, str> { - match self { - LintOrCliDiagnostic::Lint(d) => d.code(), - LintOrCliDiagnostic::FastCheck(_) => Cow::Borrowed("no-slow-types"), - } - } - - fn message(&self) -> Cow<'_, str> { - match self { - LintOrCliDiagnostic::Lint(d) => d.message(), - LintOrCliDiagnostic::FastCheck(d) => d.message(), - } - } - - fn location(&self) -> deno_ast::diagnostics::DiagnosticLocation { - match self { - LintOrCliDiagnostic::Lint(d) => d.location(), - LintOrCliDiagnostic::FastCheck(d) => d.location(), - } - } - - fn snippet(&self) -> Option<deno_ast::diagnostics::DiagnosticSnippet<'_>> { - match self { - LintOrCliDiagnostic::Lint(d) => d.snippet(), - LintOrCliDiagnostic::FastCheck(d) => d.snippet(), - } - } - - fn hint(&self) -> Option<Cow<'_, str>> { - match self { - LintOrCliDiagnostic::Lint(d) => d.hint(), - LintOrCliDiagnostic::FastCheck(d) => d.hint(), - } - } - - fn snippet_fixed( - &self, - ) -> Option<deno_ast::diagnostics::DiagnosticSnippet<'_>> { - match self { - LintOrCliDiagnostic::Lint(d) => d.snippet_fixed(), - LintOrCliDiagnostic::FastCheck(d) => d.snippet_fixed(), - } - } - - fn info(&self) -> Cow<'_, [Cow<'_, str>]> { - match self { - LintOrCliDiagnostic::Lint(d) => d.info(), - LintOrCliDiagnostic::FastCheck(d) => d.info(), - } - } - - fn docs_url(&self) -> Option<Cow<'_, str>> { - match self { - LintOrCliDiagnostic::Lint(d) => d.docs_url(), - LintOrCliDiagnostic::FastCheck(d) => d.docs_url(), - } - } -} - trait LintReporter { - fn visit_diagnostic(&mut self, d: LintOrCliDiagnostic); + fn visit_diagnostic(&mut self, d: &LintDiagnostic); fn visit_error(&mut self, file_path: &str, err: &AnyError); fn close(&mut self, check_count: usize); } @@ -789,12 +564,10 @@ impl PrettyLintReporter { } impl LintReporter for PrettyLintReporter { - fn visit_diagnostic(&mut self, d: LintOrCliDiagnostic) { + fn visit_diagnostic(&mut self, d: &LintDiagnostic) { self.lint_count += 1; - if let LintOrCliDiagnostic::Lint(d) = d { - if !d.fixes.is_empty() { - self.fixable_diagnostics += 1; - } + if !d.details.fixes.is_empty() { + self.fixable_diagnostics += 1; } log::error!("{}\n", d.display()); @@ -838,15 +611,17 @@ impl CompactLintReporter { } impl LintReporter for CompactLintReporter { - fn visit_diagnostic(&mut self, d: LintOrCliDiagnostic) { + fn visit_diagnostic(&mut self, d: &LintDiagnostic) { self.lint_count += 1; - match d.range() { - Some((text_info, range)) => { + match &d.range { + Some(range) => { + let text_info = &range.text_info; + let range = &range.range; let line_and_column = text_info.line_and_column_display(range.start); log::error!( "{}: line {}, col {} - {} ({})", - d.specifier(), + d.specifier, line_and_column.line_number, line_and_column.column_number, d.message(), @@ -854,7 +629,7 @@ impl LintReporter for CompactLintReporter { ) } None => { - log::error!("{}: {} ({})", d.specifier(), d.message(), d.code()) + log::error!("{}: {} ({})", d.specifier, d.message(), d.code()) } } } @@ -932,18 +707,22 @@ impl JsonLintReporter { } impl LintReporter for JsonLintReporter { - fn visit_diagnostic(&mut self, d: LintOrCliDiagnostic) { + fn visit_diagnostic(&mut self, d: &LintDiagnostic) { self.diagnostics.push(JsonLintDiagnostic { - filename: d.specifier().to_string(), - range: d.range().map(|(text_info, range)| JsonLintDiagnosticRange { - start: JsonDiagnosticLintPosition::new( - range.start.as_byte_index(text_info.range().start), - text_info.line_and_column_index(range.start), - ), - end: JsonDiagnosticLintPosition::new( - range.end.as_byte_index(text_info.range().start), - text_info.line_and_column_index(range.end), - ), + filename: d.specifier.to_string(), + range: d.range.as_ref().map(|range| { + let text_info = &range.text_info; + let range = range.range; + JsonLintDiagnosticRange { + start: JsonDiagnosticLintPosition::new( + range.start.as_byte_index(text_info.range().start), + text_info.line_and_column_index(range.start), + ), + end: JsonDiagnosticLintPosition::new( + range.end.as_byte_index(text_info.range().start), + text_info.line_and_column_index(range.end), + ), + } }), message: d.message().to_string(), code: d.code().to_string(), @@ -994,116 +773,3 @@ fn sort_diagnostics(diagnostics: &mut [JsonLintDiagnostic]) { } }); } - -fn get_config_rules_err_empty( - rules: LintRulesConfig, - maybe_config_file: Option<&ConfigFile>, -) -> Result<ConfiguredRules, AnyError> { - let lint_rules = get_configured_rules(rules, maybe_config_file); - if lint_rules.rules.is_empty() { - bail!("No rules have been configured") - } - Ok(lint_rules) -} - -#[derive(Debug, Clone)] -pub struct ConfiguredRules { - pub rules: Vec<&'static dyn LintRule>, - // cli specific rules - pub no_slow_types: bool, -} - -impl Default for ConfiguredRules { - fn default() -> Self { - get_configured_rules(Default::default(), None) - } -} - -impl ConfiguredRules { - fn incremental_cache_state(&self) -> Vec<&str> { - // use a hash of the rule names in order to bust the cache - let mut names = self.rules.iter().map(|r| r.code()).collect::<Vec<_>>(); - // ensure this is stable by sorting it - names.sort_unstable(); - if self.no_slow_types { - names.push("no-slow-types"); - } - names - } -} - -pub fn get_configured_rules( - rules: LintRulesConfig, - maybe_config_file: Option<&ConfigFile>, -) -> ConfiguredRules { - const NO_SLOW_TYPES_NAME: &str = "no-slow-types"; - let implicit_no_slow_types = - maybe_config_file.map(|c| c.is_package()).unwrap_or(false); - let no_slow_types = implicit_no_slow_types - && !rules - .exclude - .as_ref() - .map(|exclude| exclude.iter().any(|i| i == NO_SLOW_TYPES_NAME)) - .unwrap_or(false); - let rules = rules::get_filtered_rules( - rules - .tags - .or_else(|| Some(get_default_tags(maybe_config_file))), - rules.exclude.map(|exclude| { - exclude - .into_iter() - .filter(|c| c != NO_SLOW_TYPES_NAME) - .collect() - }), - rules.include.map(|include| { - include - .into_iter() - .filter(|c| c != NO_SLOW_TYPES_NAME) - .collect() - }), - ); - ConfiguredRules { - rules, - no_slow_types, - } -} - -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 -} - -#[cfg(test)] -mod test { - use deno_lint::rules::get_recommended_rules; - - 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 = get_configured_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 = get_recommended_rules() - .into_iter() - .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/no_slow_types.rs b/cli/tools/lint/no_slow_types.rs deleted file mode 100644 index 6bb108a88..000000000 --- a/cli/tools/lint/no_slow_types.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -use deno_ast::diagnostics::Diagnostic; -use deno_ast::ModuleSpecifier; -use deno_graph::FastCheckDiagnostic; -use deno_graph::ModuleGraph; - -/// Collects diagnostics from the module graph for the -/// given package's export URLs. -pub fn collect_no_slow_type_diagnostics( - package_export_urls: &[ModuleSpecifier], - graph: &ModuleGraph, -) -> 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() - } -} 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() + } +} diff --git a/cli/tools/registry/mod.rs b/cli/tools/registry/mod.rs index c9b742588..586327821 100644 --- a/cli/tools/registry/mod.rs +++ b/cli/tools/registry/mod.rs @@ -45,7 +45,7 @@ use crate::graph_util::ModuleGraphCreator; use crate::http_util::HttpClient; use crate::resolver::SloppyImportsResolver; use crate::tools::check::CheckOptions; -use crate::tools::lint::no_slow_types; +use crate::tools::lint::collect_no_slow_type_diagnostics; use crate::tools::registry::diagnostics::PublishDiagnostic; use crate::tools::registry::diagnostics::PublishDiagnosticsCollector; use crate::util::display::human_size; @@ -341,7 +341,7 @@ impl PublishPreparer { for package in package_configs { let export_urls = package.config_file.resolve_export_value_urls()?; let diagnostics = - no_slow_types::collect_no_slow_type_diagnostics(&export_urls, &graph); + collect_no_slow_type_diagnostics(&graph, &export_urls); if !diagnostics.is_empty() { any_pkg_had_diagnostics = true; for diagnostic in diagnostics { diff --git a/cli/tools/registry/unfurl.rs b/cli/tools/registry/unfurl.rs index f7c1049ca..a28ba445a 100644 --- a/cli/tools/registry/unfurl.rs +++ b/cli/tools/registry/unfurl.rs @@ -177,8 +177,8 @@ impl SpecifierUnfurler { if let Some(sloppy_imports_resolver) = &self.sloppy_imports_resolver { sloppy_imports_resolver .resolve(&resolved, deno_graph::source::ResolutionMode::Execution) - .as_specifier() - .clone() + .map(|res| res.into_specifier()) + .unwrap_or(resolved) } else { resolved }; diff --git a/cli/tools/run/mod.rs b/cli/tools/run/mod.rs index baa8e72db..65044fbad 100644 --- a/cli/tools/run/mod.rs +++ b/cli/tools/run/mod.rs @@ -45,13 +45,6 @@ To grant permissions, set them before the script argument. For example: let deno_dir = factory.deno_dir()?; let http_client = factory.http_client_provider(); - if cli_options.unstable_sloppy_imports() { - log::warn!( - "{} Sloppy imports are not recommended and have a negative impact on performance.", - crate::colors::yellow("Warning"), - ); - } - // Run a background task that checks for available upgrades or output // if an earlier run of this background task found a new version of Deno. #[cfg(feature = "upgrade")] |