use std::path::{Path, PathBuf};
use std::{fs, io::BufRead, io::BufReader};
use tokmd_types::FileRow;
use super::FileKind;
pub(super) fn classify_file(root: &Path, path: &str, row: &FileRow) -> FileKind {
if looks_generated_dir(path) || has_generated_sentinel(root, path) {
FileKind::Generated
} else if looks_vendored_dir(path) {
FileKind::Vendored
} else if looks_test_path(path) {
FileKind::Tests
} else if looks_doc_path(path, row) {
FileKind::Docs
} else if looks_api_path(path) {
FileKind::Api
} else if looks_ffi_path(path) {
FileKind::Ffi
} else if looks_ui_path(path) {
FileKind::Ui
} else if looks_data_path(path) {
FileKind::Data
} else if looks_build_path(path) {
FileKind::Build
} else if looks_infra_path(row) {
FileKind::Infra
} else {
FileKind::Core
}
}
pub(in crate::effort) fn tag_name(kind: &FileKind) -> &str {
match kind {
FileKind::Core => "core",
FileKind::Infra => "infra",
FileKind::Build => "build",
FileKind::Docs => "docs",
FileKind::Tests => "tests",
FileKind::Generated => "generated",
FileKind::Vendored => "vendored",
FileKind::Api => "api",
FileKind::Ffi => "ffi",
FileKind::Ui => "ui",
FileKind::Data => "data",
}
}
fn has_host_root(root: &Path) -> bool {
!root.as_os_str().is_empty()
}
fn has_generated_sentinel(root: &Path, path: &str) -> bool {
if !has_host_root(root) {
return false;
}
let full = root.join(PathBuf::from(path));
let file = match fs::File::open(&full) {
Ok(f) => f,
Err(_) => return false,
};
let reader = BufReader::new(file);
for line in reader.lines().take(40).flatten() {
if is_generated_sentinel(&line) {
return true;
}
}
false
}
fn is_generated_sentinel(text: &str) -> bool {
let lower = text.to_lowercase();
lower.contains("generated by")
|| lower.contains("do not edit")
|| lower.contains("auto-generated")
|| lower.contains("autogenerated")
}
fn looks_generated_dir(path: &str) -> bool {
let lower = path.to_lowercase();
lower.contains("/generated/")
|| lower.contains("/dist/")
|| lower.contains("/build/")
|| lower.contains("/target/")
|| lower.ends_with(".min.js")
|| lower.ends_with(".map")
}
fn looks_vendored_dir(path: &str) -> bool {
let lower = path.to_lowercase();
lower.contains("/vendor/")
|| lower.contains("/third_party/")
|| lower.contains("/node_modules/")
|| lower.contains("/deps/")
}
fn looks_test_path(path: &str) -> bool {
let lower = path.to_lowercase();
lower.contains("/test/")
|| lower.contains("/tests/")
|| lower.contains("__tests__")
|| lower.contains("/spec/")
|| lower.contains("/specs/")
|| lower.ends_with("_test.rs")
}
fn looks_doc_path(path: &str, row: &FileRow) -> bool {
let lower = path.to_lowercase();
row.lang.to_lowercase() == "markdown"
|| lower.contains("/docs/")
|| lower.ends_with("readme.md")
}
fn looks_api_path(path: &str) -> bool {
let lower = path.to_lowercase();
lower.contains("/api/") || lower.ends_with(".proto") || lower.ends_with(".openapi.json")
}
fn looks_ffi_path(path: &str) -> bool {
let lower = path.to_lowercase();
lower.contains("/ffi/") || lower.contains("/bindings/") || lower.ends_with("_ffi.rs")
}
fn looks_ui_path(path: &str) -> bool {
let lower = path.to_lowercase();
lower.contains("/ui/") || lower.contains("/web/") || lower.contains("/frontend/")
}
fn looks_data_path(path: &str) -> bool {
let lower = path.to_lowercase();
lower.contains("/data/") || lower.contains("/resources/") || lower.contains("/assets/")
}
fn looks_build_path(path: &str) -> bool {
let lower = path.to_lowercase();
lower.ends_with(".yml")
|| lower.ends_with(".yaml")
|| lower.ends_with(".toml")
|| lower.contains("/ci/")
|| lower.contains("/.github/")
}
fn looks_infra_path(row: &FileRow) -> bool {
tokmd_analysis_types::is_infra_lang(&row.lang)
}
#[cfg(test)]
mod tests {
use super::*;
fn row(path: &str, lang: &str) -> FileRow {
FileRow {
path: path.to_string(),
module: String::new(),
lang: lang.to_string(),
kind: tokmd_types::FileKind::Parent,
code: 1,
comments: 0,
blanks: 0,
lines: 1,
bytes: 1,
tokens: 1,
}
}
#[test]
fn classifies_path_heuristics_before_infra_language_fallback() {
assert!(matches!(
classify_file(
Path::new(""),
"docs/README.md",
&row("docs/README.md", "TOML")
),
FileKind::Docs
));
assert!(matches!(
classify_file(
Path::new(""),
"src/api/schema.proto",
&row("src/api/schema.proto", "Text")
),
FileKind::Api
));
assert!(matches!(
classify_file(
Path::new(""),
"src/web/app.ts",
&row("src/web/app.ts", "TypeScript")
),
FileKind::Ui
));
}
#[test]
fn empty_root_does_not_read_generated_sentinels() {
assert!(!has_generated_sentinel(Path::new(""), "src/generated.rs"));
}
#[test]
fn tag_names_match_receipt_tags() {
assert_eq!(tag_name(&FileKind::Core), "core");
assert_eq!(tag_name(&FileKind::Generated), "generated");
assert_eq!(tag_name(&FileKind::Vendored), "vendored");
assert_eq!(tag_name(&FileKind::Ffi), "ffi");
}
}