gobject-linter 0.1.2

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

use gobject_ast::model::{CallExpression, FileModel, GObjectType, ParamFlag, Property};

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

pub struct GParamSpecStaticStrings;

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

    fn description(&self) -> &'static str {
        "Ensure *_param_spec_* calls use G_PARAM_STATIC_STRINGS flag for string literals"
    }

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

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

    fn config_options(&self) -> &'static [ConfigOption] {
        static OPTIONS: LazyLock<Vec<ConfigOption>> = LazyLock::new(|| {
            vec![ConfigOption {
                name: "static_flags",
                option_type: "array<string>",
                default_value: "[]",
                example_value: "[\"ST_PARAM_READWRITE\", \"ST_PARAM_READABLE\"]",
                description: "List of custom flag constants that already include G_PARAM_STATIC_STRINGS",
            }]
        });

        &OPTIONS
    }

    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 GParamSpecStaticStrings {
    fn check_call(
        &self,
        file: &FileModel,
        call: &CallExpression,
        property: &Property,
        custom_static_flags: &[String],
        violations: &mut Vec<Violation>,
    ) {
        if call.arguments.len() < 4 {
            return;
        }

        let nick_is_literal = property.nick.is_some();
        let blurb_is_literal = property.blurb.is_some();

        let has_static_strings = property.flags.contains(&ParamFlag::StaticStrings);
        let has_static_name = property.flags.contains(&ParamFlag::StaticName);
        let has_static_nick = property.flags.contains(&ParamFlag::StaticNick);
        let has_static_blurb = property.flags.contains(&ParamFlag::StaticBlurb);

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

        let is_satisfied = if has_static_strings || has_custom_static_flag {
            true
        } else if nick_is_literal && blurb_is_literal {
            has_static_name && has_static_nick && has_static_blurb
        } else if nick_is_literal {
            has_static_name && has_static_nick
        } else if blurb_is_literal {
            has_static_name && has_static_blurb
        } else {
            has_static_name
        };

        if is_satisfied {
            return;
        }

        let needed = self.needed_flags(nick_is_literal, blurb_is_literal);
        let new_flags = self.build_fixed_flags(&property.flags, &needed);
        let needed_desc = needed
            .iter()
            .map(ParamFlag::as_str)
            .collect::<Vec<_>>()
            .join(" | ");

        let flags_expr = call.arguments.last().unwrap();
        let fix = Fix::new(
            flags_expr.location().start_byte,
            flags_expr.location().end_byte,
            new_flags,
        );

        violations.push(self.violation_with_fix_at(
            &file.path,
            &call.location,
            format!(
                "Add {} to {} flags (saves memory for static strings)",
                needed_desc,
                call.function_name()
            ),
            fix,
        ));
    }

    fn needed_flags(&self, nick_is_literal: bool, blurb_is_literal: bool) -> Vec<ParamFlag> {
        match (nick_is_literal, blurb_is_literal) {
            (true, true) => vec![ParamFlag::StaticStrings],
            (true, false) => vec![ParamFlag::StaticName, ParamFlag::StaticNick],
            (false, true) => vec![ParamFlag::StaticName, ParamFlag::StaticBlurb],
            (false, false) => vec![ParamFlag::StaticName],
        }
    }

    fn build_fixed_flags(&self, current_flags: &[ParamFlag], needed_flags: &[ParamFlag]) -> String {
        let mut new_flags: Vec<ParamFlag> = current_flags
            .iter()
            .filter(|f| {
                !matches!(
                    f,
                    ParamFlag::StaticName
                        | ParamFlag::StaticNick
                        | ParamFlag::StaticBlurb
                        | ParamFlag::StaticStrings
                )
            })
            .cloned()
            .collect();

        new_flags.extend_from_slice(needed_flags);

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