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 if !linted_nodes.contains(&node);
72 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 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}