Skip to main content

cairo_lint/lang/
mod.rs

1use cairo_lang_defs::ids::{LanguageElementId, ModuleId};
2use cairo_lang_defs::plugin::PluginDiagnostic;
3use cairo_lang_filesystem::ids::{FileId, FileLongId};
4use cairo_lang_syntax::node::SyntaxNode;
5use cairo_lang_syntax::node::helpers::QueryAttrs;
6use if_chain::if_chain;
7use std::collections::HashSet;
8
9use crate::context::{
10    get_all_checking_functions, get_name_for_diagnostic_message, is_lint_enabled_by_default,
11};
12use crate::{CairoLintToolMetadata, CorelibContext};
13
14use crate::mappings::{get_origin_module_item_as_syntax_node, get_origin_syntax_node};
15
16mod db;
17use cairo_lang_defs::db::DefsGroup;
18pub use db::{LinterAnalysisDatabase, LinterAnalysisDatabaseBuilder};
19use salsa::Database;
20
21#[derive(PartialEq, Eq, Hash, Debug, Clone)]
22pub struct LinterDiagnosticParams {
23    pub only_generated_files: bool,
24    pub tool_metadata: CairoLintToolMetadata,
25}
26
27pub trait LinterGroup: Database {
28    fn linter_diagnostics<'db>(
29        &'db self,
30        params: LinterDiagnosticParams,
31        module_id: ModuleId<'db>,
32    ) -> &'db Vec<PluginDiagnostic<'db>> {
33        linter_diagnostics(self.as_dyn_database(), params, module_id)
34    }
35
36    fn corelib_context<'db>(&'db self) -> &'db CorelibContext<'db> {
37        corelib_context(self.as_dyn_database())
38    }
39}
40
41impl<T: Database + ?Sized> LinterGroup for T {}
42
43#[tracing::instrument(skip_all, level = "trace")]
44#[salsa::tracked(returns(ref))]
45fn linter_diagnostics<'db>(
46    db: &'db dyn Database,
47    params: LinterDiagnosticParams,
48    module_id: ModuleId<'db>,
49) -> Vec<PluginDiagnostic<'db>> {
50    let mut diags: Vec<(PluginDiagnostic, FileId)> = Vec::new();
51    let Ok(module_data) = module_id.module_data(db) else {
52        return Vec::default();
53    };
54
55    let mut linted_nodes: HashSet<SyntaxNode> = HashSet::new();
56
57    for item in module_data.items(db) {
58        let mut item_diagnostics = Vec::new();
59        let module_file = db.module_main_file(module_id).unwrap();
60        let item_file = item.stable_location(db).file_id(db).long(db);
61        let is_generated_item =
62            matches!(item_file, FileLongId::Virtual(_) | FileLongId::External(_));
63
64        if is_generated_item && !params.only_generated_files {
65            let item_syntax_node = item.stable_location(db).stable_ptr().lookup(db);
66            let origin_node = get_origin_module_item_as_syntax_node(db, item);
67
68            if_chain! {
69                if let Some(node) = origin_node;
70                // We want to make sure that the corresponding item to the origin node was not yet linted. We don't want to duplicate the diagnostics for the same user code.
71                if !linted_nodes.contains(&node);
72                // We don't do the `==` check here, as the origin node always has the proc macro attributes.
73                // It also means that if the macro changed anything in the original item code,
74                // we won't be processing it, as it might lead to unexpected behavior.
75                if node.get_text_without_trivia(db).long(db).as_str().contains(item_syntax_node.get_text_without_trivia(db).long(db).as_str());
76                then {
77                    let checking_functions = get_all_checking_functions();
78                    for checking_function in checking_functions {
79                        checking_function(db, item, &mut item_diagnostics);
80                    }
81
82                    linted_nodes.insert(node);
83
84                    diags.extend(item_diagnostics.into_iter().filter_map(|mut diag| {
85                      let ptr = diag.stable_ptr;
86                      diag.stable_ptr = get_origin_syntax_node(db, &ptr)?.stable_ptr(db);
87                      Some((diag, module_file))}));
88                }
89            }
90        } else if !is_generated_item || params.only_generated_files {
91            let checking_functions = get_all_checking_functions();
92            for checking_function in checking_functions {
93                checking_function(db, item, &mut item_diagnostics);
94            }
95
96            diags.extend(item_diagnostics.into_iter().filter_map(|diag| {
97                // If the diagnostic is not mapped to an on-disk file, it mean that it's an inline macro diagnostic.
98                get_origin_syntax_node(db, &diag.stable_ptr).map(|_| (diag, module_file))
99            }));
100        }
101    }
102
103    diags
104        .into_iter()
105        .filter(|diag: &(PluginDiagnostic, FileId)| {
106            let diagnostic = &diag.0;
107            let node = diagnostic.stable_ptr.lookup(db);
108            let allowed_name = get_name_for_diagnostic_message(&diagnostic.message).unwrap();
109            let default_allowed = is_lint_enabled_by_default(&diagnostic.message).unwrap();
110            let is_rule_allowed_globally = *params
111                .tool_metadata
112                .get(allowed_name)
113                .unwrap_or(&default_allowed);
114            !node_has_ascendants_with_allow_name_attr(db, node, allowed_name)
115                && is_rule_allowed_globally
116        })
117        .map(|diag| diag.0)
118        .collect()
119}
120
121#[salsa::tracked(returns(ref))]
122fn corelib_context<'db>(db: &'db dyn Database) -> CorelibContext<'db> {
123    CorelibContext::new(db)
124}
125
126#[tracing::instrument(skip_all, level = "trace")]
127fn node_has_ascendants_with_allow_name_attr<'db>(
128    db: &'db dyn Database,
129    node: SyntaxNode<'db>,
130    allowed_name: &'static str,
131) -> bool {
132    for node in node.ancestors_with_self(db) {
133        if node.has_attr_with_arg(db, "allow", allowed_name) {
134            return true;
135        }
136    }
137    false
138}