Skip to content

Commit 20036db

Browse files
authored
Support non-model files in CustomParser (#88)
This PR adds support for other Ruby files types that should be able to get model annotations added to them (i.e. factories, fabricators, etc). In #72, AnnotateRb was changed to use `FileParser::CustomParser` to parse Ruby files instead of relying on regexes. That PR only added support for model files that used either `module Namespace...` or `class ModelName...` as the first line of Ruby. The parser did not have support for other file structures like FactoryBot factories or Fabrication fabricators. There may be other Ruby files that have code that is not currently handled by `CustomParser`, so those will have to be added in the future.
1 parent 22675b2 commit 20036db

File tree

4 files changed

+487
-55
lines changed

4 files changed

+487
-55
lines changed

lib/annotate_rb/model_annotator/file_parser/custom_parser.rb

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@ module ModelAnnotator
77
module FileParser
88
class CustomParser < Ripper
99
# Overview of Ripper: https://kddnewton.com/2022/02/14/formatting-ruby-part-1.html
10-
# Ripper API: https://kddnewton.com/ripper-docs/events
10+
# Ripper API: https://kddnewton.com/ripper-docs/
1111

1212
class << self
1313
def parse(string)
14-
_parser = new(string).tap(&:parse)
14+
_parser = new(string, "", 0).tap(&:parse)
1515
end
1616
end
1717

1818
attr_reader :comments
1919

2020
def initialize(input, ...)
2121
super
22+
@_stack_code_block = []
2223
@_input = input
24+
@_const_event_map = {}
25+
2326
@comments = []
2427
@block_starts = []
2528
@block_ends = []
@@ -48,47 +51,132 @@ def on_program(...)
4851
end
4952

5053
def on_const_ref(const)
54+
add_event(__method__, const, lineno)
5155
@block_starts << [const, lineno]
5256
super
5357
end
5458

5559
# Used for `class Foo::User`
5660
def on_const_path_ref(_left, const)
61+
add_event(__method__, const, lineno)
5762
@block_starts << [const, lineno]
5863
super
5964
end
6065

6166
def on_module(const, _bodystmt)
67+
add_event(__method__, const, lineno)
6268
@const_type_map[const] = :module unless @const_type_map[const]
6369
@block_ends << [const, lineno]
6470
super
6571
end
6672

6773
def on_class(const, _superclass, _bodystmt)
74+
add_event(__method__, const, lineno)
6875
@const_type_map[const] = :class unless @const_type_map[const]
6976
@block_ends << [const, lineno]
7077
super
7178
end
7279

80+
def on_method_add_block(method, block)
81+
add_event(__method__, method, lineno)
82+
83+
if @_stack_code_block.last == method
84+
@block_ends << [method, lineno]
85+
@_stack_code_block.pop
86+
else
87+
@block_starts << [method, lineno]
88+
end
89+
super
90+
end
91+
92+
def on_method_add_arg(method, args)
93+
add_event(__method__, method, lineno)
94+
@block_starts << [method, lineno]
95+
96+
# We keep track of blocks using a stack
97+
@_stack_code_block << method
98+
super
99+
end
100+
101+
# Gets the `FactoryBot` line in:
102+
# ```ruby
103+
# FactoryBot.define do
104+
# factory :user do
105+
# ...
106+
# end
107+
# end
108+
# ```
109+
def on_call(receiver, operator, message)
110+
# We only want to add the parsed line if the beginning of the Ruby
111+
if @block_starts.empty?
112+
add_event(__method__, receiver, lineno)
113+
@block_starts << [receiver, lineno]
114+
end
115+
116+
super
117+
end
118+
119+
# Gets the `factory` block start in:
120+
# ```ruby
121+
# factory :user, aliases: [:author, :commenter] do
122+
# ...
123+
# end
124+
# ```
125+
def on_command(message, args)
126+
add_event(__method__, message, lineno)
127+
@block_starts << [message, lineno]
128+
super
129+
end
130+
131+
# Matches the `end` in:
132+
# ```ruby
133+
# factory :user, aliases: [:author, :commenter] do
134+
# first_name { "John" }
135+
# last_name { "Doe" }
136+
# date_of_birth { 18.years.ago }
137+
# end
138+
# ```
139+
def on_do_block(block_var, bodystmt)
140+
if block_var.blank? && bodystmt.blank?
141+
@block_ends << ["end", lineno]
142+
add_event(__method__, "end", lineno)
143+
end
144+
super
145+
end
146+
73147
def on_embdoc_beg(value)
148+
add_event(__method__, value, lineno)
74149
@comments << [value.strip, lineno]
75150
super
76151
end
77152

78153
def on_embdoc_end(value)
154+
add_event(__method__, value, lineno)
79155
@comments << [value.strip, lineno]
80156
super
81157
end
82158

83159
def on_embdoc(value)
160+
add_event(__method__, value, lineno)
84161
@comments << [value.strip, lineno]
85162
super
86163
end
87164

88165
def on_comment(value)
166+
add_event(__method__, value, lineno)
89167
@comments << [value.strip, lineno]
90168
super
91169
end
170+
171+
private
172+
173+
def add_event(event, const, lineno)
174+
if !@_const_event_map[lineno]
175+
@_const_event_map[lineno] = []
176+
end
177+
178+
@_const_event_map[lineno] << [const, event]
179+
end
92180
end
93181
end
94182
end

spec/lib/annotate_rb/model_annotator/annotated_file/generator_spec.rb

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,5 +400,139 @@ class User < ApplicationRecord
400400
end
401401
end
402402
end
403+
404+
context 'when position is "before" for a FactoryBot factory' do
405+
let(:options) { AnnotateRb::Options.new({position_in_class: "before"}) }
406+
407+
let(:file_content) do
408+
<<~FILE
409+
FactoryBot.define do
410+
factory :user do
411+
admin { false }
412+
end
413+
end
414+
FILE
415+
end
416+
417+
let(:expected_content) do
418+
<<~CONTENT
419+
# == Schema Information
420+
#
421+
# Table name: users
422+
#
423+
# id :bigint not null, primary key
424+
#
425+
FactoryBot.define do
426+
factory :user do
427+
admin { false }
428+
end
429+
end
430+
CONTENT
431+
end
432+
433+
it "returns the annotated file content" do
434+
is_expected.to eq(expected_content)
435+
end
436+
end
437+
438+
context 'when position is "after" for a FactoryBot factory' do
439+
let(:options) { AnnotateRb::Options.new({position_in_class: "after"}) }
440+
441+
let(:file_content) do
442+
<<~FILE
443+
FactoryBot.define do
444+
factory :user do
445+
admin { false }
446+
end
447+
end
448+
FILE
449+
end
450+
451+
let(:expected_content) do
452+
<<~CONTENT
453+
FactoryBot.define do
454+
factory :user do
455+
admin { false }
456+
end
457+
end
458+
459+
# == Schema Information
460+
#
461+
# Table name: users
462+
#
463+
# id :bigint not null, primary key
464+
#
465+
CONTENT
466+
end
467+
468+
it "returns the annotated file content" do
469+
is_expected.to eq(expected_content)
470+
end
471+
end
472+
473+
context 'when position is "before" for a Fabrication fabricator' do
474+
let(:options) { AnnotateRb::Options.new({position_in_class: "before"}) }
475+
476+
let(:file_content) do
477+
<<~FILE
478+
Fabricator(:user) do
479+
name
480+
reminder_at { 1.day.from_now.iso8601 }
481+
end
482+
FILE
483+
end
484+
485+
let(:expected_content) do
486+
<<~CONTENT
487+
# == Schema Information
488+
#
489+
# Table name: users
490+
#
491+
# id :bigint not null, primary key
492+
#
493+
Fabricator(:user) do
494+
name
495+
reminder_at { 1.day.from_now.iso8601 }
496+
end
497+
CONTENT
498+
end
499+
500+
it "returns the annotated file content" do
501+
is_expected.to eq(expected_content)
502+
end
503+
end
504+
505+
context 'when position is "after" for a Fabrication fabricator' do
506+
let(:options) { AnnotateRb::Options.new({position_in_class: "after"}) }
507+
508+
let(:file_content) do
509+
<<~FILE
510+
Fabricator(:user) do
511+
name
512+
reminder_at { 1.day.from_now.iso8601 }
513+
end
514+
FILE
515+
end
516+
517+
let(:expected_content) do
518+
<<~CONTENT
519+
Fabricator(:user) do
520+
name
521+
reminder_at { 1.day.from_now.iso8601 }
522+
end
523+
524+
# == Schema Information
525+
#
526+
# Table name: users
527+
#
528+
# id :bigint not null, primary key
529+
#
530+
CONTENT
531+
end
532+
533+
it "returns the annotated file content" do
534+
is_expected.to eq(expected_content)
535+
end
536+
end
403537
end
404538
end

0 commit comments

Comments
 (0)