cairo_lint_core/
lib.rs

1use cairo_lang_defs::plugin::PluginDiagnostic;
2use fixes::{apply_import_fixes, collect_unused_imports, fix_semantic_diagnostic, Fix, ImportFix};
3
4use cairo_lang_syntax::node::SyntaxNode;
5
6use std::{cmp::Reverse, collections::HashMap};
7
8pub use annotate_snippets;
9use anyhow::{anyhow, Result};
10use cairo_lang_compiler::db::RootDatabase;
11use cairo_lang_diagnostics::DiagnosticEntry;
12use cairo_lang_filesystem::db::FilesGroup;
13use cairo_lang_filesystem::ids::FileId;
14use cairo_lang_semantic::{diagnostic::SemanticDiagnosticKind, SemanticDiagnostic};
15use cairo_lang_utils::Upcast;
16
17// TODO(wawel37): Don't make it public as we deploy Scarb with new cairo-lint cli there.
18// The context mod is set to public for the duration of cairo-lint-cli existence.
19pub mod context;
20pub mod diagnostics;
21pub mod fixes;
22mod helper;
23pub mod lints;
24pub mod plugin;
25mod queries;
26
27use context::{get_lint_type_from_diagnostic_message, CairoLintKind};
28
29/// Gets the fixes for a set of a compiler diagnostics (that uses Cairo lint analyzer plugin).
30/// # Arguments
31///
32/// * `db` - The reference to the RootDatabase that the diagnostics were based upon.
33/// * `diagnostics` - The list of compiler diagnostics. Make sure that the diagnostics from the Cairo lint analyzer plugin are also included.
34///
35/// # Returns
36///
37/// A HashMap where:
38/// * keys are FileIds (that points to a file that the fixes might be applied to)
39/// * values are vectors of proposed Fixes.
40pub fn get_fixes(
41    db: &RootDatabase,
42    diagnostics: Vec<SemanticDiagnostic>,
43) -> HashMap<FileId, Vec<Fix>> {
44    // Handling unused imports separately as we need to run pre-analysis on the diagnostics.
45    // to handle complex cases.
46    let unused_imports: HashMap<FileId, HashMap<SyntaxNode, ImportFix>> =
47        collect_unused_imports(db, &diagnostics);
48    let mut fixes = HashMap::new();
49    unused_imports.keys().for_each(|file_id| {
50        let file_fixes: Vec<Fix> = apply_import_fixes(db, unused_imports.get(file_id).unwrap());
51        fixes.insert(*file_id, file_fixes);
52    });
53
54    let diags_without_imports = diagnostics
55        .iter()
56        .filter(|diag| !matches!(diag.kind, SemanticDiagnosticKind::UnusedImport(_)))
57        .collect::<Vec<_>>();
58
59    for diag in diags_without_imports {
60        if let Some((fix_node, fix)) = fix_semantic_diagnostic(db, diag) {
61            let location = diag.location(db.upcast());
62            fixes
63                .entry(location.file_id)
64                .or_insert_with(Vec::new)
65                .push(Fix {
66                    span: fix_node.span(db.upcast()),
67                    suggestion: fix,
68                });
69        }
70    }
71    fixes
72}
73
74/// Applies the fixes to the file.
75///
76/// # Arguments
77///
78/// * `file_id` - The FileId of the file that the fixes should be applied to.
79/// * `fixes` - The list of fixes that should be applied to the file.
80pub fn apply_file_fixes(file_id: FileId, fixes: Vec<Fix>, db: &RootDatabase) -> Result<()> {
81    let mut fixes = fixes;
82    fixes.sort_by_key(|fix| Reverse(fix.span.start));
83    let mut fixable_diagnostics = Vec::with_capacity(fixes.len());
84    if fixes.len() <= 1 {
85        fixable_diagnostics = fixes;
86    } else {
87        // Check if we have nested diagnostics. If so it's a nightmare to fix hence just ignore it
88        for i in 0..fixes.len() - 1 {
89            let first = fixes[i].span;
90            let second = fixes[i + 1].span;
91            if first.start >= second.end {
92                fixable_diagnostics.push(fixes[i].clone());
93                if i == fixes.len() - 1 {
94                    fixable_diagnostics.push(fixes[i + 1].clone());
95                }
96            }
97        }
98    }
99    // Get all the files that need to be fixed
100    let mut files: HashMap<FileId, String> = HashMap::default();
101    files.insert(
102        file_id,
103        db.file_content(file_id)
104            .ok_or(anyhow!("{} not found", file_id.file_name(db.upcast())))?
105            .to_string(),
106    );
107    // Fix the files
108    for fix in fixable_diagnostics {
109        // Can't fail we just set the file value.
110        files
111            .entry(file_id)
112            .and_modify(|file| file.replace_range(fix.span.to_str_range(), &fix.suggestion));
113    }
114    // Dump them in place
115    std::fs::write(file_id.full_path(db.upcast()), files.get(&file_id).unwrap())?;
116    Ok(())
117}
118
119/// Checks if the diagnostic is a panic diagnostic.
120pub fn is_panic_diagnostic(diag: &PluginDiagnostic) -> bool {
121    get_lint_type_from_diagnostic_message(&diag.message) == CairoLintKind::Panic
122}