summaryrefslogtreecommitdiff
path: root/cli/tools/lint/rules
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tools/lint/rules')
-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
5 files changed, 631 insertions, 0 deletions
diff --git a/cli/tools/lint/rules/mod.rs b/cli/tools/lint/rules/mod.rs
new file mode 100644
index 000000000..2669ffda1
--- /dev/null
+++ b/cli/tools/lint/rules/mod.rs
@@ -0,0 +1,296 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+use std::borrow::Cow;
+use std::collections::HashSet;
+use std::sync::Arc;
+
+use deno_ast::ModuleSpecifier;
+use deno_config::deno_json::ConfigFile;
+use deno_config::deno_json::LintRulesConfig;
+use deno_config::workspace::WorkspaceResolver;
+use deno_core::anyhow::bail;
+use deno_core::error::AnyError;
+use deno_graph::ModuleGraph;
+use deno_lint::diagnostic::LintDiagnostic;
+use deno_lint::rules::LintRule;
+
+use crate::resolver::SloppyImportsResolver;
+
+mod no_sloppy_imports;
+mod no_slow_types;
+
+// used for publishing
+pub use no_slow_types::collect_no_slow_type_diagnostics;
+
+pub trait PackageLintRule: std::fmt::Debug + Send + Sync {
+ fn code(&self) -> &'static str;
+
+ fn tags(&self) -> &'static [&'static str] {
+ &[]
+ }
+
+ fn docs(&self) -> &'static str;
+
+ fn help_docs_url(&self) -> Cow<'static, str>;
+
+ fn lint_package(
+ &self,
+ graph: &ModuleGraph,
+ entrypoints: &[ModuleSpecifier],
+ ) -> Vec<LintDiagnostic>;
+}
+
+pub(super) trait ExtendedLintRule: LintRule {
+ /// If the rule supports the incremental cache.
+ fn supports_incremental_cache(&self) -> bool;
+
+ fn help_docs_url(&self) -> Cow<'static, str>;
+
+ fn into_base(self: Box<Self>) -> Box<dyn LintRule>;
+}
+
+pub enum FileOrPackageLintRule {
+ File(Box<dyn LintRule>),
+ Package(Box<dyn PackageLintRule>),
+}
+
+#[derive(Debug)]
+enum CliLintRuleKind {
+ DenoLint(Box<dyn LintRule>),
+ Extended(Box<dyn ExtendedLintRule>),
+ Package(Box<dyn PackageLintRule>),
+}
+
+#[derive(Debug)]
+pub struct CliLintRule(CliLintRuleKind);
+
+impl CliLintRule {
+ pub fn code(&self) -> &'static str {
+ use CliLintRuleKind::*;
+ match &self.0 {
+ DenoLint(rule) => rule.code(),
+ Extended(rule) => rule.code(),
+ Package(rule) => rule.code(),
+ }
+ }
+
+ pub fn tags(&self) -> &'static [&'static str] {
+ use CliLintRuleKind::*;
+ match &self.0 {
+ DenoLint(rule) => rule.tags(),
+ Extended(rule) => rule.tags(),
+ Package(rule) => rule.tags(),
+ }
+ }
+
+ pub fn docs(&self) -> &'static str {
+ use CliLintRuleKind::*;
+ match &self.0 {
+ DenoLint(rule) => rule.docs(),
+ Extended(rule) => rule.docs(),
+ Package(rule) => rule.docs(),
+ }
+ }
+
+ pub fn help_docs_url(&self) -> Cow<'static, str> {
+ use CliLintRuleKind::*;
+ match &self.0 {
+ DenoLint(rule) => {
+ Cow::Owned(format!("https://lint.deno.land/rules/{}", rule.code()))
+ }
+ Extended(rule) => rule.help_docs_url(),
+ Package(rule) => rule.help_docs_url(),
+ }
+ }
+
+ pub fn supports_incremental_cache(&self) -> bool {
+ use CliLintRuleKind::*;
+ match &self.0 {
+ DenoLint(_) => true,
+ Extended(rule) => rule.supports_incremental_cache(),
+ // graph rules don't go through the incremental cache, so allow it
+ Package(_) => true,
+ }
+ }
+
+ pub fn into_file_or_pkg_rule(self) -> FileOrPackageLintRule {
+ use CliLintRuleKind::*;
+ match self.0 {
+ DenoLint(rule) => FileOrPackageLintRule::File(rule),
+ Extended(rule) => FileOrPackageLintRule::File(rule.into_base()),
+ Package(rule) => FileOrPackageLintRule::Package(rule),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct ConfiguredRules {
+ pub all_rule_codes: HashSet<&'static str>,
+ pub rules: Vec<CliLintRule>,
+}
+
+impl ConfiguredRules {
+ pub fn incremental_cache_state(&self) -> Option<impl std::hash::Hash> {
+ if self.rules.iter().any(|r| !r.supports_incremental_cache()) {
+ return None;
+ }
+
+ // use a hash of the rule names in order to bust the cache
+ let mut codes = self.rules.iter().map(|r| r.code()).collect::<Vec<_>>();
+ // ensure this is stable by sorting it
+ codes.sort_unstable();
+ Some(codes)
+ }
+}
+
+pub struct LintRuleProvider {
+ sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>,
+ workspace_resolver: Option<Arc<WorkspaceResolver>>,
+}
+
+impl LintRuleProvider {
+ pub fn new(
+ sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>,
+ workspace_resolver: Option<Arc<WorkspaceResolver>>,
+ ) -> Self {
+ Self {
+ sloppy_imports_resolver,
+ workspace_resolver,
+ }
+ }
+
+ pub fn resolve_lint_rules_err_empty(
+ &self,
+ rules: LintRulesConfig,
+ maybe_config_file: Option<&ConfigFile>,
+ ) -> Result<ConfiguredRules, AnyError> {
+ let lint_rules = self.resolve_lint_rules(rules, maybe_config_file);
+ if lint_rules.rules.is_empty() {
+ bail!("No rules have been configured")
+ }
+ Ok(lint_rules)
+ }
+
+ pub fn resolve_lint_rules(
+ &self,
+ rules: LintRulesConfig,
+ maybe_config_file: Option<&ConfigFile>,
+ ) -> ConfiguredRules {
+ let deno_lint_rules = deno_lint::rules::get_all_rules();
+ let cli_lint_rules = vec![CliLintRule(CliLintRuleKind::Extended(
+ Box::new(no_sloppy_imports::NoSloppyImportsRule::new(
+ self.sloppy_imports_resolver.clone(),
+ self.workspace_resolver.clone(),
+ )),
+ ))];
+ let cli_graph_rules = vec![CliLintRule(CliLintRuleKind::Package(
+ Box::new(no_slow_types::NoSlowTypesRule),
+ ))];
+ let mut all_rule_names = HashSet::with_capacity(
+ deno_lint_rules.len() + cli_lint_rules.len() + cli_graph_rules.len(),
+ );
+ let all_rules = deno_lint_rules
+ .into_iter()
+ .map(|rule| CliLintRule(CliLintRuleKind::DenoLint(rule)))
+ .chain(cli_lint_rules)
+ .chain(cli_graph_rules)
+ .inspect(|rule| {
+ all_rule_names.insert(rule.code());
+ });
+ let rules = filtered_rules(
+ all_rules,
+ rules
+ .tags
+ .or_else(|| Some(get_default_tags(maybe_config_file))),
+ rules.exclude,
+ rules.include,
+ );
+ ConfiguredRules {
+ rules,
+ all_rule_codes: all_rule_names,
+ }
+ }
+}
+
+fn get_default_tags(maybe_config_file: Option<&ConfigFile>) -> Vec<String> {
+ let mut tags = Vec::with_capacity(2);
+ tags.push("recommended".to_string());
+ if maybe_config_file.map(|c| c.is_package()).unwrap_or(false) {
+ tags.push("jsr".to_string());
+ }
+ tags
+}
+
+fn filtered_rules(
+ all_rules: impl Iterator<Item = CliLintRule>,
+ maybe_tags: Option<Vec<String>>,
+ maybe_exclude: Option<Vec<String>>,
+ maybe_include: Option<Vec<String>>,
+) -> Vec<CliLintRule> {
+ let tags_set =
+ maybe_tags.map(|tags| tags.into_iter().collect::<HashSet<_>>());
+
+ let mut rules = all_rules
+ .filter(|rule| {
+ let mut passes = if let Some(tags_set) = &tags_set {
+ rule
+ .tags()
+ .iter()
+ .any(|t| tags_set.contains(&t.to_string()))
+ } else {
+ true
+ };
+
+ if let Some(includes) = &maybe_include {
+ if includes.contains(&rule.code().to_owned()) {
+ passes |= true;
+ }
+ }
+
+ if let Some(excludes) = &maybe_exclude {
+ if excludes.contains(&rule.code().to_owned()) {
+ passes &= false;
+ }
+ }
+
+ passes
+ })
+ .collect::<Vec<_>>();
+
+ rules.sort_by_key(|r| r.code());
+
+ rules
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::args::LintRulesConfig;
+
+ #[test]
+ fn recommended_rules_when_no_tags_in_config() {
+ let rules_config = LintRulesConfig {
+ exclude: Some(vec!["no-debugger".to_string()]),
+ include: None,
+ tags: None,
+ };
+ let rules_provider = LintRuleProvider::new(None, None);
+ let rules = rules_provider.resolve_lint_rules(rules_config, None);
+ let mut rule_names = rules
+ .rules
+ .into_iter()
+ .map(|r| r.code().to_string())
+ .collect::<Vec<_>>();
+ rule_names.sort();
+ let mut recommended_rule_names = rules_provider
+ .resolve_lint_rules(Default::default(), None)
+ .rules
+ .into_iter()
+ .filter(|r| r.tags().iter().any(|t| *t == "recommended"))
+ .map(|r| r.code().to_string())
+ .filter(|n| n != "no-debugger")
+ .collect::<Vec<_>>();
+ recommended_rule_names.sort();
+ assert_eq!(rule_names, recommended_rule_names);
+ }
+}
diff --git a/cli/tools/lint/rules/no_sloppy_imports.md b/cli/tools/lint/rules/no_sloppy_imports.md
new file mode 100644
index 000000000..08547c9da
--- /dev/null
+++ b/cli/tools/lint/rules/no_sloppy_imports.md
@@ -0,0 +1,20 @@
+Enforces specifying explicit references to paths in module specifiers.
+
+Non-explicit specifiers are ambiguous and require probing for the correct file
+path on every run, which has a performance overhead.
+
+Note: This lint rule is only active when using `--unstable-sloppy-imports`.
+
+### Invalid:
+
+```typescript
+import { add } from "./math/add";
+import { ConsoleLogger } from "./loggers";
+```
+
+### Valid:
+
+```typescript
+import { add } from "./math/add.ts";
+import { ConsoleLogger } from "./loggers/index.ts";
+```
diff --git a/cli/tools/lint/rules/no_sloppy_imports.rs b/cli/tools/lint/rules/no_sloppy_imports.rs
new file mode 100644
index 000000000..b5e057bfc
--- /dev/null
+++ b/cli/tools/lint/rules/no_sloppy_imports.rs
@@ -0,0 +1,214 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+use std::borrow::Cow;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use deno_ast::SourceRange;
+use deno_config::workspace::WorkspaceResolver;
+use deno_core::anyhow::anyhow;
+use deno_graph::source::ResolutionMode;
+use deno_graph::source::ResolveError;
+use deno_graph::Range;
+use deno_lint::diagnostic::LintDiagnosticDetails;
+use deno_lint::diagnostic::LintDiagnosticRange;
+use deno_lint::diagnostic::LintFix;
+use deno_lint::diagnostic::LintFixChange;
+use deno_lint::rules::LintRule;
+use text_lines::LineAndColumnIndex;
+
+use crate::graph_util::CliJsrUrlProvider;
+use crate::resolver::SloppyImportsResolution;
+use crate::resolver::SloppyImportsResolver;
+
+use super::ExtendedLintRule;
+
+#[derive(Debug)]
+pub struct NoSloppyImportsRule {
+ sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>,
+ // None for making printing out the lint rules easy
+ workspace_resolver: Option<Arc<WorkspaceResolver>>,
+}
+
+impl NoSloppyImportsRule {
+ pub fn new(
+ sloppy_imports_resolver: Option<Arc<SloppyImportsResolver>>,
+ workspace_resolver: Option<Arc<WorkspaceResolver>>,
+ ) -> Self {
+ NoSloppyImportsRule {
+ sloppy_imports_resolver,
+ workspace_resolver,
+ }
+ }
+}
+
+const CODE: &str = "no-sloppy-imports";
+const DOCS_URL: &str = "https://docs.deno.com/runtime/manual/tools/unstable_flags/#--unstable-sloppy-imports";
+
+impl ExtendedLintRule for NoSloppyImportsRule {
+ fn supports_incremental_cache(&self) -> bool {
+ // only allow the incremental cache when we don't
+ // do sloppy import resolution because sloppy import
+ // resolution requires knowing about the surrounding files
+ // in addition to the current one
+ self.sloppy_imports_resolver.is_none() || self.workspace_resolver.is_none()
+ }
+
+ fn help_docs_url(&self) -> Cow<'static, str> {
+ Cow::Borrowed(DOCS_URL)
+ }
+
+ fn into_base(self: Box<Self>) -> Box<dyn LintRule> {
+ self
+ }
+}
+
+impl LintRule for NoSloppyImportsRule {
+ fn lint_program_with_ast_view<'view>(
+ &self,
+ context: &mut deno_lint::context::Context<'view>,
+ _program: deno_lint::Program<'view>,
+ ) {
+ let Some(workspace_resolver) = &self.workspace_resolver else {
+ return;
+ };
+ let Some(sloppy_imports_resolver) = &self.sloppy_imports_resolver else {
+ return;
+ };
+ if context.specifier().scheme() != "file" {
+ return;
+ }
+
+ let resolver = SloppyImportCaptureResolver {
+ workspace_resolver,
+ sloppy_imports_resolver,
+ captures: Default::default(),
+ };
+
+ deno_graph::parse_module_from_ast(deno_graph::ParseModuleFromAstOptions {
+ graph_kind: deno_graph::GraphKind::All,
+ specifier: context.specifier().clone(),
+ maybe_headers: None,
+ parsed_source: context.parsed_source(),
+ // ignore resolving dynamic imports like import(`./dir/${something}`)
+ file_system: &deno_graph::source::NullFileSystem,
+ jsr_url_provider: &CliJsrUrlProvider,
+ maybe_resolver: Some(&resolver),
+ // don't bother resolving npm specifiers
+ maybe_npm_resolver: None,
+ });
+
+ for (range, sloppy_import) in resolver.captures.borrow_mut().drain() {
+ let start_range =
+ context.text_info().loc_to_source_pos(LineAndColumnIndex {
+ line_index: range.start.line,
+ column_index: range.start.character,
+ });
+ let end_range =
+ context.text_info().loc_to_source_pos(LineAndColumnIndex {
+ line_index: range.end.line,
+ column_index: range.end.character,
+ });
+ let source_range = SourceRange::new(start_range, end_range);
+ context.add_diagnostic_details(
+ Some(LintDiagnosticRange {
+ range: source_range,
+ description: None,
+ text_info: context.text_info().clone(),
+ }),
+ LintDiagnosticDetails {
+ message: "Sloppy imports are not allowed.".to_string(),
+ code: CODE.to_string(),
+ custom_docs_url: Some(DOCS_URL.to_string()),
+ fixes: context
+ .specifier()
+ .make_relative(sloppy_import.as_specifier())
+ .map(|relative| {
+ vec![LintFix {
+ description: Cow::Owned(sloppy_import.as_quick_fix_message()),
+ changes: vec![LintFixChange {
+ new_text: Cow::Owned({
+ let relative = if relative.starts_with("../") {
+ relative
+ } else {
+ format!("./{}", relative)
+ };
+ let current_text =
+ context.text_info().range_text(&source_range);
+ if current_text.starts_with('"') {
+ format!("\"{}\"", relative)
+ } else if current_text.starts_with('\'') {
+ format!("'{}'", relative)
+ } else {
+ relative
+ }
+ }),
+ range: source_range,
+ }],
+ }]
+ })
+ .unwrap_or_default(),
+ hint: None,
+ info: vec![],
+ },
+ );
+ }
+ }
+
+ fn code(&self) -> &'static str {
+ CODE
+ }
+
+ fn docs(&self) -> &'static str {
+ include_str!("no_sloppy_imports.md")
+ }
+
+ fn tags(&self) -> &'static [&'static str] {
+ &["recommended"]
+ }
+}
+
+#[derive(Debug)]
+struct SloppyImportCaptureResolver<'a> {
+ workspace_resolver: &'a WorkspaceResolver,
+ sloppy_imports_resolver: &'a SloppyImportsResolver,
+ captures: RefCell<HashMap<Range, SloppyImportsResolution>>,
+}
+
+impl<'a> deno_graph::source::Resolver for SloppyImportCaptureResolver<'a> {
+ fn resolve(
+ &self,
+ specifier_text: &str,
+ referrer_range: &Range,
+ mode: ResolutionMode,
+ ) -> Result<deno_ast::ModuleSpecifier, deno_graph::source::ResolveError> {
+ let resolution = self
+ .workspace_resolver
+ .resolve(specifier_text, &referrer_range.specifier)
+ .map_err(|err| ResolveError::Other(err.into()))?;
+
+ match resolution {
+ deno_config::workspace::MappedResolution::Normal(specifier)
+ | deno_config::workspace::MappedResolution::ImportMap(specifier) => {
+ match self.sloppy_imports_resolver.resolve(&specifier, mode) {
+ Some(res) => {
+ self
+ .captures
+ .borrow_mut()
+ .entry(referrer_range.clone())
+ .or_insert_with(|| res.clone());
+ Ok(res.into_specifier())
+ }
+ None => Ok(specifier),
+ }
+ }
+ deno_config::workspace::MappedResolution::WorkspaceNpmPackage {
+ ..
+ }
+ | deno_config::workspace::MappedResolution::PackageJson { .. } => {
+ Err(ResolveError::Other(anyhow!("")))
+ }
+ }
+ }
+}
diff --git a/cli/tools/lint/rules/no_slow_types.md b/cli/tools/lint/rules/no_slow_types.md
new file mode 100644
index 000000000..d0764d865
--- /dev/null
+++ b/cli/tools/lint/rules/no_slow_types.md
@@ -0,0 +1,3 @@
+Enforces using types that are explicit or can be simply inferred.
+
+Read more: https://jsr.io/docs/about-slow-types
diff --git a/cli/tools/lint/rules/no_slow_types.rs b/cli/tools/lint/rules/no_slow_types.rs
new file mode 100644
index 000000000..bc3f835b1
--- /dev/null
+++ b/cli/tools/lint/rules/no_slow_types.rs
@@ -0,0 +1,98 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+use std::borrow::Cow;
+
+use deno_ast::diagnostics::Diagnostic;
+use deno_ast::ModuleSpecifier;
+use deno_graph::FastCheckDiagnostic;
+use deno_graph::ModuleGraph;
+use deno_lint::diagnostic::LintDiagnostic;
+use deno_lint::diagnostic::LintDiagnosticDetails;
+use deno_lint::diagnostic::LintDiagnosticRange;
+
+use super::PackageLintRule;
+
+const CODE: &str = "no-slow-types";
+
+#[derive(Debug)]
+pub struct NoSlowTypesRule;
+
+impl PackageLintRule for NoSlowTypesRule {
+ fn code(&self) -> &'static str {
+ CODE
+ }
+
+ fn tags(&self) -> &'static [&'static str] {
+ &["jsr"]
+ }
+
+ fn docs(&self) -> &'static str {
+ include_str!("no_slow_types.md")
+ }
+
+ fn help_docs_url(&self) -> Cow<'static, str> {
+ Cow::Borrowed("https://jsr.io/docs/about-slow-types")
+ }
+
+ fn lint_package(
+ &self,
+ graph: &ModuleGraph,
+ entrypoints: &[ModuleSpecifier],
+ ) -> Vec<LintDiagnostic> {
+ collect_no_slow_type_diagnostics(graph, entrypoints)
+ .into_iter()
+ .map(|d| LintDiagnostic {
+ specifier: d.specifier().clone(),
+ range: d.range().map(|range| LintDiagnosticRange {
+ text_info: range.text_info.clone(),
+ range: range.range,
+ description: d.range_description().map(|r| r.to_string()),
+ }),
+ details: LintDiagnosticDetails {
+ message: d.message().to_string(),
+ code: CODE.to_string(),
+ hint: d.hint().map(|h| h.to_string()),
+ info: d
+ .info()
+ .iter()
+ .map(|info| Cow::Owned(info.to_string()))
+ .collect(),
+ fixes: vec![],
+ custom_docs_url: d.docs_url().map(|u| u.into_owned()),
+ },
+ })
+ .collect()
+ }
+}
+
+/// Collects diagnostics from the module graph for the
+/// given package's export URLs.
+pub fn collect_no_slow_type_diagnostics(
+ graph: &ModuleGraph,
+ package_export_urls: &[ModuleSpecifier],
+) -> Vec<FastCheckDiagnostic> {
+ let mut js_exports = package_export_urls
+ .iter()
+ .filter_map(|url| graph.get(url).and_then(|m| m.js()));
+ // fast check puts the same diagnostics in each entrypoint for the
+ // package (since it's all or nothing), so we only need to check
+ // the first one JS entrypoint
+ let Some(module) = js_exports.next() else {
+ // could happen if all the exports are JSON
+ return vec![];
+ };
+
+ if let Some(diagnostics) = module.fast_check_diagnostics() {
+ let mut diagnostics = diagnostics.clone();
+ diagnostics.sort_by_cached_key(|d| {
+ (
+ d.specifier().clone(),
+ d.range().map(|r| r.range),
+ d.code().to_string(),
+ )
+ });
+ diagnostics
+ } else {
+ Vec::new()
+ }
+}