gobject-linter 0.1.2

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

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

pub struct UseGBytesUnrefToData;

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

    fn description(&self) -> &'static str {
        "Use g_bytes_unref_to_data() instead of g_bytes_get_data() + g_bytes_unref()"
    }

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

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

    fn min_glib_version(&self) -> Option<(u32, u32)> {
        Some((2, 32))
    }

    fn check_func_impl(
        &self,
        _ast_context: &AstContext,
        config: &Config,
        func: &FunctionDefItem,
        file: &FileModel,
        violations: &mut Vec<Violation>,
    ) {
        Statement::walk_pairs(&func.body_statements, &mut |stmt1, stmt2| {
            self.try_bytes_pattern(stmt1, stmt2, file, config, violations);
        });
    }
}

impl UseGBytesUnrefToData {
    /// Try to match: dest = g_bytes_get_data(bytes, ...); g_bytes_unref(bytes);
    fn try_bytes_pattern(
        &self,
        stmt1: &Statement,
        stmt2: &Statement,
        file: &FileModel,
        config: &Config,
        violations: &mut Vec<Violation>,
    ) {
        // First statement: dest = g_bytes_get_data(bytes, &size)
        let Some((dest, bytes_var, size_arg, assignment, _call1)) =
            self.extract_bytes_get_data(stmt1)
        else {
            return;
        };

        // Second statement: g_bytes_unref(bytes)
        if self.extract_bytes_unref(stmt2, bytes_var).is_none() {
            return;
        };

        // Build the replacement
        let call = config
            .style
            .format_call("g_bytes_unref_to_data", &[bytes_var, size_arg]);
        let replacement = format!("{} = {};", dest, call);
        let message = format!(
            "Use g_bytes_unref_to_data({}, {}) instead of g_bytes_get_data() followed by g_bytes_unref()",
            bytes_var, size_arg
        );
        // Use two separate fixes to preserve comments between statements
        let stmt1_end = stmt1.location().find_semicolon_end();
        let fixes = vec![
            // Replace the first statement with the new call
            Fix::new(stmt1.location().start_byte, stmt1_end, replacement),
            // Delete the entire second line
            Fix::delete_line(stmt2.location()),
        ];

        violations.push(self.violation_with_fixes_at(
            &file.path,
            &assignment.location,
            message,
            fixes,
        ));
    }

    /// Extract components from: dest = g_bytes_get_data(bytes, size_arg)
    /// Returns (dest_text, bytes_var, size_arg, assignment, call)
    fn extract_bytes_get_data<'a>(
        &self,
        stmt: &'a Statement,
    ) -> Option<(
        &'a str,
        &'a str,
        &'a str,
        &'a Assignment,
        &'a CallExpression,
    )> {
        let Statement::Expression(expr_stmt) = stmt else {
            return None;
        };
        let Expression::Assignment(assignment) = expr_stmt.as_ref() else {
            return None;
        };

        // Check operator is "="
        if assignment.operator != AssignmentOp::Assign {
            return None;
        }

        // Right side should be a call to g_bytes_get_data
        let Expression::Call(call) = assignment.rhs.as_ref() else {
            return None;
        };

        if !call.is_function("g_bytes_get_data") {
            return None;
        }

        // Need exactly 2 arguments
        if call.arguments.len() != 2 {
            return None;
        }

        // Extract argument text from source
        let bytes_var = call.get_arg_text(0)?;
        let size_arg = call.get_arg_text(1)?;

        let dest_var = assignment.lhs_as_text();
        if dest_var.is_empty() {
            return None;
        }

        Some((dest_var, bytes_var, size_arg, assignment, call))
    }

    /// Extract call from: g_bytes_unref(expected_var)
    /// Returns the end_byte of the statement (including semicolon)
    fn extract_bytes_unref(&self, stmt: &Statement, expected_var: &str) -> Option<usize> {
        let Statement::Expression(expr_stmt) = stmt else {
            return None;
        };

        let Expression::Call(call) = expr_stmt.as_ref() else {
            return None;
        };

        if !call.is_function("g_bytes_unref") {
            return None;
        }

        // Need exactly 1 argument
        if call.arguments.len() != 1 {
            return None;
        }

        // Check argument matches expected variable
        let arg_text = call.get_arg_text(0)?;
        if arg_text != expected_var {
            return None;
        }

        Some(expr_stmt.location().find_semicolon_end())
    }
}