Skip to main content

codehelion_core/discovery/
generated.rs

1//! Detection of machine-generated files.
2//!
3//! Generated files are excluded before any clone candidate is produced: they
4//! duplicate their generator's templates, not hand-written logic, so reporting
5//! them as clones is noise. Detection is a marker scan over the file's first
6//! lines; the marker set is configurable so a project can add its own
7//! generator's banner.
8
9/// Markers recognised by default in a generated file's header, in lower case.
10///
11/// Chosen against the banners real generators write rather than against the one
12/// convention that has a written rule. `@generated` and `do not edit` between
13/// them cover the code generators that follow a convention; the two spellings
14/// of *automatically generated* cover the binding generators, which follow
15/// none and which are what makes a foreign-function crate mostly machine
16/// output.
17///
18/// What is deliberately absent is `code generated by`, which reads as a banner
19/// but is also how a hand-written file says its neighbour is generated —
20/// "utilities used by code generated by the derive macro" and the like. Every
21/// generator that writes it also writes *do not edit* on the same line, so
22/// dropping it loses nothing and stops the phrase from hiding source that
23/// merely talks about generated code.
24pub const DEFAULT_MARKERS: &[&str] = &[
25    "@generated",
26    "do not edit",
27    "automatically generated",
28    "auto-generated",
29    "autogenerated",
30];
31
32/// Number of leading lines scanned for a marker by default.
33///
34/// A banner is a header: a generator writes it before anything else, at most
35/// under a licence notice. Reading further does not find more banners, it finds
36/// prose — a doc comment explaining what the module does for generated code —
37/// and hides a hand-written file for talking about the subject.
38pub const DEFAULT_SCAN_LINES: usize = 10;
39
40/// A configured set of generated-file markers.
41///
42/// Markers are held folded to lower case and matched that way. A generator
43/// picks its own capitalisation, and the same banner arrives shouted from one
44/// tool and in sentence case from the next.
45#[derive(Debug, Clone)]
46pub struct GeneratedMarkers {
47    markers: Vec<String>,
48    scan_lines: usize,
49}
50
51impl Default for GeneratedMarkers {
52    fn default() -> Self {
53        let markers: Vec<String> = DEFAULT_MARKERS.iter().map(|m| (*m).to_string()).collect();
54        Self::new(&markers, DEFAULT_SCAN_LINES)
55    }
56}
57
58impl GeneratedMarkers {
59    /// Build a marker set from explicit markers and a scan depth.
60    #[must_use]
61    pub fn new(markers: &[String], scan_lines: usize) -> Self {
62        Self {
63            markers: markers.iter().map(|m| m.to_lowercase()).collect(),
64            scan_lines,
65        }
66    }
67
68    /// The default markers plus `extra`, keeping the default scan depth.
69    #[must_use]
70    pub fn with_additional(extra: &[String]) -> Self {
71        let mut markers: Vec<String> = DEFAULT_MARKERS.iter().map(|m| (*m).to_string()).collect();
72        markers.extend(extra.iter().cloned());
73        Self::new(&markers, DEFAULT_SCAN_LINES)
74    }
75
76    /// Whether the head of a file marks it as generated.
77    ///
78    /// `head` should be the file's leading text; only the first configured
79    /// number of lines is inspected.
80    #[must_use]
81    pub fn is_generated(&self, head: &str) -> bool {
82        head.lines().take(self.scan_lines).any(|line| {
83            let line = line.to_lowercase();
84            self.markers.iter().any(|marker| line.contains(marker))
85        })
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    /// Banners taken verbatim from published crates, one per generator family:
94    /// the two that follow a convention and the binding generators that write
95    /// whatever their author liked.
96    #[test]
97    fn default_markers_match_the_banners_generators_write() {
98        let markers = GeneratedMarkers::default();
99        assert!(markers.is_generated("// This file is @generated by prost-build."));
100        assert!(markers.is_generated("// Code generated by protoc. DO NOT EDIT."));
101        assert!(markers.is_generated("/* automatically generated by rust-bindgen 0.72.1 */"));
102        assert!(
103            markers.is_generated("// DO NOT EDIT THIS FILE. IT WAS AUTOMATICALLY GENERATED BY:")
104        );
105        assert!(
106            markers.is_generated("//! Generated by `cargo codegen grammar`, do not edit by hand.")
107        );
108        assert!(markers.is_generated("// --- Autogenerated from scripts/autogen.py ---"));
109        assert!(markers.is_generated("// auto-generated by: jiff-cli generate crc32"));
110    }
111
112    #[test]
113    fn hand_written_files_are_not_generated() {
114        let markers = GeneratedMarkers::default();
115        assert!(!markers.is_generated("//! A normal module.\nfn real() {}"));
116    }
117
118    /// A file that talks about generated code is not generated code. The
119    /// phrase turns up in doc comments on the modules that support a derive
120    /// macro, which are hand-written and are exactly where duplication between
121    /// a macro's helpers is worth reporting.
122    #[test]
123    fn prose_about_generated_code_is_not_a_banner() {
124        let markers = GeneratedMarkers::default();
125        assert!(!markers.is_generated(
126            "//! Utilities used by macros and by the derive crate.\n\
127             //!\n\
128             //! These are defined here rather than in code generated by macros\n\
129             //! so that they can be compiled once.\n"
130        ));
131    }
132
133    #[test]
134    fn markers_past_the_scan_window_are_ignored() {
135        let markers = GeneratedMarkers::new(&["@generated".to_string()], 2);
136        let head = "line1\nline2\n// @generated\n";
137        assert!(!markers.is_generated(head));
138    }
139
140    #[test]
141    fn additional_markers_extend_the_defaults() {
142        let markers = GeneratedMarkers::with_additional(&["AUTOGENERATED".to_string()]);
143        assert!(markers.is_generated("# AUTOGENERATED FILE\n"));
144        assert!(markers.is_generated("// @generated\n"));
145    }
146}