Skip to content

fix AST dependency analysis coverage#523

Open
tinovyatkin wants to merge 3 commits into
mainfrom
codex/fix-509-ast-dependency-analysis
Open

fix AST dependency analysis coverage#523
tinovyatkin wants to merge 3 commits into
mainfrom
codex/fix-509-ast-dependency-analysis

Conversation

@tinovyatkin

Copy link
Copy Markdown
Collaborator

Summary

  • replace partial expression and statement matches in GraphBuilder with Ruff's exhaustive AST visitor
  • preserve assignment, delete, import, global, and attribute-access dependency semantics while traversing all expression variants
  • add snapshot coverage for named, lambda, await, yield, yield-from, starred, slice, annotated assignment, augmented assignment, delete, and type-alias dependencies

Root cause

The dependency collector silently ignored AST variants through fallback match arms, so runtime references nested under those nodes never reached tree shaking and their definitions could be removed.

Impact

Valid Python constructs now retain every runtime definition they reference. The visitor also delegates unknown future AST structure to Ruff's walker instead of silently dropping it.

Validation

  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo nextest run --workspace -E 'not test(test_ecosystem_all)' (173 passed)
  • focused snapshot fixtures for AST coverage and structural match dependencies

The full local suite was also attempted; only test_ecosystem_all failed because the pre-existing ecosystem package checkouts do not contain their expected source trees.

Fixes #509

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@tinovyatkin, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9ad6fa36-ddcc-4d77-bb7f-bb88eb2fbcd4

📥 Commits

Reviewing files that changed from the base of the PR and between 0e85b64 and 66570da.

⛔ Files ignored due to path filters (6)
  • crates/cribo/tests/fixtures/tree_shake_ast_dependency_coverage/main.py is excluded by !**/tests/fixtures/**
  • crates/cribo/tests/fixtures/tree_shake_ast_dependency_coverage/operations.py is excluded by !**/tests/fixtures/**
  • crates/cribo/tests/snapshots/bundled_code@tree_shake_ast_dependency_coverage.snap is excluded by !**/*.snap
  • crates/cribo/tests/snapshots/execution_results@tree_shake_ast_dependency_coverage.snap is excluded by !**/*.snap
  • crates/cribo/tests/snapshots/requirements@tree_shake_ast_dependency_coverage.snap is excluded by !**/*.snap
  • crates/cribo/tests/snapshots/ruff_lint_results@tree_shake_ast_dependency_coverage.snap is excluded by !**/*.snap
📒 Files selected for processing (1)
  • crates/cribo/src/graph_builder.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-509-ast-dependency-analysis

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.


async def exercise() -> str:
named = (result := named_value())
callback = lambda: lambda_value()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping this lambda intentionally: the fixture exists to exercise dependency traversal through Expr::Lambda; replacing it with lambda_value would remove that regression coverage while preserving only output behavior.

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

📊 Ecosystem Test Results

📋 Test Status

Test Summary:

  • Total: 49
  • ✅ Passed: 48
  • ❌ Failed: 0
  • ⚠️ Errors: 0
  • ⏭️ Skipped: 1

📈 Benchmark Results

📊 View detailed benchmark report

📦 Package Bundling Metrics
Package Bundle Time Bundle Size
httpx 270.93 ms 321.42 KB
idna 54.53 ms 245.81 KB
pyyaml 378.23 ms 237.84 KB
requests 169.9 ms 216.71 KB
rich 1052.7 ms 951.26 KB

Benchmark metrics are tracked via Bencher.dev

📊 View detailed performance trends and comparisons on the Bencher dashboard.

Generated by ecosystem-tests workflow

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchcodex/fix-509-ast-dependency-analysis
Testbedubuntu-latest
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
nanoseconds (ns)
(Result Δ%)
Upper Boundary
nanoseconds (ns)
(Limit %)
build_dependency_graph📈 view plot
🚷 view threshold
612.93 ns
(-92.26%)Baseline: 7,920.34 ns
13,018.24 ns
(4.71%)
bundle_simple_project📈 view plot
🚷 view threshold
1,031,100.00 ns
(-82.83%)Baseline: 6,004,598.12 ns
25,934,677.17 ns
(3.98%)
resolve_module_path📈 view plot
🚷 view threshold
119.64 ns
(+7.66%)Baseline: 111.13 ns
144.31 ns
(82.91%)
🐰 View full continuous benchmarking report in Bencher

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refactors the core AST dependency analysis logic within the GraphBuilder by integrating Ruff's comprehensive AST visitor. This change significantly enhances the accuracy and completeness of runtime dependency detection, addressing a root cause where certain Python AST constructs were previously overlooked. The improved analysis ensures that all relevant definitions are correctly identified for tree-shaking, leading to more robust and reliable code bundling.

Highlights

  • Improved AST Dependency Analysis: The custom partial expression and statement matching in GraphBuilder has been replaced with Ruff's exhaustive AST visitor, ensuring more comprehensive dependency detection.
  • Enhanced Coverage for Runtime References: The dependency collector now correctly preserves assignment, delete, import, global, and attribute-access dependency semantics across all expression variants, preventing silent omissions.
  • Expanded Snapshot Testing: New snapshot tests have been added to cover named, lambda, await, yield, yield-from, starred, slice, annotated assignment, augmented assignment, delete, and type-alias dependencies, validating the improved coverage.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the AST dependency collection logic in graph_builder.rs by introducing a DependencyCollector that implements Ruff's Visitor trait, replacing manual traversal helpers. A bug was identified in extract_dotted_name where reversing the collected parts of a dotted name results in an incorrect path, and a suggestion was made to simplify the logic by reusing build_full_dotted_name.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread crates/cribo/src/graph_builder.rs Outdated
Comment on lines +146 to +189
if let Some(full_name) = Self::extract_dotted_name(attribute) {
self.read_vars.insert(full_name.clone());
if full_name.contains('.') {
let root = full_name
.split('.')
.next()
.expect("full_name should have at least one part");
self.read_vars.insert(root.to_owned());
}
}
}

fn build_full_dotted_name(expr: &Expr) -> Option<String> {
match expr {
Expr::Name(name) => Some(name.id.to_string()),
Expr::Attribute(attribute) => Self::build_full_dotted_name(&attribute.value)
.map(|base| format!("{}.{}", base, attribute.attr)),
_ => None,
}
}

fn extract_dotted_name(attribute: &ast::ExprAttribute) -> Option<String> {
fn build_dotted_name(expr: &Expr, parts: &mut Vec<String>) -> bool {
match expr {
Expr::Name(name) => {
parts.push(name.id.to_string());
true
}
Expr::Attribute(attribute) if build_dotted_name(&attribute.value, parts) => {
parts.push(attribute.attr.to_string());
true
}
_ => false,
}
}

let mut parts = Vec::new();
if build_dotted_name(&attribute.value, &mut parts) {
parts.reverse();
Some(parts.join("."))
} else {
None
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a bug in extract_dotted_name where it calls parts.reverse(). Because build_dotted_name is recursive and processes the base Expr::Name first, the elements are already pushed in the correct left-to-right order (e.g., ["xml", "etree", "ElementTree"]). Reversing them results in a reversed path like "ElementTree.etree.xml", which causes incorrect dependency resolution and root module extraction.

Instead of fixing extract_dotted_name, we can completely eliminate it and reuse build_full_dotted_name(&attribute.value), which is already correct, simpler, and recursive.

Suggested change
if let Some(full_name) = Self::extract_dotted_name(attribute) {
self.read_vars.insert(full_name.clone());
if full_name.contains('.') {
let root = full_name
.split('.')
.next()
.expect("full_name should have at least one part");
self.read_vars.insert(root.to_owned());
}
}
}
fn build_full_dotted_name(expr: &Expr) -> Option<String> {
match expr {
Expr::Name(name) => Some(name.id.to_string()),
Expr::Attribute(attribute) => Self::build_full_dotted_name(&attribute.value)
.map(|base| format!("{}.{}", base, attribute.attr)),
_ => None,
}
}
fn extract_dotted_name(attribute: &ast::ExprAttribute) -> Option<String> {
fn build_dotted_name(expr: &Expr, parts: &mut Vec<String>) -> bool {
match expr {
Expr::Name(name) => {
parts.push(name.id.to_string());
true
}
Expr::Attribute(attribute) if build_dotted_name(&attribute.value, parts) => {
parts.push(attribute.attr.to_string());
true
}
_ => false,
}
}
let mut parts = Vec::new();
if build_dotted_name(&attribute.value, &mut parts) {
parts.reverse();
Some(parts.join("."))
} else {
None
}
}
if let Some(full_name) = Self::build_full_dotted_name(&attribute.value) {
self.read_vars.insert(full_name.clone());
if full_name.contains('.') {
let root = full_name
.split('.')
.next()
.expect("full_name should have at least one part");
self.read_vars.insert(root.to_owned());
}
}
}
fn build_full_dotted_name(expr: &Expr) -> Option<String> {
match expr {
Expr::Name(name) => Some(name.id.to_string()),
Expr::Attribute(attribute) => Self::build_full_dotted_name(&attribute.value)
.map(|base| format!("{}.{}", base, attribute.attr)),
_ => None,
}
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Addressed in eaad8f6. track_attribute_access now reuses build_full_dotted_name(&attribute.value), and the reversed helper has been removed.

@tinovyatkin tinovyatkin changed the title [codex] fix AST dependency analysis coverage fix AST dependency analysis coverage Jul 15, 2026
@tinovyatkin tinovyatkin marked this pull request as ready for review July 15, 2026 22:25
Copilot AI review requested due to automatic review settings July 15, 2026 22:25
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

📊 Source Code Metrics (this PR vs main)

File Cyclomatic Cognitive Functions LLOC MI
crates/cribo/src/graph_builder.rs 255 (main: 303) 🟢 207 (main: 347) 🟢 40 (main: 37) 🔴 426 (main: 522) 🟢 0 ⚪

Generated by mehen v0.6.0 — the code quality watcher.

@tinovyatkin

Copy link
Copy Markdown
Collaborator Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a core correctness gap in Cribo’s tree-shaking dependency analysis by replacing incomplete, hand-written AST matching in GraphBuilder with Ruff’s exhaustive AST visitor traversal. This ensures runtime references nested under previously-unhandled AST variants are consistently collected, preventing valid code from losing required definitions during tree shaking.

Changes:

  • Refactor dependency collection in crates/cribo/src/graph_builder.rs to use a DependencyCollector implementing Ruff’s Visitor, eliminating silent fallback arms that skipped AST variants.
  • Preserve/encode dependency semantics for assignments, deletes, imports, globals, and attribute-access tracking while delegating the remainder of traversal to Ruff’s walker.
  • Add a new snapshot fixture (tree_shake_ast_dependency_coverage) plus snapshots covering the newly-supported AST shapes end-to-end (bundled code, execution, requirements, Ruff lint results).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
crates/cribo/src/graph_builder.rs Replaces partial AST matching with a Ruff Visitor-based dependency collector for complete traversal and consistent dependency capture.
crates/cribo/tests/fixtures/tree_shake_ast_dependency_coverage/main.py New fixture entrypoint to exercise dependency collection across additional AST variants.
crates/cribo/tests/fixtures/tree_shake_ast_dependency_coverage/operations.py New fixture module containing constructs like named expressions, lambda/await/yield, starred/slice, annotated/augmented assignments, delete, and type alias.
crates/cribo/tests/snapshots/bundled_code@tree_shake_ast_dependency_coverage.snap Snapshot of emitted bundle for the new AST-coverage fixture.
crates/cribo/tests/snapshots/execution_results@tree_shake_ast_dependency_coverage.snap Snapshot asserting the bundled output executes and matches expected stdout.
crates/cribo/tests/snapshots/requirements@tree_shake_ast_dependency_coverage.snap Snapshot confirming no third-party requirements are detected for the fixture.
crates/cribo/tests/snapshots/ruff_lint_results@tree_shake_ast_dependency_coverage.snap Snapshot confirming Ruff reports no lint issues in the bundled output for this fixture.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a719037c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/cribo/src/graph_builder.rs Outdated
if let Some(value) = &ann_assign.value {
self.visit_expr(value);
}
self.visit_annotation(&ann_assign.annotation);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not treat local annotations as runtime reads

When this visitor is called from collect_vars_in_body for function or method bodies, x: SomeType does not evaluate SomeType at runtime in Python, but this line visits the annotation and adds it to eventual_read_vars. For called functions with local variable annotations referencing otherwise unused first-party symbols, tree-shaking will now retain those symbols even though executing the function never needs them, so the AST coverage fix regresses pruning accuracy.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Addressed in eaad8f6. Function and method bodies now skip local annotation reads; nested classes re-enable runtime annotation dependencies, with snapshot coverage for both pruning and retention.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eaad8f6876

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/cribo/src/graph_builder.rs Outdated
self.visit_assignment_target(&ann_assign.target);
}
Stmt::ClassDef(_) => self.visit_nested_scope(stmt, true),
Stmt::FunctionDef(_) => self.visit_nested_scope(stmt, false),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve nested annotation dependencies

When a reachable function contains a nested def inner(x: make_type()): ... (or a method signature in a nested class) and does not otherwise read make_type, executing the outer function still evaluates that signature annotation on the nested definition under the default Python semantics. This branch sets annotations_are_runtime to false before walking the entire FunctionDef, so visit_parameters and the return annotation are skipped and tree-shaking can drop the annotation dependency, leaving bundled code that raises NameError or loses annotation side effects when the outer function runs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Addressed in 66570da. Nested function and class definition-time metadata now traverses before switching local variable-annotation semantics for the body. The snapshot fixture now proves both nested function and nested method signature call annotations are retained, while function/method-local annotation-only helpers are still pruned. Validation: focused snapshot fixture, full clippy, and 173 non-ecosystem tests pass.

@tinovyatkin

Copy link
Copy Markdown
Collaborator Author

@codex review

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dependency analysis silently ignores valid Python AST variants

2 participants