gobject-linter 0.1.2

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

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

pub struct GErrorInit;

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

    fn description(&self) -> &'static str {
        "Ensure GError* variables are initialized to NULL"
    }

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

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

    fn check_func_impl(
        &self,
        _ast_context: &AstContext,
        _config: &Config,
        func: &FunctionDefItem,
        file: &FileModel,
        violations: &mut Vec<Violation>,
    ) {
        self.check_block(&func.body_statements, file, violations);
    }
}

impl GErrorInit {
    fn check_block(&self, stmts: &[Statement], file: &FileModel, violations: &mut Vec<Violation>) {
        for (i, stmt) in stmts.iter().enumerate() {
            if let Statement::Declaration(decl) = stmt {
                self.check_declaration(decl, &stmts[i + 1..], file, violations);
            }
            stmt.for_each_child_block(|block| {
                self.check_block(block, file, violations);
            });
        }
    }

    fn check_declaration(
        &self,
        decl: &VariableDecl,
        following: &[Statement],
        file: &FileModel,
        violations: &mut Vec<Violation>,
    ) {
        if !decl.type_info.is_base_type("GError") || !decl.type_info.is_pointer() {
            return;
        }

        let is_initialized_to_null = match &decl.initializer {
            None => false,
            Some(expr) if expr.is_null() || expr.is_zero() => true,
            Some(_) => return,
        };

        if is_initialized_to_null {
            return;
        }

        if self.first_use_is_assignment(&decl.name, following) {
            return;
        }

        let insert_pos = decl.location.end_byte - 1;

        let fix = Fix::new(insert_pos, insert_pos, " = NULL".to_string());

        violations.push(self.violation_with_fix_at(
            &file.path,
            &decl.location,
            format!("GError *{} must be initialized to NULL", decl.name),
            fix,
        ));
    }

    fn first_use_is_assignment(&self, var: &str, stmts: &[Statement]) -> bool {
        for stmt in stmts {
            let mut references_var = false;
            stmt.visit_expressions(&mut |expr| {
                if expr.contains_identifier(var) {
                    references_var = true;
                }
            });
            if !references_var {
                continue;
            }
            return stmt.is_assignment_to(var, |_| true);
        }
        true
    }
}