summaryrefslogtreecommitdiff
path: root/cli/tools/lint/mod.rs
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2024-02-19 10:28:41 -0500
committerGitHub <noreply@github.com>2024-02-19 15:28:41 +0000
commit66424032a2c78c6010c0a1a1b22a26d081166660 (patch)
tree610e95beba5685ef1ba322375bf31a3fd6c5a187 /cli/tools/lint/mod.rs
parent2b279ad630651e973d5a31586f58809f005bc925 (diff)
feat(unstable/lint): no-slow-types for JSR packages (#22430)
1. Renames zap/fast-check to instead be a `no-slow-types` lint rule. 1. This lint rule is automatically run when doing `deno lint` for packages (deno.json files with a name, version, and exports field) 1. This lint rules still occurs on publish. It can be skipped by running with `--no-slow-types`
Diffstat (limited to 'cli/tools/lint/mod.rs')
-rw-r--r--cli/tools/lint/mod.rs798
1 files changed, 798 insertions, 0 deletions
diff --git a/cli/tools/lint/mod.rs b/cli/tools/lint/mod.rs
new file mode 100644
index 000000000..e4a88f91c
--- /dev/null
+++ b/cli/tools/lint/mod.rs
@@ -0,0 +1,798 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+//! 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::glob::FilePatterns;
+use deno_core::anyhow::bail;
+use deno_core::error::generic_error;
+use deno_core::error::AnyError;
+use deno_core::parking_lot::Mutex;
+use deno_core::serde_json;
+use deno_graph::FastCheckDiagnostic;
+use deno_lint::diagnostic::LintDiagnostic;
+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;
+use std::io::Read;
+use std::path::Path;
+use std::path::PathBuf;
+use std::sync::Arc;
+
+use crate::args::Flags;
+use crate::args::LintFlags;
+use crate::args::LintOptions;
+use crate::args::LintReporterKind;
+use crate::args::LintRulesConfig;
+use crate::cache::IncrementalCache;
+use crate::colors;
+use crate::factory::CliFactory;
+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::fs::FileCollector;
+use crate::util::path::is_script_ext;
+use crate::util::sync::AtomicFlag;
+
+pub mod no_slow_types;
+
+static STDIN_FILE_NAME: &str = "$deno$stdin.ts";
+
+fn create_reporter(kind: LintReporterKind) -> Box<dyn LintReporter + Send> {
+ match kind {
+ LintReporterKind::Pretty => Box::new(PrettyLintReporter::new()),
+ LintReporterKind::Json => Box::new(JsonLintReporter::new()),
+ LintReporterKind::Compact => Box::new(CompactLintReporter::new()),
+ }
+}
+
+pub async fn lint(flags: Flags, lint_flags: LintFlags) -> Result<(), AnyError> {
+ if let Some(watch_flags) = &lint_flags.watch {
+ if lint_flags.is_stdin() {
+ return Err(generic_error(
+ "Lint watch on standard input is not supported.",
+ ));
+ }
+ file_watcher::watch_func(
+ flags,
+ file_watcher::PrintConfig::new("Lint", !watch_flags.no_clear_screen),
+ move |flags, watcher_communicator, changed_paths| {
+ let lint_flags = lint_flags.clone();
+ Ok(async move {
+ let factory = CliFactory::from_flags(flags).await?;
+ let cli_options = factory.cli_options();
+ let lint_options = cli_options.resolve_lint_options(lint_flags)?;
+ let files = collect_lint_files(lint_options.files.clone()).and_then(
+ |files| {
+ if files.is_empty() {
+ Err(generic_error("No target files found."))
+ } else {
+ Ok(files)
+ }
+ },
+ )?;
+ _ = watcher_communicator.watch_paths(files.clone());
+
+ let lint_paths = if let Some(paths) = changed_paths {
+ // lint all files on any changed (https://github.com/denoland/deno/issues/12446)
+ files
+ .iter()
+ .any(|path| {
+ canonicalize_path(path)
+ .map(|p| paths.contains(&p))
+ .unwrap_or(false)
+ })
+ .then_some(files)
+ .unwrap_or_else(|| [].to_vec())
+ } else {
+ files
+ };
+
+ lint_files(factory, lint_options, lint_paths).await?;
+ Ok(())
+ })
+ },
+ )
+ .await?;
+ } else {
+ let factory = CliFactory::from_flags(flags).await?;
+ let cli_options = factory.cli_options();
+ let is_stdin = lint_flags.is_stdin();
+ let lint_options = cli_options.resolve_lint_options(lint_flags)?;
+ let files = &lint_options.files;
+ let success = if is_stdin {
+ let reporter_kind = lint_options.reporter_kind;
+ let reporter_lock = Arc::new(Mutex::new(create_reporter(reporter_kind)));
+ let lint_rules = get_config_rules_err_empty(
+ lint_options.rules,
+ cli_options.maybe_config_file().as_ref(),
+ )?;
+ let file_path = cli_options.initial_cwd().join(STDIN_FILE_NAME);
+ let r = lint_stdin(&file_path, lint_rules.rules);
+ let success = handle_lint_result(
+ &file_path.to_string_lossy(),
+ r,
+ reporter_lock.clone(),
+ );
+ reporter_lock.lock().close(1);
+ success
+ } else {
+ let target_files =
+ collect_lint_files(files.clone()).and_then(|files| {
+ if files.is_empty() {
+ Err(generic_error("No target files found."))
+ } else {
+ Ok(files)
+ }
+ })?;
+ debug!("Found {} files", target_files.len());
+ lint_files(factory, lint_options, target_files).await?
+ };
+ if !success {
+ std::process::exit(1);
+ }
+ }
+
+ Ok(())
+}
+
+async fn lint_files(
+ factory: CliFactory,
+ lint_options: LintOptions,
+ paths: Vec<PathBuf>,
+) -> Result<bool, AnyError> {
+ let caches = factory.caches()?;
+ let maybe_config_file = factory.cli_options().maybe_config_file().as_ref();
+ let lint_rules =
+ get_config_rules_err_empty(lint_options.rules, maybe_config_file)?;
+ let incremental_cache = Arc::new(IncrementalCache::new(
+ caches.lint_incremental_cache_db(),
+ &lint_rules.incremental_cache_state(),
+ &paths,
+ ));
+ let target_files_len = paths.len();
+ let reporter_kind = lint_options.reporter_kind;
+ // todo(dsherret): abstract away this lock behind a performant interface
+ let reporter_lock =
+ Arc::new(Mutex::new(create_reporter(reporter_kind.clone())));
+ let has_error = Arc::new(AtomicFlag::default());
+
+ let mut futures = Vec::with_capacity(2);
+ if lint_rules.no_slow_types {
+ if let Some(config_file) = maybe_config_file {
+ let members = config_file.to_workspace_members()?;
+ let has_error = has_error.clone();
+ let reporter_lock = reporter_lock.clone();
+ let module_graph_builder = factory.module_graph_builder().await?.clone();
+ let path_urls = paths
+ .iter()
+ .filter_map(|p| ModuleSpecifier::from_file_path(p).ok())
+ .collect::<HashSet<_>>();
+ futures.push(deno_core::unsync::spawn(async move {
+ let graph = module_graph_builder.create_publish_graph(&members).await?;
+ // todo(dsherret): this isn't exactly correct as linting isn't properly
+ // setup to handle workspaces. Iterating over the workspace members
+ // should be done at a higher level because it also needs to take into
+ // account the config per workspace member.
+ for member in &members {
+ let export_urls = member.config_file.resolve_export_value_urls()?;
+ if !export_urls.iter().any(|url| path_urls.contains(url)) {
+ continue; // entrypoint is not specified, so skip
+ }
+ let diagnostics = no_slow_types::collect_no_slow_type_diagnostics(
+ &export_urls,
+ &graph,
+ );
+ if !diagnostics.is_empty() {
+ has_error.raise();
+ let mut reporter = reporter_lock.lock();
+ for diagnostic in &diagnostics {
+ reporter
+ .visit_diagnostic(LintOrCliDiagnostic::FastCheck(diagnostic));
+ }
+ }
+ }
+ Ok(())
+ }));
+ }
+ }
+
+ futures.push({
+ let has_error = has_error.clone();
+ let lint_rules = lint_rules.rules.clone();
+ let reporter_lock = reporter_lock.clone();
+ let incremental_cache = incremental_cache.clone();
+ deno_core::unsync::spawn(async move {
+ run_parallelized(paths, {
+ move |file_path| {
+ let file_text = 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(());
+ }
+
+ let r = lint_file(&file_path, file_text, lint_rules);
+ if let Ok((file_diagnostics, file_source)) = &r {
+ if file_diagnostics.is_empty() {
+ // update the incremental cache if there were no diagnostics
+ incremental_cache
+ .update_file(&file_path, file_source.text_info().text_str())
+ }
+ }
+
+ let success = handle_lint_result(
+ &file_path.to_string_lossy(),
+ r,
+ reporter_lock.clone(),
+ );
+ if !success {
+ has_error.raise();
+ }
+
+ Ok(())
+ }
+ })
+ .await
+ })
+ });
+
+ deno_core::futures::future::try_join_all(futures).await?;
+
+ incremental_cache.wait_completion().await;
+ reporter_lock.lock().close(target_files_len);
+
+ Ok(!has_error.is_raised())
+}
+
+fn collect_lint_files(files: FilePatterns) -> Result<Vec<PathBuf>, AnyError> {
+ FileCollector::new(|path, _| is_script_ext(path))
+ .ignore_git_folder()
+ .ignore_node_modules()
+ .ignore_vendor_folder()
+ .collect_file_patterns(files)
+}
+
+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)
+ };
+
+ if json {
+ let json_rules: Vec<serde_json::Value> = lint_rules
+ .iter()
+ .map(|rule| {
+ serde_json::json!({
+ "code": rule.code(),
+ "tags": rule.tags(),
+ "docs": rule.docs(),
+ })
+ })
+ .collect();
+ let json_str = serde_json::to_string_pretty(&json_rules).unwrap();
+ println!("{json_str}");
+ } else {
+ // The rules should still be printed even if `--quiet` option is enabled,
+ // so use `println!` here instead of `info!`.
+ println!("Available rules:");
+ for rule in lint_rules.iter() {
+ print!(" - {}", colors::cyan(rule.code()));
+ if rule.tags().is_empty() {
+ println!();
+ } else {
+ println!(" [{}]", colors::gray(rule.tags().join(", ")))
+ }
+ println!(
+ "{}",
+ colors::gray(format!(
+ " help: https://lint.deno.land/#{}",
+ rule.code()
+ ))
+ );
+ 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(
+ file_path: &Path,
+ source_code: String,
+ lint_rules: Vec<&'static dyn LintRule>,
+) -> Result<(Vec<LintDiagnostic>, ParsedSource), AnyError> {
+ let specifier = specifier_from_file_path(file_path)?;
+ let media_type = MediaType::from_specifier(&specifier);
+
+ let linter = create_linter(lint_rules);
+
+ let (source, file_diagnostics) = linter.lint_file(LintFileOptions {
+ specifier,
+ media_type,
+ source_code: source_code.clone(),
+ })?;
+
+ Ok((file_diagnostics, source))
+}
+
+/// 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>,
+) -> Result<(Vec<LintDiagnostic>, ParsedSource), AnyError> {
+ let mut source_code = String::new();
+ if stdin().read_to_string(&mut source_code).is_err() {
+ return Err(generic_error("Failed to read from stdin"));
+ }
+
+ let linter = create_linter(lint_rules);
+
+ let (source, file_diagnostics) = linter.lint_file(LintFileOptions {
+ specifier: specifier_from_file_path(file_path)?,
+ source_code: source_code.clone(),
+ media_type: MediaType::TypeScript,
+ })?;
+
+ Ok((file_diagnostics, source))
+}
+
+fn handle_lint_result(
+ file_path: &str,
+ result: Result<(Vec<LintDiagnostic>, ParsedSource), AnyError>,
+ reporter_lock: Arc<Mutex<Box<dyn LintReporter + Send>>>,
+) -> bool {
+ let mut reporter = reporter_lock.lock();
+
+ match result {
+ Ok((mut file_diagnostics, _source)) => {
+ file_diagnostics.sort_by(|a, b| match a.specifier.cmp(&b.specifier) {
+ std::cmp::Ordering::Equal => a.range.start.cmp(&b.range.start),
+ file_order => file_order,
+ });
+ for d in &file_diagnostics {
+ reporter.visit_diagnostic(LintOrCliDiagnostic::Lint(d));
+ }
+ file_diagnostics.is_empty()
+ }
+ Err(err) => {
+ reporter.visit_error(file_path, &err);
+ false
+ }
+ }
+}
+
+#[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_error(&mut self, file_path: &str, err: &AnyError);
+ fn close(&mut self, check_count: usize);
+}
+
+#[derive(Serialize)]
+struct LintError {
+ file_path: String,
+ message: String,
+}
+
+struct PrettyLintReporter {
+ lint_count: u32,
+}
+
+impl PrettyLintReporter {
+ fn new() -> PrettyLintReporter {
+ PrettyLintReporter { lint_count: 0 }
+ }
+}
+
+impl LintReporter for PrettyLintReporter {
+ fn visit_diagnostic(&mut self, d: LintOrCliDiagnostic) {
+ self.lint_count += 1;
+
+ eprintln!("{}", d.display());
+ }
+
+ fn visit_error(&mut self, file_path: &str, err: &AnyError) {
+ eprintln!("Error linting: {file_path}");
+ eprintln!(" {err}");
+ }
+
+ fn close(&mut self, check_count: usize) {
+ match self.lint_count {
+ 1 => info!("Found 1 problem"),
+ n if n > 1 => info!("Found {} problems", self.lint_count),
+ _ => (),
+ }
+
+ match check_count {
+ n if n <= 1 => info!("Checked {} file", n),
+ n if n > 1 => info!("Checked {} files", n),
+ _ => unreachable!(),
+ }
+ }
+}
+
+struct CompactLintReporter {
+ lint_count: u32,
+}
+
+impl CompactLintReporter {
+ fn new() -> CompactLintReporter {
+ CompactLintReporter { lint_count: 0 }
+ }
+}
+
+impl LintReporter for CompactLintReporter {
+ fn visit_diagnostic(&mut self, d: LintOrCliDiagnostic) {
+ self.lint_count += 1;
+
+ match d.range() {
+ Some((text_info, range)) => {
+ let line_and_column = text_info.line_and_column_display(range.start);
+ eprintln!(
+ "{}: line {}, col {} - {} ({})",
+ d.specifier(),
+ line_and_column.line_number,
+ line_and_column.column_number,
+ d.message(),
+ d.code(),
+ )
+ }
+ None => {
+ eprintln!("{}: {} ({})", d.specifier(), d.message(), d.code())
+ }
+ }
+ }
+
+ fn visit_error(&mut self, file_path: &str, err: &AnyError) {
+ eprintln!("Error linting: {file_path}");
+ eprintln!(" {err}");
+ }
+
+ fn close(&mut self, check_count: usize) {
+ match self.lint_count {
+ 1 => info!("Found 1 problem"),
+ n if n > 1 => info!("Found {} problems", self.lint_count),
+ _ => (),
+ }
+
+ match check_count {
+ n if n <= 1 => info!("Checked {} file", n),
+ n if n > 1 => info!("Checked {} files", n),
+ _ => unreachable!(),
+ }
+ }
+}
+
+// WARNING: Ensure doesn't change because it's used in the JSON output
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct JsonDiagnosticLintPosition {
+ /// The 1-indexed line number.
+ pub line: usize,
+ /// The 0-indexed column index.
+ pub col: usize,
+ pub byte_pos: usize,
+}
+
+impl JsonDiagnosticLintPosition {
+ pub fn new(byte_index: usize, loc: deno_ast::LineAndColumnIndex) -> Self {
+ JsonDiagnosticLintPosition {
+ line: loc.line_index + 1,
+ col: loc.column_index,
+ byte_pos: byte_index,
+ }
+ }
+}
+
+// WARNING: Ensure doesn't change because it's used in the JSON output
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+struct JsonLintDiagnosticRange {
+ pub start: JsonDiagnosticLintPosition,
+ pub end: JsonDiagnosticLintPosition,
+}
+
+// WARNING: Ensure doesn't change because it's used in the JSON output
+#[derive(Clone, Serialize)]
+struct JsonLintDiagnostic {
+ pub filename: String,
+ pub range: Option<JsonLintDiagnosticRange>,
+ pub message: String,
+ pub code: String,
+ pub hint: Option<String>,
+}
+
+#[derive(Serialize)]
+struct JsonLintReporter {
+ diagnostics: Vec<JsonLintDiagnostic>,
+ errors: Vec<LintError>,
+}
+
+impl JsonLintReporter {
+ fn new() -> JsonLintReporter {
+ JsonLintReporter {
+ diagnostics: Vec::new(),
+ errors: Vec::new(),
+ }
+ }
+}
+
+impl LintReporter for JsonLintReporter {
+ fn visit_diagnostic(&mut self, d: LintOrCliDiagnostic) {
+ 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),
+ ),
+ }),
+ message: d.message().to_string(),
+ code: d.code().to_string(),
+ hint: d.hint().map(|h| h.to_string()),
+ });
+ }
+
+ fn visit_error(&mut self, file_path: &str, err: &AnyError) {
+ self.errors.push(LintError {
+ file_path: file_path.to_string(),
+ message: err.to_string(),
+ });
+ }
+
+ fn close(&mut self, _check_count: usize) {
+ sort_diagnostics(&mut self.diagnostics);
+ let json = serde_json::to_string_pretty(&self);
+ println!("{}", json.unwrap());
+ }
+}
+
+fn sort_diagnostics(diagnostics: &mut [JsonLintDiagnostic]) {
+ // Sort so that we guarantee a deterministic output which is useful for tests
+ diagnostics.sort_by(|a, b| {
+ use std::cmp::Ordering;
+ let file_order = a.filename.cmp(&b.filename);
+ match file_order {
+ Ordering::Equal => match &a.range {
+ Some(a_range) => match &b.range {
+ Some(b_range) => {
+ let line_order = a_range.start.line.cmp(&b_range.start.line);
+ match line_order {
+ Ordering::Equal => a_range.start.col.cmp(&b_range.start.col),
+ _ => line_order,
+ }
+ }
+ None => Ordering::Less,
+ },
+ None => match &b.range {
+ Some(_) => Ordering::Greater,
+ None => Ordering::Equal,
+ },
+ },
+ _ => file_order,
+ }
+ });
+}
+
+fn get_config_rules_err_empty(
+ rules: LintRulesConfig,
+ maybe_config_file: Option<&deno_config::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 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<&deno_config::ConfigFile>,
+) -> ConfiguredRules {
+ const NO_SLOW_TYPES_NAME: &str = "no-slow-types";
+ let implicit_no_slow_types = maybe_config_file
+ .map(|c| c.is_package() || !c.json.workspaces.is_empty())
+ .unwrap_or(false);
+ if rules.tags.is_none() && rules.include.is_none() && rules.exclude.is_none()
+ {
+ ConfiguredRules {
+ rules: rules::get_recommended_rules(),
+ no_slow_types: implicit_no_slow_types,
+ }
+ } else {
+ 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(vec!["recommended".to_string()])),
+ 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,
+ }
+ }
+}
+
+#[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);
+ }
+}