1use std::{collections::HashSet, path::Path};
2
3use anyhow::Result;
4use colored::Colorize;
5use indicatif::ProgressBar;
6use rayon::prelude::*;
7use serde::Serialize;
8
9use crate::{
10 ast_context::AstContext,
11 config::{Config, RuleConfig, RuleLevel},
12 inline_ignore,
13 rules::{ConfigOption, *},
14};
15
16pub type ScanResult = Result<(Vec<Violation>, Vec<(&'static str, std::time::Duration)>)>;
17
18fn get_source_snippet(source: &[u8], line: usize) -> Option<String> {
21 let content = std::str::from_utf8(source).ok()?;
22 let lines: Vec<&str> = content.lines().collect();
23
24 if line == 0 || line > lines.len() {
25 return None;
26 }
27
28 let start_line = line.saturating_sub(8); let end_line = (line + 3).min(lines.len());
31
32 let mut snippet_lines = Vec::new();
33 let mut last_was_collapsed = false;
34
35 for (i, line_text) in lines.iter().enumerate().take(end_line).skip(start_line) {
36 let trimmed = line_text.trim();
37 let is_target_line = i + 1 == line;
38
39 let is_noise = !is_target_line && matches!(trimmed, "" | "{" | "}" | "{}" | "};");
41
42 if is_noise {
43 if !last_was_collapsed {
45 snippet_lines.push("...".to_string());
46 last_was_collapsed = true;
47 }
48 } else {
49 let prefix = if is_target_line { ">" } else { "" };
50 snippet_lines.push(format!("{}{}", prefix, line_text));
51 last_was_collapsed = false;
52 }
53 }
54
55 Some(snippet_lines.join("\n"))
56}
57
58fn populate_snippets(violations: &mut [Violation], ast_context: &AstContext) {
60 for violation in violations.iter_mut() {
61 if violation.snippet.is_none()
62 && let Some(file) = ast_context.project.files.get(&violation.file)
63 {
64 violation.snippet = get_source_snippet(&file.source, violation.line);
65 }
66 }
67}
68
69fn filter_violations_in_place(
71 violations: &mut Vec<Violation>,
72 project_root: &Path,
73 config: &Config,
74 rule_config: &RuleConfig,
75) -> Result<()> {
76 let ignore_matcher = config.build_rule_ignore_matcher(rule_config)?;
77
78 violations.retain(|v| {
79 let relative_path = v.file.strip_prefix(project_root).unwrap_or(&v.file);
80 !ignore_matcher.is_match(relative_path)
81 });
82
83 Ok(())
84}
85
86pub struct RuleEntry<'a> {
87 pub rule: Box<dyn Rule>,
88 pub level: RuleLevel,
89 pub rule_config: &'a RuleConfig,
90}
91
92#[macro_export]
95macro_rules! for_each_rule {
96 ($callback:ident) => {
97 $callback! {
98 (dead_code, DeadCode),
99 (unused_vfunc, UnusedVfunc),
100 (include_order, IncludeOrder),
101 (inconsistent_function_signature, InconsistentFunctionSignature),
102 (use_pragma_once, UsePragmaOnce),
103 (missing_implementation, MissingImplementation),
104 (missing_autoptr_cleanup, MissingAutoptrCleanup),
105 (missing_g_begin_decls, MissingGBeginDecls),
106 (missing_export_macro, MissingExportMacro),
107 (no_g_auto_macros, NoGAutoMacros),
108 (deprecated_add_private, DeprecatedAddPrivate),
109 (matching_declare_define, MatchingDeclareDefine),
110 (use_g_new, UseGNew),
111 (use_g_object_class_install_properties, UseGObjectClassInstallProperties),
112 (use_g_settings_typed, UseGSettingsTyped),
113 (use_g_value_set_static_string, UseGValueSetStaticString),
114 (use_g_variant_new_typed, UseGVariantNewTyped),
115 (strcmp_explicit_comparison, StrcmpExplicitComparison),
116 (type_style, TypeStyle),
117 (use_g_strcmp0, UseGStrcmp0),
118 (use_clear_functions, UseClearFunctions),
119 (use_explicit_default_flags, UseExplicitDefaultFlags),
120 (g_param_spec_null_nick_blurb, GParamSpecNullNickBlurb),
121 (g_param_spec_static_strings, GParamSpecStaticStrings),
122 (property_canonical_name, PropertyCanonicalName),
123 (g_error_init, GErrorInit),
124 (g_error_leak, GErrorLeak),
125 (g_source_id_not_stored, GSourceIdNotStored),
126 (property_enum_convention, PropertyEnumConvention),
127 (property_enum_coverage, PropertyEnumCoverage),
128 (property_switch_exhaustiveness, PropertySwitchExhaustiveness),
129 (signal_canonical_name, SignalCanonicalName),
130 (signal_enum_coverage, SignalEnumCoverage),
131 (g_object_virtual_methods_chain_up, GObjectVirtualMethodsChainUp),
132 (g_task_source_tag, GTaskSourceTag),
133 (unnecessary_null_check, UnnecessaryNullCheck),
134 (use_g_set_object, UseGSetObject),
135 (use_g_set_str, UseGSetStr),
136 (use_auto_cleanup, UseAutoCleanup),
137 (use_g_bytes_unref_to_data, UseGBytesUnrefToData),
138 (use_g_file_load_bytes, UseGFileLoadBytes),
139 (use_g_gnuc_flag_enum, UseGGnucFlagEnum),
140 (use_g_object_new_with_properties, UseGObjectNewWithProperties),
141 (use_g_object_notify_by_pspec, UseGObjectNotifyByPspec),
142 (use_g_string_free_and_steal, UseGStringFreeAndSteal),
143 (use_g_source_once, UseGSourceOnce),
144 (use_g_source_constants, UseGSourceConstants),
145 (use_g_steal_pointer, UseGStealPointer),
146 (use_g_str_has_prefix_suffix, UseGStrHasPrefixSuffix),
147 (use_g_ascii_functions, UseGAsciiFunctions),
148 (use_g_strlcpy, UseGStrlcpy),
149 (untranslated_string, UntranslatedString),
150 (gi_missing_since, GiMissingSince),
151 (gi_not_bindings_friendly, GiNotBindingsFriendly),
152 }
153 };
154}
155
156macro_rules! impl_create_all_rules {
157 ($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
158 pub fn create_all_rules<'a>(config: &'a Config) -> Vec<RuleEntry<'a>> {
160 vec![
161 $({
162 let rule = $rule_type;
163 let min_ver = rule.min_glib_version();
164 let opt_in = rule.opt_in();
165 let requires_auto_cleanup = rule.requires_auto_cleanup();
166 let level = if is_rule_compatible(config, min_ver) {
167 let default_level = if opt_in {
168 RuleLevel::Ignore
169 } else {
170 config.default_level.unwrap_or(RuleLevel::Warn)
171 };
172 let configured = config.rules.$config_field.level.unwrap_or(default_level);
173 apply_msvc_compatibility(config, stringify!($config_field), requires_auto_cleanup, configured)
174 } else {
175 RuleLevel::Ignore
176 };
177 RuleEntry {
178 rule: Box::new(rule),
179 level,
180 rule_config: &config.rules.$config_field,
181 }
182 },)*
183 ]
184 }
185 };
186}
187
188fn is_rule_compatible(config: &Config, required: Option<(u32, u32)>) -> bool {
190 let Some((req_major, req_minor)) = required else {
191 return true;
192 };
193 if let Some((major, minor)) = config.min_glib_version {
194 major > req_major || (major == req_major && minor >= req_minor)
195 } else {
196 true
197 }
198}
199
200fn apply_msvc_compatibility(
202 config: &Config,
203 rule_name: &str,
204 requires_auto_cleanup: bool,
205 configured_level: RuleLevel,
206) -> RuleLevel {
207 match (rule_name, config.msvc_compatible) {
208 ("no_g_auto_macros", false) => return RuleLevel::Ignore,
209 ("no_g_auto_macros", true) => return RuleLevel::Error,
210 (_, false) => return configured_level,
211 (_, true) => (),
213 }
214
215 if requires_auto_cleanup {
217 return RuleLevel::Ignore;
218 }
219
220 configured_level
221}
222
223for_each_rule!(impl_create_all_rules);
224
225macro_rules! impl_validate_config {
226 ($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
227 pub fn validate_config(config: &Config) -> Result<()> {
231 $(
232 {
233 let rule_config = &config.rules.$config_field;
234 if let Some(level) = rule_config.level
235 && level.is_enabled()
236 {
237 let rule = $rule_type;
238 let name = stringify!($config_field);
239
240 if let Some((req_major, req_minor)) = rule.min_glib_version()
241 && !is_rule_compatible(config, Some((req_major, req_minor)))
242 {
243 let (cfg_major, cfg_minor) = config.min_glib_version.unwrap_or((2, 0));
244 anyhow::bail!(
245 "Rule '{}' requires GLib >= {}.{}, but min_glib_version is {}.{}",
246 name, req_major, req_minor, cfg_major, cfg_minor,
247 );
248 }
249
250 if config.msvc_compatible && rule.requires_auto_cleanup() {
251 anyhow::bail!(
252 "Rule '{}' requires g_auto* macros, which are unavailable with msvc_compatible = true",
253 name,
254 );
255 }
256
257 if name == "no_g_auto_macros" && !config.msvc_compatible {
258 anyhow::bail!(
259 "Rule 'no_g_auto_macros' is only meaningful with msvc_compatible = true",
260 );
261 }
262 }
263 }
264 )*
265 Ok(())
266 }
267 };
268}
269
270for_each_rule!(impl_validate_config);
271
272fn validate_inline_ignores(
275 inline_ignores: &std::collections::HashMap<
276 &Path,
277 std::collections::HashMap<usize, Vec<String>>,
278 >,
279 rules: &[RuleEntry<'_>],
280 project_root: &Path,
281) -> Vec<String> {
282 let mut warnings = Vec::new();
283
284 let valid_rules: HashSet<String> = rules
286 .iter()
287 .map(|entry| entry.rule.name().to_string())
288 .collect();
289
290 for (file_path, file_ignores) in inline_ignores {
292 for (line_num, ignored_rules) in file_ignores {
293 for rule_name in ignored_rules {
294 if rule_name == "all" || rule_name == "*" {
296 continue;
297 }
298
299 if !valid_rules.contains(rule_name) {
301 let relative_path = file_path.strip_prefix(project_root).unwrap_or(file_path);
302 let warning = format!(
303 "{}:{}:1: {} Unknown rule '{}' in ignore directive",
304 relative_path.display(),
305 line_num,
306 "warning:".yellow(),
307 rule_name
308 );
309 warnings.push(warning);
310 }
311 }
312 }
313 }
314
315 warnings
316}
317
318pub fn scan_with_ast(
320 ast_context: &AstContext,
321 config: &Config,
322 project_root: &Path,
323 spinner: Option<&ProgressBar>,
324 generate_snippets: bool,
325) -> ScanResult {
326 let mut violations = Vec::new();
327
328 let inline_ignores: std::collections::HashMap<
330 &Path,
331 std::collections::HashMap<usize, Vec<String>>,
332 > = ast_context
333 .project
334 .files
335 .iter()
336 .map(|(path, file)| {
337 let ignores = inline_ignore::parse_ignore_directives(file);
338 (path.as_path(), ignores)
339 })
340 .collect();
341
342 let rules = create_all_rules(config);
344
345 let warnings = validate_inline_ignores(&inline_ignores, &rules, project_root);
347 for warning in warnings {
348 eprintln!("{}", warning);
349 }
350
351 for entry in &rules {
353 let known: HashSet<&str> = entry.rule.config_options().iter().map(|o| o.name).collect();
354 for key in entry.rule_config.options.keys() {
355 if !known.contains(key.as_str()) {
356 eprintln!(
357 "{}: unknown option '{}' for rule '{}'",
358 "warning".yellow(),
359 key,
360 entry.rule.name()
361 );
362 }
363 }
364 }
365
366 if let Some(sp) = spinner {
367 sp.set_message("Running linter rules...");
368 }
369
370 let per_rule: Vec<(Result<Vec<Violation>>, &str, std::time::Duration)> = rules
372 .par_iter()
373 .enumerate()
374 .map(|(rule_index, entry)| {
375 if !entry.level.is_enabled() {
376 return (Ok(Vec::new()), entry.rule.name(), std::time::Duration::ZERO);
377 }
378
379 let rule_start = std::time::Instant::now();
380 let mut rule_violations = Vec::new();
381 entry
382 .rule
383 .check_all(ast_context, config, &mut rule_violations);
384
385 for v in &mut rule_violations {
386 v.rule_index = rule_index;
387 v.level = entry.level;
388 }
389
390 if generate_snippets {
391 populate_snippets(&mut rule_violations, ast_context);
392 }
393 let filter_result = filter_violations_in_place(
394 &mut rule_violations,
395 project_root,
396 config,
397 entry.rule_config,
398 );
399 let elapsed = rule_start.elapsed();
400
401 match filter_result {
402 Ok(()) => (Ok(rule_violations), entry.rule.name(), elapsed),
403 Err(e) => (Err(e), entry.rule.name(), elapsed),
404 }
405 })
406 .collect();
407
408 let mut rule_timings: Vec<(&str, std::time::Duration)> = Vec::new();
409 for (rule_violations, name, elapsed) in per_rule {
410 if !elapsed.is_zero() {
411 rule_timings.push((name, elapsed));
412 }
413 violations.extend(rule_violations?);
414 }
415
416 deduplicate_by_rule_precedence(&mut violations);
419
420 violations.retain(|v| {
422 !inline_ignore::should_ignore_violation(&v.file, v.line, v.rule, &inline_ignores)
423 });
424
425 violations.sort_by(|a, b| {
426 a.file
427 .cmp(&b.file)
428 .then(a.line.cmp(&b.line))
429 .then(a.column.cmp(&b.column))
430 .then(a.rule.cmp(b.rule))
431 });
432
433 Ok((violations, rule_timings))
434}
435
436pub fn list_all_rules(config: &Config) {
438 let rules = create_all_rules(config);
439
440 let fixable_count = rules.iter().filter(|e| e.rule.fixable()).count();
441
442 println!(
443 "{} {}",
444 "Available lint rules".bold(),
445 format!("({} total, {} auto-fixable)", rules.len(), fixable_count).dimmed()
446 );
447
448 for entry in &rules {
449 let status = match entry.level {
450 RuleLevel::Error => "E".red().bold(),
451 RuleLevel::Warn => "W".yellow().bold(),
452 RuleLevel::Ignore => "-".dimmed(),
453 };
454 let name = entry.rule.name().cyan().bold();
455 let category = format!("[{}]", entry.rule.category()).magenta();
456 let desc = entry.rule.description().dimmed();
457
458 let mut tags = Vec::new();
459 if entry.rule.fixable() {
460 tags.push("[auto-fix]".yellow().to_string());
461 }
462 if entry.rule.opt_in() {
463 tags.push("[opt-in]".blue().to_string());
464 }
465 if entry.rule.requires_meson() {
466 tags.push("[meson]".blue().to_string());
467 }
468 if entry.rule.requires_auto_cleanup() {
469 tags.push("[no-msvc]".blue().to_string());
470 }
471 if let Some((major, minor)) = entry.rule.min_glib_version()
472 && (major > 2 || (major == 2 && minor > 0))
473 {
474 tags.push(format!("[glib>={major}.{minor}]").dimmed().to_string());
475 }
476 let tags_str = if tags.is_empty() {
477 String::new()
478 } else {
479 format!(" {}", tags.join(" "))
480 };
481 println!(" {} {} {}{} - {}", status, name, category, tags_str, desc);
482 }
483}
484
485pub fn list_all_rules_json(config: &Config) -> String {
487 #[derive(Serialize)]
488 struct RuleMetadata {
489 name: String,
490 description: String,
491 #[serde(skip_serializing_if = "Option::is_none")]
492 long_description: Option<String>,
493 category: String,
494 fixable: bool,
495 opt_in: bool,
496 #[serde(skip_serializing_if = "Option::is_none")]
497 opt_in_reason: Option<String>,
498 requires_meson: bool,
499 min_glib_version: String,
500 requires_auto_cleanup: bool,
501 config_options: Vec<ConfigOption>,
502 }
503
504 #[derive(Serialize)]
505 struct RulesOutput {
506 rules: Vec<RuleMetadata>,
507 total: usize,
508 fixable_count: usize,
509 }
510
511 let rules = create_all_rules(config);
512 let fixable_count = rules.iter().filter(|e| e.rule.fixable()).count();
513
514 let metadata: Vec<RuleMetadata> = rules
515 .iter()
516 .map(|entry| {
517 let level_default = if entry.rule.opt_in() {
519 "\"ignore\""
520 } else {
521 "\"warn\""
522 };
523 let mut all_options = vec![
524 ConfigOption {
525 name: "level",
526 option_type: "string",
527 default_value: level_default,
528 example_value: "\"error\"",
529 description: "Rule severity level: \"error\", \"warn\", or \"ignore\"",
530 },
531 ConfigOption {
532 name: "ignore",
533 option_type: "array<string>",
534 default_value: "[]",
535 example_value: "[\"tests/**\", \"examples/*.c\"]",
536 description: "Glob patterns for files to ignore for this rule",
537 },
538 ];
539 all_options.extend_from_slice(entry.rule.config_options());
540
541 RuleMetadata {
542 name: entry.rule.name().to_string(),
543 description: entry.rule.description().to_string(),
544 long_description: entry
545 .rule
546 .long_description()
547 .map(std::string::ToString::to_string),
548 category: entry.rule.category().as_str().to_string(),
549 fixable: entry.rule.fixable(),
550 opt_in: entry.rule.opt_in(),
551 opt_in_reason: entry.rule.opt_in_reason().map(String::from),
552 requires_meson: entry.rule.requires_meson(),
553 min_glib_version: entry
554 .rule
555 .min_glib_version()
556 .map_or_else(|| "2.0".to_string(), |(maj, min)| format!("{maj}.{min}")),
557 requires_auto_cleanup: entry.rule.requires_auto_cleanup(),
558 config_options: all_options,
559 }
560 })
561 .collect();
562
563 let output = RulesOutput {
564 total: rules.len(),
565 fixable_count,
566 rules: metadata,
567 };
568
569 serde_json::to_string_pretty(&output).unwrap()
570}
571
572fn deduplicate_by_rule_precedence(violations: &mut Vec<Violation>) {
575 if violations.len() <= 1 {
576 return;
577 }
578
579 violations.sort_by(|a, b| {
582 a.file
583 .cmp(&b.file)
584 .then(a.line.cmp(&b.line))
585 .then(a.column.cmp(&b.column))
586 .then(b.rule_index.cmp(&a.rule_index))
587 });
588
589 violations.dedup_by(|b, a| a.file == b.file && a.line == b.line && a.column == b.column);
592}