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}