gobject-linter 0.1.2

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

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

pub struct GParamSpecNullNickBlurb;

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

    fn description(&self) -> &'static str {
        "Ensure g_param_spec_* functions have NULL for nick and blurb parameters"
    }

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

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

    fn check_gobject_type(
        &self,
        _ast_context: &AstContext,
        config: &Config,
        gobject_type: &GObjectType,
        file: &FileModel,
        violations: &mut Vec<Violation>,
    ) {
        let static_flags = config.get_string_list(self.name(), "static_flags");

        for assignment in &gobject_type.properties {
            let Some(call) = assignment.param_spec_call() else {
                continue;
            };
            self.check_call(file, call, assignment.property(), &static_flags, violations);
        }
    }
}

impl GParamSpecNullNickBlurb {
    fn check_call(
        &self,
        file: &FileModel,
        call: &CallExpression,
        property: &Property,
        custom_static_flags: &[String],
        violations: &mut Vec<Violation>,
    ) {
        if call.arguments.len() < 3 {
            return;
        }

        let has_custom_static_flag = property.flags.iter().any(|flag| {
            if let ParamFlag::Unknown(name) = flag {
                custom_static_flags.contains(name)
            } else {
                false
            }
        });

        if has_custom_static_flag {
            return;
        }

        let Some(nick_expr) = call.get_arg(1) else {
            return;
        };
        let Some(blurb_expr) = call.get_arg(2) else {
            return;
        };

        let nick_is_null = nick_expr.is_null();
        let blurb_is_null = blurb_expr.is_null();

        let mut issues = Vec::new();
        if !nick_is_null {
            issues.push("nick (parameter 2)");
        }
        if !blurb_is_null {
            issues.push("blurb (parameter 3)");
        }

        if issues.is_empty() {
            return;
        }

        let string_fix = if !nick_is_null && !blurb_is_null {
            Fix::new(
                nick_expr.location().start_byte,
                blurb_expr.location().end_byte,
                "NULL, NULL",
            )
        } else if !nick_is_null {
            Fix::new(
                nick_expr.location().start_byte,
                nick_expr.location().end_byte,
                "NULL",
            )
        } else {
            Fix::new(
                blurb_expr.location().start_byte,
                blurb_expr.location().end_byte,
                "NULL",
            )
        };

        let mut fixes = vec![string_fix];

        if let Some(new_flags) = self.compute_new_flags(&property.flags) {
            let flags_expr = call.arguments.last().unwrap();
            fixes.push(Fix::new(
                flags_expr.location().start_byte,
                flags_expr.location().end_byte,
                new_flags,
            ));
        }

        violations.push(self.violation_with_fixes_at(
            &file.path,
            &call.location,
            format!(
                "{} should have NULL for {}",
                call.function_name(),
                issues.join(" and ")
            ),
            fixes,
        ));
    }

    fn compute_new_flags(&self, current_flags: &[ParamFlag]) -> Option<String> {
        let needs_removal = current_flags.iter().any(|f| {
            matches!(
                f,
                ParamFlag::StaticNick | ParamFlag::StaticBlurb | ParamFlag::StaticStrings
            )
        });
        let has_name = current_flags
            .iter()
            .any(|f| matches!(f, ParamFlag::StaticName));

        if !needs_removal && has_name {
            return None;
        }

        let mut new_flags: Vec<ParamFlag> = current_flags
            .iter()
            .filter(|f| {
                !matches!(
                    f,
                    ParamFlag::StaticNick | ParamFlag::StaticBlurb | ParamFlag::StaticStrings
                )
            })
            .cloned()
            .collect();

        if !new_flags.iter().any(|f| matches!(f, ParamFlag::StaticName)) {
            new_flags.push(ParamFlag::StaticName);
        }

        Some(if new_flags.is_empty() {
            "0".to_string()
        } else {
            new_flags
                .iter()
                .map(ParamFlag::as_str)
                .collect::<Vec<_>>()
                .join(" | ")
        })
    }
}