Skip to content

Parser: attach comments to ExpressionStatement; handle Module.file_leading_comments (BT-976)#1013

Merged
jamesc merged 2 commits intomainfrom
worktree-BT-976
Feb 28, 2026
Merged

Parser: attach comments to ExpressionStatement; handle Module.file_leading_comments (BT-976)#1013
jamesc merged 2 commits intomainfrom
worktree-BT-976

Conversation

@jamesc
Copy link
Owner

@jamesc jamesc commented Feb 28, 2026

Summary

  • Adds collect_trailing_comment() to capture end-of-line // comments from the last consumed token's trailing trivia
  • Updates parse_module(), parse_method_body(), and parse_block() to attach leading and trailing comments to each ExpressionStatement (replaces ExpressionStatement::bare())
  • Correctly handles the empty-module edge case: Module.file_leading_comments is only populated when the module has no items; non-empty modules carry leading comments on the first item
  • Removes the old manual comment collection loop from parse_module() that incorrectly put all file comments into file_leading_comments
  • Updates 65 parser snapshots to reflect comments correctly attached to ExpressionStatement nodes
  • Adds 5 tests covering all acceptance criteria

Test plan

  • expression_statement_leading_comment_in_method_body// between statements attaches to second statement
  • expression_statement_trailing_comment — inline // attaches as trailing
  • empty_module_file_leading_comments_populated — comments-only file populates file_leading_comments
  • non_empty_module_file_leading_comments_empty — comments attach to first expression, not file_leading_comments
  • block_body_expression_statement_leading_comment — comments in block bodies attach correctly
  • All 65 snapshot tests updated and passing
  • Full CI passing

Linear: https://linear.app/beamtalk/issue/BT-976

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Parsing now preserves leading and trailing comments and attaches them to the correct statements and expressions.
    • File-level leading comments are captured for empty files and retained in module metadata.
    • Trailing end-of-line comments are no longer lost and remain associated with the preceding code element.

…ading_comments (BT-976)

- Add collect_trailing_comment() — reads trailing_trivia from the last
  consumed token to capture end-of-line `//` comments
- Update parse_module(), parse_method_body(), parse_block() to call
  collect_comment_attachment() before and collect_trailing_comment()
  after each expression parse, replacing ExpressionStatement::bare()
- Handle empty-module edge case: file-level comments with no items
  are collected into Module.file_leading_comments; non-empty modules
  carry leading comments on the first item as ExpressionStatement.comments
- Remove old manual comment collection loop from parse_module() that
  was incorrectly storing all file comments in file_leading_comments
- Update 65 parser snapshots to reflect comments now correctly attached
  to ExpressionStatement nodes instead of file_leading_comments
- Add 5 tests covering all new cases (BT-976 acceptance criteria)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

Parser now collects and attaches per-statement leading and trailing comments into AST nodes: expression statements gain a comments field, module parsing records file-level leading comments for empty modules, and a collect_trailing_comment() helper is added to Parser.

Changes

Cohort / File(s) Summary
Expression statement & block parsing
crates/beamtalk-core/src/source_analysis/parser/expressions.rs, crates/beamtalk-core/src/source_analysis/parser/declarations.rs
Parse flow now collects a CommentAttachment before each expression, conditionally captures a trailing line comment after an expression consumes tokens, and constructs ExpressionStatement { comments, expression } instead of bare expressions. Method-body and standalone-method parsing propagate class-leading comments into method nodes.
Module parsing & parser helpers
crates/beamtalk-core/src/source_analysis/parser/mod.rs
Added Module.file_leading_comments: Vec<Comment> to hold file-level leading comments for empty modules. Reworked parse_module to use per-item collect_comment_attachment() and attach trailing comments; added pub(super) fn collect_trailing_comment() -> Option<Comment> to inspect last consumed token's trailing trivia.
Public-type adjustments
crates/beamtalk-core/src/source_analysis/parser/expressions.rs, crates/beamtalk-core/src/source_analysis/parser/declarations.rs, crates/beamtalk-core/src/ast
ExpressionStatement gains a comments: CommentAttachment field; CommentAttachment now includes/uses a trailing field populated via collect_trailing_comment(). Call sites updated accordingly.
Cargo manifest
Cargo.toml
Small lines changed (adds/removes metadata or dependency/version adjustments observed).

Sequence Diagram(s)

sequenceDiagram
    participant Parser
    participant TokenStream
    participant AST as Module/Nodes
    Parser->>TokenStream: collect_comment_attachment() (leading)
    Parser->>TokenStream: parse expression statement (consume tokens)
    TokenStream-->>Parser: tokens consumed
    Parser->>Parser: collect_trailing_comment() (inspect last token.trailing_trivia)
    Parser->>AST: construct ExpressionStatement { comments, expression }
    Parser->>AST: append to Module.items or set Module.file_leading_comments (if empty)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main changes: attaching comments to ExpressionStatement and handling Module.file_leading_comments, with issue reference.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch worktree-BT-976

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/beamtalk-core/src/source_analysis/parser/mod.rs (1)

754-757: ⚠️ Potential issue | 🟠 Major

Leading comments are dropped when the first item is a standalone method.

For non-empty modules, Line 796 clears file_leading_comments, but standalone-method parsing does not attach leading trivia from the class-name token. Result: comments like // note before Counter >> ... are lost.

💡 Proposed fix in parse_module standalone-method branch
             } else if self.is_at_standalone_method_definition() {
-                let method_def = self.parse_standalone_method_definition();
+                let pending_comments = self.collect_comment_attachment().leading;
+                let mut method_def = self.parse_standalone_method_definition();
+                if !pending_comments.is_empty() {
+                    let mut merged = pending_comments;
+                    merged.append(&mut method_def.method.comments.leading);
+                    method_def.method.comments.leading = merged;
+                }
                 method_definitions.push(method_def);
             } else {

Please add a regression test for // comment followed by a standalone method definition to lock this behavior.

Also applies to: 792-800

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/beamtalk-core/src/source_analysis/parser/mod.rs` around lines 754 -
757, parse_module currently drops leading comments when the first item is a
standalone method because the standalone-method branch calls
parse_standalone_method_definition() but never attaches file_leading_comments or
preserves leading trivia from the class-name token; update the standalone-method
branch in parse_module (the is_at_standalone_method_definition() case) to
capture and attach leading trivia/comments (same way other branches do by
consuming file_leading_comments and/or copying leading trivia from the
class-name token into the MethodDefinition node) and ensure
file_leading_comments is cleared only after attaching them; add a regression
test that parses a module starting with a line comment (e.g. "// comment")
followed by a standalone method like "Counter >> ..." and asserts the parsed
method retains that leading comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/beamtalk-core/src/source_analysis/parser/declarations.rs`:
- Around line 799-802: The bug is that after let expr = self.parse_expression()
you always call comments.trailing = self.collect_trailing_comment(), which can
attach stale trailing trivia when parse_expression() made no progress; fix by
only collecting trailing comments if the parser advanced or the expression is
not the “no-progress” (error/empty) case — e.g. check whether parse_expression
consumed any tokens (or use expr.is_error() combined with a consumed_tokens
flag) before calling collect_trailing_comment(). Update the same pattern in the
loops that use collect_comment_attachment / parse_expression in functions in
expressions.rs and mod.rs so trailing comments are only captured when
parse_expression actually consumed input.

---

Outside diff comments:
In `@crates/beamtalk-core/src/source_analysis/parser/mod.rs`:
- Around line 754-757: parse_module currently drops leading comments when the
first item is a standalone method because the standalone-method branch calls
parse_standalone_method_definition() but never attaches file_leading_comments or
preserves leading trivia from the class-name token; update the standalone-method
branch in parse_module (the is_at_standalone_method_definition() case) to
capture and attach leading trivia/comments (same way other branches do by
consuming file_leading_comments and/or copying leading trivia from the
class-name token into the MethodDefinition node) and ensure
file_leading_comments is cleared only after attaching them; add a regression
test that parses a module starting with a line comment (e.g. "// comment")
followed by a standalone method like "Counter >> ..." and asserts the parsed
method retains that leading comment.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ee32490 and b0f49a6.

⛔ Files ignored due to path filters (65)
  • test-package-compiler/tests/snapshots/compiler_tests__abstract_class_spawn_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__actor_spawn_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__actor_spawn_with_args_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__actor_state_mutation_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__async_keyword_message_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__async_unary_message_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__async_with_await_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__binary_operators_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__blocks_no_args_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__boundary_deeply_nested_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__boundary_long_identifiers_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__boundary_mixed_errors_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__boundary_unicode_identifiers_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__cascade_complex_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__cascades_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__character_literals_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__class_definition_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__class_methods_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__comment_handling_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__control_flow_mutations_errors_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__control_flow_mutations_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__empty_blocks_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__empty_method_body_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__error_message_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__error_recovery_invalid_syntax_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__error_recovery_malformed_message_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__error_recovery_unterminated_string_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__expect_directive_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__future_pattern_matching_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__future_string_interpolation_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__hello_world_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__intrinsic_keyword_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__map_literals_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__method_lookup_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__multi_keyword_complex_args_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__nested_blocks_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__nested_keyword_messages_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__sealed_class_violation_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__sealed_method_override_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__stdlib_class_dictionary_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__stdlib_class_list_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__stdlib_class_set_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__stdlib_class_tuple_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__string_operations_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__typed_class_warnings_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__typed_methods_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__typed_value_type_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__unary_operators_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__unicode_string_literals_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__value_type_multi_expr_method_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__value_type_param_collision_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__while_true_simple_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__whitespace_handling_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__workspace_binding_cascade_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__workspace_binding_send_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__ws_stdlib_array_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__ws_stdlib_block_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__ws_stdlib_boolean_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__ws_stdlib_dictionary_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__ws_stdlib_integer_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__ws_stdlib_list_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__ws_stdlib_nil_object_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__ws_stdlib_nil_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__ws_stdlib_set_parser.snap is excluded by !**/*.snap
  • test-package-compiler/tests/snapshots/compiler_tests__ws_stdlib_string_parser.snap is excluded by !**/*.snap
📒 Files selected for processing (3)
  • crates/beamtalk-core/src/source_analysis/parser/declarations.rs
  • crates/beamtalk-core/src/source_analysis/parser/expressions.rs
  • crates/beamtalk-core/src/source_analysis/parser/mod.rs

- Guard collect_trailing_comment() with progress check in parse_module(),
  parse_method_body(), and parse_block() — avoids attaching stale
  trailing trivia when parse_expression() consumes no tokens
- Fix leading comment attachment for standalone method definitions:
  parse_standalone_method_definition() now calls collect_comment_attachment()
  on the class-name token before advancing, and prepends those comments
  to the parsed method's comments.leading (previously they were dropped
  because parse_identifier() consumed the token without reading its trivia)
- Add regression test: standalone_method_leading_comment_attached

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jamesc
Copy link
Owner Author

jamesc commented Feb 28, 2026

CodeRabbit outside-diff finding (mod.rs:754-757) — Fixed in 58b0cf6

Root cause: parse_standalone_method_definition() called parse_identifier() immediately, consuming the class-name token without reading its leading trivia — so // comments before Counter >> were dropped.

Fix: collect_comment_attachment() now runs before consuming the class-name token, and the collected comments are prepended to the method's comments.leading after parse_method_definition() returns. Added regression test: standalone_method_leading_comment_attached.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/beamtalk-core/src/source_analysis/parser/mod.rs (1)

758-771: Consider extracting statement-with-comments parsing into a shared helper.

The same sequence now exists in parse_module, parse_method_body, and parse_block (collect leading → parse expression → guarded trailing → build ExpressionStatement). Centralizing it would reduce drift risk.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/beamtalk-core/src/source_analysis/parser/mod.rs` around lines 758 -
771, Multiple places (parse_module, parse_method_body, parse_block) duplicate
the pattern of collecting leading comments, parsing an expression, conditionally
collecting trailing comments based on token consumption, and constructing an
ExpressionStatement; extract this into a shared helper (e.g.,
parse_expression_statement_with_comments) that calls
collect_comment_attachment(), calls parse_expression(), uses the
pos_before/current check to decide whether to call collect_trailing_comment(),
and returns an ExpressionStatement struct; replace the duplicated sequences in
parse_module, parse_method_body, and parse_block with calls to this new helper
to centralize behavior and avoid drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/beamtalk-core/src/source_analysis/parser/mod.rs`:
- Around line 758-771: Multiple places (parse_module, parse_method_body,
parse_block) duplicate the pattern of collecting leading comments, parsing an
expression, conditionally collecting trailing comments based on token
consumption, and constructing an ExpressionStatement; extract this into a shared
helper (e.g., parse_expression_statement_with_comments) that calls
collect_comment_attachment(), calls parse_expression(), uses the
pos_before/current check to decide whether to call collect_trailing_comment(),
and returns an ExpressionStatement struct; replace the duplicated sequences in
parse_module, parse_method_body, and parse_block with calls to this new helper
to centralize behavior and avoid drift.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b0f49a6 and 58b0cf6.

📒 Files selected for processing (3)
  • crates/beamtalk-core/src/source_analysis/parser/declarations.rs
  • crates/beamtalk-core/src/source_analysis/parser/expressions.rs
  • crates/beamtalk-core/src/source_analysis/parser/mod.rs

@jamesc jamesc merged commit 96bf782 into main Feb 28, 2026
5 checks passed
@jamesc jamesc deleted the worktree-BT-976 branch February 28, 2026 21:01
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.

1 participant