use serde::Serialize;
use serde_json::{Value, json};
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
const SOURCE_DIRECTORIES: &[&str] = &[
"api",
"app",
"client",
"cmd",
"functions",
"include",
"internal",
"lib",
"pkg",
"server",
"sources",
"src",
];
const PACKAGE_PARENTS: &[&str] = &["apps", "crates", "packages", "services", "workspaces"];
const MANIFESTS: &[&str] = &[
"Cargo.toml",
"Package.swift",
"Gemfile",
"build.gradle",
"build.gradle.kts",
"composer.json",
"go.mod",
"package.json",
"pom.xml",
"pyproject.toml",
"setup.py",
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
Included,
Excluded,
Ambiguous,
}
#[derive(Clone, Debug, Serialize)]
pub struct Entry {
pub path: String,
pub status: Status,
pub reason: String,
pub package_root: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct Scope {
pub mode: &'static str,
pub roots: Vec<String>,
pub entries: Vec<Entry>,
}
impl Scope {
pub fn included(&self) -> impl Iterator<Item = &Entry> {
self.entries
.iter()
.filter(|entry| entry.status == Status::Included)
}
pub fn ambiguous(&self) -> usize {
self.entries
.iter()
.filter(|entry| entry.status == Status::Ambiguous)
.count()
}
pub fn summary(&self) -> Value {
let mut reasons: BTreeMap<&str, usize> = BTreeMap::new();
for entry in &self.entries {
if entry.status != Status::Included {
*reasons.entry(entry.reason.as_str()).or_default() += 1;
}
}
json!({
"mode": self.mode,
"roots": self.roots,
"included": self.included().count(),
"ambiguous": self.ambiguous(),
"excluded": self.entries.iter()
.filter(|e| e.status == Status::Excluded).count(),
"reasons": reasons,
})
}
pub fn limitation(&self) -> Option<String> {
let ambiguous = self.ambiguous();
(ambiguous > 0).then(|| format!(
"{ambiguous} source files sit under no recognised source root and were not assessed. \
Read them with `supercov quality scope`, and declare your roots with \
SUPERCOV_SOURCE_ROOTS=src,app if they are first-party code."
))
}
}
fn declared_entry_points(directory: &Path) -> Vec<PathBuf> {
let mut targets: Vec<String> = Vec::new();
fn strings(value: &Value, depth: usize, out: &mut Vec<String>) {
if depth > 4 {
return;
}
match value {
Value::String(text) => out.push(text.clone()),
Value::Array(items) => items.iter().for_each(|v| strings(v, depth + 1, out)),
Value::Object(fields) => fields.values().for_each(|v| strings(v, depth + 1, out)),
_ => {}
}
}
if let Ok(text) = std::fs::read_to_string(directory.join("package.json"))
&& let Ok(manifest) = serde_json::from_str::<Value>(&text)
{
for key in ["main", "module", "browser", "bin", "exports"] {
if let Some(value) = manifest.get(key) {
strings(value, 0, &mut targets);
}
}
}
if let Ok(text) = std::fs::read_to_string(directory.join("Cargo.toml")) {
for line in text.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("path")
&& let Some(value) = rest.split('"').nth(1)
{
targets.push(value.to_owned());
}
}
if directory.join("build.rs").is_file() {
targets.push("build.rs".to_owned());
}
}
targets
.into_iter()
.filter(|target| {
!target
.split('/')
.any(|segment| super::ignored_directory(segment.trim_start_matches("./")))
})
.filter_map(|target| {
let prefix = target.split('*').next()?.trim_end_matches('/');
let prefix = prefix.strip_prefix("./").unwrap_or(prefix);
if prefix.is_empty() || prefix.starts_with('/') || prefix.starts_with("..") {
return None;
}
let path = directory.join(prefix);
if path.is_dir() {
return Some(path);
}
if !path.is_file() {
return None;
}
let parent = path.parent()?;
Some(if parent == directory {
path
} else {
parent.to_owned()
})
})
.collect()
}
fn variable_manifest(directory: &Path) -> Option<bool> {
let mut project_file = false;
for entry in std::fs::read_dir(directory).ok()?.flatten() {
let name = entry.file_name().to_string_lossy().to_ascii_lowercase();
if name.ends_with(".gradle") || name.ends_with(".gradle.kts") {
return Some(false);
}
if name.ends_with(".csproj") || name.ends_with(".fsproj") || name.ends_with(".vbproj") {
project_file = true;
}
}
project_file.then_some(true)
}
pub fn declares_a_package(path: &str) -> bool {
let name = path.rsplit('/').next().unwrap_or(path);
let lower = name.to_ascii_lowercase();
MANIFESTS.contains(&name)
|| lower.ends_with(".gradle")
|| lower.ends_with(".gradle.kts")
|| lower.ends_with(".csproj")
|| lower.ends_with(".sln")
}
fn manifest_in(directory: &Path) -> Option<bool> {
if MANIFESTS.iter().any(|name| directory.join(name).is_file()) {
return Some(false);
}
variable_manifest(directory)
}
fn declared_members(root: &Path) -> BTreeSet<PathBuf> {
let mut found = BTreeSet::new();
let mut add = |pattern: &str| {
match pattern.strip_suffix("/*") {
Some(parent) => {
if let Ok(entries) = std::fs::read_dir(root.join(parent)) {
for entry in entries.flatten() {
if entry.path().is_dir() {
found.insert(entry.path());
}
}
}
}
None => {
let path = root.join(pattern);
if path.is_dir() {
found.insert(path);
}
}
}
};
if let Ok(text) = std::fs::read_to_string(root.join("package.json"))
&& let Ok(manifest) = serde_json::from_str::<Value>(&text)
{
let listed = manifest["workspaces"]
.as_array()
.cloned()
.or_else(|| manifest["workspaces"]["packages"].as_array().cloned())
.unwrap_or_default();
for pattern in listed.iter().filter_map(|v| v.as_str()) {
add(pattern);
}
}
if let Ok(text) = std::fs::read_to_string(root.join("Cargo.toml")) {
let mut inside = false;
for line in text.lines() {
let line = line.trim();
if line.starts_with('[') {
inside = line == "[workspace]";
continue;
}
if inside && line.starts_with("members") {
inside = true;
}
if inside && let Some(start) = line.find('"') {
let rest = &line[start + 1..];
if let Some(end) = rest.find('"') {
add(&rest[..end]);
}
}
}
}
found
}
fn package_roots(root: &Path) -> BTreeSet<PathBuf> {
fn visit(root: &Path, directory: &Path, depth: usize, found: &mut BTreeSet<PathBuf>) {
if depth > 5 {
return;
}
let Ok(entries) = std::fs::read_dir(directory) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name().to_string_lossy().into_owned();
if !path.is_dir() || name.starts_with('.') || super::ignored_directory_at(&path) {
continue;
}
if path.join(".git").exists() {
continue;
}
let relative = path.strip_prefix(root).unwrap_or(&path);
if super::skipped_path(&relative.to_string_lossy().replace('\\', "/")).is_some() {
continue;
}
let under_parent = relative
.components()
.any(|c| PACKAGE_PARENTS.contains(&c.as_os_str().to_string_lossy().as_ref()));
match manifest_in(&path) {
Some(true) => {
found.insert(path.clone());
}
Some(false) if depth == 0 || under_parent => {
found.insert(path.clone());
}
_ => {}
}
visit(root, &path, depth + 1, found);
}
}
let mut found = BTreeSet::from([root.to_owned()]);
visit(root, root, 0, &mut found);
found.extend(declared_members(root));
found
}
fn beside_the_product(file: &str) -> Option<&'static str> {
let lower = file.to_ascii_lowercase();
let (directories, name) = match lower.rsplit_once('/') {
Some((head, name)) => (head, name),
None => ("", lower.as_str()),
};
for segment in directories.split('/') {
match segment {
"scripts" => return Some("tool script"),
"examples" | "example" => return Some("example"),
"benches" | "benchmarks" => return Some("benchmark"),
"docs" | "doc" | "documentation" => return Some("documentation"),
_ => {}
}
}
if name
.split('.')
.skip(1)
.any(|part| matches!(part, "bench" | "benchmark"))
{
return Some("benchmark");
}
let parts: Vec<&str> = name.split('.').collect();
if parts.len() > 2 && parts.contains(&"config") {
return Some("build or tool configuration");
}
if name.starts_with('.')
&& parts
.get(1)
.is_some_and(|tool| tool.len() > 3 && tool.ends_with("rc"))
{
return Some("build or tool configuration");
}
None
}
const SERVED_DIRECTORIES: &[&str] = &[
"assets",
"bower_components",
"lib",
"libs",
"plugins",
"public",
"static",
"third-party",
"third_party",
"vendor",
"vendors",
"www",
"wwwroot",
];
const LIBRARIES: &[&str] = &[
"ace",
"angular",
"axios",
"backbone",
"bootstrap",
"chart",
"ckeditor",
"codemirror",
"d3",
"datatables",
"excanvas",
"flot",
"fontawesome",
"handlebars",
"highlight",
"html5shiv",
"jqvmap",
"jquery",
"knockout",
"lodash",
"mapael",
"modernizr",
"moment",
"morris",
"mustache",
"pdfmake",
"popper",
"prism",
"raphael",
"redoc",
"require",
"respond",
"select2",
"slick",
"socket.io",
"sparkline",
"summernote",
"sweetalert",
"swagger-ui",
"swiper",
"tinymce",
"toastr",
"underscore",
"vfs_fonts",
"zepto",
];
fn third_party(path: &Path, file: &str) -> Option<&'static str> {
let lower = file.to_ascii_lowercase();
let (directories, name) = lower.rsplit_once('/').unwrap_or(("", lower.as_str()));
if ![".js", ".mjs", ".cjs"]
.iter()
.any(|ext| name.ends_with(ext))
{
return None;
}
let mut head = Vec::new();
std::io::Read::read_to_end(
&mut std::io::Read::take(std::fs::File::open(path).ok()?, 64 << 10),
&mut head,
)
.ok()?;
let head = String::from_utf8_lossy(&head);
let long: usize = head.lines().map(str::len).filter(|&n| n > 500).sum();
if head.len() > 2000 && long * 2 > head.len() {
return Some("minified code");
}
if !directories
.split('/')
.any(|segment| SERVED_DIRECTORIES.contains(&segment))
{
return None;
}
let banner = head
.char_indices()
.take_while(|(index, _)| *index < 2048)
.last()
.map_or("", |(index, c)| &head[..index + c.len_utf8()]);
let licensed = banner.contains("/*!")
|| banner.contains("@license")
|| banner.contains("Licensed under the MIT")
|| banner.contains("Licensed under MIT")
|| banner.contains("Licensed under the Apache");
let named = LIBRARIES.iter().any(|library| {
name.match_indices(library).any(|(at, _)| {
let before = name[..at].chars().last();
let after = name[at + library.len()..].chars().next();
before.is_none_or(|c| matches!(c, '.' | '-' | '_'))
&& after.is_some_and(|c| matches!(c, '.' | '-' | '_'))
})
});
(licensed || named).then_some("vendored third-party library")
}
fn conventional_directories(package: &Path) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(package) else {
return Vec::new();
};
let mut found = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let name = entry.file_name().to_string_lossy().to_ascii_lowercase();
if super::is_test_directory(&name) {
continue;
}
if SOURCE_DIRECTORIES.contains(&name.as_str()) || path.join("__init__.py").is_file() {
found.push(path);
}
}
found
}
fn go_modules(packages: &BTreeSet<PathBuf>) -> Vec<PathBuf> {
packages
.iter()
.filter(|package| package.join("go.mod").is_file())
.cloned()
.collect()
}
fn relative(root: &Path, path: &Path) -> String {
path.strip_prefix(root)
.unwrap_or(path)
.to_string_lossy()
.replace('\\', "/")
}
pub fn classify(root: &Path, files: &[PathBuf], configured: Option<&[String]>) -> Scope {
let explicit = configured.is_some_and(|roots| !roots.is_empty());
let roots: BTreeSet<PathBuf> = if explicit {
configured
.unwrap_or_default()
.iter()
.map(|name| root.join(name.trim()))
.filter(|path| path.exists())
.collect()
} else {
let packages = package_roots(root);
let mut roots = BTreeSet::new();
for package in &packages {
if package != root && variable_manifest(package) == Some(true) {
roots.insert(package.clone());
continue;
}
let mut candidates: Vec<PathBuf> = conventional_directories(package);
candidates.extend(declared_entry_points(package));
if candidates.is_empty() && package != root {
roots.insert(package.clone());
} else {
roots.extend(candidates);
}
}
roots
};
let packages = if explicit {
BTreeSet::new()
} else {
package_roots(root)
};
let modules = if explicit {
Vec::new()
} else {
go_modules(&packages)
};
let nearest = |path: &Path| -> Option<String> {
packages
.iter()
.filter(|package| path.starts_with(package) && *package != root)
.max_by_key(|package| package.components().count())
.map(|package| relative(root, package))
};
let mut entries = Vec::new();
for path in files {
let file = relative(root, path);
let entry = |status, reason: &str| Entry {
path: file.clone(),
status,
reason: reason.to_owned(),
package_root: nearest(path),
};
if let Some(skipped) = super::skipped_path(&file) {
entries.push(entry(Status::Excluded, skipped.reason()));
} else if let Some(reason) = beside_the_product(&file) {
entries.push(entry(Status::Excluded, reason));
} else if let Some(reason) = third_party(path, &file) {
entries.push(entry(Status::Excluded, reason));
} else if roots.iter().any(|dir| path.starts_with(dir))
|| (path.extension().is_some_and(|e| e == "go")
&& modules.iter().any(|module| path.starts_with(module)))
{
entries.push(entry(
Status::Included,
if explicit {
"explicit source root"
} else {
"discovered package source root"
},
));
} else if explicit {
entries.push(entry(Status::Excluded, "outside explicit source roots"));
} else {
entries.push(entry(Status::Ambiguous, "unclassified first-party source"));
}
}
entries.sort_by(|a, b| a.path.cmp(&b.path));
Scope {
mode: if explicit { "explicit" } else { "automatic" },
roots: roots.iter().map(|path| relative(root, path)).collect(),
entries,
}
}
pub fn configured_roots() -> Option<Vec<String>> {
let value = std::env::var("SUPERCOV_SOURCE_ROOTS").ok()?;
let roots: Vec<String> = value
.split(',')
.map(|part| part.trim().to_owned())
.filter(|part| !part.is_empty())
.collect();
(!roots.is_empty()).then_some(roots)
}