Skip to content

Merge YARD macro support#1194

Merged
castwide merged 5 commits into
masterfrom
yard-macros
May 28, 2026
Merged

Merge YARD macro support#1194
castwide merged 5 commits into
masterfrom
yard-macros

Conversation

@castwide
Copy link
Copy Markdown
Owner

No description provided.

Comment thread spec/fixtures/gem-with-yard-macros/gem-with-yard-macros.gemspec
Comment thread spec/source_map/clip_spec.rb Outdated
lekemula and others added 5 commits May 28, 2026 02:30
* Port macros from lekemula/solargraph@lm-named-macros (4572e07..389def6)

Original 11-commit diff:
lekemula/solargraph@524c94e...389def6

Squashes the original branch and ports it onto current upstream/master,
where the YardMap class was gutted and replaced with DocMap/GemPins
(upstream 94006fb).

Differences from the original implementation:

- Parser layer: original work added `simple_convert` and `process_dsl_method`
  to `parser/rubyvm/{node_methods,node_processors/send_node}`. Upstream
  removed the rubyvm parser entirely. Rewrote both for the parser_gem AST
  shape: lowercase node types (`:send`, `:hash`, `:const`, `:array`),
  `:send` children indexed as `[receiver, method_name, *args]`, literals
  split into `:int`/`:float`/`:sym`/`:str` instead of `:LIT`.

- ApiMap integration: original `process_macros(pins)` hooked into a `pins`
  parameter that no longer exists. Adapted to the new `catalog(bench)`
  flow — consumes `iced_pins + live_pins + doc_map.pins`, filters
  `Pin::Ephemeral::ClassMethodSend` from iced and live separately before
  the store update. Kept the original logging.

- MethodDirective: original `Parser.process_node(...).first.last`
  regressed `spec/source_map/mapper_spec.rb:89`. Upstream had since added
  a `Pin::Method` filter inline; backported that into the extracted
  directive module.

- Spec relocation: `spec/yard_map_spec.rb` was deleted upstream. The
  `loads macros from gems` test moved to `spec/yard_map/mapper_spec.rb`
  and uses the new `pins_with(name)` (DocMap-based) helper. Assertion
  tightened from `macros.count > 0` to checking that the
  `MyStruct.my_attribute` method pin exists and exposes the macro by
  name.

- All other new files (Macro, Directives::*, Pin::Ephemeral::*,
  gem-with-yard-macros fixture, api_map_spec/clip_spec additions) landed
  unchanged from the squashed branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix invalid gemspec for gem-with-yard-macros fixture

The skeleton gemspec from `bundle gem` left TODO placeholders in
summary, description, homepage, and metadata fields, which Bundler
rejects in CI. Replaced with real values describing the fixture's
purpose and trimmed the file list to `lib/**/*.rb` so it doesn't depend
on `git ls-files` working in the CI checkout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix rubocop offenses

- Autocorrected style issues across the new/ported files (string quoting,
  empty-method one-liners, redundant cop disables, def-without-parens, etc).
- Excluded the gem-with-yard-macros fixture from rubocop entirely; it's a
  `bundle gem` skeleton that exists to be loaded as a gem, not as project
  source.
- Bumped Metrics/ModuleLength.Max in the todo file from 167 to 195 to
  accommodate the simple_convert helpers added to ParserGem::NodeMethods.
- Cleaned up YARD `@param` mismatches in Macro and ClassMethodSend, and
  rewrote one multi-line block chain in Macro#generate_yardoc_from.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Trim gem-with-yard-macros fixture to essentials

Removed the `bundle gem` skeleton boilerplate (LICENSE, README, CHANGELOG,
CODE_OF_CONDUCT, Rakefile, bin/, the gem's own Gemfile/Gemfile.lock, RBS
sig, .gitignore). None are needed: the fixture exists only to be resolved
as a path gem and have its YARD macro loaded. What remains is the
gemspec, the macro definition, and version.rb.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Set source: :yard_map on directive-generated pins

`Pin::Base#assert_source_provided` raises (under SOLARGRAPH_ASSERTS=on,
as the overcommit CI job runs) when a pin is created without a `source:`.
The extracted attribute/override directive modules built `Pin::Method`,
`Pin::Parameter`, and `Pin::Reference::Override` pins without one.
Tagged them `:yard_map` since they originate from YARD `@!` directives.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix pre-existing rubocop offenses in untouched files

The `.rubocop_todo.yml` CI job runs `rubocop -c .rubocop.yml` across the
whole repo and was failing on 8 offenses unrelated to this PR. Fixed them
in place rather than suppressing:

- Style/ArgumentsForwarding: anonymous block forwarding (`&`) in
  Solargraph.with_clean_env, UniqueType#each, Host#show_message_request.
- Style/ArrayIntersect: `(a & b).any?` -> `a.intersect?(b)` in
  TypeChecker#parameterized_arity_problems_for.
- Lint/UnreachableCode: the body of Pin::Method#combine_same_type_arity_
  signatures is intentionally preserved behind a debug stub `return`
  (upstream 6d8ce95); wrapped it in a scoped rubocop:disable with a
  comment explaining why, instead of deleting the kept code.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix ArgumentValue struct init on Ruby < 3.2

`ArgumentValue = Struct.new(:value)` was constructed with a keyword
argument (`ArgumentValue.new(value: ...)`). On Ruby 3.1 a plain Struct
treats that as a positional Hash, so `#value` returned `{ value: x }`
instead of `x`. That garbled `ClassMethodSend#argument_values`, which
shifted every macro placeholder (`$1`, `$2`, ...) — producing method
pins like `value` and dropping real ones. Added `keyword_init: true`.

Fixes the 6 macro specs failing on the Ruby 3.1 CI matrix job.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Drop 'head' from RSpec matrix temporarily

ruby/setup-ruby@v1 currently 404s on `head` for ubuntu-24.04
("Unavailable version head for ruby"). Removed it from the matrix so CI
isn't blocked; left a @todo to restore once setup-ruby publishes it.

See: https://github.com/castwide/solargraph/actions/runs/25863741955/job/76000137015?pr=1187

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix strong typechecking

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* More typecheck fixes

* Even more typecheck fixes - is this a dead code?

* ApiMap fix

* Mapper fix

/home/runner/work/solargraph/solargraph/lib/solargraph/yard_map/mapper.rb:28
- Unresolved call to filename on Solargraph::Location, nil

* NodeMethods fix

/home/runner/work/solargraph/solargraph/lib/solargraph/parser/parser_gem/node_methods.rb:107
- Declared return type ::String, ::Integer, ::Float, ::Symbol, ::Array,
::Hash, ::Solargraph::Source::Chain, nil does not match inferred type
::String, ::Parser::AST::Node, ::Array, ::Hash,
::Solargraph::Source::Chain, nil for
Solargraph::Parser::ParserGem::NodeMethods.simple_convert
/home/runner/work/solargraph/solargraph/lib/solargraph/parser/parser_gem/node_methods.rb:107
- Declared return type ::String, ::Integer, ::Float, ::Symbol, ::Array,
::Hash, ::Solargraph::Source::Chain, nil does not match inferred type
::String, ::Parser::AST::Node, ::Array, ::Hash,
::Solargraph::Source::Chain, nil for
Solargraph::Parser::ParserGem::NodeMethods#simple_convert
* Port macros from lekemula/solargraph@lm-named-macros (4572e07..389def6)

Original 11-commit diff:
lekemula/solargraph@524c94e...389def6

Squashes the original branch and ports it onto current upstream/master,
where the YardMap class was gutted and replaced with DocMap/GemPins
(upstream 94006fb).

Differences from the original implementation:

- Parser layer: original work added `simple_convert` and `process_dsl_method`
  to `parser/rubyvm/{node_methods,node_processors/send_node}`. Upstream
  removed the rubyvm parser entirely. Rewrote both for the parser_gem AST
  shape: lowercase node types (`:send`, `:hash`, `:const`, `:array`),
  `:send` children indexed as `[receiver, method_name, *args]`, literals
  split into `:int`/`:float`/`:sym`/`:str` instead of `:LIT`.

- ApiMap integration: original `process_macros(pins)` hooked into a `pins`
  parameter that no longer exists. Adapted to the new `catalog(bench)`
  flow — consumes `iced_pins + live_pins + doc_map.pins`, filters
  `Pin::Ephemeral::ClassMethodSend` from iced and live separately before
  the store update. Kept the original logging.

- MethodDirective: original `Parser.process_node(...).first.last`
  regressed `spec/source_map/mapper_spec.rb:89`. Upstream had since added
  a `Pin::Method` filter inline; backported that into the extracted
  directive module.

- Spec relocation: `spec/yard_map_spec.rb` was deleted upstream. The
  `loads macros from gems` test moved to `spec/yard_map/mapper_spec.rb`
  and uses the new `pins_with(name)` (DocMap-based) helper. Assertion
  tightened from `macros.count > 0` to checking that the
  `MyStruct.my_attribute` method pin exists and exposes the macro by
  name.

- All other new files (Macro, Directives::*, Pin::Ephemeral::*,
  gem-with-yard-macros fixture, api_map_spec/clip_spec additions) landed
  unchanged from the squashed branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix invalid gemspec for gem-with-yard-macros fixture

The skeleton gemspec from `bundle gem` left TODO placeholders in
summary, description, homepage, and metadata fields, which Bundler
rejects in CI. Replaced with real values describing the fixture's
purpose and trimmed the file list to `lib/**/*.rb` so it doesn't depend
on `git ls-files` working in the CI checkout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix rubocop offenses

- Autocorrected style issues across the new/ported files (string quoting,
  empty-method one-liners, redundant cop disables, def-without-parens, etc).
- Excluded the gem-with-yard-macros fixture from rubocop entirely; it's a
  `bundle gem` skeleton that exists to be loaded as a gem, not as project
  source.
- Bumped Metrics/ModuleLength.Max in the todo file from 167 to 195 to
  accommodate the simple_convert helpers added to ParserGem::NodeMethods.
- Cleaned up YARD `@param` mismatches in Macro and ClassMethodSend, and
  rewrote one multi-line block chain in Macro#generate_yardoc_from.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Trim gem-with-yard-macros fixture to essentials

Removed the `bundle gem` skeleton boilerplate (LICENSE, README, CHANGELOG,
CODE_OF_CONDUCT, Rakefile, bin/, the gem's own Gemfile/Gemfile.lock, RBS
sig, .gitignore). None are needed: the fixture exists only to be resolved
as a path gem and have its YARD macro loaded. What remains is the
gemspec, the macro definition, and version.rb.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Set source: :yard_map on directive-generated pins

`Pin::Base#assert_source_provided` raises (under SOLARGRAPH_ASSERTS=on,
as the overcommit CI job runs) when a pin is created without a `source:`.
The extracted attribute/override directive modules built `Pin::Method`,
`Pin::Parameter`, and `Pin::Reference::Override` pins without one.
Tagged them `:yard_map` since they originate from YARD `@!` directives.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix pre-existing rubocop offenses in untouched files

The `.rubocop_todo.yml` CI job runs `rubocop -c .rubocop.yml` across the
whole repo and was failing on 8 offenses unrelated to this PR. Fixed them
in place rather than suppressing:

- Style/ArgumentsForwarding: anonymous block forwarding (`&`) in
  Solargraph.with_clean_env, UniqueType#each, Host#show_message_request.
- Style/ArrayIntersect: `(a & b).any?` -> `a.intersect?(b)` in
  TypeChecker#parameterized_arity_problems_for.
- Lint/UnreachableCode: the body of Pin::Method#combine_same_type_arity_
  signatures is intentionally preserved behind a debug stub `return`
  (upstream 6d8ce95); wrapped it in a scoped rubocop:disable with a
  comment explaining why, instead of deleting the kept code.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix ArgumentValue struct init on Ruby < 3.2

`ArgumentValue = Struct.new(:value)` was constructed with a keyword
argument (`ArgumentValue.new(value: ...)`). On Ruby 3.1 a plain Struct
treats that as a positional Hash, so `#value` returned `{ value: x }`
instead of `x`. That garbled `ClassMethodSend#argument_values`, which
shifted every macro placeholder (`$1`, `$2`, ...) — producing method
pins like `value` and dropping real ones. Added `keyword_init: true`.

Fixes the 6 macro specs failing on the Ruby 3.1 CI matrix job.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Drop 'head' from RSpec matrix temporarily

ruby/setup-ruby@v1 currently 404s on `head` for ubuntu-24.04
("Unavailable version head for ruby"). Removed it from the matrix so CI
isn't blocked; left a @todo to restore once setup-ruby publishes it.

See: https://github.com/castwide/solargraph/actions/runs/25863741955/job/76000137015?pr=1187

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix strong typechecking

* Resolve paths for macro methods

* Handle macros dynamically

* Reinstate optional cache clearing

* Deprecate Ephemeral::ClassMethodSend

* Avoid chains for macro resolution when possible

* Minor refactor

* Pending specs

* Clarify macro spec for keyword arguments

* Linting

* Erroneous reversions

* Typechecking

* Minor spec tweak for Ruby 3.x

* More erroneous reversions

---------

Co-authored-by: Lekë Mula <l.mula@finlink.de>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* Unused macro methods

* Update Ruby to 4.0 in typecheck workflow

* Revert unneeded sg-ignore

* Update to Ruby 4.0 in plugins workflow
* Infer method calls with parameter names in return types

* Retain existing tags

* JIT macro expansion

* Expand types from chain calls
@castwide castwide merged commit 13b7a0e into master May 28, 2026
26 of 28 checks passed
@lekemula
Copy link
Copy Markdown
Contributor

lekemula commented May 29, 2026

Hey @castwide, I tried to test this today on my project and I found something strange. The LSP was stuck in a seemingly endless loop, and it was never finishing cataloging.

These are the last 500lines of the logs, which seem to be kept looping over and over again:

solargraph_main.log

One thing that caught my eye, in the whole river of the logs, was the ApiMap#add_methods_from_reference(type=Kernel) repetition.

This issue does not seem to appear in the v0.59.2. Everything worked fine once I rolled back.

I hope this helps somehow. 🤞 Let me know if I can provide you with further details.

@castwide
Copy link
Copy Markdown
Owner Author

@lekemula Did you encounter this with the solargraph-rspec project? I tried in VS Code with solargraph-rspec on the main branch running solargraph on the yard-macros branch. I didn't encounter an endless loop, but I did notice the following:

  • It had to document 115 gems because I cleared the cache. This took a while, but the UI worked while the gems got mapped iteratively.
  • After documenting the 115 gems, the progress status got stuck at "Mapping workspace: 25/28 files." After I closed and reopened the editor, it finished mapping and appeared to work normally.
  • The UI was slow but responsive.

Is there a particular operation you attempted that triggered the endless loop? If you encountered the problem on a different project, can you provide a reproducible example?

@lekemula
Copy link
Copy Markdown
Contributor

lekemula commented May 29, 2026

@castwide, sorry for the ambiguity, but I meant the closed-source project at my current job.

So I ran the solargraph profile, and that revealed that the issue is indeed ApiMap#process_macros taking too long to process:

image

v0.59.2_definition_benchmark.json.gz

main_definition_benchmark.json.gz

Note: The process on main was manually interrupted after some time, so the time there could be much longer.

Here are some stats regarding our project:

[ERROR][2026-05-29 23:26:17] "rpc"	"solargraph"	"stderr"	"[DEBUG] ApiMap#process_macros: processing macros for 7838 source maps\n"
[ERROR][2026-05-29 23:26:17] "rpc"	"solargraph"	"stderr"	"[DEBUG] ApiMap#process_macros: store has 198 macro method namepins\n"

I fear that the exact DSL methods inference might be a little too expensive an operation. Maybe we should go with a more "naive approach" for the sake of performance?


SIDE NOTE: regarding solargraph profile: I noticed that the bulk of the work of cataloging is now on definition_benchmark.json.gz (due to Library#sync_catalog) instead of Host#catalog which the command currently uses for catalog_benchmark.json.gz part. That's the reason I attached them here. We should probably adapt the command accordingly.

=== Timing Results ===
Parsing & mapping: 32068.54ms
Catalog building: 2.33ms

Profiles saved to:
  - /Users/lekemula/Projects/finlink/loanlink-api/tmp/profiles-main/parse_benchmark.json.gz
  - /Users/lekemula/Projects/finlink/loanlink-api/tmp/profiles-main/catalog_benchmark.json.gz
  - /Users/lekemula/Projects/finlink/loanlink-api/tmp/profiles-main/definition_benchmark.json.gz

@castwide
Copy link
Copy Markdown
Owner Author

@lekemula Thanks for clarifying. It looks like my attempt to minimize the amount of macro processing performed during catalog operations is insufficient for very large codebases.

I fear that the exact DSL methods inference might be a little too expensive an operation. Maybe we should go with a more "naive approach" for the sake of performance?

I tend to agree.

Would you like to profile your solution in #1202? If its performance is acceptable, we can merge that instead. AFAICT, the worst case scenario is that the language server might surface some incompletely inferred macros as undefined instead of something more useful. I think I'd prefer that minor wart to a critical performance bottleneck. Hopefully I'll be able to address general improvements to type expansion in #1204.

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.

2 participants