#[cfg(test)]
use std::collections::BTreeMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
#[cfg(test)]
use std::path::PathBuf;
use std::path::{Component, Path};
#[cfg(test)]
use std::sync::{Mutex, OnceLock};
const GENERATED_CONTENT_LINES: usize = 5;
#[cfg(test)]
static FILE_PROBES: OnceLock<Mutex<BTreeMap<PathBuf, usize>>> = OnceLock::new();
pub(crate) fn is_generated_file(project_root: &Path, path: &Path) -> bool {
if path_has_generated_shape(path) {
return true;
}
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
project_root.join(path)
};
if path_has_generated_shape(&absolute) {
return true;
}
first_lines_have_generated_marker(&absolute)
}
pub(crate) fn is_generated_file_from_source(path: &Path, source: &str) -> bool {
path_has_generated_shape(path) || source_has_generated_marker(source)
}
pub(crate) fn is_generated_file_with_cached_hint(
project_root: &Path,
relative_file: &str,
cached_hint: Option<bool>,
) -> bool {
cached_hint.unwrap_or_else(|| is_generated_file(project_root, Path::new(relative_file)))
}
pub(crate) fn path_has_generated_shape(path: &Path) -> bool {
if path.components().any(is_generated_segment) {
return true;
}
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
return false;
};
let file_name = file_name.to_ascii_lowercase();
file_name.ends_with("_pb.ts")
|| file_name.ends_with("_pb.js")
|| file_name.ends_with("_pb.d.ts")
|| file_name.contains(".generated.")
}
fn is_generated_segment(component: Component<'_>) -> bool {
let Component::Normal(segment) = component else {
return false;
};
matches!(
segment.to_str().map(str::to_ascii_lowercase).as_deref(),
Some("gen" | "generated" | "__generated__")
)
}
fn first_lines_have_generated_marker(path: &Path) -> bool {
#[cfg(test)]
bump_file_probe_count(path);
let Ok(file) = File::open(path) else {
return false;
};
let reader = BufReader::new(file);
let mut prefix = String::new();
for line in reader.lines().take(GENERATED_CONTENT_LINES) {
let Ok(line) = line else {
break;
};
prefix.push_str(&line);
prefix.push('\n');
}
source_has_generated_marker(&prefix)
}
#[cfg(test)]
fn debug_file_probes() -> &'static Mutex<BTreeMap<PathBuf, usize>> {
FILE_PROBES.get_or_init(|| Mutex::new(BTreeMap::new()))
}
#[cfg(test)]
fn bump_file_probe_count(path: &Path) {
if let Ok(mut probes) = debug_file_probes().lock() {
*probes.entry(path.to_path_buf()).or_default() += 1;
}
}
#[cfg(test)]
#[doc(hidden)]
pub fn reset_file_probe_count_for_debug(project_root: &Path) {
let project_root =
std::fs::canonicalize(project_root).unwrap_or_else(|_| project_root.to_path_buf());
if let Ok(mut probes) = debug_file_probes().lock() {
probes.retain(|path, _| !path.starts_with(&project_root));
}
}
#[cfg(test)]
#[doc(hidden)]
pub fn file_probe_count_for_debug(project_root: &Path) -> usize {
let project_root =
std::fs::canonicalize(project_root).unwrap_or_else(|_| project_root.to_path_buf());
debug_file_probes()
.lock()
.map(|probes| {
probes
.iter()
.filter(|(path, _)| path.starts_with(&project_root))
.map(|(_, count)| *count)
.sum()
})
.unwrap_or_default()
}
fn source_has_generated_marker(source: &str) -> bool {
let prefix = source
.lines()
.take(GENERATED_CONTENT_LINES)
.collect::<Vec<_>>()
.join("\n")
.to_ascii_lowercase();
if prefix.contains("@generated")
|| prefix.contains("code generated by")
|| prefix.contains("do not edit")
{
return true;
}
prefix.contains("eslint-disable")
&& (prefix.contains("codegen")
|| prefix.contains("generated")
|| prefix.contains("auto-generated")
|| prefix.contains("automatically generated"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_shape_catches_conservative_generated_names() {
assert!(path_has_generated_shape(Path::new("src/gen/types.ts")));
assert!(path_has_generated_shape(Path::new(
"src/__generated__/api.ts"
)));
assert!(path_has_generated_shape(Path::new("src/foo_pb.ts")));
assert!(path_has_generated_shape(Path::new("src/foo_pb.d.ts")));
assert!(path_has_generated_shape(Path::new("src/foo.generated.ts")));
assert!(!path_has_generated_shape(Path::new("src/general.ts")));
}
#[test]
fn content_marker_only_checks_file_prefix() {
assert!(source_has_generated_marker(
"// Code generated by protoc-gen-es. DO NOT EDIT.\nexport const x = 1;"
));
assert!(source_has_generated_marker(
"/* eslint-disable */\n// emitted by codegen\nexport const x = 1;"
));
assert!(!source_has_generated_marker(
"export const handwritten = true;\n// 2\n// 3\n// 4\n// 5\n// DO NOT EDIT"
));
}
}