Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add ExternalLinter API #1384

Open
wants to merge 1 commit into
base: v0.68
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/dlint/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ fn run_linter(
let all_rules = get_all_rules();
let all_rule_codes = all_rules
.iter()
.map(|rule| rule.code())
.map(|rule| rule.code().into())
.collect::<HashSet<_>>();
let rules = if let Some(config) = maybe_config {
config.get_rules()
Expand Down Expand Up @@ -134,6 +134,7 @@ fn run_linter(
default_jsx_factory: Some("React.createElement".to_string()),
default_jsx_fragment_factory: Some("React.Fragment".to_string()),
},
external_linter: None,
})?;

let mut number_of_errors = diagnostics.len();
Expand Down
35 changes: 20 additions & 15 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::ignore_directives::{
LineIgnoreDirective,
};
use crate::linter::LinterContext;
use crate::rules::{self, LintRule};
use crate::rules;
use deno_ast::swc::ast::Expr;
use deno_ast::swc::common::comments::Comment;
use deno_ast::swc::common::util::take::Take;
Expand All @@ -20,6 +20,7 @@ use deno_ast::{
};
use deno_ast::{MediaType, ModuleSpecifier};
use deno_ast::{MultiThreadedComments, Scope};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;

Expand All @@ -33,7 +34,6 @@ pub struct Context<'a> {
scope: Scope,
control_flow: ControlFlow,
traverse_flow: TraverseFlow,
all_rule_codes: &'a HashSet<&'static str>,
check_unknown_rules: bool,
#[allow(clippy::redundant_allocation)] // This type comes from SWC.
jsx_factory: Option<Arc<Box<Expr>>>,
Expand Down Expand Up @@ -112,7 +112,6 @@ impl<'a> Context<'a> {
diagnostics: Vec::new(),
traverse_flow: TraverseFlow::default(),
check_unknown_rules: linter_ctx.check_unknown_rules,
all_rule_codes: &linter_ctx.all_rule_codes,
jsx_factory,
jsx_fragment_factory,
}
Expand Down Expand Up @@ -266,7 +265,7 @@ impl<'a> Context<'a> {
/// works for diagnostics reported by other rules.
pub(crate) fn ban_unused_ignore(
&self,
specified_rules: &[Box<dyn LintRule>],
known_rules_codes: &HashSet<Cow<'static, str>>,
) -> Vec<LintDiagnostic> {
const CODE: &str = "ban-unused-ignore";

Expand All @@ -280,10 +279,8 @@ impl<'a> Context<'a> {
return vec![];
}

let executed_builtin_codes: HashSet<&'static str> =
specified_rules.iter().map(|r| r.code()).collect();
let is_unused_code = |&(code, status): &(&String, &CodeStatus)| {
let is_unknown = !executed_builtin_codes.contains(code.as_str());
let is_unknown = !known_rules_codes.contains(code.as_str());
!status.used && !is_unknown
};

Expand Down Expand Up @@ -334,15 +331,17 @@ impl<'a> Context<'a> {
// struct.
/// Lint rule implementation for `ban-unknown-rule-code`.
/// This should be run after all normal rules.
pub(crate) fn ban_unknown_rule_code(&mut self) -> Vec<LintDiagnostic> {
let is_unknown_rule =
|code: &&String| !self.all_rule_codes.contains(code.as_str());

pub(crate) fn ban_unknown_rule_code(
&mut self,
enabled_rules: &HashSet<Cow<'static, str>>,
) -> Vec<LintDiagnostic> {
let mut diagnostics = Vec::new();

if let Some(file_ignore) = self.file_ignore_directive.as_ref() {
for unknown_rule_code in
file_ignore.codes().keys().filter(is_unknown_rule)
for unknown_rule_code in file_ignore
.codes()
.keys()
.filter(|code| !enabled_rules.contains(code.as_str()))
{
let d = self.create_diagnostic(
Some(self.create_diagnostic_range(file_ignore.range())),
Expand All @@ -358,8 +357,10 @@ impl<'a> Context<'a> {
}

for line_ignore in self.line_ignore_directives.values() {
for unknown_rule_code in
line_ignore.codes().keys().filter(is_unknown_rule)
for unknown_rule_code in line_ignore
.codes()
.keys()
.filter(|code| !enabled_rules.contains(code.as_str()))
{
let d = self.create_diagnostic(
Some(self.create_diagnostic_range(line_ignore.range())),
Expand Down Expand Up @@ -451,6 +452,10 @@ impl<'a> Context<'a> {
.push(self.create_diagnostic(maybe_range, details));
}

pub fn add_external_diagnostics(&mut self, diagnostics: &[LintDiagnostic]) {
self.diagnostics.extend_from_slice(diagnostics);
}

pub(crate) fn create_diagnostic(
&self,
maybe_range: Option<LintDiagnosticRange>,
Expand Down
13 changes: 8 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub use deno_ast::view::ProgramRef;

#[cfg(test)]
mod lint_tests {
use std::borrow::Cow;
use std::collections::HashSet;

use crate::diagnostic::LintDiagnostic;
Expand All @@ -40,7 +41,7 @@ mod lint_tests {
fn lint(
source: &str,
rules: Vec<Box<dyn LintRule>>,
all_rule_codes: HashSet<&'static str>,
all_rule_codes: HashSet<Cow<'static, str>>,
) -> Vec<LintDiagnostic> {
let linter = Linter::new(LinterOptions {
rules,
Expand All @@ -58,6 +59,7 @@ mod lint_tests {
default_jsx_factory: None,
default_jsx_fragment_factory: None,
},
external_linter: None,
})
.expect("Failed to lint");
diagnostics
Expand All @@ -66,7 +68,7 @@ mod lint_tests {
fn lint_with_ast(
parsed_source: &ParsedSource,
rules: Vec<Box<dyn LintRule>>,
all_rule_codes: HashSet<&'static str>,
all_rule_codes: HashSet<Cow<'static, str>>,
) -> Vec<LintDiagnostic> {
let linter = Linter::new(LinterOptions {
rules,
Expand All @@ -80,6 +82,7 @@ mod lint_tests {
default_jsx_factory: None,
default_jsx_fragment_factory: None,
},
None,
)
}

Expand All @@ -89,7 +92,7 @@ mod lint_tests {
recommended_rules(get_all_rules()),
get_all_rules()
.into_iter()
.map(|rule| rule.code())
.map(|rule| rule.code().into())
.collect(),
)
}
Expand All @@ -102,7 +105,7 @@ mod lint_tests {
recommended_rules(get_all_rules()),
get_all_rules()
.into_iter()
.map(|rule| rule.code())
.map(|rule| rule.code().into())
.collect(),
)
}
Expand All @@ -116,7 +119,7 @@ mod lint_tests {
vec![rule],
get_all_rules()
.into_iter()
.map(|rule| rule.code())
.map(|rule| rule.code().into())
.collect(),
)
}
Expand Down
52 changes: 46 additions & 6 deletions src/linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ use deno_ast::diagnostics::Diagnostic;
use deno_ast::MediaType;
use deno_ast::ParsedSource;
use deno_ast::{ModuleSpecifier, ParseDiagnostic};
use std::borrow::Cow;
use std::collections::HashSet;
use std::sync::Arc;

pub struct LinterOptions {
/// Rules to lint with.
pub rules: Vec<Box<dyn LintRule>>,
/// Collection of all the lint rule codes.
pub all_rule_codes: HashSet<&'static str>,
pub all_rule_codes: HashSet<Cow<'static, str>>,
/// Defaults to "deno-lint-ignore-file"
pub custom_ignore_file_directive: Option<&'static str>,
/// Defaults to "deno-lint-ignore"
Expand All @@ -40,7 +42,7 @@ pub(crate) struct LinterContext {
pub check_unknown_rules: bool,
/// Rules are sorted by priority
pub rules: Vec<Box<dyn LintRule>>,
pub all_rule_codes: HashSet<&'static str>,
pub all_rule_codes: HashSet<Cow<'static, str>>,
}

impl LinterContext {
Expand All @@ -65,11 +67,19 @@ impl LinterContext {
}
}

pub struct ExternalLinterResult {
pub diagnostics: Vec<LintDiagnostic>,
pub rules: Vec<String>,
}
pub type ExternalLinterCb =
Arc<dyn Fn(ParsedSource) -> Result<ExternalLinterResult, anyhow::Error>>;

pub struct LintFileOptions {
pub specifier: ModuleSpecifier,
pub source_code: String,
pub media_type: MediaType,
pub config: LintConfig,
pub external_linter: Option<ExternalLinterCb>,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -107,6 +117,7 @@ impl Linter {
&parsed_source,
options.config.default_jsx_factory,
options.config.default_jsx_fragment_factory,
options.external_linter,
);

Ok((parsed_source, diagnostics))
Expand All @@ -120,26 +131,45 @@ impl Linter {
&self,
parsed_source: &ParsedSource,
config: LintConfig,
maybe_external_linter: Option<ExternalLinterCb>,
) -> Vec<LintDiagnostic> {
let _mark = PerformanceMark::new("Linter::lint_with_ast");
self.lint_inner(
parsed_source,
config.default_jsx_factory,
config.default_jsx_fragment_factory,
maybe_external_linter,
)
}

// TODO(bartlomieju): this struct does too much - not only it checks for ignored
// lint rules, it also runs 2 additional rules. These rules should be rewritten
// to use a regular way of writing a rule and not live on the `Context` struct.
fn collect_diagnostics(&self, mut context: Context) -> Vec<LintDiagnostic> {
fn collect_diagnostics(
&self,
mut context: Context,
external_rule_codes: Vec<String>,
) -> Vec<LintDiagnostic> {
let _mark = PerformanceMark::new("Linter::collect_diagnostics");

let mut diagnostics = context.check_ignore_directive_usage();

let mut all_rules = self.ctx.all_rule_codes.clone();
all_rules.extend(
external_rule_codes
.iter()
.map(|code| code.to_string().into()),
);
let enabled_rules: HashSet<Cow<'static, str>> = external_rule_codes
.into_iter()
.map(|code| code.into())
.chain(self.ctx.rules.iter().map(|r| r.code().into()))
.collect();

// Run `ban-unknown-rule-code`
diagnostics.extend(context.ban_unknown_rule_code());
diagnostics.extend(context.ban_unknown_rule_code(&all_rules));
// Run `ban-unused-ignore`
diagnostics.extend(context.ban_unused_ignore(&self.ctx.rules));
diagnostics.extend(context.ban_unused_ignore(&enabled_rules));

// Finally sort by position the diagnostics originates on then by code
diagnostics.sort_by(|a, b| {
Expand All @@ -159,6 +189,7 @@ impl Linter {
parsed_source: &ParsedSource,
default_jsx_factory: Option<String>,
default_jsx_fragment_factory: Option<String>,
maybe_external_linter: Option<ExternalLinterCb>,
) -> Vec<LintDiagnostic> {
let _mark = PerformanceMark::new("Linter::lint_inner");

Expand Down Expand Up @@ -200,7 +231,16 @@ impl Linter {
rule.lint_program_with_ast_view(&mut context, pg);
}

self.collect_diagnostics(context)
let mut external_rule_codes = vec![];
if let Some(cb) = maybe_external_linter {
let result = cb(parsed_source.clone());
// TODO(bartlomijue): get rid of this unwrap
let external_linter_result = result.unwrap();
context.add_external_diagnostics(&external_linter_result.diagnostics);
external_rule_codes = external_linter_result.rules;
}

self.collect_diagnostics(context, external_rule_codes)
});

diagnostics
Expand Down
3 changes: 2 additions & 1 deletion src/test_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ fn lint(
rules: vec![rule],
all_rule_codes: get_all_rules()
.into_iter()
.map(|rule| rule.code())
.map(|rule| rule.code().into())
.collect(),
custom_ignore_diagnostic_directive: None,
custom_ignore_file_directive: None,
Expand All @@ -341,6 +341,7 @@ fn lint(
default_jsx_factory: Some("React.createElement".to_owned()),
default_jsx_fragment_factory: Some("React.Fragment".to_owned()),
},
external_linter: None,
});
match lint_result {
Ok((source, diagnostics)) => (source, diagnostics),
Expand Down
Loading