agentlint/analysis/
mod.rs1mod commands;
2mod content;
3mod structure;
4
5use std::path::Path;
6
7use crate::config::Config;
8use crate::diagnostic::sort_diagnostics;
9use crate::project::ProjectFacts;
10use crate::scanner::SourceFile;
11use crate::{Diagnostic, LintReport, Severity};
12
13pub(crate) fn analyze(
14 root: &Path,
15 files: &[SourceFile],
16 project: &ProjectFacts,
17 config: &Config,
18 minimum_severity: Option<Severity>,
19) -> LintReport {
20 let mut diagnostics = Vec::new();
21 let mut total_tokens = 0;
22
23 for file in files {
24 structure::check(root, file, &mut diagnostics);
25 content::check_file(root, file, &mut diagnostics);
26 commands::check(root, file, project, &mut diagnostics);
27 total_tokens += content::approximate_tokens(&file.content);
28 }
29 let duplicated_tokens = content::check_cross_file(files, &mut diagnostics);
30
31 diagnostics = diagnostics
32 .into_iter()
33 .filter_map(|diagnostic| config.apply(diagnostic))
34 .filter(|diagnostic| {
35 minimum_severity.is_none_or(|minimum| diagnostic.severity.rank() >= minimum.rank())
36 })
37 .collect();
38 sort_diagnostics(&mut diagnostics);
39
40 LintReport {
41 version: env!("CARGO_PKG_VERSION"),
42 root: root.to_path_buf(),
43 files_scanned: files.len(),
44 estimated_total_tokens: total_tokens,
45 estimated_duplicated_tokens: duplicated_tokens,
46 diagnostics,
47 fixed_files: Vec::new(),
48 }
49}
50
51pub(super) fn line_column(text: &str, offset: usize) -> (usize, usize) {
52 let prefix = &text[..offset.min(text.len())];
53 let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
54 let column = prefix
55 .rsplit('\n')
56 .next()
57 .map_or(1, |part| part.chars().count() + 1);
58 (line, column)
59}
60
61pub(super) fn push(items: &mut Vec<Diagnostic>, diagnostic: Diagnostic) {
62 if !items.iter().any(|existing| {
63 existing.rule == diagnostic.rule
64 && existing.file == diagnostic.file
65 && existing.line == diagnostic.line
66 && existing.message == diagnostic.message
67 }) {
68 items.push(diagnostic);
69 }
70}