codehelion-core 0.1.0

Engine and intermediate representation for the codehelion source-audit tool.
Documentation
//! Detection of machine-generated files.
//!
//! Generated files are excluded before any clone candidate is produced: they
//! duplicate their generator's templates, not hand-written logic, so reporting
//! them as clones is noise. Detection is a marker scan over the file's first
//! lines; the marker set is configurable so a project can add its own
//! generator's banner.

/// Markers recognised by default in a generated file's header, in lower case.
///
/// Chosen against the banners real generators write rather than against the one
/// convention that has a written rule. `@generated` and `do not edit` between
/// them cover the code generators that follow a convention; the two spellings
/// of *automatically generated* cover the binding generators, which follow
/// none and which are what makes a foreign-function crate mostly machine
/// output.
///
/// What is deliberately absent is `code generated by`, which reads as a banner
/// but is also how a hand-written file says its neighbour is generated —
/// "utilities used by code generated by the derive macro" and the like. Every
/// generator that writes it also writes *do not edit* on the same line, so
/// dropping it loses nothing and stops the phrase from hiding source that
/// merely talks about generated code.
pub const DEFAULT_MARKERS: &[&str] = &[
    "@generated",
    "do not edit",
    "automatically generated",
    "auto-generated",
    "autogenerated",
];

/// Number of leading lines scanned for a marker by default.
///
/// A banner is a header: a generator writes it before anything else, at most
/// under a licence notice. Reading further does not find more banners, it finds
/// prose — a doc comment explaining what the module does for generated code —
/// and hides a hand-written file for talking about the subject.
pub const DEFAULT_SCAN_LINES: usize = 10;

/// A configured set of generated-file markers.
///
/// Markers are held folded to lower case and matched that way. A generator
/// picks its own capitalisation, and the same banner arrives shouted from one
/// tool and in sentence case from the next.
#[derive(Debug, Clone)]
pub struct GeneratedMarkers {
    markers: Vec<String>,
    scan_lines: usize,
}

impl Default for GeneratedMarkers {
    fn default() -> Self {
        let markers: Vec<String> = DEFAULT_MARKERS.iter().map(|m| (*m).to_string()).collect();
        Self::new(&markers, DEFAULT_SCAN_LINES)
    }
}

impl GeneratedMarkers {
    /// Build a marker set from explicit markers and a scan depth.
    #[must_use]
    pub fn new(markers: &[String], scan_lines: usize) -> Self {
        Self {
            markers: markers.iter().map(|m| m.to_lowercase()).collect(),
            scan_lines,
        }
    }

    /// The default markers plus `extra`, keeping the default scan depth.
    #[must_use]
    pub fn with_additional(extra: &[String]) -> Self {
        let mut markers: Vec<String> = DEFAULT_MARKERS.iter().map(|m| (*m).to_string()).collect();
        markers.extend(extra.iter().cloned());
        Self::new(&markers, DEFAULT_SCAN_LINES)
    }

    /// Whether the head of a file marks it as generated.
    ///
    /// `head` should be the file's leading text; only the first configured
    /// number of lines is inspected.
    #[must_use]
    pub fn is_generated(&self, head: &str) -> bool {
        head.lines().take(self.scan_lines).any(|line| {
            let line = line.to_lowercase();
            self.markers.iter().any(|marker| line.contains(marker))
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Banners taken verbatim from published crates, one per generator family:
    /// the two that follow a convention and the binding generators that write
    /// whatever their author liked.
    #[test]
    fn default_markers_match_the_banners_generators_write() {
        let markers = GeneratedMarkers::default();
        assert!(markers.is_generated("// This file is @generated by prost-build."));
        assert!(markers.is_generated("// Code generated by protoc. DO NOT EDIT."));
        assert!(markers.is_generated("/* automatically generated by rust-bindgen 0.72.1 */"));
        assert!(
            markers.is_generated("// DO NOT EDIT THIS FILE. IT WAS AUTOMATICALLY GENERATED BY:")
        );
        assert!(
            markers.is_generated("//! Generated by `cargo codegen grammar`, do not edit by hand.")
        );
        assert!(markers.is_generated("// --- Autogenerated from scripts/autogen.py ---"));
        assert!(markers.is_generated("// auto-generated by: jiff-cli generate crc32"));
    }

    #[test]
    fn hand_written_files_are_not_generated() {
        let markers = GeneratedMarkers::default();
        assert!(!markers.is_generated("//! A normal module.\nfn real() {}"));
    }

    /// A file that talks about generated code is not generated code. The
    /// phrase turns up in doc comments on the modules that support a derive
    /// macro, which are hand-written and are exactly where duplication between
    /// a macro's helpers is worth reporting.
    #[test]
    fn prose_about_generated_code_is_not_a_banner() {
        let markers = GeneratedMarkers::default();
        assert!(!markers.is_generated(
            "//! Utilities used by macros and by the derive crate.\n\
             //!\n\
             //! These are defined here rather than in code generated by macros\n\
             //! so that they can be compiled once.\n"
        ));
    }

    #[test]
    fn markers_past_the_scan_window_are_ignored() {
        let markers = GeneratedMarkers::new(&["@generated".to_string()], 2);
        let head = "line1\nline2\n// @generated\n";
        assert!(!markers.is_generated(head));
    }

    #[test]
    fn additional_markers_extend_the_defaults() {
        let markers = GeneratedMarkers::with_additional(&["AUTOGENERATED".to_string()]);
        assert!(markers.is_generated("# AUTOGENERATED FILE\n"));
        assert!(markers.is_generated("// @generated\n"));
    }
}