From 7e10abf5e1bfa1ff203ce470b40622c7ab70b7f6 Mon Sep 17 00:00:00 2001 From: Harnoor Lal Date: Fri, 27 Feb 2026 10:49:58 -0800 Subject: [PATCH 1/4] chore(cargo): move tempfile to dev-dependencies --- Cargo.lock | 54 +++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a6d82cd..b1f4495 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1145,9 +1145,9 @@ dependencies = [ [[package]] name = "gix-url" -version = "0.35.1" +version = "0.35.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507752d41afcdf5961ab494eb062c3bf21f68b2ee67e45568e9028cccdd00c34" +checksum = "d28e8af3d42581190da884f013caf254d2fd4d6ab102408f08d21bfa11de6c8d" dependencies = [ "bstr", "gix-path", @@ -1441,9 +1441,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +checksum = "b3e3d65f018c6ae946ab16e80944b97096ed73c35b221d1c478a6c81d8f57940" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1456,9 +1456,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +checksum = "a17c2b211d863c7fde02cbea8a3c1a439b98e109286554f2860bdded7ff83818" dependencies = [ "proc-macro2", "quote", @@ -1529,14 +1529,14 @@ checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.11.0", "libc", - "redox_syscall 0.7.1", + "redox_syscall 0.7.3", ] [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1706,9 +1706,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "owo-colors" -version = "4.2.3" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "oxc-miette" @@ -1867,9 +1867,9 @@ dependencies = [ [[package]] name = "oxc_resolver" -version = "11.18.0" +version = "11.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76e0792c2256558ef3587dbe324d13c5436181c45553cf494bcf9583612a81c8" +checksum = "9e80d8d09f2231a841bbc0d4c8ee91b8542fa1f74f6e02c43161d394415577cc" dependencies = [ "cfg-if", "compact_str", @@ -2021,9 +2021,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "portable-atomic" @@ -2235,9 +2235,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ "bitflags 2.11.0", ] @@ -2287,9 +2287,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rustc-hash" @@ -2299,9 +2299,9 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags 2.11.0", "errno", @@ -2573,9 +2573,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.25.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", "getrandom 0.4.1", @@ -3309,18 +3309,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 29ad17d..b42e659 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ tree-sitter = "0.24" tree-sitter-python = "0.23" crossbeam-queue = "0.3" dashmap = "6" -tempfile = "3" rustyline = "15" gix = { version = "0.79.0", default-features = false, features = ["max-performance-safe"] } notify = { version = "7", default-features = false, features = ["macos_fsevent"] } @@ -54,6 +53,7 @@ ignore = "0.4" assert_cmd = "2" predicates = "3" proptest = "1" +tempfile = "3" [[bench]] name = "benchmarks" From 79126a2361a6ff0b7a8feb6470580977547fe027 Mon Sep 17 00:00:00 2001 From: Harnoor Lal Date: Fri, 27 Feb 2026 10:51:50 -0800 Subject: [PATCH 2/4] refactor: document invariants on provably-safe unwrap calls --- src/cache.rs | 17 ++++++++++++----- src/main.rs | 2 +- src/query.rs | 2 +- src/repl.rs | 10 ++++++++-- src/session.rs | 14 +++++++++++--- src/walker.rs | 7 +++++-- 6 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 61e59b3..31f8ee3 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -190,12 +190,13 @@ impl ParseCache { if data.len() < HEADER_SIZE { return Self::new(); } - let magic = u32::from_le_bytes(data[0..4].try_into().unwrap()); - let version = u32::from_le_bytes(data[4..8].try_into().unwrap()); + let magic = u32::from_le_bytes(data[0..4].try_into().expect("4-byte slice fits u32")); + let version = u32::from_le_bytes(data[4..8].try_into().expect("4-byte slice fits u32")); if magic != CACHE_MAGIC || version != CACHE_VERSION { return Self::new(); } - let graph_len = u64::from_le_bytes(data[8..16].try_into().unwrap()) as usize; + let graph_len = + u64::from_le_bytes(data[8..16].try_into().expect("8-byte slice fits u64")) as usize; let graph_end = HEADER_SIZE + graph_len; if data.len() < graph_end { return Self::new(); @@ -288,7 +289,10 @@ impl ParseCache { } if changed_files.is_empty() { - let cached = self.cached_graph.take().unwrap(); + let cached = self + .cached_graph + .take() + .expect("cached_graph populated by load"); return GraphCacheResult::Hit { graph: cached.graph, unresolvable_dynamic: cached.unresolvable_dynamic, @@ -299,7 +303,10 @@ impl ParseCache { } // Files changed — extract graph and preserve mtimes for incremental save - let cached = self.cached_graph.take().unwrap(); + let cached = self + .cached_graph + .take() + .expect("cached_graph populated by load"); self.stale_file_mtimes = Some(cached.file_mtimes); self.stale_unresolved = Some(cached.unresolved_specifiers); GraphCacheResult::Stale { diff --git a/src/main.rs b/src/main.rs index 91bb39b..d504fac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -485,7 +485,7 @@ fn save_snapshot( sc: report::StderrColor, ) -> Result<(), Error> { let snapshot = result.to_snapshot(entry_rel); - let data = serde_json::to_string_pretty(&snapshot).unwrap(); + let data = serde_json::to_string_pretty(&snapshot).expect("snapshot serializes to JSON"); std::fs::write(path, &data).map_err(|e| Error::SnapshotWrite(path.to_path_buf(), e))?; if !quiet { eprintln!("{} to {}", sc.status("Snapshot saved"), path.display()); diff --git a/src/query.rs b/src/query.rs index 316b6e1..bf32e49 100644 --- a/src/query.rs +++ b/src/query.rs @@ -568,7 +568,7 @@ fn all_shortest_chains( let mut capped = false; for path in &partial_paths { - let &head = path.last().unwrap(); + let &head = path.last().expect("partial paths are non-empty"); if head == entry { next_partial.push(path.clone()); continue; diff --git a/src/repl.rs b/src/repl.rs index 0bb0125..c09d84e 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -689,7 +689,10 @@ fn dispatch_imports(session: &Session, path: &str, opts: &CommandOptions, sc: St }) }) .collect(); - println!("{}", serde_json::to_string_pretty(&entries).unwrap()); + println!( + "{}", + serde_json::to_string_pretty(&entries).expect("entries serialize to JSON") + ); return; } if imports.is_empty() { @@ -727,7 +730,10 @@ fn dispatch_importers(session: &Session, path: &str, opts: &CommandOptions, sc: }) }) .collect(); - println!("{}", serde_json::to_string_pretty(&entries).unwrap()); + println!( + "{}", + serde_json::to_string_pretty(&entries).expect("entries serialize to JSON") + ); return; } if importers.is_empty() { diff --git a/src/session.rs b/src/session.rs index 23223e6..9c02d44 100644 --- a/src/session.rs +++ b/src/session.rs @@ -201,7 +201,11 @@ impl Session { include_dynamic, ); self.ensure_weights(include_dynamic); - let weights = &self.cached_weights.as_ref().unwrap().weights; + let weights = &self + .cached_weights + .as_ref() + .expect("ensure_weights populates cache") + .weights; let cuts = query::find_cut_modules( &self.graph, &chains, @@ -233,7 +237,7 @@ impl Session { let snap_a = self .cached_trace .as_ref() - .unwrap() + .expect("ensure_trace populates cache") .result .to_snapshot(&self.entry_label()); let snap_b = query::trace(&self.graph, other_id, opts) @@ -448,7 +452,11 @@ impl Session { /// Trace and produce a display-ready report. pub fn trace_report(&mut self, opts: &TraceOptions, top_modules: i32) -> TraceReport { self.ensure_trace(opts); - let result = &self.cached_trace.as_ref().unwrap().result; + let result = &self + .cached_trace + .as_ref() + .expect("ensure_trace populates cache") + .result; build_trace_report( result, &self.entry, diff --git a/src/walker.rs b/src/walker.rs index e50e72a..d160ff0 100644 --- a/src/walker.rs +++ b/src/walker.rs @@ -119,7 +119,10 @@ fn concurrent_discover( imports, unresolvable_dynamic: result.unresolvable_dynamic, }; - results.lock().unwrap().push(file_result); + results + .lock() + .expect("results mutex not poisoned") + .push(file_result); if active.fetch_sub(1, Ordering::AcqRel) == 1 { // This was the last active item; all work is done @@ -139,7 +142,7 @@ fn concurrent_discover( } }); - let mut files = results.into_inner().unwrap(); + let mut files = results.into_inner().expect("results mutex not poisoned"); files.par_sort_unstable_by(|a, b| a.path.cmp(&b.path)); let warnings = std::iter::from_fn(|| warnings.pop()).collect(); DiscoverResult { files, warnings } From 92791d4e81a260af36b7b861fe6b5e0679f8938e Mon Sep 17 00:00:00 2001 From: Harnoor Lal Date: Fri, 27 Feb 2026 10:53:17 -0800 Subject: [PATCH 3/4] fix(repl): enable cross-platform file watching backends --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b42e659..b8a6486 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ crossbeam-queue = "0.3" dashmap = "6" rustyline = "15" gix = { version = "0.79.0", default-features = false, features = ["max-performance-safe"] } -notify = { version = "7", default-features = false, features = ["macos_fsevent"] } +notify = "7" [target.'cfg(target_os = "linux")'.dependencies] tikv-jemallocator = "0.6" From fdca5aaf2eefb643037ff1a5d4c8b105f3e008e2 Mon Sep 17 00:00:00 2001 From: Harnoor Lal Date: Fri, 27 Feb 2026 10:54:33 -0800 Subject: [PATCH 4/4] docs: add v0.4.1 changelog entry --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a52668..8df1483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to chainsaw are documented here. Each release corresponds to a [milestone](https://github.com/rocketman-code/chainsaw/milestones?state=closed) on GitHub. +## [0.4.1] - 2026-02-27 + +Packaging fixes, Linux performance, cross-platform file watching. + +### Added + +- jemalloc as global allocator on Linux, reducing cold-build syscalls ~100x vs musl ([#178]) + +### Fixed + +- REPL file watching silently degraded on Linux -- only macOS FSEvents backend was compiled ([#176]) +- `tempfile` crate shipped in production binary despite being test-only ([#176]) +- Bare `.unwrap()` calls in production code now document their invariants via `expect()` ([#176]) + ## [0.4.0] - 2026-02-22 Structured output layer, REPL polish, file watching. @@ -168,6 +182,7 @@ First publish. Dependency graph analysis for TypeScript/JavaScript and Python. - Python: C extension loader precedence matching CPython - `--chain`/`--cut` when target is the entry point itself ([#69]) +[0.4.1]: https://github.com/rocketman-code/chainsaw/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/rocketman-code/chainsaw/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/rocketman-code/chainsaw/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/rocketman-code/chainsaw/compare/819d7f1...v0.2.0 @@ -266,3 +281,5 @@ First publish. Dependency graph analysis for TypeScript/JavaScript and Python. [#172]: https://github.com/rocketman-code/chainsaw/issues/172 [#173]: https://github.com/rocketman-code/chainsaw/pull/173 [#174]: https://github.com/rocketman-code/chainsaw/pull/174 +[#176]: https://github.com/rocketman-code/chainsaw/issues/176 +[#178]: https://github.com/rocketman-code/chainsaw/issues/178