omnicoach 0.1.0

Lightweight, high-signal static analysis and command linter for Python and Rust
Documentation
//! Flags private functions and methods defined before their private callers.

use crate::code_lint::ast::{self, ParsedFile};
use crate::code_lint::contract::{CodeRule, RuleTarget};
use crate::diagnostic::{Diagnostic, Language, RuleName, ViolationTemplate, violation_template};
use crate::rule_declaration::{
    Classification, Consensus, Declaration, Example, ImpactedQuality, Precision, Reference,
    RuleDoc, RuleOptions, Topic,
};
use std::path::Path;

const TEMPLATE: ViolationTemplate = violation_template! {
    summary: "Private callee `{function}` is defined before its private caller `{caller}`.",
    rationale: "Defining lower-abstraction private helpers above `{caller}` inverts the top-down reading flow.",
    suggestion: "Move `{function}` below `{caller}` so higher-level callers precede lower-level callees.",
};

/// The rule's declaration.
pub const RULE: CodeRule = CodeRule {
    declaration: Declaration {
        name: RuleName("callee-before-caller"),
        template: &TEMPLATE,
        languages: &[Language::Python, Language::Rust],
        options: RuleOptions::code_rule(()),
        classification: Classification {
            topics: &[Topic::DECLARATION_ORDER],
            precision: Precision::Exact,
            consensus: Consensus::Opinionated,
            impacted_quality: ImpactedQuality::Maintainability,
        },
        doc: RuleDoc {
            summary: "Flags private functions and methods defined before their private callers.",
            what_it_does: indoc::indoc! {r"
                Flags a private function or method declared above a private function that calls
                it, in a module, Python `class` or Rust inherent `impl` block: `_parse` above
                `_load` when `_load` calls `_parse`. Calls between public functions and recursive
                calls are not constrained, and a function already flagged by
                `private-before-public-function` or `uncolocated-helper` is not reported again."},
            why_is_this_bad: indoc::indoc! {r"
                Unlike public entrypoints, a private helper exists solely as an internal
                decomposition step of its callers. Defining private callees above their private
                callers forces readers to encounter low-level leaf utilities before the
                higher-level helper logic that gives them context.

                Order private helpers from higher-level callers down to lower-level callees."},
            known_problems: Some(indoc::indoc! {r"
                - Only functions are ordered: a private `struct`, `class` or constant can sit above
                  the private function that uses it.
                - In Rust macro arguments, a pattern binding or named argument that shares a sibling
                  function's name counts as a call, which can create a false ordering constraint."}),
            references: &[
                Reference {
                    title: "Robert C. Martin: Clean Code — Chapter 5: Formatting (The Stepdown Rule)",
                    url: "https://www.oreilly.com/library/view/clean-code-a/9780136083238/",
                },
                Reference {
                    title: "Soloway & Ehrlich (1984): Empirical Studies of Programming Knowledge",
                    url: "https://ieeexplore.ieee.org/document/5010283",
                },
            ],
            examples: &[
                Example {
                    language: Language::Python,
                    flagged: indoc::indoc! {r"
                        def parse_document(raw: str) -> list[str]:
                            return _split_lines(raw)

                        def _trim_line(line: str) -> str:
                            return line.strip()

                        def _split_lines(raw: str) -> list[str]:
                            return [_trim_line(line) for line in raw.splitlines()]
                    "},
                    flagged_span: "_trim_line",
                    fixed: indoc::indoc! {r"
                        def parse_document(raw: str) -> list[str]:
                            return _split_lines(raw)

                        def _split_lines(raw: str) -> list[str]:
                            return [_trim_line(line) for line in raw.splitlines()]

                        def _trim_line(line: str) -> str:
                            return line.strip()
                    "},
                },
                Example {
                    language: Language::Rust,
                    flagged: indoc::indoc! {r"
                        pub fn parse_config(raw: &str) -> Vec<&str> {
                            parse_lines(raw)
                        }

                        fn strip_line(line: &str) -> &str {
                            line.trim()
                        }

                        fn parse_lines(raw: &str) -> Vec<&str> {
                            raw.lines().map(strip_line).collect()
                        }
                    "},
                    flagged_span: "strip_line",
                    fixed: indoc::indoc! {r"
                        pub fn parse_config(raw: &str) -> Vec<&str> {
                            parse_lines(raw)
                        }

                        fn parse_lines(raw: &str) -> Vec<&str> {
                            raw.lines().map(strip_line).collect()
                        }

                        fn strip_line(line: &str) -> &str {
                            line.trim()
                        }
                    "},
                },
            ],
        },
    },
    target: RuleTarget::SourceOnly,
    check: check_file,
};

fn check_file(rule: &CodeRule, path: &Path, file: &ParsedFile, (): ()) -> Vec<Diagnostic> {
    ast::collect_call_cluster_findings(file)
        .callee_before_caller
        .iter()
        .map(|finding| {
            rule.diagnostic_at_node(
                path,
                &finding.name_node,
                &[("function", &finding.function), ("caller", &finding.caller)],
            )
        })
        .collect()
}

#[cfg(test)]
crate::test_utils::rule_test!(
    RULE,
    {
        Python => {
            pass: [
                top_down_caller_before_callee_across_tiers => r#"
                    def compile_all(raw: str) -> str:
                        return compile_single(raw)

                    def compile_single(raw: str) -> str:
                        return _parse_tokens(raw)

                    def _parse_tokens(raw: str) -> str:
                        return _strip_whitespace(raw)

                    def _strip_whitespace(raw: str) -> str:
                        return raw.strip()
                "#,
                public_callee_before_public_caller_exempt => r#"
                    def parse_item(raw: str) -> str:
                        return raw.strip()

                    def parse_batch(items: list[str]) -> list[str]:
                        return [parse_item(item) for item in items]
                "#,
                mutual_recursion_exempt => r#"
                    def _even(number: int) -> bool:
                        return True if number == 0 else _odd(number - 1)

                    def _odd(number: int) -> bool:
                        return False if number == 0 else _even(number - 1)
                "#,
                function_flagged_by_earlier_stage_not_reported_again => r#"
                    class Pipeline:
                        def _flagged_by_stage_one(self, raw: str) -> str:
                            return raw.strip()

                        def run(self, raw: str) -> str:
                            return self._caller_below(raw)

                        def _caller_below(self, raw: str) -> str:
                            return self._flagged_by_stage_one(raw)
                "#,
                local_binding_shadowing_exempt => r#"
                    def evaluate(number: int) -> bool:
                        return _check(number) and _validate(number)

                    def _check(number: int) -> bool:
                        return number > 0

                    def _validate(number: int) -> bool:
                        _check = lambda value: value < 10
                        return _check(number)
                "#,
            ],
            fail: [
                exclusive_helper_callee_before_caller => r#"
                    def run_pipeline(raw: str) -> str:
                        return _step_one(raw)

                    def _step_two(raw: str) -> str:
                        return raw.strip()

                    def _step_one(raw: str) -> str:
                        return _step_two(raw)
                "# => "_step_two",
                class_private_callee_before_private_caller => r#"
                    class Decoder:
                        def decode(self, raw: str) -> str:
                            return self._decode_frame(raw)

                        def _parse_byte(self, raw: str) -> str:
                            return raw.strip()

                        def _decode_frame(self, raw: str) -> str:
                            return self._parse_byte(raw)
                "# => "_parse_byte",
            ],
        },
        Rust => {
            pass: [
                top_down_impl_order => r#"
                    pub struct Compiler;

                    impl Compiler {
                        pub fn new() -> Self {
                            Self
                        }

                        pub fn rebuild(&self) -> Self {
                            Self::new()
                        }

                        pub fn compile(&self, src: &str) -> usize {
                            self.lower_ast(src)
                        }

                        fn lower_ast(&self, src: &str) -> usize {
                            Self::count_bytes(src)
                        }

                        fn count_bytes(src: &str) -> usize {
                            src.len()
                        }
                    }
                "#,
                mutual_recursion_exempt => r#"
                    pub fn evaluate(n: usize) -> bool {
                        is_even(n)
                    }

                    fn is_even(n: usize) -> bool {
                        if n == 0 { true } else { is_odd(n - 1) }
                    }

                    fn is_odd(n: usize) -> bool {
                        if n == 0 { false } else { is_even(n - 1) }
                    }
                "#,
                local_binding_shadowing_exempt => r#"
                    pub fn evaluate(n: usize) -> bool {
                        check(n) && validate(n)
                    }

                    fn check(n: usize) -> bool {
                        n > 0
                    }

                    fn validate(n: usize) -> bool {
                        let check = |value: usize| value < 10;
                        check(n)
                    }
                "#,
                trait_impl_exempt => r#"
                    pub trait Codec {
                        fn finish(&self) -> usize;
                        fn encode(&self) -> usize;
                    }

                    pub struct Frame;

                    impl Codec for Frame {
                        fn finish(&self) -> usize {
                            0
                        }

                        fn encode(&self) -> usize {
                            self.finish()
                        }
                    }
                "#,
                macro_field_access_is_not_a_method_call => r#"
                    pub struct Buffer {
                        len: usize,
                    }

                    impl Buffer {
                        pub fn describe(&self) -> String {
                            self.render() + &self.len().to_string()
                        }

                        fn len(&self) -> usize {
                            self.len
                        }

                        fn render(&self) -> String {
                            format!("{}", self.len)
                        }
                    }
                "#,
            ],
            fail: [
                private_callee_before_private_caller_in_module => r#"
                    fn leaf_helper(input: &str) -> &str {
                        input.trim()
                    }

                    fn middle_helper(input: &str) -> usize {
                        leaf_helper(input).len()
                    }
                "# => "leaf_helper",
                call_inside_macro_counts => r#"
                    fn is_blank(input: &str) -> bool {
                        input.trim().is_empty()
                    }

                    fn validate(input: &str) {
                        assert!(!is_blank(input));
                    }
                "# => "is_blank",
                impl_private_callee_before_private_caller => r#"
                    pub struct Lexer;

                    impl Lexer {
                        pub fn tokenize(&self, input: &str) -> usize {
                            self.scan_tokens(input)
                        }

                        fn is_delimiter(ch: char) -> bool {
                            ch.is_whitespace()
                        }

                        fn scan_tokens(&self, input: &str) -> usize {
                            input.chars().filter(|&ch| Self::is_delimiter(ch)).count()
                        }
                    }
                "# => "is_delimiter",
            ],
        },
    }
);