use crate::config::Config;
use crate::directives::DirectiveTracker;
#[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::rules::RuleRegistry;
use crate::syntax::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)
}
fn build_ignored_ranges(&self, tree: &SyntaxNode) -> Vec<(usize, usize)> {
use crate::directives::extract_directive_from_node;
let mut tracker = DirectiveTracker::new();
let mut ignored_ranges = Vec::new();
let mut current_ignore_start: Option<usize> = None;
for node in tree.preorder() {
let node = match node {
rowan::WalkEvent::Enter(n) => n,
rowan::WalkEvent::Leave(_) => continue,
};
if let Some(directive) = extract_directive_from_node(&node) {
tracker.process_directive(&directive);
if matches!(directive, crate::directives::Directive::Start(_))
&& tracker.is_linting_ignored()
&& current_ignore_start.is_none()
{
let start: usize = node.text_range().end().into();
current_ignore_start = Some(start);
log::debug!("Ignore region starts at byte {}", start);
} else if matches!(directive, crate::directives::Directive::End(_))
&& !tracker.is_linting_ignored()
&& let Some(start) = current_ignore_start
{
let end: usize = node.text_range().start().into();
log::debug!(
"Ignore region ends at byte {}, adding range ({}, {})",
end,
start,
end
);
ignored_ranges.push((start, end));
current_ignore_start = None;
}
}
}
if let Some(start) = current_ignore_start {
log::debug!("Unclosed ignore region from byte {}", start);
ignored_ranges.push((start, usize::MAX));
}
log::debug!("Total ignored ranges: {:?}", ignored_ranges);
ignored_ranges
}
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 ignored_ranges = self.build_ignored_ranges(tree);
for rule in self.registry.rules() {
log::debug!("Running lint rule: {}", rule.name());
let rule_diagnostics = rule.check(tree, input, config, metadata);
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
}
}