use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use aft::config::Config;
use aft::inspect::scanners::dead_code::run_dead_code_scan;
use aft::inspect::{
CallgraphExport, CallgraphOutboundCall, CallgraphSnapshot, InspectCategory, InspectJob,
InspectScanSuccess, JobKey,
};
use aft::parser::SymbolCache;
use serde_json::json;
fn fixture_project(files: &[(&str, &str)]) -> (tempfile::TempDir, PathBuf, Vec<PathBuf>) {
let temp_dir = tempfile::tempdir().expect("tempdir");
let root = temp_dir.path().join("project");
fs::create_dir_all(&root).expect("create project root");
let paths = files
.iter()
.map(|(relative, contents)| {
let path = root.join(relative);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create parent");
}
fs::write(&path, contents).expect("write fixture file");
path
})
.collect::<Vec<_>>();
(temp_dir, root, paths)
}
fn job(
root: &Path,
scope_files: Vec<PathBuf>,
callgraph_snapshot: Option<CallgraphSnapshot>,
) -> InspectJob {
InspectJob {
job_id: 1,
key: JobKey::for_project_category(InspectCategory::DeadCode),
category: InspectCategory::DeadCode,
scope_files,
project_root: root.to_path_buf(),
inspect_dir: root.join(".aft-cache").join("inspect"),
config: Arc::new(Config {
project_root: Some(root.to_path_buf()),
..Config::default()
}),
symbol_cache: Arc::new(RwLock::new(SymbolCache::new())),
callgraph_snapshot: callgraph_snapshot.map(Arc::new),
}
}
fn snapshot(
files: Vec<PathBuf>,
exported_symbols: Vec<CallgraphExport>,
outbound_calls: Vec<CallgraphOutboundCall>,
entry_points: Vec<PathBuf>,
) -> CallgraphSnapshot {
CallgraphSnapshot {
generated_at: None,
files,
exported_symbols,
outbound_calls,
entry_points: entry_points.into_iter().collect::<BTreeSet<_>>(),
}
}
fn export(root: &Path, file: &str, symbol: &str, kind: &str, line: u32) -> CallgraphExport {
CallgraphExport {
file: root.join(file),
symbol: symbol.to_string(),
kind: kind.to_string(),
line,
}
}
fn outbound(
root: &Path,
caller_file: &str,
caller_symbol: &str,
target: &str,
line: u32,
) -> CallgraphOutboundCall {
CallgraphOutboundCall {
caller_file: root.join(caller_file),
caller_symbol: caller_symbol.to_string(),
target: target.to_string(),
line,
}
}
fn target(root: &Path, file: &str, symbol: &str) -> String {
format!("{}::{symbol}", root.join(file).display())
}
fn scan(job: InspectJob) -> InspectScanSuccess {
run_dead_code_scan(&job).outcome.expect("scan succeeds")
}
#[test]
fn inspect_dead_code_unavailable_callgraph_returns_empty_result() {
let (_temp_dir, root, paths) = fixture_project(&[("src/foo.ts", "export function foo() {}\n")]);
let success = scan(job(&root, paths, None));
assert!(success.contributions.is_empty());
assert_eq!(success.aggregate["count"], 0);
assert_eq!(success.aggregate["by_language"], json!({}));
assert_eq!(success.aggregate["callgraph_available"], false);
assert_eq!(success.aggregate["drill_down_capped"], false);
}
#[test]
fn inspect_dead_code_reports_exported_uncalled_function() {
let (_temp_dir, root, paths) =
fixture_project(&[("src/foo.ts", "export function unused() {}\n")]);
let graph = snapshot(
paths.clone(),
vec![export(&root, "src/foo.ts", "unused", "function", 1)],
Vec::new(),
Vec::new(),
);
let success = scan(job(&root, paths, Some(graph)));
assert_eq!(success.aggregate["count"], 1);
assert_eq!(success.aggregate["by_language"]["typescript"], 1);
assert_eq!(
success.aggregate["items"].as_array().expect("items").len(),
1
);
assert_eq!(
success.aggregate["items"][0],
json!({"file": "src/foo.ts", "symbol": "unused", "kind": "function", "line": 1})
);
}
#[test]
fn inspect_dead_code_does_not_report_export_reachable_from_entry_point() {
let (_temp_dir, root, paths) = fixture_project(&[
("src/foo.ts", "export function used() {}\n"),
("src/main.ts", "export function main() {\n used();\n}\n"),
]);
let graph = snapshot(
paths.clone(),
vec![
export(&root, "src/foo.ts", "used", "function", 1),
export(&root, "src/main.ts", "main", "function", 1),
],
vec![outbound(
&root,
"src/main.ts",
"main",
&target(&root, "src/foo.ts", "used"),
2,
)],
vec![root.join("src/main.ts")],
);
let success = scan(job(&root, paths, Some(graph)));
assert_eq!(success.aggregate["count"], 0);
assert!(success.aggregate["items"]
.as_array()
.expect("items")
.is_empty());
}
#[test]
fn inspect_dead_code_keeps_multi_hop_entry_point_reachability_alive() {
let (_temp_dir, root, paths) = fixture_project(&[
("src/entry.ts", "export function entry() {\n b();\n}\n"),
("src/b.ts", "export function b() {\n c();\n}\n"),
("src/c.ts", "export function c() {}\n"),
]);
let graph = snapshot(
paths.clone(),
vec![
export(&root, "src/entry.ts", "entry", "function", 1),
export(&root, "src/b.ts", "b", "function", 1),
export(&root, "src/c.ts", "c", "function", 1),
],
vec![
outbound(
&root,
"src/entry.ts",
"entry",
&target(&root, "src/b.ts", "b"),
2,
),
outbound(&root, "src/b.ts", "b", &target(&root, "src/c.ts", "c"), 2),
],
vec![root.join("src/entry.ts")],
);
let success = scan(job(&root, paths, Some(graph)));
assert_eq!(success.aggregate["count"], 0);
}
#[test]
fn inspect_dead_code_keeps_same_name_exports_distinct() {
let (_temp_dir, root, paths) = fixture_project(&[
("src/entry.ts", "export function main() {\n foo();\n}\n"),
("src/dead.ts", "export function foo() {}\n"),
("src/alive.ts", "export function foo() {}\n"),
]);
let graph = snapshot(
paths.clone(),
vec![
export(&root, "src/entry.ts", "main", "function", 1),
export(&root, "src/dead.ts", "foo", "function", 1),
export(&root, "src/alive.ts", "foo", "function", 1),
],
vec![outbound(
&root,
"src/entry.ts",
"main",
&target(&root, "src/alive.ts", "foo"),
2,
)],
vec![root.join("src/entry.ts")],
);
let success = scan(job(&root, paths, Some(graph)));
assert_eq!(success.aggregate["count"], 1);
assert_eq!(
success.aggregate["items"][0],
json!({"file": "src/dead.ts", "symbol": "foo", "kind": "function", "line": 1})
);
}
#[test]
fn inspect_dead_code_attributes_calls_to_recorded_caller_symbol() {
let (_temp_dir, root, paths) = fixture_project(&[
(
"src/a.ts",
"export function a() {\n return 1;\n}\nexport function b() {}\n",
),
("src/target.ts", "export function target() {}\n"),
]);
let graph = snapshot(
paths.clone(),
vec![
export(&root, "src/a.ts", "a", "function", 1),
export(&root, "src/a.ts", "b", "function", 4),
export(&root, "src/target.ts", "target", "function", 1),
],
vec![outbound(
&root,
"src/a.ts",
"a",
&target(&root, "src/target.ts", "target"),
6,
)],
vec![root.join("src/a.ts")],
);
let success = scan(job(&root, paths, Some(graph)));
assert_eq!(success.aggregate["count"], 1);
assert_eq!(
success.aggregate["items"][0],
json!({"file": "src/a.ts", "symbol": "b", "kind": "function", "line": 4})
);
}
#[test]
fn inspect_dead_code_flags_binary_export_but_suppresses_library_public_api() {
let (_temp_dir, root, paths) = fixture_project(&[
(
"Cargo.toml",
"[package]\nname = \"inspect-fixture\"\nversion = \"0.1.0\"\n",
),
("src/main.rs", "pub fn unused_internal() {}\nfn main() {}\n"),
("src/lib.rs", "pub fn public_api() {}\n"),
]);
let source_files = vec![root.join("src/main.rs"), root.join("src/lib.rs")];
let graph = snapshot(
source_files,
vec![
export(&root, "src/main.rs", "unused_internal", "function", 1),
export(&root, "src/lib.rs", "public_api", "function", 1),
],
Vec::new(),
vec![root.join("src/main.rs")],
);
let success = scan(job(&root, paths, Some(graph)));
assert_eq!(success.aggregate["count"], 1);
assert_eq!(
success.aggregate["items"][0],
json!({"file": "src/main.rs", "symbol": "unused_internal", "kind": "function", "line": 1})
);
}
#[test]
fn inspect_dead_code_reports_unreachable_cycle_exports() {
let (_temp_dir, root, paths) = fixture_project(&[
("src/a.ts", "export function a() {\n b();\n}\n"),
("src/b.ts", "export function b() {\n a();\n}\n"),
]);
let graph = snapshot(
paths.clone(),
vec![
export(&root, "src/a.ts", "a", "function", 1),
export(&root, "src/b.ts", "b", "function", 1),
],
vec![
outbound(&root, "src/a.ts", "a", &target(&root, "src/b.ts", "b"), 2),
outbound(&root, "src/b.ts", "b", &target(&root, "src/a.ts", "a"), 2),
],
Vec::new(),
);
let success = scan(job(&root, paths, Some(graph)));
let dead_symbols = success.aggregate["items"]
.as_array()
.expect("items")
.iter()
.map(|item| {
(
item["file"].as_str().expect("file").to_string(),
item["symbol"].as_str().expect("symbol").to_string(),
)
})
.collect::<BTreeSet<_>>();
assert_eq!(success.aggregate["count"], 2);
assert_eq!(success.aggregate["by_language"]["typescript"], 2);
assert_eq!(
dead_symbols,
BTreeSet::from([
("src/a.ts".to_string(), "a".to_string()),
("src/b.ts".to_string(), "b".to_string()),
])
);
}
#[test]
fn inspect_dead_code_does_not_report_entry_point_exports() {
let (_temp_dir, root, paths) =
fixture_project(&[("src/main.ts", "export function main() {}\n")]);
let graph = snapshot(
paths.clone(),
vec![export(&root, "src/main.ts", "main", "function", 1)],
Vec::new(),
vec![root.join("src/main.ts")],
);
let success = scan(job(&root, paths, Some(graph)));
assert_eq!(success.aggregate["count"], 0);
}
#[test]
fn inspect_dead_code_does_not_report_package_json_main_export() {
let (_temp_dir, root, paths) = fixture_project(&[
("package.json", "{\"main\":\"src/public.ts\"}\n"),
("src/public.ts", "export function publicApi() {}\n"),
]);
let source_files = vec![root.join("src/public.ts")];
let graph = snapshot(
source_files.clone(),
vec![export(&root, "src/public.ts", "publicApi", "function", 1)],
Vec::new(),
Vec::new(),
);
let success = scan(job(&root, paths, Some(graph)));
assert_eq!(success.aggregate["count"], 0);
}
#[test]
fn inspect_dead_code_resolves_extensionless_package_json_main_export() {
let (_temp_dir, root, paths) = fixture_project(&[
("package.json", "{\"main\":\"src/index\"}\n"),
("src/index.ts", "export function publicApi() {}\n"),
]);
let source_files = vec![root.join("src/index.ts")];
let graph = snapshot(
source_files,
vec![export(&root, "src/index.ts", "publicApi", "function", 1)],
Vec::new(),
Vec::new(),
);
let success = scan(job(&root, paths, Some(graph)));
assert_eq!(success.aggregate["count"], 0);
}
#[test]
fn inspect_dead_code_keeps_cross_package_barrel_reexport_import_live() {
let (_temp_dir, root, _paths) = fixture_project(&[
(
"package.json",
r#"{"private":true,"workspaces":["packages/*"]}"#,
),
(
"packages/bridge/package.json",
r#"{"name":"@scope/bridge","exports":"./src/index.ts"}"#,
),
(
"packages/bridge/src/index.ts",
"export type { LiveEnvelope } from \"./protocol.js\";\n",
),
(
"packages/bridge/src/protocol.ts",
"export interface LiveEnvelope { id: string; }\nexport interface DeadEnvelope { id: string; }\n",
),
("packages/app/package.json", r#"{"name":"app"}"#),
(
"packages/app/src/consumer.ts",
"import type { LiveEnvelope as DownstreamEnvelope } from \"@scope/bridge\";\ntype ConsumerEnvelope = DownstreamEnvelope;\n",
),
]);
let source_files = vec![
root.join("packages/bridge/src/index.ts"),
root.join("packages/bridge/src/protocol.ts"),
root.join("packages/app/src/consumer.ts"),
];
let graph = snapshot(
source_files.clone(),
vec![
export(
&root,
"packages/bridge/src/index.ts",
"LiveEnvelope",
"interface",
1,
),
export(
&root,
"packages/bridge/src/protocol.ts",
"LiveEnvelope",
"interface",
1,
),
export(
&root,
"packages/bridge/src/protocol.ts",
"DeadEnvelope",
"interface",
2,
),
],
Vec::new(),
vec![root.join("packages/app/src/consumer.ts")],
);
let success = scan(job(&root, source_files, Some(graph)));
let dead_symbols = success.aggregate["items"]
.as_array()
.expect("items")
.iter()
.map(|item| item["symbol"].as_str().expect("symbol").to_string())
.collect::<BTreeSet<_>>();
assert_eq!(success.aggregate["count"], 1, "{:#}", success.aggregate);
assert!(
!dead_symbols.contains("LiveEnvelope"),
"barrel-imported cross-package type should be live: {:#}",
success.aggregate
);
assert!(dead_symbols.contains("DeadEnvelope"));
}
#[test]
fn inspect_dead_code_keeps_workspace_barrel_default_export_import_live() {
let (_temp_dir, root, _paths) = fixture_project(&[
(
"package.json",
r#"{"private":true,"workspaces":["packages/*"]}"#,
),
(
"packages/bridge/package.json",
r#"{"name":"@scope/bridge","exports":"./src/index.ts"}"#,
),
(
"packages/bridge/src/index.ts",
"export { default } from \"./value\";\n",
),
(
"packages/bridge/src/value.ts",
"export default function LiveDefault() { return 1; }\nexport function deadHelper() { return 2; }\n",
),
("packages/app/package.json", r#"{"name":"app"}"#),
(
"packages/app/src/consumer.ts",
"import LiveDefault from \"@scope/bridge\";\nLiveDefault();\n",
),
]);
let source_files = vec![
root.join("packages/bridge/src/index.ts"),
root.join("packages/bridge/src/value.ts"),
root.join("packages/app/src/consumer.ts"),
];
let graph = snapshot(
source_files.clone(),
vec![
export(
&root,
"packages/bridge/src/value.ts",
"LiveDefault",
"function",
1,
),
export(
&root,
"packages/bridge/src/value.ts",
"LiveDefault",
"default_export",
1,
),
export(
&root,
"packages/bridge/src/value.ts",
"deadHelper",
"function",
2,
),
],
Vec::new(),
vec![root.join("packages/app/src/consumer.ts")],
);
let success = scan(job(&root, source_files, Some(graph)));
let dead_symbols = success.aggregate["items"]
.as_array()
.expect("items")
.iter()
.map(|item| item["symbol"].as_str().expect("symbol").to_string())
.collect::<BTreeSet<_>>();
assert_eq!(success.aggregate["count"], 1, "{:#}", success.aggregate);
assert!(
!dead_symbols.contains("LiveDefault"),
"barrel-imported default export should be live: {:#}",
success.aggregate
);
assert!(dead_symbols.contains("deadHelper"));
}
#[test]
fn inspect_dead_code_does_not_keep_namespace_imported_exports_live_from_dead_file() {
let (_temp_dir, root, paths) = fixture_project(&[
(
"src/mod.ts",
"export function thing() { return 1; }\nexport function helper() { return 2; }\n",
),
(
"src/dead_consumer.ts",
"import * as mod from \"./mod\";\nmod.thing();\n",
),
]);
let graph = snapshot(
paths.clone(),
vec![
export(&root, "src/mod.ts", "thing", "function", 1),
export(&root, "src/mod.ts", "helper", "function", 2),
],
vec![outbound(
&root,
"src/dead_consumer.ts",
"<top-level>",
&target(&root, "src/mod.ts", "thing"),
2,
)],
Vec::new(),
);
let success = scan(job(&root, paths, Some(graph)));
let dead_symbols = success.aggregate["items"]
.as_array()
.expect("items")
.iter()
.map(|item| item["symbol"].as_str().expect("symbol").to_string())
.collect::<BTreeSet<_>>();
assert_eq!(success.aggregate["count"], 2, "{:#}", success.aggregate);
assert!(dead_symbols.contains("thing"));
assert!(dead_symbols.contains("helper"));
}
#[test]
fn inspect_dead_code_keeps_namespace_imported_exports_live_from_reachable_file() {
let (_temp_dir, root, paths) = fixture_project(&[
(
"src/mod.ts",
"export function thing() { return 1; }\nexport function helper() { return 2; }\n",
),
(
"src/consumer.ts",
"import * as mod from \"./mod\";\nmod.thing();\n",
),
]);
let graph = snapshot(
paths.clone(),
vec![
export(&root, "src/mod.ts", "thing", "function", 1),
export(&root, "src/mod.ts", "helper", "function", 2),
],
vec![outbound(
&root,
"src/consumer.ts",
"<top-level>",
&target(&root, "src/mod.ts", "thing"),
2,
)],
vec![root.join("src/consumer.ts")],
);
let success = scan(job(&root, paths, Some(graph)));
assert_eq!(success.aggregate["count"], 0, "{:#}", success.aggregate);
assert!(success.aggregate["items"]
.as_array()
.expect("items")
.is_empty());
}
#[test]
fn inspect_dead_code_skips_malformed_child_package_json_when_resolving_package_name() {
let (_temp_dir, root, _paths) = fixture_project(&[
(
"package.json",
r#"{"private":true,"workspaces":["packages/*"]}"#,
),
(
"packages/bridge/package.json",
r#"{"name":"@scope/bridge","exports":"./src/index.ts"}"#,
),
("packages/bridge/src/package.json", "{ malformed json"),
(
"packages/bridge/src/index.ts",
"export function liveFunction() { return 1; }\n",
),
("packages/app/package.json", r#"{"name":"app"}"#),
(
"packages/app/src/consumer.ts",
"import { liveFunction } from \"@scope/bridge\";\nliveFunction();\n",
),
]);
let source_files = vec![
root.join("packages/bridge/src/index.ts"),
root.join("packages/app/src/consumer.ts"),
];
let graph = snapshot(
source_files.clone(),
vec![export(
&root,
"packages/bridge/src/index.ts",
"liveFunction",
"function",
1,
)],
Vec::new(),
vec![root.join("packages/app/src/consumer.ts")],
);
let success = scan(job(&root, source_files, Some(graph)));
assert_eq!(success.aggregate["count"], 0, "{:#}", success.aggregate);
}
#[test]
fn inspect_dead_code_keeps_relative_named_import_live_from_reachable_file() {
let (_temp_dir, root, paths) = fixture_project(&[
(
"src/main.ts",
"import { Live } from './leaf';\nexport function main() { return 1; }\n",
),
(
"src/leaf.ts",
"export function Live() { return 1; }\nexport function Dead() { return 2; }\n",
),
]);
let graph = snapshot(
paths.clone(),
vec![
export(&root, "src/main.ts", "main", "function", 2),
export(&root, "src/leaf.ts", "Live", "function", 1),
export(&root, "src/leaf.ts", "Dead", "function", 2),
],
Vec::new(),
vec![root.join("src/main.ts")],
);
let success = scan(job(&root, paths, Some(graph)));
let dead_symbols = success.aggregate["items"]
.as_array()
.expect("items")
.iter()
.map(|item| item["symbol"].as_str().expect("symbol").to_string())
.collect::<BTreeSet<_>>();
assert_eq!(success.aggregate["count"], 1, "{:#}", success.aggregate);
assert!(!dead_symbols.contains("Live"));
assert!(dead_symbols.contains("Dead"));
}
#[test]
fn inspect_dead_code_does_not_keep_relative_named_import_live_from_dead_file() {
let (_temp_dir, root, paths) = fixture_project(&[
(
"src/dead_consumer.ts",
"import { Live } from './leaf';\nexport function unusedConsumer() { return 1; }\n",
),
(
"src/leaf.ts",
"export function Live() { return 1; }\nexport function Dead() { return 2; }\n",
),
]);
let graph = snapshot(
paths.clone(),
vec![
export(
&root,
"src/dead_consumer.ts",
"unusedConsumer",
"function",
2,
),
export(&root, "src/leaf.ts", "Live", "function", 1),
export(&root, "src/leaf.ts", "Dead", "function", 2),
],
Vec::new(),
Vec::new(),
);
let success = scan(job(&root, paths, Some(graph)));
let dead_symbols = success.aggregate["items"]
.as_array()
.expect("items")
.iter()
.map(|item| item["symbol"].as_str().expect("symbol").to_string())
.collect::<BTreeSet<_>>();
assert_eq!(success.aggregate["count"], 3, "{:#}", success.aggregate);
assert!(dead_symbols.contains("unusedConsumer"));
assert!(dead_symbols.contains("Live"));
assert!(dead_symbols.contains("Dead"));
}
#[test]
fn inspect_dead_code_keeps_public_typescript_barrel_reexport_leaf_live() {
let (_temp_dir, root, _paths) = fixture_project(&[
("package.json", r#"{"main":"src/index.ts"}"#),
("src/index.ts", "export { Live } from './leaf';\n"),
(
"src/leaf.ts",
"export function Live() { return 1; }\nexport function Dead() { return 2; }\n",
),
]);
let source_files = vec![root.join("src/index.ts"), root.join("src/leaf.ts")];
let graph = snapshot(
source_files.clone(),
vec![
export(&root, "src/index.ts", "Live", "re_export", 1),
export(&root, "src/leaf.ts", "Live", "function", 1),
export(&root, "src/leaf.ts", "Dead", "function", 2),
],
Vec::new(),
Vec::new(),
);
let success = scan(job(&root, source_files, Some(graph)));
let dead_symbols = success.aggregate["items"]
.as_array()
.expect("items")
.iter()
.map(|item| item["symbol"].as_str().expect("symbol").to_string())
.collect::<BTreeSet<_>>();
assert_eq!(success.aggregate["count"], 1, "{:#}", success.aggregate);
assert!(!dead_symbols.contains("Live"));
assert!(dead_symbols.contains("Dead"));
}
#[test]
fn inspect_dead_code_keeps_public_rust_pub_use_leaf_live() {
let (_temp_dir, root, _paths) = fixture_project(&[
(
"Cargo.toml",
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n",
),
("src/lib.rs", "pub use foo::Foo;\nmod foo;\n"),
("src/foo.rs", "pub struct Foo;\npub struct Dead;\n"),
]);
let source_files = vec![root.join("src/lib.rs"), root.join("src/foo.rs")];
let graph = snapshot(
source_files.clone(),
vec![
export(&root, "src/lib.rs", "Foo", "struct", 1),
export(&root, "src/foo.rs", "Foo", "struct", 1),
export(&root, "src/foo.rs", "Dead", "struct", 2),
],
Vec::new(),
Vec::new(),
);
let success = scan(job(&root, source_files, Some(graph)));
let dead_symbols = success.aggregate["items"]
.as_array()
.expect("items")
.iter()
.map(|item| item["symbol"].as_str().expect("symbol").to_string())
.collect::<BTreeSet<_>>();
assert_eq!(success.aggregate["count"], 1, "{:#}", success.aggregate);
assert!(!dead_symbols.contains("Foo"));
assert!(dead_symbols.contains("Dead"));
}
#[test]
fn inspect_dead_code_parses_rust_scoped_targets_after_file_separator() {
let (_temp_dir, root, paths) = fixture_project(&[
("src/main.rs", "pub fn main() { Handler::handle(); }\n"),
("src/handler.rs", "pub fn handle() {}\n"),
]);
let graph = snapshot(
paths.clone(),
vec![
export(&root, "src/main.rs", "main", "function", 1),
export(&root, "src/handler.rs", "Handler::handle", "method", 1),
],
vec![outbound(
&root,
"src/main.rs",
"main",
&target(&root, "src/handler.rs", "Handler::handle"),
1,
)],
vec![root.join("src/main.rs")],
);
let success = scan(job(&root, paths, Some(graph)));
assert_eq!(success.aggregate["count"], 0, "{:#}", success.aggregate);
}
#[test]
fn inspect_dead_code_ignored_manifest_does_not_suppress_fallback_entry_points() {
let (_temp_dir, root, _paths) = fixture_project(&[
(".gitignore", "fixtures/\n"),
("fixtures/pkg/package.json", r#"{"main":"src/index.ts"}"#),
(
"fixtures/pkg/src/index.ts",
"export function ignoredPublicApi() { return 1; }\n",
),
(
"src/index.ts",
"export function publicApi() { return 1; }\n",
),
]);
let source_files = vec![root.join("src/index.ts")];
let graph = snapshot(
source_files.clone(),
vec![export(&root, "src/index.ts", "publicApi", "function", 1)],
Vec::new(),
Vec::new(),
);
let success = scan(job(&root, source_files, Some(graph)));
assert_eq!(success.aggregate["count"], 0, "{:#}", success.aggregate);
}
#[test]
fn inspect_dead_code_caps_drill_down_after_one_hundred_items() {
let source = (0..101)
.map(|index| format!("export function unused_{index}() {{}}\n"))
.collect::<String>();
let (_temp_dir, root, paths) = fixture_project(&[("src/many.ts", &source)]);
let exports = (0..101)
.map(|index| {
export(
&root,
"src/many.ts",
&format!("unused_{index}"),
"function",
index + 1,
)
})
.collect::<Vec<_>>();
let graph = snapshot(paths.clone(), exports, Vec::new(), Vec::new());
let success = scan(job(&root, paths, Some(graph)));
assert_eq!(success.aggregate["count"], 101);
assert_eq!(success.aggregate["by_language"]["typescript"], 101);
assert_eq!(
success.aggregate["items"].as_array().expect("items").len(),
100
);
assert_eq!(success.aggregate["drill_down_capped"], true);
}
#[test]
fn inspect_dead_code_contribution_shape_matches_contract() {
let (_temp_dir, root, paths) = fixture_project(&[
(
"src/foo.ts",
"export class Foo {}\nexport function helper() { return Bar(); }\n",
),
("src/bar.ts", "export function Bar() {}\n"),
]);
let graph = snapshot(
paths.clone(),
vec![
export(&root, "src/foo.ts", "Foo", "class", 1),
export(&root, "src/foo.ts", "helper", "function", 2),
export(&root, "src/bar.ts", "Bar", "function", 1),
],
vec![
outbound(
&root,
"src/foo.ts",
"helper",
&target(&root, "src/bar.ts", "Bar"),
2,
),
outbound(&root, "src/foo.ts", "helper", "external_dependency", 3),
],
Vec::new(),
);
let success = scan(job(&root, paths, Some(graph)));
let contribution = success
.contributions
.iter()
.find(|contribution| contribution.file_path == root.join("src/foo.ts"))
.expect("foo contribution");
assert_eq!(
contribution.contribution,
json!({
"file": "src/foo.ts",
"exports": [
{"symbol": "Foo", "kind": "class", "line": 1, "is_entry_point": false},
{"symbol": "helper", "kind": "function", "line": 2, "is_entry_point": false}
],
"internal_calls": [
{"caller_symbol": "helper", "file": "src/bar.ts", "symbol": "Bar", "line": 2}
],
"liveness_roots": []
})
);
}