gobject-linter 0.1.2

A fast tree-sitter-based linter for GObject/C code
Documentation
use std::sync::LazyLock;

use gobject_ast::model::{
    FileModel, Parameter, StructField, TopLevelItem, TypeDefItem, TypeInfo, TypedefTarget,
};

use crate::{
    ast_context::AstContext,
    config::Config,
    rules::{ConfigOption, Fix, Rule, Violation},
};

pub struct TypeStyle;

impl Rule for TypeStyle {
    fn name(&self) -> &'static str {
        "type_style"
    }

    fn description(&self) -> &'static str {
        "Enforce consistent use of GLib or C standard type aliases"
    }

    fn category(&self) -> crate::rules::Category {
        crate::rules::Category::Style
    }

    fn fixable(&self) -> bool {
        true
    }

    fn config_options(&self) -> &'static [ConfigOption] {
        static OPTIONS: LazyLock<Vec<ConfigOption>> = LazyLock::new(|| {
            vec![ConfigOption {
                name: "style",
                option_type: "string",
                default_value: "\"glib\"",
                example_value: "\"c\"",
                description: "Type style to enforce: \"glib\" (prefer gint, gchar, …) or \"c\" (prefer int, char, int32_t, …)",
            }]
        });

        &OPTIONS
    }

    fn check_all(
        &self,
        ast_context: &AstContext,
        config: &Config,
        violations: &mut Vec<Violation>,
    ) {
        let style = config
            .get_rule_config(self.name())
            .and_then(|rc| rc.options.get("style"))
            .and_then(|v| v.as_str())
            .unwrap_or("glib");

        for (_, file) in ast_context.iter_all_files() {
            for item in file.iter_all_items() {
                self.check_item(item, file, style, violations);
            }
        }
    }
}

impl TypeStyle {
    fn check_item(
        &self,
        item: &TopLevelItem,
        file: &FileModel,
        style: &str,
        violations: &mut Vec<Violation>,
    ) {
        match item {
            TopLevelItem::FunctionDeclaration(decl) => {
                self.check_type(&decl.return_type, file, style, violations);
                self.check_params(&decl.parameters, file, style, violations);
            }
            TopLevelItem::FunctionDefinition(def) => {
                self.check_type(&def.return_type, file, style, violations);
                self.check_params(&def.parameters, file, style, violations);
                for var in def.iter_local_declarations() {
                    self.check_type(&var.type_info, file, style, violations);
                }
            }
            TopLevelItem::TypeDefinition(typedef_item) => {
                self.check_typedef(typedef_item, file, style, violations);
            }
            TopLevelItem::Declaration(decl) => {
                self.check_type(&decl.type_info, file, style, violations);
            }
            _ => {}
        }
    }

    fn check_typedef(
        &self,
        item: &TypeDefItem,
        file: &FileModel,
        style: &str,
        violations: &mut Vec<Violation>,
    ) {
        match item {
            TypeDefItem::Typedef {
                target,
                struct_fields,
                ..
            } => {
                match target {
                    TypedefTarget::Type(type_info) => {
                        self.check_type(type_info, file, style, violations);
                    }
                    TypedefTarget::Callback {
                        return_type,
                        parameters,
                    } => {
                        self.check_type(return_type, file, style, violations);
                        self.check_params(parameters, file, style, violations);
                    }
                }
                self.check_fields(struct_fields, file, style, violations);
            }
            TypeDefItem::Struct { fields, .. } => {
                self.check_fields(fields, file, style, violations);
            }
            TypeDefItem::Enum(_) => {}
        }
    }

    fn check_fields(
        &self,
        fields: &[StructField],
        file: &FileModel,
        style: &str,
        violations: &mut Vec<Violation>,
    ) {
        for field in fields {
            field.walk(&mut |f| {
                self.check_type(&f.field_type, file, style, violations);
            });
        }
    }

    fn check_params(
        &self,
        parameters: &[Parameter],
        file: &FileModel,
        style: &str,
        violations: &mut Vec<Violation>,
    ) {
        for param in parameters {
            if let Parameter::Regular { type_info, .. } = param {
                self.check_type(type_info, file, style, violations);
            }
        }
    }

    fn check_type(
        &self,
        type_info: &TypeInfo,
        file: &FileModel,
        style: &str,
        violations: &mut Vec<Violation>,
    ) {
        let Some(basic) = type_info.as_basic() else {
            return;
        };

        let canonical = if style == "c" {
            basic.canonical_c(&type_info.base_type)
        } else {
            basic.canonical_glib()
        };

        let Some(canonical) = canonical else { return };

        if type_info.base_type == canonical {
            return;
        }

        let loc = &type_info.location;
        let source_text = loc.as_str().unwrap_or("");
        let new_text = source_text.replacen(&type_info.base_type, canonical, 1);
        let fix = Fix::new(loc.start_byte, loc.end_byte, new_text);

        violations.push(self.violation_with_fix_at(
            &file.path,
            &type_info.location,
            format!("use `{}` instead of `{}`", canonical, type_info.base_type),
            fix,
        ));
    }
}