pub(crate) mod format;
mod css;
mod csv;
mod dotenv;
mod fallback;
mod heuristics;
mod html;
mod javascript;
mod js;
mod json;
mod position;
mod schemes;
mod toml;
mod yaml;
#[cfg(test)]
pub(crate) mod corpus;
#[cfg(test)]
mod fuzz;
pub(crate) use format::{FileType, determine_file_type};
pub(crate) use heuristics::PathType;
pub(crate) use position::Position;
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct Path {
pub(crate) value: String,
#[serde(rename = "type")]
pub(crate) kind: PathType,
#[serde(flatten)]
pub(crate) position: Position,
pub(crate) context: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum ErrorCategory {
Parsing,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum Severity {
Info,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct ExtractionError {
pub(crate) category: ErrorCategory,
pub(crate) severity: Severity,
pub(crate) message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct Extraction {
pub(crate) success: bool,
pub(crate) paths: Vec<Path>,
pub(crate) errors: Vec<ExtractionError>,
}
impl Extraction {
fn found(paths: Vec<Path>) -> Self {
Self {
success: true,
paths,
errors: Vec::new(),
}
}
fn failed(category: ErrorCategory, severity: Severity, message: String) -> Self {
Self {
success: false,
paths: Vec::new(),
errors: vec![ExtractionError {
category,
severity,
message,
}],
}
}
}
pub(crate) type Extracted = Result<Vec<Path>, String>;
pub(crate) fn extract(content: &str, language_id: &str) -> Extraction {
match extract_by_file_type(content, determine_file_type(language_id)) {
Ok(paths) => Extraction::found(paths),
Err(message) => Extraction::failed(ErrorCategory::Parsing, Severity::Error, message),
}
}
fn extract_by_file_type(content: &str, file_type: FileType) -> Extracted {
match file_type {
FileType::Csv => Ok(csv::extract(content)),
FileType::Toml => Ok(toml::extract(content)),
FileType::Dotenv => dotenv::extract(content),
FileType::Javascript | FileType::Typescript => javascript::extract(content),
FileType::Json => Ok(json::extract(content)),
FileType::Css => css::extract(content),
FileType::Html => html::extract(content),
FileType::Yaml => Ok(yaml::extract(content)),
FileType::Unknown => Ok(fallback::extract(content)),
}
}
pub(crate) fn is_generic_scan(language_id: &str) -> bool {
determine_file_type(language_id) == FileType::Unknown
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_pattern_that_exhausts_its_budget_refuses_rather_than_answering() {
let document = format!("import {} from\n", "x".repeat(400)).repeat(150);
let result = extract(&document, "javascript");
assert!(!result.success, "a spent budget must not read as success");
assert!(
result.paths.is_empty(),
"a refusal carries no paths: {:?}",
result.paths
);
assert_eq!(result.errors.len(), 1);
assert_eq!(result.errors[0].category, ErrorCategory::Parsing);
assert_eq!(result.errors[0].severity, Severity::Error);
assert!(
result.errors[0].message.contains("gave up"),
"the message should say the pattern gave up: {}",
result.errors[0].message
);
}
#[test]
fn an_unsupported_language_is_scanned_rather_than_refused() {
let result = extract("shutil.copy(\"./src/a.py\", DEST)", "python");
assert!(result.success);
assert!(result.errors.is_empty());
assert_eq!(result.paths.len(), 1);
assert_eq!(result.paths[0].value, "./src/a.py");
assert_eq!(result.paths[0].context, "Text scan");
}
#[test]
fn nothing_still_says_extraction_is_unsupported() {
for language in ["python", "rust", "markdown", "unknown"] {
let result = extract("x = 1", language);
assert!(
result.errors.is_empty(),
"{language} produced {:?}",
result.errors
);
}
}
#[test]
fn an_empty_document_succeeds_with_nothing() {
for language in [
"json",
"toml",
"csv",
"dotenv",
"javascript",
"css",
"html",
"yaml",
"unknown",
] {
let result = extract("", language);
assert!(result.success, "{language}");
assert!(result.paths.is_empty(), "{language}");
assert!(result.errors.is_empty(), "{language}");
}
}
#[test]
fn every_supported_language_id_dispatches() {
for language in [
"csv",
"toml",
"dotenv",
"env",
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"json",
"jsonc",
"html",
"css",
"scss",
"less",
"yaml",
] {
assert!(extract("", language).success, "{language}");
}
}
#[test]
fn only_the_untyped_languages_are_a_generic_scan() {
for language in [
"json",
"toml",
"csv",
"dotenv",
"javascript",
"css",
"html",
"yaml",
] {
assert!(!is_generic_scan(language), "{language}");
}
for language in ["python", "markdown", "xml", "unknown"] {
assert!(is_generic_scan(language), "{language}");
}
}
}