use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Component, Path};
const GENERATED_CONTENT_LINES: usize = 5;
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: bool,
) -> bool {
cached_hint || 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 {
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)
}
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"
));
}
}