summaryrefslogtreecommitdiff
path: root/cli/tools
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2024-07-25 09:07:59 -0400
committerGitHub <noreply@github.com>2024-07-25 09:07:59 -0400
commit763f05e74dfd0032b238603f625893a52e363591 (patch)
treec6a71559472755919358afa53eecac206cad80a9 /cli/tools
parentef78d317f084ffe633253acd138a48a425113fa7 (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.rs242
-rw-r--r--cli/tools/lint/mod.rs584
-rw-r--r--cli/tools/lint/no_slow_types.rs38
-rw-r--r--cli/tools/lint/rules/mod.rs296
-rw-r--r--cli/tools/lint/rules/no_sloppy_imports.md20
-rw-r--r--cli/tools/lint/rules/no_sloppy_imports.rs214
-rw-r--r--cli/tools/lint/rules/no_slow_types.md3
-rw-r--r--cli/tools/lint/rules/no_slow_types.rs98
-rw-r--r--cli/tools/registry/mod.rs4
-rw-r--r--cli/tools/registry/unfurl.rs4
-rw-r--r--cli/tools/run/mod.rs7
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")]