1use std::collections::{HashSet, VecDeque};
2use std::sync::Arc;
3
4use cairo_lang_defs::ids::{LanguageElementId, ModuleId};
5use cairo_lang_defs::plugin::PluginDiagnostic;
6use cairo_lang_filesystem::db::{ext_as_virtual, get_parent_and_mapping, translate_location};
7use cairo_lang_filesystem::ids::{CodeOrigin, FileId, FileLongId, SpanInFile, Tracked};
8use cairo_lang_parser::db::ParserGroup;
9use cairo_lang_syntax::node::SyntaxNode;
10use cairo_lang_syntax::node::helpers::QueryAttrs;
11use cairo_lang_syntax::node::kind::SyntaxKind;
12use cairo_lang_utils::ordered_hash_set::OrderedHashSet;
13use if_chain::if_chain;
14
15use crate::context::{
16 get_all_checking_functions, get_name_for_diagnostic_message, is_lint_enabled_by_default,
17};
18use crate::{CairoLintToolMetadata, CorelibContext};
19
20use crate::mappings::{get_origin_module_item_as_syntax_node, get_origin_syntax_node};
21
22mod db;
23use cairo_lang_defs::db::DefsGroup;
24pub use db::{LinterAnalysisDatabase, LinterAnalysisDatabaseBuilder};
25use salsa::Database;
26
27#[derive(PartialEq, Eq, Hash, Debug, Clone)]
28pub struct LinterDiagnosticParams {
29 pub only_generated_files: bool,
30 pub tool_metadata: CairoLintToolMetadata,
31}
32
33pub trait LinterGroup: Database {
34 fn linter_diagnostics<'db>(
35 &'db self,
36 params: LinterDiagnosticParams,
37 module_id: ModuleId<'db>,
38 ) -> &'db Vec<PluginDiagnostic<'db>> {
39 linter_diagnostics(self.as_dyn_database(), params, module_id)
40 }
41
42 fn node_resultants<'db>(&'db self, node: SyntaxNode<'db>) -> Option<&'db Vec<SyntaxNode<'db>>> {
43 node_resultants(self.as_dyn_database(), (), node).as_ref()
44 }
45
46 fn file_and_subfiles_with_corresponding_modules<'db>(
47 &'db self,
48 file: FileId<'db>,
49 ) -> &'db Option<(HashSet<FileId<'db>>, HashSet<ModuleId<'db>>)> {
50 file_and_subfiles_with_corresponding_modules(self.as_dyn_database(), file)
51 }
52
53 fn find_generated_nodes<'db>(
54 &'db self,
55 node_descendant_files: Arc<[FileId<'db>]>,
56 node: SyntaxNode<'db>,
57 ) -> &'db OrderedHashSet<SyntaxNode<'db>> {
58 find_generated_nodes(self.as_dyn_database(), node_descendant_files, node)
59 }
60
61 fn corelib_context<'db>(&'db self) -> &'db CorelibContext<'db> {
62 corelib_context(self.as_dyn_database())
63 }
64}
65
66impl<T: Database + ?Sized> LinterGroup for T {}
67
68#[tracing::instrument(skip_all, level = "trace")]
69#[salsa::tracked(returns(ref))]
70fn linter_diagnostics<'db>(
71 db: &'db dyn Database,
72 params: LinterDiagnosticParams,
73 module_id: ModuleId<'db>,
74) -> Vec<PluginDiagnostic<'db>> {
75 let mut diags: Vec<(PluginDiagnostic, FileId)> = Vec::new();
76 let Ok(module_data) = module_id.module_data(db) else {
77 return Vec::default();
78 };
79 for item in module_data.items(db) {
80 let mut item_diagnostics = Vec::new();
81 let module_file = db.module_main_file(module_id).unwrap();
82 let item_file = item.stable_location(db).file_id(db).long(db);
83 let is_generated_item =
84 matches!(item_file, FileLongId::Virtual(_) | FileLongId::External(_));
85
86 if is_generated_item && !params.only_generated_files {
87 let item_syntax_node = item.stable_location(db).stable_ptr().lookup(db);
88 let origin_node = get_origin_module_item_as_syntax_node(db, item);
89
90 if_chain! {
91 if let Some(node) = origin_node;
92 if let Some(resultants) = db.node_resultants(node);
93 if resultants.len() == 1;
98 if node.get_text_without_trivia(db).long(db).as_str().contains(item_syntax_node.get_text_without_trivia(db).long(db).as_str());
102 then {
103 let checking_functions = get_all_checking_functions();
104 for checking_function in checking_functions {
105 checking_function(db, item, &mut item_diagnostics);
106 }
107
108 diags.extend(item_diagnostics.into_iter().filter_map(|mut diag| {
109 let ptr = diag.stable_ptr;
110 diag.stable_ptr = get_origin_syntax_node(db, &ptr)?.stable_ptr(db);
111 Some((diag, module_file))}));
112 }
113 }
114 } else if !is_generated_item || params.only_generated_files {
115 let checking_functions = get_all_checking_functions();
116 for checking_function in checking_functions {
117 checking_function(db, item, &mut item_diagnostics);
118 }
119
120 diags.extend(item_diagnostics.into_iter().filter_map(|diag| {
121 get_origin_syntax_node(db, &diag.stable_ptr).map(|_| (diag, module_file))
123 }));
124 }
125 }
126
127 diags
128 .into_iter()
129 .filter(|diag: &(PluginDiagnostic, FileId)| {
130 let diagnostic = &diag.0;
131 let node = diagnostic.stable_ptr.lookup(db);
132 let allowed_name = get_name_for_diagnostic_message(&diagnostic.message).unwrap();
133 let default_allowed = is_lint_enabled_by_default(&diagnostic.message).unwrap();
134 let is_rule_allowed_globally = *params
135 .tool_metadata
136 .get(allowed_name)
137 .unwrap_or(&default_allowed);
138 !node_has_ascendants_with_allow_name_attr(db, node, allowed_name)
139 && is_rule_allowed_globally
140 })
141 .map(|diag| diag.0)
142 .collect()
143}
144
145#[tracing::instrument(level = "trace", skip(db))]
146#[salsa::tracked(returns(ref))]
147fn node_resultants<'db>(
148 db: &'db dyn Database,
149 _: Tracked,
150 node: SyntaxNode<'db>,
151) -> Option<Vec<SyntaxNode<'db>>> {
152 let main_file = node.stable_ptr(db).file_id(db);
153
154 let (files, _) = db
155 .file_and_subfiles_with_corresponding_modules(main_file)
156 .as_ref()?;
157
158 let files: Arc<[FileId]> = files
159 .iter()
160 .filter(|file| **file != main_file)
161 .cloned()
162 .collect();
163 let resultants = db.find_generated_nodes(files, node);
164
165 Some(resultants.into_iter().cloned().collect())
166}
167
168#[tracing::instrument(level = "trace", skip(db))]
169#[salsa::tracked(returns(ref))]
170pub fn file_and_subfiles_with_corresponding_modules<'db>(
171 db: &'db dyn Database,
172 file: FileId<'db>,
173) -> Option<(HashSet<FileId<'db>>, HashSet<ModuleId<'db>>)> {
174 let mut modules: HashSet<_> = db.file_modules(file).ok()?.iter().copied().collect();
175 let mut files = HashSet::from([file]);
176 let mut modules_queue: VecDeque<_> = modules.iter().copied().collect();
192 while let Some(module_id) = modules_queue.pop_front() {
193 for file_id in db.module_files(module_id).ok()?.iter() {
194 if files.insert(*file_id) {
195 for module_id in db.file_modules(*file_id).ok()?.iter() {
196 if modules.insert(*module_id) {
197 modules_queue.push_back(*module_id);
198 }
199 }
200 }
201 }
202 }
203 Some((files, modules))
204}
205
206#[tracing::instrument(level = "trace", skip(db))]
207#[salsa::tracked(returns(ref))]
208pub fn find_generated_nodes<'db>(
209 db: &'db dyn Database,
210 node_descendant_files: Arc<[FileId<'db>]>,
211 node: SyntaxNode<'db>,
212) -> OrderedHashSet<SyntaxNode<'db>> {
213 let start_file = node.stable_ptr(db).file_id(db);
214
215 let mut result = OrderedHashSet::default();
216
217 let mut is_replaced = false;
218
219 for file in node_descendant_files.iter().cloned() {
220 let Some((
221 SpanInFile {
222 file_id: parent,
223 span: _,
224 },
225 mappings,
226 )) = get_parent_and_mapping(db, file)
227 else {
228 continue;
229 };
230
231 if parent != start_file {
232 continue;
233 }
234
235 let Ok(file_syntax) = db.file_syntax(file) else {
236 continue;
237 };
238
239 let mappings: Vec<_> = mappings
240 .iter()
241 .filter(|mapping| match mapping.origin {
242 CodeOrigin::CallSite(_) => true,
243 CodeOrigin::Start(start) => start == node.span(db).start,
244 CodeOrigin::Span(span) => node.span(db).contains(span),
245 })
246 .cloned()
247 .collect();
248 if mappings.is_empty() {
249 continue;
250 }
251
252 let is_replacing_og_item = match file.long(db) {
253 FileLongId::Virtual(vfs) => vfs.original_item_removed,
254 FileLongId::External(id) => ext_as_virtual(db, *id).original_item_removed,
255 _ => unreachable!(),
256 };
257
258 let mut new_nodes: OrderedHashSet<_> = Default::default();
259
260 for mapping in &mappings {
261 for token in file_syntax.lookup_offset(db, mapping.span.start).tokens(db) {
262 if token.kind(db) == SyntaxKind::TerminalEndOfFile {
267 continue;
268 }
269 let nodes: Vec<_> = token
270 .ancestors_with_self(db)
271 .map_while(|new_node| {
272 translate_location(&mappings, new_node.span(db))
273 .map(|span_in_parent| (new_node, span_in_parent))
274 })
275 .take_while(|(_, span_in_parent)| node.span(db).contains(*span_in_parent))
276 .collect();
277
278 if let Some((last_node, _)) = nodes.last().cloned() {
279 let (new_node, _) = nodes
280 .into_iter()
281 .rev()
282 .take_while(|(node, _)| node.span(db) == last_node.span(db))
283 .last()
284 .unwrap();
285
286 new_nodes.insert(new_node);
287 }
288 }
289 }
290
291 if !new_nodes.is_empty() {
293 is_replaced = is_replaced || is_replacing_og_item;
294 }
295
296 for new_node in new_nodes {
297 result.extend(
298 find_generated_nodes(db, Arc::clone(&node_descendant_files), new_node)
299 .into_iter()
300 .cloned(),
301 );
302 }
303 }
304
305 if !is_replaced {
306 result.insert(node);
307 }
308
309 result
310}
311
312#[salsa::tracked(returns(ref))]
313fn corelib_context<'db>(db: &'db dyn Database) -> CorelibContext<'db> {
314 CorelibContext::new(db)
315}
316
317#[tracing::instrument(skip_all, level = "trace")]
318fn node_has_ascendants_with_allow_name_attr<'db>(
319 db: &'db dyn Database,
320 node: SyntaxNode<'db>,
321 allowed_name: &'static str,
322) -> bool {
323 for node in node.ancestors_with_self(db) {
324 if node.has_attr_with_arg(db, "allow", allowed_name) {
325 return true;
326 }
327 }
328 false
329}