use std::{
collections::{BTreeMap, BTreeSet},
fs, io,
path::{Component, Path, PathBuf},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
const GENERATED_DIRECTORIES: &[&str] = &[
".cache",
"generated",
".git",
".mcdc-pool",
".next",
".nuxt",
".output",
".supercov",
"build",
"coverage",
"dist",
"node_modules",
"out",
"playwright-report",
"results",
"test-results",
"vendor",
];
pub fn target_is_build_output(directory: &Path) -> bool {
const BUILDS: &[&str] = &[
"Cargo.toml",
"pom.xml",
"build.gradle",
"build.gradle.kts",
"settings.gradle",
"settings.gradle.kts",
"build.sbt",
"project.clj",
];
const MARKERS: &[&str] = &["CACHEDIR.TAG", ".rustc_info.json"];
directory
.parent()
.is_some_and(|parent| BUILDS.iter().any(|name| parent.join(name).is_file()))
|| MARKERS.iter().any(|name| directory.join(name).is_file())
}
const SOURCE_DIRECTORIES: &[&str] = &[
"app",
"src",
"lib",
"server",
"client",
"functions",
"api",
"components",
"pages",
];
const NEXT_ROOT_FILES: &[&str] = &[
"proxy",
"middleware",
"instrumentation",
"instrumentation-client",
];
const NEXT_APP_FILES: &[&str] = &[
"page",
"layout",
"route",
"loading",
"error",
"global-error",
"not-found",
"template",
"default",
"forbidden",
"unauthorized",
"icon",
"apple-icon",
"opengraph-image",
"twitter-image",
"sitemap",
"robots",
"manifest",
];
const PACKAGE_PARENTS: &[&str] = &["apps", "packages", "services", "workspaces"];
const TEST_DIRECTORIES: &[&str] = &[
"__tests__",
"test",
"tests",
"spec",
"specs",
"e2e",
"fixture",
"fixtures",
"mock",
"mocks",
"__mocks__",
];
const CONFIG_TOOLS: &[&str] = &[
"babel",
"eslint",
"graphql",
"jest",
"next",
"nuxt",
"playwright",
"postcss",
"prettier",
"remix",
"rollup",
"stylelint",
"tailwind",
"tsup",
"vite",
"vitest",
"webpack",
];
const DOT_CONFIG_TOOLS: &[&str] = &["babel", "eslint", "graphql", "prettier", "stylelint"];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SourceScopeStatus {
Included,
Excluded,
Ambiguous,
}
pub const BUILT_ASSET_REASON: &str = "built asset";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SourceScopeEntry {
pub file: String,
pub status: SourceScopeStatus,
pub reason: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub package_root: Option<String>,
}
impl SourceScopeEntry {
pub fn is_generated_output(&self) -> bool {
self.status == SourceScopeStatus::Excluded && self.reason == BUILT_ASSET_REASON
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SourceScopeMode {
Automatic,
Explicit,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SourceScope {
pub version: u32,
pub mode: SourceScopeMode,
pub roots: Vec<String>,
pub entries: Vec<SourceScopeEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SourceLimitation {
pub id: String,
pub kind: String,
pub file: String,
pub line: usize,
pub column: usize,
pub source: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DiscoveredSourceScope {
pub source_files: Vec<String>,
pub source_roots: Vec<String>,
pub scope: SourceScope,
pub limitations: Vec<SourceLimitation>,
}
#[derive(Debug)]
pub enum SourceDiscoveryError {
Io { path: PathBuf, source: io::Error },
NonUtf8Path(PathBuf),
InvalidRoot(PathBuf),
}
impl std::fmt::Display for SourceDiscoveryError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io { path, source } => write!(formatter, "{}: {source}", path.display()),
Self::NonUtf8Path(path) => {
write!(
formatter,
"source path is not valid UTF-8: {}",
path.display()
)
}
Self::InvalidRoot(path) => write!(
formatter,
"source root is not a regular file or directory: {}",
path.display()
),
}
}
}
impl std::error::Error for SourceDiscoveryError {}
fn io_error(path: &Path, source: io::Error) -> SourceDiscoveryError {
SourceDiscoveryError::Io {
path: path.to_owned(),
source,
}
}
pub const SOURCE_ROOTS_VARIABLE: &str = "SUPERCOV_SOURCE_ROOTS";
pub const OUTSIDE_EXPLICIT_ROOTS: &str = "outside explicit source roots";
pub fn configured_source_roots(environment: &BTreeMap<String, String>) -> Option<Vec<String>> {
let roots = environment
.get(SOURCE_ROOTS_VARIABLE)?
.split(',')
.map(str::trim)
.filter(|root| !root.is_empty())
.map(str::to_owned)
.collect::<Vec<_>>();
(!roots.is_empty()).then_some(roots)
}
pub fn fold_roots_into_configuration(configuration: &mut Vec<u8>, roots: Option<&[String]>) {
let Some(roots) = roots.filter(|roots| !roots.is_empty()) else {
return;
};
configuration.push(0);
configuration
.extend_from_slice(format!("{SOURCE_ROOTS_VARIABLE}={}", roots.join(",")).as_bytes());
}
#[derive(Debug, Clone)]
pub struct ExplicitSourceRoots {
root: PathBuf,
roots: Vec<PathBuf>,
configured: Vec<String>,
}
impl ExplicitSourceRoots {
pub fn resolve(root: &Path, configured: &[String]) -> Result<Self, SourceDiscoveryError> {
let root = lexical_normalize(root);
let roots = existing_roots(configured.iter().map(|value| resolve(&root, value)), true)?;
Ok(Self {
root,
roots,
configured: configured.to_vec(),
})
}
pub fn from_environment(
root: &Path,
environment: &BTreeMap<String, String>,
) -> Result<Option<Self>, SourceDiscoveryError> {
configured_source_roots(environment)
.map(|configured| Self::resolve(root, &configured))
.transpose()
}
pub fn from_environment_in(
project: &Path,
workspace: &Path,
environment: &BTreeMap<String, String>,
) -> Result<Option<Self>, SourceDiscoveryError> {
let Some(configured) = configured_source_roots(environment) else {
return Ok(None);
};
let project = lexical_normalize(project);
let rebased = configured
.iter()
.map(|value| {
let path = Path::new(value);
if !path.is_absolute() {
return value.clone();
}
lexical_normalize(path)
.strip_prefix(&project)
.map(|rest| {
let rest = rest.to_string_lossy().into_owned();
if rest.is_empty() { ".".into() } else { rest }
})
.unwrap_or_else(|_| value.clone())
})
.collect::<Vec<_>>();
let mut roots = Self::resolve(workspace, &rebased)?;
roots.configured = configured;
Ok(Some(roots))
}
pub fn contains(&self, relative: &str) -> bool {
root_contains(&self.roots, &lexical_normalize(&self.root.join(relative)))
}
pub fn configured(&self) -> &[String] {
&self.configured
}
pub fn refuse_if_empty(&self, remaining: usize, language: &str) -> Result<(), String> {
if remaining > 0 {
return Ok(());
}
let missing = self
.configured
.iter()
.filter(|value| fs::symlink_metadata(resolve(&self.root, value)).is_err())
.cloned()
.collect::<Vec<_>>();
let mut message = format!(
"{SOURCE_ROOTS_VARIABLE}={} left no {language} source to measure",
self.configured.join(",")
);
if !missing.is_empty() {
message.push_str(&format!("; {} does not exist", missing.join(", ")));
}
message.push_str("; name the directories or files that hold your own code");
Err(message)
}
pub fn scope(
&self,
included: &[String],
excluded: &[(String, &'static str)],
) -> Result<SourceScope, SourceDiscoveryError> {
let mut entries = included
.iter()
.map(|file| SourceScopeEntry {
file: file.clone(),
status: SourceScopeStatus::Included,
reason: "explicit source root".into(),
package_root: None,
})
.chain(
excluded
.iter()
.filter(|(_, reason)| *reason == OUTSIDE_EXPLICIT_ROOTS)
.map(|(file, reason)| SourceScopeEntry {
file: file.clone(),
status: SourceScopeStatus::Excluded,
reason: (*reason).into(),
package_root: None,
}),
)
.collect::<Vec<_>>();
entries.sort_by(|left, right| left.file.cmp(&right.file));
Ok(SourceScope {
version: 1,
mode: SourceScopeMode::Explicit,
roots: self.local_roots()?,
entries,
})
}
pub fn local_roots(&self) -> Result<Vec<String>, SourceDiscoveryError> {
self.roots
.iter()
.map(|path| local_path(&self.root, path))
.collect()
}
pub fn narrow<T>(
&self,
sources: &mut Vec<T>,
excluded: &mut Vec<(String, &'static str)>,
file: impl Fn(&T) -> &str,
) {
let (kept, outside): (Vec<T>, Vec<T>) = sources
.drain(..)
.partition(|source| self.contains(file(source)));
excluded.extend(
outside
.iter()
.map(|source| (file(source).to_owned(), OUTSIDE_EXPLICIT_ROOTS)),
);
*sources = kept;
}
}
fn existing_roots(
candidates: impl IntoIterator<Item = PathBuf>,
named: bool,
) -> Result<Vec<PathBuf>, SourceDiscoveryError> {
let mut existing = BTreeSet::new();
for path in candidates {
match fs::symlink_metadata(&path) {
Ok(metadata) if metadata.file_type().is_file() || metadata.file_type().is_dir() => {
existing.insert(path);
}
Ok(_) if !named => {}
Ok(_) => return Err(SourceDiscoveryError::InvalidRoot(path)),
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(io_error(&path, error)),
}
}
Ok(existing.into_iter().collect())
}
fn root_contains(roots: &[PathBuf], path: &Path) -> bool {
roots.iter().any(|directory| {
fs::symlink_metadata(directory)
.map(|metadata| {
if metadata.file_type().is_dir() {
within(directory, path)
} else {
directory == path
}
})
.unwrap_or(false)
})
}
fn lexical_normalize(path: &Path) -> PathBuf {
let mut output = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if !output.pop() {
output.push(component.as_os_str());
}
}
_ => output.push(component.as_os_str()),
}
}
output
}
fn resolve(root: &Path, value: impl AsRef<Path>) -> PathBuf {
let value = value.as_ref();
let joined;
let path = if value.is_absolute() {
value
} else {
joined = root.join(value);
&joined
};
lexical_normalize(path)
}
fn local_path(root: &Path, path: &Path) -> Result<String, SourceDiscoveryError> {
let local = path
.strip_prefix(root)
.map_err(|_| SourceDiscoveryError::InvalidRoot(path.to_owned()))?;
if local.as_os_str().is_empty() {
return Ok(".".into());
}
local
.components()
.map(|component| {
component
.as_os_str()
.to_str()
.map(str::to_owned)
.ok_or_else(|| SourceDiscoveryError::NonUtf8Path(path.to_owned()))
})
.collect::<Result<Vec<_>, _>>()
.map(|parts| parts.join("/"))
}
fn generated_directory(name: &str) -> bool {
GENERATED_DIRECTORIES.contains(&name)
}
fn owned_workspace_store(path: &Path) -> bool {
crate::workspace::owned_workspace_path(path)
}
fn nested_checkout(path: &Path) -> bool {
fs::symlink_metadata(path.join(".git")).is_ok()
}
fn built_asset(file: &str) -> bool {
let mut segments = file.rsplit('/');
let Some(name) = segments.next() else {
return false;
};
let in_asset_directory =
segments.any(|segment| matches!(segment, "assets" | "static" | "public"));
let Some(stem) = name
.strip_suffix(".js")
.or_else(|| name.strip_suffix(".mjs"))
.or_else(|| name.strip_suffix(".cjs"))
else {
return false;
};
if stem.len() < 10 || stem.as_bytes()[stem.len() - 9] != b'-' {
return false;
}
let hash = &stem[stem.len() - 8..];
let looks_hashed = hash
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
&& (hash.bytes().any(|byte| byte.is_ascii_digit())
|| (hash.bytes().any(|byte| byte.is_ascii_uppercase())
&& hash.bytes().any(|byte| byte.is_ascii_lowercase())));
in_asset_directory && looks_hashed
}
fn root_tool_directory(root: &Path, path: &Path) -> bool {
path.parent() == Some(root)
&& path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with('.'))
}
fn source_file(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
[
".js", ".jsx", ".ts", ".tsx", ".cjs", ".cjsx", ".cts", ".ctsx", ".mjs", ".mjsx", ".mts",
".mtsx",
]
.iter()
.any(|extension| lower.ends_with(extension))
}
fn declaration_file(file: &str) -> bool {
let lower = file.to_ascii_lowercase();
lower.ends_with(".d.ts") || lower.ends_with(".d.cts") || lower.ends_with(".d.mts")
}
fn test_or_fixture(file: &str) -> bool {
let lower = file.to_ascii_lowercase();
if lower
.split('/')
.any(|segment| TEST_DIRECTORIES.contains(&segment))
{
return true;
}
lower
.split(['/', '_', '.', '-'])
.any(|part| matches!(part, "test" | "spec"))
}
fn depends_on_next(manifest: &Value) -> bool {
["dependencies", "devDependencies", "peerDependencies"]
.iter()
.any(|field| {
manifest
.get(field)
.is_some_and(|all| all.get("next").is_some())
})
}
fn next_app_file(app_directories: &[PathBuf], path: &Path) -> bool {
let Some(local) = app_directories
.iter()
.find_map(|directory| path.strip_prefix(directory).ok())
else {
return false;
};
let Some(name) = local.file_name().and_then(|name| name.to_str()) else {
return false;
};
let conventional = name.split_once('.').is_some_and(|(stem, extension)| {
NEXT_APP_FILES.contains(&stem) && !extension.contains('.')
});
conventional
&& local.parent().is_none_or(|folders| {
folders
.components()
.all(|folder| !folder.as_os_str().to_string_lossy().starts_with('_'))
})
}
fn below_named_root<'a>(roots: &[PathBuf], path: &'a Path) -> Option<&'a Path> {
roots
.iter()
.filter_map(|root| path.strip_prefix(root).ok())
.min_by_key(|local| local.components().count())
}
fn benchmark(file: &str) -> bool {
let lower = file.to_ascii_lowercase();
let (directories, name) = lower.rsplit_once('/').unwrap_or(("", lower.as_str()));
directories
.split('/')
.any(|segment| matches!(segment, "bench" | "benches" | "benchmark" | "benchmarks"))
|| name
.split('.')
.skip(1)
.any(|part| matches!(part, "bench" | "benchmark"))
}
fn example(file: &str, package_root: Option<&str>) -> bool {
let local = package_root
.and_then(|package| file.strip_prefix(package))
.and_then(|rest| rest.strip_prefix('/'))
.unwrap_or(file);
local.split_once('/').is_some_and(|(first, _)| {
matches!(first.to_ascii_lowercase().as_str(), "example" | "examples")
})
}
fn tool_script(file: &str) -> bool {
file.to_ascii_lowercase()
.split('/')
.any(|segment| segment == "scripts")
}
fn config_file(file: &str) -> bool {
let lower = file.to_ascii_lowercase();
let name = lower.rsplit('/').next().unwrap_or(&lower);
source_file(name)
&& (CONFIG_TOOLS
.iter()
.any(|tool| name.starts_with(&format!("{tool}.config.")))
|| DOT_CONFIG_TOOLS
.iter()
.any(|tool| name.starts_with(&format!(".{tool}rc.")))
|| (!lower.contains('/')
&& (name.contains(".config.")
|| name.contains(".conf.")
|| name.starts_with("build.")
|| name.starts_with("gulpfile.")
|| name.starts_with("gruntfile."))))
}
fn read_directory(path: &Path) -> Result<Vec<fs::DirEntry>, SourceDiscoveryError> {
let mut entries = fs::read_dir(path)
.map_err(|error| io_error(path, error))?
.collect::<Result<Vec<_>, _>>()
.map_err(|error| io_error(path, error))?;
entries.sort_by_key(fs::DirEntry::file_name);
Ok(entries)
}
fn ignored_by_enclosing_repository(root: &Path) -> bool {
if root.join(".git").exists() {
return false;
}
let Some(repository) = root
.ancestors()
.skip(1)
.position(|ancestor| ancestor.join(".git").exists())
else {
return false;
};
root.ancestors()
.skip(1)
.take(repository + 1)
.collect::<Vec<_>>()
.into_iter()
.rev()
.fold(false, |ignored, directory| {
let (rules, _) = ignore::gitignore::Gitignore::new(directory.join(".gitignore"));
match rules.matched_path_or_any_parents(root, true) {
ignore::Match::Ignore(_) => true,
ignore::Match::Whitelist(_) => false,
ignore::Match::None => ignored,
}
})
}
fn files_under(
root: &Path,
directory: &Path,
output: &mut Vec<PathBuf>,
) -> Result<(), SourceDiscoveryError> {
let metadata = fs::symlink_metadata(directory).map_err(|error| io_error(directory, error))?;
if !metadata.file_type().is_dir() {
return Err(SourceDiscoveryError::InvalidRoot(directory.to_owned()));
}
let project = root.to_owned();
let walker = ignore::WalkBuilder::new(directory)
.standard_filters(false)
.git_ignore(true)
.parents(!ignored_by_enclosing_repository(directory))
.require_git(true)
.follow_links(false)
.sort_by_file_name(|left, right| left.cmp(right))
.filter_entry(move |entry| {
entry.depth() == 0
|| !entry.file_type().is_some_and(|kind| kind.is_dir())
|| entry.file_name().to_str().is_some_and(|name| {
!generated_directory(name)
&& !owned_workspace_store(entry.path())
&& !nested_checkout(entry.path())
&& !root_tool_directory(&project, entry.path())
})
})
.build();
for entry in walker {
let entry =
entry.map_err(|error| io_error(directory, io::Error::other(error.to_string())))?;
let Some(kind) = entry.file_type() else {
continue;
};
if kind.is_symlink() || !kind.is_file() {
continue;
}
let name = entry
.file_name()
.to_str()
.ok_or_else(|| SourceDiscoveryError::NonUtf8Path(entry.path().to_owned()))?;
if source_file(name) {
output.push(entry.into_path());
}
}
Ok(())
}
fn read_json(path: &Path) -> Option<Value> {
serde_json::from_slice(&fs::read(path).ok()?).ok()
}
fn package_directories(root: &Path) -> Result<Vec<PathBuf>, SourceDiscoveryError> {
fn visit(
root: &Path,
directory: &Path,
depth: usize,
found: &mut BTreeSet<PathBuf>,
) -> Result<(), SourceDiscoveryError> {
if depth > 5 {
return Ok(());
}
for entry in read_directory(directory)? {
let name = entry
.file_name()
.into_string()
.map_err(|name| SourceDiscoveryError::NonUtf8Path(directory.join(name)))?;
let path = entry.path();
let file_type = entry.file_type().map_err(|error| io_error(&path, error))?;
if !file_type.is_dir()
|| file_type.is_symlink()
|| generated_directory(&name)
|| owned_workspace_store(&path)
|| nested_checkout(&path)
|| root_tool_directory(root, &path)
{
continue;
}
let local = local_path(root, &path)?;
let under_package_parent = local
.split('/')
.any(|segment| PACKAGE_PARENTS.contains(&segment));
if path.join("package.json").is_file() && (depth == 0 || under_package_parent) {
found.insert(path.clone());
}
visit(root, &path, depth + 1, found)?;
}
Ok(())
}
let mut found = BTreeSet::from([root.to_owned()]);
visit(root, root, 0, &mut found)?;
for directory in declared_workspace_packages(root)? {
found.insert(directory);
}
Ok(found.into_iter().collect())
}
fn declared_workspace_packages(root: &Path) -> Result<Vec<PathBuf>, SourceDiscoveryError> {
let mut patterns = Vec::new();
let manifest = read_json(&root.join("package.json")).unwrap_or(Value::Null);
let declared = match manifest.get("workspaces") {
Some(Value::Array(items)) => Some(items),
Some(Value::Object(object)) => object.get("packages").and_then(Value::as_array),
_ => None,
};
if let Some(items) = declared {
patterns.extend(items.iter().filter_map(Value::as_str).map(str::to_owned));
}
if let Ok(pnpm) = fs::read_to_string(root.join("pnpm-workspace.yaml")) {
let mut in_packages = false;
for line in pnpm.lines() {
let trimmed = line.trim();
if trimmed.starts_with("packages:") {
in_packages = true;
continue;
}
if in_packages {
if let Some(item) = trimmed.strip_prefix("- ") {
patterns.push(item.trim_matches(|c| c == '"' || c == '\'').to_owned());
} else if !trimmed.is_empty() && !trimmed.starts_with('#') {
in_packages = false;
}
}
}
}
let mut found = Vec::new();
for pattern in patterns {
let pattern = pattern.trim_start_matches("./").trim_end_matches('/');
if pattern.starts_with('!') || pattern.contains("**") {
continue;
}
let (parent, wildcard) = match pattern.strip_suffix("/*") {
Some(parent) => (parent, true),
None => (pattern, false),
};
if parent.is_empty()
|| parent
.split('/')
.any(|segment| segment.is_empty() || segment == ".." || segment.contains('*'))
{
continue;
}
let base = root.join(parent);
let candidates: Vec<PathBuf> = if wildcard {
match fs::read_dir(&base) {
Ok(entries) => entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.collect(),
Err(_) => Vec::new(),
}
} else {
vec![base]
};
for candidate in candidates {
let is_dir = fs::symlink_metadata(&candidate)
.is_ok_and(|metadata| metadata.file_type().is_dir());
if is_dir
&& candidate.join("package.json").is_file()
&& !nested_checkout(&candidate)
&& !owned_workspace_store(&candidate)
{
found.push(candidate);
}
}
}
Ok(found)
}
fn string_targets(value: &Value, depth: usize, output: &mut Vec<String>) {
if depth > 8 {
return;
}
match value {
Value::String(value) => output.push(value.clone()),
Value::Array(values) => {
for value in values {
string_targets(value, depth + 1, output);
}
}
Value::Object(values) => {
for value in values.values() {
string_targets(value, depth + 1, output);
}
}
_ => {}
}
}
fn entry_targets(directory: &Path, manifest: &Value) -> Vec<PathBuf> {
let mut targets = Vec::new();
for key in ["main", "module", "browser", "bin", "exports"] {
if let Some(value) = manifest.get(key) {
string_targets(value, 0, &mut targets);
}
}
targets
.into_iter()
.filter_map(|target| {
if !target.starts_with('.') || target.contains("node_modules") {
return None;
}
let prefix = target.split('*').next()?.trim_end_matches('/');
(!prefix.is_empty()).then(|| resolve(directory, prefix))
})
.collect()
}
pub(crate) fn strip_jsonc_comments(contents: &str) -> String {
let mut output = String::with_capacity(contents.len());
let mut chars = contents.chars().peekable();
let mut string = false;
let mut escaped = false;
while let Some(character) = chars.next() {
if string {
output.push(character);
if escaped {
escaped = false;
} else if character == '\\' {
escaped = true;
} else if character == '"' {
string = false;
}
continue;
}
if character == '"' {
string = true;
output.push(character);
} else if character == '/' && chars.peek() == Some(&'/') {
chars.next();
for comment in chars.by_ref() {
if comment == '\n' {
output.push('\n');
break;
}
}
} else if character == '/' && chars.peek() == Some(&'*') {
chars.next();
let mut previous = '\0';
for comment in chars.by_ref() {
if previous == '*' && comment == '/' {
break;
}
previous = comment;
}
} else {
output.push(character);
}
}
output
}
pub(crate) fn strip_trailing_commas(contents: &str) -> String {
let chars = contents.chars().collect::<Vec<_>>();
let mut output = String::with_capacity(contents.len());
let mut string = false;
let mut escaped = false;
for (index, character) in chars.iter().copied().enumerate() {
if string {
output.push(character);
if escaped {
escaped = false;
} else if character == '\\' {
escaped = true;
} else if character == '"' {
string = false;
}
continue;
}
if character == '"' {
string = true;
output.push(character);
} else if character == ','
&& chars[index + 1..]
.iter()
.find(|character| !character.is_whitespace())
.is_some_and(|character| matches!(character, '}' | ']'))
{
} else {
output.push(character);
}
}
output
}
fn tsconfig_roots(directory: &Path) -> Vec<PathBuf> {
let path = directory.join("tsconfig.json");
let Some(contents) = fs::read_to_string(path).ok() else {
return Vec::new();
};
let jsonc = strip_trailing_commas(&strip_jsonc_comments(&contents));
let Some(config) = serde_json::from_str::<Value>(&jsonc).ok() else {
return Vec::new();
};
let mut values = Vec::new();
if let Some(root_dir) = config
.get("compilerOptions")
.and_then(|options| options.get("rootDir"))
.and_then(Value::as_str)
{
values.push(root_dir.to_owned());
}
if let Some(include) = config.get("include").and_then(Value::as_array) {
values.extend(include.iter().filter_map(Value::as_str).map(str::to_owned));
}
if values.is_empty() {
return vec![directory.to_owned()];
}
values
.into_iter()
.filter_map(|value| {
if value.starts_with('!') {
return None;
}
let prefix = value
.find(['?', '*', '{', '['])
.map_or(value.as_str(), |index| &value[..index])
.trim_end_matches('/');
(!prefix.is_empty() && !declaration_file(prefix)).then(|| resolve(directory, prefix))
})
.collect()
}
fn within(parent: &Path, child: &Path) -> bool {
child == parent || child.starts_with(parent)
}
fn nearest_package_root<'a>(path: &Path, packages: &'a [PathBuf]) -> Option<&'a PathBuf> {
packages
.iter()
.filter(|directory| within(directory, path))
.max_by_key(|directory| directory.components().count())
}
fn scope_limitation(file: &str) -> SourceLimitation {
let digest = Sha256::digest(file.as_bytes());
let id = digest[..10]
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
SourceLimitation {
id: format!("scope:{id}"),
kind: "source-scope".into(),
file: file.into(),
line: 1,
column: 1,
source: file.into(),
reason: "First-party JavaScript/TypeScript source could not be classified automatically. Configure SUPERCOV_SOURCE_ROOTS or move it under a discovered package source root.".into(),
}
}
fn declared_test_setup(packages: &[PathBuf]) -> BTreeSet<PathBuf> {
let mut paths = BTreeSet::new();
for package in packages {
let manifest = read_json(&package.join("package.json")).unwrap_or(Value::Null);
let Some(jest) = manifest.get("jest") else {
continue;
};
let root = jest
.get("rootDir")
.and_then(Value::as_str)
.map(|value| {
resolve(
package,
value.replace("<rootDir>", &package.to_string_lossy()),
)
})
.unwrap_or_else(|| package.clone());
for field in [
"setupFiles",
"setupFilesAfterEnv",
"globalSetup",
"globalTeardown",
] {
let values = match jest.get(field) {
Some(Value::Array(values)) => values.iter().collect::<Vec<_>>(),
Some(value) => vec![value],
None => Vec::new(),
};
for value in values.into_iter().filter_map(Value::as_str) {
let path = resolve(&root, value.replace("<rootDir>", &root.to_string_lossy()));
if path.is_file() {
paths.insert(path);
}
}
}
}
paths
}
pub fn discover_source_scope(
root: &Path,
configured_roots: Option<&[String]>,
) -> Result<DiscoveredSourceScope, SourceDiscoveryError> {
let root = lexical_normalize(root);
let root_metadata = fs::symlink_metadata(&root).map_err(|error| io_error(&root, error))?;
if !root_metadata.file_type().is_dir() {
return Err(SourceDiscoveryError::InvalidRoot(root));
}
let packages = package_directories(&root)?;
let test_setup = declared_test_setup(&packages);
let explicit = configured_roots.is_some_and(|roots| !roots.is_empty());
let include_roots = if explicit {
configured_roots
.unwrap_or_default()
.iter()
.map(|directory| resolve(&root, directory))
.collect::<Vec<_>>()
} else {
packages
.iter()
.flat_map(|directory| {
let manifest = read_json(&directory.join("package.json")).unwrap_or(Value::Null);
let next_files = depends_on_next(&manifest)
.then_some(NEXT_ROOT_FILES)
.into_iter()
.flatten()
.flat_map(|name| {
["ts", "tsx", "js", "jsx", "mjs"]
.map(|extension| directory.join(format!("{name}.{extension}")))
});
let candidates = SOURCE_DIRECTORIES
.iter()
.map(|name| directory.join(name))
.chain(next_files)
.chain(entry_targets(directory, &manifest))
.chain(tsconfig_roots(directory))
.collect::<Vec<_>>();
if directory != &root
&& !candidates
.iter()
.any(|candidate| fs::symlink_metadata(candidate).is_ok())
{
return vec![directory.clone()];
}
candidates
})
.collect()
};
let existing_roots = existing_roots(include_roots, explicit)?;
let app_directories = packages
.iter()
.filter(|directory| {
depends_on_next(&read_json(&directory.join("package.json")).unwrap_or(Value::Null))
})
.flat_map(|directory| [directory.join("app"), directory.join("src/app")])
.collect::<Vec<_>>();
let mut all_files = Vec::new();
files_under(&root, &root, &mut all_files)?;
all_files.sort();
let mut entries = Vec::new();
let mut included = Vec::new();
let mut limitations = Vec::new();
for path in all_files {
let file = local_path(&root, &path)?;
let package_root = nearest_package_root(&path, &packages)
.map(|package| local_path(&root, package))
.transpose()?
.filter(|package| package != ".");
let entry = |status, reason: &str| SourceScopeEntry {
file: file.clone(),
status,
reason: reason.into(),
package_root: package_root.clone(),
};
let named = explicit
.then(|| below_named_root(&existing_roots, &path))
.flatten();
let guessed = match named {
Some(local) => local.to_string_lossy().replace('\\', "/"),
None => file.clone(),
};
if declaration_file(&file) {
entries.push(entry(SourceScopeStatus::Excluded, "TypeScript declaration"));
} else if test_setup.contains(&path) {
entries.push(entry(SourceScopeStatus::Excluded, "declared test setup"));
} else if test_or_fixture(&guessed) && !next_app_file(&app_directories, &path) {
entries.push(entry(SourceScopeStatus::Excluded, "test or fixture source"));
} else if tool_script(&guessed) {
entries.push(entry(
SourceScopeStatus::Excluded,
"conventional tool script",
));
} else if benchmark(&guessed) {
entries.push(entry(SourceScopeStatus::Excluded, "benchmark"));
} else if example(&file, package_root.as_deref()) {
entries.push(entry(SourceScopeStatus::Excluded, "example"));
} else if config_file(&file) {
entries.push(entry(
SourceScopeStatus::Excluded,
"build/test/tool configuration",
));
} else if built_asset(&file) {
entries.push(entry(SourceScopeStatus::Excluded, BUILT_ASSET_REASON));
} else if root_contains(&existing_roots, &path) {
included.push(path);
entries.push(entry(
SourceScopeStatus::Included,
if explicit {
"explicit source root"
} else {
"discovered package source root"
},
));
} else if explicit {
entries.push(entry(
SourceScopeStatus::Excluded,
"outside explicit source roots",
));
} else {
entries.push(entry(
SourceScopeStatus::Ambiguous,
"unclassified first-party source",
));
limitations.push(scope_limitation(&file));
}
}
let source_files = included
.iter()
.map(|path| local_path(&root, path))
.collect::<Result<Vec<_>, _>>()?;
let source_roots = existing_roots
.iter()
.map(|path| local_path(&root, path))
.collect::<Result<Vec<_>, _>>()?;
Ok(DiscoveredSourceScope {
source_files,
source_roots: source_roots.clone(),
scope: SourceScope {
version: 1,
mode: if explicit {
SourceScopeMode::Explicit
} else {
SourceScopeMode::Automatic
},
roots: source_roots,
entries,
},
limitations,
})
}
#[cfg(test)]
mod tests {
use std::{
fs,
time::{SystemTime, UNIX_EPOCH},
};
use super::*;
fn repository(label: &str, files: &[(&str, &str)]) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let root = std::env::temp_dir().join(format!(
"supercov-source-{label}-{}-{nonce}",
std::process::id()
));
fs::create_dir_all(&root).unwrap();
for (file, contents) in files {
let path = root.join(file);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, contents).unwrap();
}
root
}
fn entry<'a>(scope: &'a DiscoveredSourceScope, file: &str) -> &'a SourceScopeEntry {
scope
.scope
.entries
.iter()
.find(|entry| entry.file == file)
.unwrap()
}
#[test]
fn a_file_the_project_tells_git_to_ignore_is_not_its_source() {
let files = [
("package.json", r#"{"workspaces":["packages/*"]}"#),
("packages/core/package.json", r#"{"name":"core"}"#),
("packages/core/.gitignore", "lib-dist\n"),
("packages/core/src/index.ts", "export const a = 1;"),
(
"packages/core/lib-dist/browser/worker.dev.js",
"var bundled = 1;",
),
];
let repository_root = repository("gitignored", &files);
fs::create_dir_all(repository_root.join(".git")).unwrap();
let discovered = discover_source_scope(&repository_root, None).unwrap();
assert!(
discovered
.source_files
.contains(&"packages/core/src/index.ts".into())
);
assert!(
!discovered
.source_files
.iter()
.any(|file| file.contains("lib-dist")),
"{:?}",
discovered.source_files
);
fs::remove_dir_all(repository_root).unwrap();
let plain = repository("not-a-repository", &files);
let discovered = discover_source_scope(&plain, None).unwrap();
assert!(
discovered
.scope
.entries
.iter()
.any(|entry| entry.file.contains("lib-dist"))
);
fs::remove_dir_all(plain).unwrap();
}
#[test]
fn a_project_kept_where_an_enclosing_repository_ignores_it_keeps_its_source() {
let files = [
("scratch/.gitignore", "*\n"),
("scratch/app/package.json", r#"{"name":"app"}"#),
("scratch/app/.gitignore", "dist\n"),
("scratch/app/src/index.js", "export const a = 1;"),
("scratch/app/dist/bundle.js", "var bundled = 1;"),
];
let repository_root = repository("enclosing-ignore", &files);
fs::create_dir_all(repository_root.join(".git")).unwrap();
let discovered = discover_source_scope(&repository_root.join("scratch/app"), None).unwrap();
assert_eq!(discovered.source_files, ["src/index.js"]);
fs::remove_file(repository_root.join("scratch/.gitignore")).unwrap();
fs::write(repository_root.join(".gitignore"), "*.gen.js\n").unwrap();
fs::write(repository_root.join("scratch/app/src/api.gen.js"), "var a;").unwrap();
let discovered = discover_source_scope(&repository_root.join("scratch/app"), None).unwrap();
assert_eq!(discovered.source_files, ["src/index.js"]);
fs::remove_dir_all(repository_root).unwrap();
}
#[test]
fn excludes_declared_jest_setup_without_hiding_neighboring_application_code() {
let root = repository(
"jest-setup",
&[
(
"package.json",
r#"{"jest":{"setupFilesAfterEnv":["<rootDir>/src/bootstrap.js"]}}"#,
),
(
"src/bootstrap.js",
"jest.mock('storage', () => ({read: () => null}));",
),
("src/index.js", "export const value = 1;"),
],
);
let discovered = discover_source_scope(&root, None).unwrap();
assert_eq!(discovered.source_files, ["src/index.js"]);
assert_eq!(
entry(&discovered, "src/bootstrap.js").reason,
"declared test setup"
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn example_apps_and_conf_files_are_not_source() {
let root = repository(
"examples",
&[
("package.json", r#"{"exports":{".":"./dist/index.js"}}"#),
("tsconfig.json", r#"{"exclude":["dist","examples"]}"#),
("src/v4.ts", "export const v4 = () => ''"),
("wdio.conf.js", "exports.config = {}"),
("karma.conf.cjs", "module.exports = () => {}"),
("examples/browser-esmodules/package.json", "{}"),
("examples/browser-esmodules/example.js", "console.log(1)"),
("examples/utils/testpage.js", "export default 1"),
("example/demo.js", "console.log(1)"),
("app/examples/page.tsx", "export default function Page() {}"),
],
);
let discovered = discover_source_scope(&root, None).unwrap();
assert_eq!(
discovered.source_files,
["app/examples/page.tsx", "src/v4.ts"]
);
for file in [
"examples/browser-esmodules/example.js",
"examples/utils/testpage.js",
"example/demo.js",
] {
assert_eq!(entry(&discovered, file).reason, "example", "{file}");
}
for file in ["wdio.conf.js", "karma.conf.cjs"] {
assert_eq!(
entry(&discovered, file).reason,
"build/test/tool configuration",
"{file}"
);
}
fs::remove_dir_all(root).unwrap();
}
#[test]
fn discovers_conventional_and_workspace_sources_and_blocks_ambiguity() {
let root = repository(
"automatic",
&[
("package.json", r#"{"workspaces":["packages/*"]}"#),
("src/index.ts", "export const root = true"),
("lib/helper.js", "export const helper = true"),
("src/index.test.ts", "test('root', () => {})"),
("tests/e2e.spec.ts", "test('e2e', () => {})"),
("scripts/release.mjs", "export const release = true"),
("vite.config.ts", "export default {}"),
("build.mjs", "export default async function build() {}"),
(".eslintrc.cjs", "module.exports = {}"),
(".graphqlrc.ts", "export default {}"),
("orphan.ts", "export const missed = true"),
("packages/ui/package.json", r#"{"module":"./src/index.ts"}"#),
("packages/ui/src/index.ts", "export const ui = true"),
("packages/ui/tests/ui.spec.ts", "test('ui', () => {})"),
("dist/generated.js", "generated"),
(".cache/tool/generated.js", "cached"),
("src/parse.bench.ts", "bench('parse', () => {})"),
(
"benchmarks/value-slice.bench.ts",
"bench('slice', () => {})",
),
("src/benchmarkReport.ts", "export const report = 1"),
],
);
let discovered = discover_source_scope(&root, None).unwrap();
assert_eq!(
discovered.source_files,
[
"lib/helper.js",
"packages/ui/src/index.ts",
"src/benchmarkReport.ts",
"src/index.ts"
]
);
for file in ["src/parse.bench.ts", "benchmarks/value-slice.bench.ts"] {
assert_eq!(entry(&discovered, file).reason, "benchmark", "{file}");
}
assert_eq!(
entry(&discovered, "orphan.ts").status,
SourceScopeStatus::Ambiguous
);
assert_eq!(
entry(&discovered, "scripts/release.mjs").reason,
"conventional tool script"
);
assert_eq!(
entry(&discovered, "build.mjs").reason,
"build/test/tool configuration"
);
assert_eq!(
entry(&discovered, "packages/ui/src/index.ts").package_root,
Some("packages/ui".into())
);
assert_eq!(discovered.limitations.len(), 1);
assert_eq!(discovered.limitations[0].file, "orphan.ts");
assert_eq!(discovered.limitations[0].id.len(), "scope:".len() + 20);
assert!(
discovered
.scope
.entries
.iter()
.all(|entry| !entry.file.contains(".cache"))
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn declared_workspaces_outside_conventional_parents_are_package_roots() {
let root = repository(
"declared-workspaces",
&[
("package.json", r#"{"workspaces":["app_extensions/*"]}"#),
("app/main.ts", "product"),
("app_extensions/discounts/package.json", "{}"),
("app_extensions/discounts/src/index.ts", "extension"),
("app_extensions/upsells/package.json", "{}"),
("app_extensions/upsells/frontend/embed.ts", "embed"),
("app_extensions/upsells/dist/embed.js", "built"),
],
);
let discovered = discover_source_scope(&root, None).unwrap();
assert_eq!(
discovered.source_files,
[
"app/main.ts",
"app_extensions/discounts/src/index.ts",
"app_extensions/upsells/frontend/embed.ts",
]
);
assert!(
discovered.limitations.is_empty(),
"declared packages must not be blockers: {:?}",
discovered.limitations
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn hashed_bundles_in_asset_directories_are_built_assets() {
let root = repository(
"built-assets",
&[
("package.json", r#"{"workspaces":["app_extensions/*"]}"#),
("app/main.ts", "product"),
("app_extensions/upsells/package.json", "{}"),
("app_extensions/upsells/frontend/embed.ts", "source"),
(
"app_extensions/upsells/assets/app-embed-Be-aUw9g.js",
"bundle",
),
("app_extensions/upsells/assets/stylex-DAnmLURx.js", "bundle"),
(
"app_extensions/upsells/assets/theme-helper.js",
"hand written",
),
],
);
let discovered = discover_source_scope(&root, None).unwrap();
assert!(
!discovered
.source_files
.iter()
.any(|file| file.contains("-Be-aUw9g.js") || file.contains("-DAnmLURx.js"))
);
assert_eq!(
entry(
&discovered,
"app_extensions/upsells/assets/app-embed-Be-aUw9g.js"
)
.reason,
"built asset"
);
assert!(
discovered.limitations.is_empty(),
"bundles are not blockers: {:?}",
discovered.limitations
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn root_level_hidden_directories_are_tooling_not_source() {
let root = repository(
"root-hidden",
&[
("package.json", "{}"),
("app/main.ts", "product"),
(".shopify/bundle/upsells/frontend/embed.js", "cli bundle"),
(".vercel/output/functions/index.js", "deploy output"),
("app/.generated/schema.ts", "generated types"),
],
);
let discovered = discover_source_scope(&root, None).unwrap();
assert_eq!(
discovered.source_files,
["app/.generated/schema.ts", "app/main.ts"]
);
assert!(
discovered.limitations.is_empty(),
"{:?}",
discovered.limitations
);
assert!(discovered.scope.entries.iter().all(
|entry| !entry.file.starts_with(".shopify/") && !entry.file.starts_with(".vercel/")
));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn nested_checkouts_are_neither_source_nor_limitations() {
let root = repository(
"nested-checkout",
&[
("package.json", "{}"),
("app/main.ts", "product"),
(".claude/worktrees/agent-1/.git", "gitdir: /elsewhere"),
(".claude/worktrees/agent-1/app/main.ts", "copy"),
("vendor-fork/.git/HEAD", "ref: refs/heads/main"),
("vendor-fork/src/index.ts", "clone"),
],
);
let discovered = discover_source_scope(&root, None).unwrap();
assert_eq!(discovered.source_files, ["app/main.ts"]);
assert!(
discovered.limitations.is_empty(),
"nested checkouts must not be blocking limitations: {:?}",
discovered.limitations
);
assert!(
discovered
.scope
.entries
.iter()
.all(|entry| !entry.file.starts_with(".claude/")
&& !entry.file.starts_with("vendor-fork/")),
"nested checkout files must not appear in scope at all"
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn roots_enter_a_runs_identity_only_when_named() {
let mut unchanged = b"pytest\0-q".to_vec();
fold_roots_into_configuration(&mut unchanged, None);
assert_eq!(unchanged, b"pytest\0-q");
fold_roots_into_configuration(&mut unchanged, Some(&[]));
assert_eq!(unchanged, b"pytest\0-q");
let mut folded = b"pytest\0-q".to_vec();
fold_roots_into_configuration(&mut folded, Some(&["src".to_owned(), "a.py".to_owned()]));
assert_eq!(folded, b"pytest\0-q\0SUPERCOV_SOURCE_ROOTS=src,a.py");
}
#[test]
fn the_variable_is_read_the_same_way_for_every_frontend() {
let read = |value: &str| {
configured_source_roots(&BTreeMap::from([(
SOURCE_ROOTS_VARIABLE.to_owned(),
value.to_owned(),
)]))
};
assert_eq!(
read(" src , app ,"),
Some(vec!["src".to_owned(), "app".to_owned()])
);
assert_eq!(
read(" , "),
None,
"naming nothing leaves discovery automatic"
);
assert_eq!(configured_source_roots(&BTreeMap::new()), None);
}
#[test]
fn a_next_application_is_measured_where_next_keeps_its_code() {
let root = repository(
"next-layout",
&[
("package.json", r#"{"dependencies":{"next":"16.1.1"}}"#),
(
"tsconfig.json",
r#"{"include":["next-env.d.ts","**/*.ts",".next/types/**/*.ts"]}"#,
),
("next-env.d.ts", "declare const next: unknown;"),
("app/page.tsx", "page"),
("app/api/wordpress/test/route.ts", "route"),
("app/api/wordpress/test/route.test.ts", "test"),
("app/api/wordpress/test/helper.ts", "helper"),
("app/__tests__/page.tsx", "test"),
("components/button.tsx", "component"),
("pages/api/legacy.ts", "page"),
("proxy.ts", "proxy"),
("instrumentation.ts", "instrumentation"),
("simulators/echo.ts", "simulator"),
],
);
let discovered = discover_source_scope(&root, None).unwrap();
assert_eq!(
discovered.source_files,
[
"app/api/wordpress/test/route.ts",
"app/page.tsx",
"components/button.tsx",
"instrumentation.ts",
"pages/api/legacy.ts",
"proxy.ts",
]
);
assert_eq!(
discovered.source_roots,
[
"app",
"components",
"instrumentation.ts",
"pages",
"proxy.ts"
],
"a declaration file is not a root"
);
for file in [
"app/api/wordpress/test/route.test.ts",
"app/api/wordpress/test/helper.ts",
"app/__tests__/page.tsx",
] {
assert_eq!(entry(&discovered, file).reason, "test or fixture source");
}
assert_eq!(
entry(&discovered, "simulators/echo.ts").status,
SourceScopeStatus::Ambiguous
);
let roots = vec!["app".into()];
let named = discover_source_scope(&root, Some(&roots)).unwrap();
assert_eq!(
named.source_files,
["app/api/wordpress/test/route.ts", "app/page.tsx"]
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn a_route_file_name_means_nothing_outside_a_next_application() {
let root = repository(
"not-next",
&[
("package.json", "{}"),
("app/main.ts", "main"),
("app/test/route.ts", "a test helper"),
("proxy.ts", "unknown"),
],
);
let discovered = discover_source_scope(&root, None).unwrap();
assert_eq!(discovered.source_files, ["app/main.ts"]);
assert_eq!(
entry(&discovered, "app/test/route.ts").reason,
"test or fixture source"
);
assert_eq!(
entry(&discovered, "proxy.ts").status,
SourceScopeStatus::Ambiguous
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn a_named_root_settles_what_it_names_and_guesses_only_below_it() {
let root = repository(
"named-roots",
&[
("package.json", "{}"),
("src/main.ts", "main"),
("src/test-utils/build.ts", "helper"),
("src/test-utils/__tests__/build.test.ts", "test"),
("scripts/release.ts", "script"),
("scripts/fixtures/sample.ts", "fixture"),
("tools/one.spec.ts", "named file"),
],
);
let roots = vec!["src".into()];
let discovered = discover_source_scope(&root, Some(&roots)).unwrap();
assert_eq!(discovered.source_files, ["src/main.ts"]);
let roots = vec![
"src/test-utils".into(),
"scripts".into(),
"tools/one.spec.ts".into(),
];
let discovered = discover_source_scope(&root, Some(&roots)).unwrap();
assert_eq!(
discovered.source_files,
[
"scripts/release.ts",
"src/test-utils/build.ts",
"tools/one.spec.ts",
]
);
assert_eq!(
entry(&discovered, "src/test-utils/__tests__/build.test.ts").reason,
"test or fixture source"
);
assert_eq!(
entry(&discovered, "scripts/fixtures/sample.ts").reason,
"test or fixture source"
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn explicit_roots_are_authoritative_and_outside_files_are_not_limitations() {
let root = repository(
"explicit",
&[
("package.json", "{}"),
("product/main.ts", "product"),
("orphan.ts", "outside"),
],
);
let roots = vec!["product".into()];
let discovered = discover_source_scope(&root, Some(&roots)).unwrap();
assert_eq!(discovered.source_files, ["product/main.ts"]);
assert_eq!(discovered.scope.mode, SourceScopeMode::Explicit);
assert_eq!(
entry(&discovered, "orphan.ts").reason,
"outside explicit source roots"
);
assert!(discovered.limitations.is_empty());
fs::remove_dir_all(root).unwrap();
}
#[test]
fn parses_jsonc_tsconfig_defaults_and_unicode_paths_without_byte_corruption() {
let root = repository(
"jsonc",
&[
("package.json", r#"{"main":"./dist/index.js"}"#),
(
"tsconfig.json",
"{ // unicode survives: ž\n \"compilerOptions\": {\"target\": \"es2022\",},\n}",
),
("events.ts", "event"),
("žalias.ts", "unicode"),
("library.test.ts", "test"),
],
);
let discovered = discover_source_scope(&root, None).unwrap();
assert!(discovered.source_roots.contains(&".".into()));
assert_eq!(discovered.source_files, ["events.ts", "žalias.ts"]);
assert!(discovered.limitations.is_empty());
fs::remove_dir_all(root).unwrap();
}
#[cfg(unix)]
#[test]
fn never_follows_source_directory_or_explicit_root_symlinks() {
use std::os::unix::fs::symlink;
let root = repository(
"symlink",
&[("package.json", "{}"), ("src/real.ts", "real")],
);
let outside = repository("outside", &[("secret.ts", "secret")]);
symlink(&outside, root.join("linked")).unwrap();
symlink(outside.join("secret.ts"), root.join("src/linked.ts")).unwrap();
let discovered = discover_source_scope(&root, None).unwrap();
assert_eq!(discovered.source_files, ["src/real.ts"]);
let explicit = vec!["linked".into()];
assert!(matches!(
discover_source_scope(&root, Some(&explicit)),
Err(SourceDiscoveryError::InvalidRoot(_))
));
fs::remove_dir_all(root).unwrap();
fs::remove_dir_all(outside).unwrap();
}
#[test]
fn skips_only_marker_owned_workspace_stores_not_user_supercov_directories() {
let root = repository(
"workspace-store",
&[
("package.json", "{}"),
("src/main.ts", "main"),
("supercov/user.ts", "user code"),
],
);
let before = discover_source_scope(&root, None).unwrap();
assert_eq!(
entry(&before, "supercov/user.ts").status,
SourceScopeStatus::Ambiguous
);
fs::write(
root.join("supercov/.supercov-workspace-store"),
b"Supercov instrumented workspace. Safe to delete.\n",
)
.unwrap();
let after = discover_source_scope(&root, None).unwrap();
assert!(
after
.scope
.entries
.iter()
.all(|entry| !entry.file.starts_with("supercov/"))
);
fs::remove_dir_all(root).unwrap();
}
}