Skip to main content

gobject_linter/rules/
mod.rs

1use std::path::PathBuf;
2
3use gobject_ast::model::{
4    EnumInfo, FileModel, FunctionDeclItem, FunctionDefItem, GObjectType, SourceLocation,
5};
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    ast_context::AstContext,
10    config::{Config, RuleLevel},
11};
12
13/// Rule category (similar to Clippy's lint categories)
14#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Serialize)]
15pub enum Category {
16    /// Code that is outright wrong or very useless
17    Correctness,
18    /// Code that is most likely wrong or useless
19    Suspicious,
20    /// Code that should be written in a more idiomatic way
21    Style,
22    /// Code that does something simple but in a complex way
23    Complexity,
24    /// Code that can be written to run faster
25    Perf,
26    /// Lints which are rather strict or have occasional false positives
27    Pedantic,
28    /// Lints which prevent the use of language/library features
29    Restriction,
30    /// Code that may cause portability issues across platforms/compilers
31    Portability,
32    /// GObject Introspection annotation issues
33    Introspection,
34}
35
36impl Category {
37    pub fn as_str(&self) -> &'static str {
38        match self {
39            Self::Correctness => "correctness",
40            Self::Suspicious => "suspicious",
41            Self::Style => "style",
42            Self::Complexity => "complexity",
43            Self::Perf => "perf",
44            Self::Pedantic => "pedantic",
45            Self::Restriction => "restriction",
46            Self::Portability => "portability",
47            Self::Introspection => "introspection",
48        }
49    }
50}
51
52impl std::fmt::Display for Category {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            Self::Correctness => write!(f, "Correctness"),
56            Self::Suspicious => write!(f, "Suspicious"),
57            Self::Style => write!(f, "Style"),
58            Self::Complexity => write!(f, "Complexity"),
59            Self::Perf => write!(f, "Performance"),
60            Self::Pedantic => write!(f, "Pedantic"),
61            Self::Restriction => write!(f, "Restriction"),
62            Self::Portability => write!(f, "Portability"),
63            Self::Introspection => write!(f, "Introspection"),
64        }
65    }
66}
67
68/// Represents an automated fix for a violation
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct Fix {
71    /// Byte offset where the fix starts
72    pub start_byte: usize,
73    /// Byte offset where the fix ends (exclusive)
74    pub end_byte: usize,
75    /// Replacement text (`None` = pure deletion)
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub replacement: Option<String>,
78}
79
80impl Fix {
81    /// Create a fix that replaces a byte range with new text
82    pub fn new(start_byte: usize, end_byte: usize, replacement: impl Into<String>) -> Self {
83        Self {
84            start_byte,
85            end_byte,
86            replacement: Some(replacement.into()),
87        }
88    }
89
90    /// Create a fix that deletes a byte range
91    pub fn delete(start_byte: usize, end_byte: usize) -> Self {
92        Self {
93            start_byte,
94            end_byte,
95            replacement: None,
96        }
97    }
98
99    /// Delete an entire line (including indentation and newline)
100    pub fn delete_line(location: &SourceLocation) -> Self {
101        let (start, end) = location.find_line_range();
102        Self::delete(start, end)
103    }
104
105    /// Delete a line and any preceding blank line
106    pub fn delete_line_and_leading_blank(location: &SourceLocation) -> Self {
107        let (start, end) = location.find_line_bounds();
108        Self::delete(start, end)
109    }
110
111    /// Delete a line and any following blank line
112    pub fn delete_line_and_trailing_blank(location: &SourceLocation) -> Self {
113        let (start, end) = location.find_line_bounds_with_following_blank();
114        Self::delete(start, end)
115    }
116}
117
118/// Describes a simple function rename: find calls to `from`, suggest `to`.
119pub struct FunctionRename {
120    /// Function name to search for
121    pub from: &'static str,
122    /// Replacement function name, or `None` for warn-only (no fix)
123    pub to: Option<&'static str>,
124    /// Violation message
125    pub message: &'static str,
126}
127
128/// Configuration option metadata for a rule
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ConfigOption {
131    /// Option name (e.g., "config_header")
132    pub name: &'static str,
133    /// Option type (e.g., "string", "array<string>", "boolean")
134    pub option_type: &'static str,
135    /// Default value as a string representation (e.g., "\"config.h\"", "[]")
136    pub default_value: &'static str,
137    /// Example value for documentation (e.g., "[\"cairo_*\", \"Pango*\"]")
138    pub example_value: &'static str,
139    /// Description of what this option does
140    pub description: &'static str,
141}
142
143pub mod dead_code;
144pub mod deprecated_add_private;
145pub mod g_error_init;
146pub mod g_error_leak;
147pub mod g_object_virtual_methods_chain_up;
148pub mod g_param_spec_null_nick_blurb;
149pub mod g_param_spec_static_strings;
150pub mod g_source_id_not_stored;
151pub mod g_task_source_tag;
152pub mod gi_missing_since;
153pub mod gi_not_bindings_friendly;
154pub mod include_order;
155pub mod inconsistent_function_signature;
156pub mod matching_declare_define;
157pub mod missing_autoptr_cleanup;
158pub mod missing_export_macro;
159pub mod missing_g_begin_decls;
160pub mod missing_implementation;
161pub mod no_g_auto_macros;
162pub mod property_canonical_name;
163pub mod property_enum_convention;
164pub mod property_enum_coverage;
165pub mod property_switch_exhaustiveness;
166pub mod signal_canonical_name;
167pub mod signal_enum_coverage;
168pub mod strcmp_explicit_comparison;
169pub mod type_style;
170pub mod unnecessary_null_check;
171pub mod untranslated_string;
172pub mod unused_vfunc;
173pub mod use_auto_cleanup;
174pub mod use_clear_functions;
175pub mod use_explicit_default_flags;
176pub mod use_g_ascii_functions;
177pub mod use_g_bytes_unref_to_data;
178pub mod use_g_file_load_bytes;
179pub mod use_g_gnuc_flag_enum;
180pub mod use_g_new;
181pub mod use_g_object_class_install_properties;
182pub mod use_g_object_new_with_properties;
183pub mod use_g_object_notify_by_pspec;
184pub mod use_g_set_object;
185pub mod use_g_set_str;
186pub mod use_g_settings_typed;
187pub mod use_g_source_constants;
188pub mod use_g_source_once;
189pub mod use_g_steal_pointer;
190pub mod use_g_str_has_prefix_suffix;
191pub mod use_g_strcmp0;
192pub mod use_g_string_free_and_steal;
193pub mod use_g_strlcpy;
194pub mod use_g_value_set_static_string;
195pub mod use_g_variant_new_typed;
196pub mod use_pragma_once;
197
198pub use dead_code::DeadCode;
199pub use deprecated_add_private::DeprecatedAddPrivate;
200pub use g_error_init::GErrorInit;
201pub use g_error_leak::GErrorLeak;
202pub use g_object_virtual_methods_chain_up::GObjectVirtualMethodsChainUp;
203pub use g_param_spec_null_nick_blurb::GParamSpecNullNickBlurb;
204pub use g_param_spec_static_strings::GParamSpecStaticStrings;
205pub use g_source_id_not_stored::GSourceIdNotStored;
206pub use g_task_source_tag::GTaskSourceTag;
207pub use gi_missing_since::GiMissingSince;
208pub use gi_not_bindings_friendly::GiNotBindingsFriendly;
209pub use include_order::IncludeOrder;
210pub use inconsistent_function_signature::InconsistentFunctionSignature;
211pub use matching_declare_define::MatchingDeclareDefine;
212pub use missing_autoptr_cleanup::MissingAutoptrCleanup;
213pub use missing_export_macro::MissingExportMacro;
214pub use missing_g_begin_decls::MissingGBeginDecls;
215pub use missing_implementation::MissingImplementation;
216pub use no_g_auto_macros::NoGAutoMacros;
217pub use property_canonical_name::PropertyCanonicalName;
218pub use property_enum_convention::PropertyEnumConvention;
219pub use property_enum_coverage::PropertyEnumCoverage;
220pub use property_switch_exhaustiveness::PropertySwitchExhaustiveness;
221pub use signal_canonical_name::SignalCanonicalName;
222pub use signal_enum_coverage::SignalEnumCoverage;
223pub use strcmp_explicit_comparison::StrcmpExplicitComparison;
224pub use type_style::TypeStyle;
225pub use unnecessary_null_check::UnnecessaryNullCheck;
226pub use untranslated_string::UntranslatedString;
227pub use unused_vfunc::UnusedVfunc;
228pub use use_auto_cleanup::UseAutoCleanup;
229pub use use_clear_functions::UseClearFunctions;
230pub use use_explicit_default_flags::UseExplicitDefaultFlags;
231pub use use_g_ascii_functions::UseGAsciiFunctions;
232pub use use_g_bytes_unref_to_data::UseGBytesUnrefToData;
233pub use use_g_file_load_bytes::UseGFileLoadBytes;
234pub use use_g_gnuc_flag_enum::UseGGnucFlagEnum;
235pub use use_g_new::UseGNew;
236pub use use_g_object_class_install_properties::UseGObjectClassInstallProperties;
237pub use use_g_object_new_with_properties::UseGObjectNewWithProperties;
238pub use use_g_object_notify_by_pspec::UseGObjectNotifyByPspec;
239pub use use_g_set_object::UseGSetObject;
240pub use use_g_set_str::UseGSetStr;
241pub use use_g_settings_typed::UseGSettingsTyped;
242pub use use_g_source_constants::UseGSourceConstants;
243pub use use_g_source_once::UseGSourceOnce;
244pub use use_g_steal_pointer::UseGStealPointer;
245pub use use_g_str_has_prefix_suffix::UseGStrHasPrefixSuffix;
246pub use use_g_strcmp0::UseGStrcmp0;
247pub use use_g_string_free_and_steal::UseGStringFreeAndSteal;
248pub use use_g_strlcpy::UseGStrlcpy;
249pub use use_g_value_set_static_string::UseGValueSetStaticString;
250pub use use_g_variant_new_typed::UseGVariantNewTyped;
251pub use use_pragma_once::UsePragmaOnce;
252
253#[derive(Debug, Clone, serde::Serialize)]
254pub struct Violation {
255    pub file: PathBuf,
256    pub line: usize,
257    pub column: usize,
258    pub message: String,
259    pub rule: &'static str,
260    pub category: Category,
261    pub level: RuleLevel,
262    pub snippet: Option<String>,
263    /// Rule execution order - higher means more specific/later rules take
264    /// precedence
265    pub rule_index: usize,
266    /// Optional automated fixes (multiple edits can be applied)
267    pub fixes: Vec<Fix>,
268}
269
270/// Trait that all linting rules must implement
271pub trait Rule: Send + Sync {
272    /// The unique identifier for this rule (e.g., "missing_implementation")
273    fn name(&self) -> &'static str;
274
275    /// Human-readable description of what this rule checks
276    fn description(&self) -> &'static str;
277
278    /// Long-form markdown documentation (optional)
279    fn long_description(&self) -> Option<&'static str> {
280        None
281    }
282
283    /// Rule category
284    fn category(&self) -> Category;
285
286    /// Whether this rule supports automated fixes via --fix
287    fn fixable(&self) -> bool {
288        false
289    }
290
291    /// Whether this rule requires meson introspection to produce results.
292    /// Rules returning true silently skip when no build directory is found.
293    fn requires_meson(&self) -> bool {
294        false
295    }
296
297    /// Configuration options supported by this rule
298    fn config_options(&self) -> &'static [ConfigOption] {
299        &[]
300    }
301
302    /// Minimum GLib version required by this rule.
303    /// `None` means no version requirement (compatible with all versions).
304    fn min_glib_version(&self) -> Option<(u32, u32)> {
305        None
306    }
307
308    /// Whether this rule suggests g_auto* macros (disabled when
309    /// msvc_compatible=true)
310    fn requires_auto_cleanup(&self) -> bool {
311        false
312    }
313
314    /// Whether this rule is disabled by default (user must explicitly enable
315    /// it)
316    fn opt_in(&self) -> bool {
317        false
318    }
319
320    /// Why this rule is opt-in (only meaningful when opt_in() returns true)
321    fn opt_in_reason(&self) -> Option<&'static str> {
322        None
323    }
324
325    /// Check a function implementation (from C files)
326    /// Override this to check function bodies and implementations
327    #[allow(unused_variables)]
328    fn check_func_impl(
329        &self,
330        ast_context: &AstContext,
331        config: &Config,
332        func: &FunctionDefItem,
333        file: &FileModel,
334        violations: &mut Vec<Violation>,
335    ) {
336        // Default: no-op
337    }
338
339    /// Check a function declaration (from header files)
340    /// Override this to check function declarations and signatures
341    #[allow(unused_variables)]
342    fn check_func_decl(
343        &self,
344        ast_context: &AstContext,
345        config: &Config,
346        func: &FunctionDeclItem,
347        file: &FileModel,
348        violations: &mut Vec<Violation>,
349    ) {
350        // Default: no-op
351    }
352
353    /// Check an enum definition
354    #[allow(unused_variables)]
355    fn check_enum(
356        &self,
357        ast_context: &AstContext,
358        config: &Config,
359        enum_info: &EnumInfo,
360        file: &FileModel,
361        violations: &mut Vec<Violation>,
362    ) {
363        // Default: no-op
364    }
365
366    /// Check a GObject type declaration/definition
367    /// Override this to check properties, signals, or other GObject-level
368    /// concerns
369    #[allow(unused_variables)]
370    fn check_gobject_type(
371        &self,
372        ast_context: &AstContext,
373        config: &Config,
374        gobject_type: &GObjectType,
375        file: &FileModel,
376        violations: &mut Vec<Violation>,
377    ) {
378        // Default: no-op
379    }
380
381    /// Check the AST and add violations to the provided vector
382    /// Default implementation calls check_func_impl for C files,
383    /// check_func_decl for headers, and check_gobject_type for all files.
384    /// Override this if you need custom iteration logic beyond per-item
385    /// checking
386    fn check_all(
387        &self,
388        ast_context: &AstContext,
389        config: &Config,
390        violations: &mut Vec<Violation>,
391    ) {
392        for (path, file) in ast_context.iter_all_files() {
393            let ext = path.extension().and_then(|e| e.to_str());
394            if ext == Some("c") {
395                for func in file.iter_function_definitions() {
396                    self.check_func_impl(ast_context, config, func, file, violations);
397                }
398            }
399            if ext == Some("h") {
400                for func in file.iter_function_declarations() {
401                    self.check_func_decl(ast_context, config, func, file, violations);
402                }
403            }
404            for gt in file.iter_all_gobject_types() {
405                self.check_gobject_type(ast_context, config, gt, file, violations);
406            }
407            for enum_info in file.iter_all_enums() {
408                self.check_enum(ast_context, config, enum_info, file, violations);
409            }
410        }
411    }
412
413    /// Find calls to functions listed in `renames` and emit violations.
414    /// When `to` is `Some`, generates a fix that reformats the entire call.
415    fn check_function_renames(
416        &self,
417        func: &FunctionDefItem,
418        file: &FileModel,
419        config: &Config,
420        violations: &mut Vec<Violation>,
421        renames: &[FunctionRename],
422    ) {
423        let names: Vec<&str> = renames.iter().map(|r| r.from).collect();
424        for call in func.find_calls(&names) {
425            let Some(func_name) = call.function_name_str() else {
426                continue;
427            };
428            let Some(rename) = renames.iter().find(|r| r.from == func_name) else {
429                continue;
430            };
431
432            if let Some(new_name) = rename.to {
433                let args: Vec<&str> = call
434                    .arguments
435                    .iter()
436                    .filter_map(|arg| arg.location().as_str())
437                    .collect();
438                let replacement = config.style.format_call(new_name, &args);
439                let fix = Fix::new(
440                    call.location.start_byte,
441                    call.location.end_byte,
442                    replacement,
443                );
444                violations.push(self.violation_with_fix_at(
445                    &file.path,
446                    &call.location,
447                    rename.message.to_string(),
448                    fix,
449                ));
450            } else {
451                violations.push(self.violation_at(
452                    &file.path,
453                    &call.location,
454                    rename.message.to_string(),
455                ));
456            }
457        }
458    }
459
460    /// Helper to create a violation with the rule name automatically filled in
461    fn violation(
462        &self,
463        file: &std::path::Path,
464        line: usize,
465        column: usize,
466        message: String,
467    ) -> Violation {
468        self.violation_with_fixes(file, line, column, message, vec![])
469    }
470
471    /// Helper to create a violation with an automated fix
472    fn violation_with_fix(
473        &self,
474        file: &std::path::Path,
475        line: usize,
476        column: usize,
477        message: String,
478        fix: Fix,
479    ) -> Violation {
480        self.violation_with_fixes(file, line, column, message, vec![fix])
481    }
482
483    /// Helper to create a violation with multiple automated fixes
484    fn violation_with_fixes(
485        &self,
486        file: &std::path::Path,
487        line: usize,
488        column: usize,
489        message: String,
490        fixes: Vec<Fix>,
491    ) -> Violation {
492        Violation {
493            file: file.to_path_buf(),
494            line,
495            column,
496            message,
497            rule: self.name(),
498            category: self.category(),
499            level: RuleLevel::Error, // Will be overridden by scanner
500            snippet: None,
501            rule_index: 0,
502            fixes,
503        }
504    }
505
506    fn violation_at(
507        &self,
508        file: &std::path::Path,
509        location: &SourceLocation,
510        message: String,
511    ) -> Violation {
512        self.violation(file, location.line, location.column, message)
513    }
514
515    fn violation_with_fix_at(
516        &self,
517        file: &std::path::Path,
518        location: &SourceLocation,
519        message: String,
520        fix: Fix,
521    ) -> Violation {
522        self.violation_with_fix(file, location.line, location.column, message, fix)
523    }
524
525    fn violation_with_fixes_at(
526        &self,
527        file: &std::path::Path,
528        location: &SourceLocation,
529        message: String,
530        fixes: Vec<Fix>,
531    ) -> Violation {
532        self.violation_with_fixes(file, location.line, location.column, message, fixes)
533    }
534}