Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .alfonso/release-notes/next/inspect-graph-source-limits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- aft_inspect's dead-code and unused-export checks skip generated and minified JavaScript and TypeScript files and read source files only up to 4 MiB, so one huge file can no longer exhaust memory (#1337).
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"slice_class": "non_ranking",
"targeted_mechanism": "none",
"kind": "harness",
"fixtures": [
"harness-goldens"
]
}
7 changes: 5 additions & 2 deletions crates/aft/src/inspect/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,10 @@ impl From<serde_json::Error> for InspectCacheError {
/// undecodable dead-code rows remain explicit coverage gaps.
/// v43: source-size admission excludes oversized complexity/duplicate inputs
/// and reports their coverage gaps instead of reusing unbounded scan results.
pub(crate) const TIER2_CONTRIBUTION_CACHE_VERSION: u32 = 43;
/// v44: dead-code and unused-export readers admit at most 4 MiB of source,
/// omit generated/minified JS/TS, and invalidate graph-wide deadness claims
/// when a source file exceeds the limit.
pub(crate) const TIER2_CONTRIBUTION_CACHE_VERSION: u32 = 44;
// Reserved metadata category: its integer is an epoch, never a scan timestamp.
const CACHE_VERSION_CATEGORY: &str = "__cache_version__";

Expand Down Expand Up @@ -3998,7 +4001,7 @@ pub(crate) mod tests {
decoded.contribution["exports"][0]["is_type_like"].as_bool(),
Some(true)
);
assert_eq!(TIER2_CONTRIBUTION_CACHE_VERSION, 43);
assert_eq!(TIER2_CONTRIBUTION_CACHE_VERSION, 44);
}

#[test]
Expand Down
298 changes: 288 additions & 10 deletions crates/aft/src/inspect/manager.rs

Large diffs are not rendered by default.

47 changes: 42 additions & 5 deletions crates/aft/src/inspect/oxc_engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

use crate::inspect::scanners::source_text::{
graph_source_permit, read_graph_source_text, SourceText, GRAPH_TOO_LARGE,
};
use oxc_span::SourceType;

use facts::parse_file_facts;
Expand Down Expand Up @@ -84,14 +87,28 @@ impl OxcFactsCache {
path: &Path,
force_reparse: bool,
stats: &mut OxcFactsCacheStats,
) -> std::io::Result<FileFacts> {
) -> Result<FileFacts, String> {
let source_type = SourceType::from_path(path).unwrap_or_default();
let source_type_key = source_type_cache_key(source_type);
let metadata = fs::metadata(path)?;
let metadata = fs::metadata(path).map_err(|error| format!("read: {error}"))?;
let mtime = metadata.modified().unwrap_or(std::time::UNIX_EPOCH);
let size = metadata.len();
let path_key = path.to_path_buf();
let source = crate::project_content::read_to_string(path)?;
let _permit = graph_source_permit(size);
let source = match read_graph_source_text(path) {
Ok(SourceText::Text(source)) => source,
Ok(SourceText::TooLarge) => return Err(GRAPH_TOO_LARGE.to_string()),
Ok(SourceText::NotUtf8) => {
return Err("read: stream did not contain valid UTF-8".to_string())
}
Ok(SourceText::Vanished) => {
return Err(format!(
"read: {}",
std::io::Error::from(std::io::ErrorKind::NotFound)
))
}
Err(error) => return Err(format!("read: {error}")),
};

if !force_reparse {
if let Some(entry) = self.entries_by_path.get(&path_key) {
Expand Down Expand Up @@ -255,6 +272,12 @@ pub fn analyze_files_with_cache(
let progress = crate::run_tool_call::progress::FileProgress::current();

for (idx, path) in files.iter().enumerate() {
if crate::inspect::scanners::source_text::graph_source_excluded(&project_root, path) {
if let Some(progress) = &progress {
progress.finished();
}
continue;
}
match cache.facts_for_file(
FileId(idx),
path,
Expand All @@ -264,7 +287,7 @@ pub fn analyze_files_with_cache(
Ok(file_facts) => facts.push(file_facts),
Err(error) => errors.push(OxcEngineError {
file: path.clone(),
message: format!("read: {error}"),
message: error,
}),
}
if let Some(progress) = &progress {
Expand Down Expand Up @@ -332,14 +355,19 @@ fn analyze_preparsed_facts(
let public_api_files = normalize_option_paths(&options.public_api_files);
let executable_root_exports =
normalize_executable_root_exports(&options.executable_root_exports);
let file_verdicts = compute_verdicts(
let mut file_verdicts = compute_verdicts(
&project_root,
&resolved_modules,
&entry_points,
&public_api_files,
&executable_root_exports,
options.entry_reachability,
);
// An unread importer can reference any module, including through a dynamic
// import. Missing edges cannot establish that any export is unused.
if errors.iter().any(|error| error.message == GRAPH_TOO_LARGE) {
mark_oversized_graph_uncertain(&mut file_verdicts);
}
let resolved_edges = edges
.iter()
.filter(|edge| edge.resolved_file.is_some())
Expand All @@ -366,6 +394,15 @@ fn analyze_preparsed_facts(
}
}

fn mark_oversized_graph_uncertain(files: &mut [OxcFileVerdicts]) {
for file in files {
for export in &mut file.exports {
export.verdict = LivenessVerdict::Uncertain;
export.reason = GRAPH_TOO_LARGE.to_string();
}
}
}

#[derive(Debug, Default)]
struct NormalizedFileSet {
files: Vec<PathBuf>,
Expand Down
Loading
Loading