use std::collections::HashSet;
use crate::config::Config;
#[cfg(not(target_arch = "wasm32"))]
use crate::linter::code_block_collector::concatenate_with_blanks_and_mapping;
use crate::linter::diagnostics::Diagnostic;
#[cfg(not(target_arch = "wasm32"))]
use crate::linter::external_linters::ExternalLinterRegistry;
#[cfg(all(not(target_arch = "wasm32"), feature = "lsp"))]
use crate::linter::external_linters::run_linter;
#[cfg(not(target_arch = "wasm32"))]
use crate::linter::external_linters::{find_missing_linter_commands, log_missing_linter_commands};
use crate::linter::index::LintIndex;
use crate::linter::rules::LintContext;
use crate::linter::rules::RuleRegistry;
use crate::syntax::{SyntaxKind, SyntaxNode};
#[cfg(not(target_arch = "wasm32"))]
use crate::utils::collect_code_blocks;
pub struct LintRunner {
registry: RuleRegistry,
#[cfg(not(target_arch = "wasm32"))]
external_linters: ExternalLinterRegistry,
}
impl LintRunner {
pub fn new(registry: RuleRegistry) -> Self {
Self {
registry,
#[cfg(not(target_arch = "wasm32"))]
external_linters: ExternalLinterRegistry::new(),
}
}
pub fn run(&self, tree: &SyntaxNode, input: &str, config: &Config) -> Vec<Diagnostic> {
self.run_with_metadata(tree, input, config, None)
}
pub fn run_with_metadata(
&self,
tree: &SyntaxNode,
input: &str,
config: &Config,
metadata: Option<&crate::metadata::DocumentMetadata>,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let mut want_kinds: HashSet<SyntaxKind> = HashSet::new();
let mut want_tokens = false;
for rule in self.registry.rules() {
want_kinds.extend(rule.node_interests().iter().copied());
want_tokens |= rule.wants_text_tokens();
}
let index = LintIndex::build(tree, &want_kinds, want_tokens);
let ignored_ranges = index.ignored_ranges();
let cx = LintContext {
tree,
input,
config,
metadata,
index: &index,
};
for rule in self.registry.rules() {
log::debug!("Running lint rule: {}", rule.name());
let rule_diagnostics = rule.check(&cx);
log::debug!(
"Rule {} found {} diagnostic(s)",
rule.name(),
rule_diagnostics.len()
);
for diagnostic in rule_diagnostics {
let byte_offset: usize = diagnostic.location.range.start().into();
let is_ignored = ignored_ranges
.iter()
.any(|(start, end)| byte_offset >= *start && byte_offset < *end);
log::debug!(
"Diagnostic at byte {}: is_ignored={}",
byte_offset,
is_ignored
);
if !is_ignored {
diagnostics.push(diagnostic);
}
}
}
diagnostics.sort_by_key(|d| (d.location.line, d.location.column));
diagnostics
}
#[cfg(all(not(target_arch = "wasm32"), feature = "lsp"))]
pub async fn run_with_external_linters(
&self,
tree: &SyntaxNode,
input: &str,
config: &Config,
metadata: Option<&crate::metadata::DocumentMetadata>,
) -> Vec<Diagnostic> {
let mut diagnostics = self.run_with_metadata(tree, input, config, metadata);
if config.linters.is_empty() {
return diagnostics;
}
let missing_linter_commands = find_missing_linter_commands(
config.linters.values().map(String::as_str),
&self.external_linters,
);
log_missing_linter_commands(&missing_linter_commands);
let code_blocks = collect_code_blocks(tree, input);
for (language, linter_name) in &config.linters {
let Some(linter_info) = self.external_linters.get(linter_name) else {
log::warn!(
"Skipping unknown external linter '{}' configured for language '{}'",
linter_name,
language
);
continue;
};
if missing_linter_commands.contains(linter_info.command) {
continue;
}
if !self
.external_linters
.supports_language(linter_name, language)
.unwrap_or(false)
{
log::warn!(
"Skipping external linter '{}' for unsupported language '{}'; supported languages: {}",
linter_name,
language,
linter_info.supported_languages.join(", ")
);
continue;
}
if let Some(blocks) = code_blocks.get(language) {
if blocks.is_empty() {
continue;
}
log::debug!(
"Running external linter '{}' for {} code blocks in language '{}'",
linter_name,
blocks.len(),
language
);
let concatenated_result = concatenate_with_blanks_and_mapping(blocks);
match run_linter(
linter_name,
language,
&concatenated_result.content,
input,
&self.external_linters,
Some(&concatenated_result.mappings),
)
.await
{
Ok(external_diagnostics) => {
log::debug!(
"External linter '{}' found {} diagnostic(s)",
linter_name,
external_diagnostics.len()
);
diagnostics.extend(external_diagnostics);
}
Err(e) => {
log::warn!("External linter '{}' failed: {}", linter_name, e);
}
}
}
}
diagnostics.sort_by_key(|d| (d.location.line, d.location.column));
diagnostics
}
#[cfg(not(target_arch = "wasm32"))]
pub fn run_with_external_linters_sync(
&self,
tree: &SyntaxNode,
input: &str,
config: &Config,
metadata: Option<&crate::metadata::DocumentMetadata>,
) -> Vec<Diagnostic> {
let mut diagnostics = self.run_with_metadata(tree, input, config, metadata);
if config.linters.is_empty() {
return diagnostics;
}
let missing_linter_commands = find_missing_linter_commands(
config.linters.values().map(String::as_str),
&self.external_linters,
);
log_missing_linter_commands(&missing_linter_commands);
let code_blocks = collect_code_blocks(tree, input);
for (language, linter_name) in &config.linters {
let Some(linter_info) = self.external_linters.get(linter_name) else {
log::warn!(
"Skipping unknown external linter '{}' configured for language '{}'",
linter_name,
language
);
continue;
};
if missing_linter_commands.contains(linter_info.command) {
continue;
}
if !self
.external_linters
.supports_language(linter_name, language)
.unwrap_or(false)
{
log::warn!(
"Skipping external linter '{}' for unsupported language '{}'; supported languages: {}",
linter_name,
language,
linter_info.supported_languages.join(", ")
);
continue;
}
if let Some(blocks) = code_blocks.get(language) {
if blocks.is_empty() {
continue;
}
log::debug!(
"Running external linter '{}' for {} code blocks in language '{}'",
linter_name,
blocks.len(),
language
);
let concatenated_result = concatenate_with_blanks_and_mapping(blocks);
match crate::linter::external_linters_sync::run_linter_sync(
linter_name,
language,
&concatenated_result.content,
input,
&self.external_linters,
Some(&concatenated_result.mappings),
) {
Ok(external_diagnostics) => {
log::debug!(
"External linter '{}' found {} diagnostic(s)",
linter_name,
external_diagnostics.len()
);
diagnostics.extend(external_diagnostics);
}
Err(e) => {
log::warn!("External linter '{}' failed: {}", linter_name, e);
}
}
}
}
diagnostics.sort_by_key(|d| (d.location.line, d.location.column));
diagnostics
}
}