gobject-linter 0.1.2

A fast tree-sitter-based linter for GObject/C code
Documentation
use gobject_ast::model::{
    CallExpression, Expression, FileModel, FunctionDefItem, GObjectType, ParamFlag, Property,
};

const SINGLE_NAME_ARG: &[(&str, usize)] = &[
    ("g_object_notify", 1),
    ("g_object_set_property", 1),
    ("g_object_get_property", 1),
    ("g_object_class_find_property", 1),
    ("g_object_class_override_property", 2),
    ("gtk_cell_layout_add_attribute", 2),
    ("gtk_cell_area_cell_get_property", 2),
];
const VARARGS_PROP_VALUE: &[(&str, usize)] = &[
    ("g_object_set", 1),
    ("g_object_get", 1),
    ("g_object_new", 1),
    ("g_object_new_with_properties", 1),
    ("gtk_show_about_dialog", 1),
    ("gtk_cell_area_add_with_properties", 2),
    ("gtk_cell_area_set", 2),
    ("gtk_cell_area_get", 2),
    ("gtk_cell_layout_set_attributes", 2),
    ("gtk_text_buffer_create_tag", 2),
    ("gtk_tree_view_column_new_with_attributes", 2),
    ("gtk_tree_view_column_set_attributes", 2),
    ("gtk_tree_view_insert_column_with_attributes", 4),
];

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

pub struct PropertyCanonicalName;

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

    fn description(&self) -> &'static str {
        "Ensure property names are canonical (use dashes, not underscores)"
    }

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

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

    fn check_gobject_type(
        &self,
        _ast_context: &AstContext,
        _config: &Config,
        gobject_type: &GObjectType,
        file: &FileModel,
        violations: &mut Vec<Violation>,
    ) {
        for assignment in &gobject_type.properties {
            let Some(call) = assignment.param_spec_call() else {
                continue;
            };
            self.check_param_spec_call(file, call, assignment.property(), violations);
        }
    }

    fn check_func_impl(
        &self,
        _ast_context: &AstContext,
        _config: &Config,
        func: &FunctionDefItem,
        file: &FileModel,
        violations: &mut Vec<Violation>,
    ) {
        for call in func.find_calls_matching(|name| {
            SINGLE_NAME_ARG.iter().any(|(n, _)| *n == name)
                || VARARGS_PROP_VALUE.iter().any(|(n, _)| *n == name)
        }) {
            let name = call.function_name_str().unwrap();

            if let Some((_, idx)) = SINGLE_NAME_ARG.iter().find(|(n, _)| *n == name) {
                if let Some(arg) = call.arguments.get(*idx) {
                    self.check_property_name_arg(arg, file, violations);
                }
            } else if let Some((_, n_skip)) = VARARGS_PROP_VALUE.iter().find(|(n, _)| *n == name) {
                for arg in call.arguments.iter().skip(*n_skip).step_by(2) {
                    self.check_property_name_arg(arg, file, violations);
                }
            }
        }
    }
}

impl PropertyCanonicalName {
    fn check_param_spec_call(
        &self,
        file: &FileModel,
        call: &CallExpression,
        property: &Property,
        violations: &mut Vec<Violation>,
    ) {
        if call.arguments.len() < 2 {
            return;
        }

        if !property.name.contains('_') {
            return;
        }

        let has_static_name = property.flags.contains(&ParamFlag::StaticName)
            || property.flags.contains(&ParamFlag::StaticStrings);

        let name_value = &property.name;
        let canonical_name = name_value.replace('_', "-");
        let replacement = format!("\"{}\"", canonical_name);

        let Some(expr) = call.get_arg(0) else {
            return;
        };

        let string_lit_location = match expr {
            Expression::StringLiteral(lit) => &lit.location,
            _ => return,
        };

        let fix = Fix::new(
            string_lit_location.start_byte,
            string_lit_location.end_byte,
            replacement,
        );

        let message = if has_static_name {
            format!(
                "Property name '{}' is not canonical (contains underscores). \
                     With G_PARAM_STATIC_NAME this will cause: \
                     g_param_spec_internal: assertion '!(flags & G_PARAM_STATIC_NAME) || is_canonical (name)' failed. \
                     Use '{}' instead",
                name_value, canonical_name
            )
        } else {
            format!(
                "Property name '{}' should use dashes instead of underscores. \
                     Use '{}' for consistency with GObject conventions",
                name_value, canonical_name
            )
        };

        violations.push(self.violation_with_fix_at(&file.path, string_lit_location, message, fix));
    }

    fn check_property_name_arg(
        &self,
        expr: &Expression,
        file: &FileModel,
        violations: &mut Vec<Violation>,
    ) {
        let Expression::StringLiteral(string_lit) = expr else {
            return;
        };
        let raw = &string_lit.value;

        let Some(first_close) = raw[1..].find('"') else {
            return;
        };
        let prop_name = &raw[1..1 + first_close];

        if !prop_name.contains('_') {
            return;
        }

        let canonical = prop_name.replace('_', "-");
        let replacement = format!("\"{}\"", canonical);

        let fix = Fix::new(
            string_lit.location.start_byte,
            string_lit.location.start_byte + 1 + first_close + 1,
            replacement,
        );

        violations.push(self.violation_with_fix_at(
            &file.path,
            &string_lit.location,
            format!(
                "Property name '{}' should use dashes instead of underscores: '{}'",
                prop_name, canonical
            ),
            fix,
        ));
    }
}