annotaterb 4.4.1 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/README.md +29 -0
  4. data/VERSION +1 -1
  5. data/lib/annotate_rb/config_generator.rb +28 -0
  6. data/lib/annotate_rb/eager_loader.rb +6 -2
  7. data/lib/annotate_rb/model_annotator/annotated_file/generator.rb +92 -0
  8. data/lib/annotate_rb/model_annotator/annotated_file/updater.rb +46 -0
  9. data/lib/annotate_rb/model_annotator/annotated_file.rb +10 -0
  10. data/lib/annotate_rb/model_annotator/annotator.rb +2 -2
  11. data/lib/annotate_rb/model_annotator/file_name_resolver.rb +5 -0
  12. data/lib/annotate_rb/model_annotator/file_parser/annotation_finder.rb +103 -0
  13. data/lib/annotate_rb/model_annotator/file_parser/custom_parser.rb +217 -0
  14. data/lib/annotate_rb/model_annotator/file_parser/parsed_file.rb +94 -0
  15. data/lib/annotate_rb/model_annotator/file_parser/parsed_file_result.rb +54 -0
  16. data/lib/annotate_rb/model_annotator/file_parser.rb +12 -0
  17. data/lib/annotate_rb/model_annotator/model_class_getter.rb +7 -0
  18. data/lib/annotate_rb/model_annotator/model_files_getter.rb +4 -8
  19. data/lib/annotate_rb/model_annotator/model_wrapper.rb +11 -2
  20. data/lib/annotate_rb/model_annotator/pattern_getter.rb +2 -0
  21. data/lib/annotate_rb/model_annotator/related_files_list_builder.rb +3 -3
  22. data/lib/annotate_rb/model_annotator/single_file_annotation_remover.rb +10 -12
  23. data/lib/annotate_rb/model_annotator/single_file_annotator.rb +15 -8
  24. data/lib/annotate_rb/model_annotator/single_file_annotator_instruction.rb +1 -1
  25. data/lib/annotate_rb/model_annotator/single_file_remove_annotation_instruction.rb +1 -1
  26. data/lib/annotate_rb/model_annotator/zeitwerk_class_getter.rb +113 -0
  27. data/lib/annotate_rb/model_annotator.rb +3 -4
  28. data/lib/annotate_rb/options.rb +4 -0
  29. data/lib/annotate_rb/parser.rb +9 -3
  30. data/lib/annotate_rb/runner.rb +5 -4
  31. data/lib/annotate_rb/tasks/annotate_models_migrate.rake +5 -0
  32. data/lib/annotate_rb.rb +1 -0
  33. data/lib/generators/annotate_rb/config/USAGE +6 -0
  34. data/lib/generators/annotate_rb/config/config_generator.rb +15 -0
  35. data/lib/generators/annotate_rb/hook/USAGE +7 -0
  36. data/lib/generators/annotate_rb/hook/hook_generator.rb +15 -0
  37. data/lib/generators/annotate_rb/install/install_generator.rb +3 -4
  38. data/lib/generators/annotate_rb/update_config/USAGE +6 -0
  39. data/lib/generators/annotate_rb/update_config/update_config_generator.rb +15 -0
  40. metadata +20 -8
  41. data/lib/annotate_rb/model_annotator/annotation_pattern_generator.rb +0 -19
  42. data/lib/annotate_rb/model_annotator/file_builder.rb +0 -57
  43. data/lib/annotate_rb/model_annotator/file_components.rb +0 -81
  44. data/lib/annotate_rb/model_annotator/magic_comment_parser.rb +0 -32
  45. /data/lib/generators/annotate_rb/{install → hook}/templates/annotate_rb.rake +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a337f543f32142efaffbfd396196692d42a98438352b3bdd406c104c25a9958a
4
- data.tar.gz: 27d091e7fed5953ccdd26c4b4874e45a0fcff541630595fa467e010629efb9ba
3
+ metadata.gz: 2c9e326fa73164a6a2e43edc402e6990cfbb5d953ac5fd1ccf8c71fa6d358ee1
4
+ data.tar.gz: dabf1afd54a8dec7a96ed8c259ca84e0a6085618713be78f43f075a51a22db7f
5
5
  SHA512:
6
- metadata.gz: 44125b3f65246ad1b5a0dd0d354487462ea6584c6147165650268244fd8d3c9826cf337873f24c31e008efa1c560b37bcb71e36af7fc05c9bc381d48e2b1e4b0
7
- data.tar.gz: 7979e2309e3c09ee11fc090a424e7006a94634c7a970fe34b61028b3a90d495b684cc2a4a6fa78221b288dfe5e6682567835fb6839ada3903dfaa599a4f8cfdc
6
+ metadata.gz: 12e277c2d09e37e1001e64b0f08542ff03981bf5bbe37e86ed3dca01b1ec52c5b1805ce6f5cb1a8e5b25c3b9d655cb2345d0c9d878ecf4ae6bbfd265870445ac
7
+ data.tar.gz: 88d7c9ba17c3abbf38e635365314d1c4b372e67ec5ef0dcba9f7f4f5b6ef90d0e285a2150887127fa75f847a1ee2e8e4c7a30548729a4f903cf71f42be793177
data/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## [v4.5.0](https://github.com/drwl/annotaterb/tree/v4.5.0) (2024-02-08)
4
+
5
+ [Full Changelog](https://github.com/drwl/annotaterb/compare/v4.4.1...v4.5.0)
6
+
7
+ **Closed issues:**
8
+
9
+ - Add an automated way to migrate from the old annotate gem [\#73](https://github.com/drwl/annotaterb/issues/73)
10
+ - Default array value is double-quoted/escaped [\#57](https://github.com/drwl/annotaterb/issues/57)
11
+
12
+ **Merged pull requests:**
13
+
14
+ - Bump version to v4.5.0 [\#79](https://github.com/drwl/annotaterb/pull/79) ([drwl](https://github.com/drwl))
15
+ - Update README on the new Rails generator commands [\#78](https://github.com/drwl/annotaterb/pull/78) ([drwl](https://github.com/drwl))
16
+ - Bump github/codeql-action from 2 to 3 [\#77](https://github.com/drwl/annotaterb/pull/77) ([dependabot[bot]](https://github.com/apps/dependabot))
17
+ - Add command to generate a configuration file [\#76](https://github.com/drwl/annotaterb/pull/76) ([drwl](https://github.com/drwl))
18
+ - Bump actions/checkout from 3 to 4 [\#75](https://github.com/drwl/annotaterb/pull/75) ([dependabot[bot]](https://github.com/apps/dependabot))
19
+ - CI: Configure dependabot to update GH Actions [\#74](https://github.com/drwl/annotaterb/pull/74) ([olleolleolle](https://github.com/olleolleolle))
20
+ - Refactor `FileBuilder` and `MagicCommentParser` [\#71](https://github.com/drwl/annotaterb/pull/71) ([drwl](https://github.com/drwl))
21
+ - Test running annotations after a migration [\#70](https://github.com/drwl/annotaterb/pull/70) ([drwl](https://github.com/drwl))
22
+ - Add integration test for rake task installer [\#69](https://github.com/drwl/annotaterb/pull/69) ([drwl](https://github.com/drwl))
23
+ - Add integration test for annotating routes [\#68](https://github.com/drwl/annotaterb/pull/68) ([drwl](https://github.com/drwl))
24
+ - Remove optional args [\#67](https://github.com/drwl/annotaterb/pull/67) ([drwl](https://github.com/drwl))
25
+ - Remove optional arg from `AnnotationPatternGenerator` [\#66](https://github.com/drwl/annotaterb/pull/66) ([drwl](https://github.com/drwl))
26
+ - Remove `ARGV` use during runtime [\#65](https://github.com/drwl/annotaterb/pull/65) ([drwl](https://github.com/drwl))
27
+ - Add integration test for annotating a singular file [\#64](https://github.com/drwl/annotaterb/pull/64) ([drwl](https://github.com/drwl))
28
+ - Generate changelog for v4.4.1 [\#63](https://github.com/drwl/annotaterb/pull/63) ([drwl](https://github.com/drwl))
29
+ - Add support for factory\_bot's default suffixed pattern [\#59](https://github.com/drwl/annotaterb/pull/59) ([drwl](https://github.com/drwl))
30
+
31
+ ## [v4.4.1](https://github.com/drwl/annotaterb/tree/v4.4.1) (2023-09-11)
32
+
33
+ [Full Changelog](https://github.com/drwl/annotaterb/compare/v4.4.0...v4.4.1)
34
+
35
+ **Merged pull requests:**
36
+
37
+ - Bump version to v4.4.1 [\#62](https://github.com/drwl/annotaterb/pull/62) ([drwl](https://github.com/drwl))
38
+ - Fix annotation for columns with `Date` and `DateTime` default values [\#61](https://github.com/drwl/annotaterb/pull/61) ([drwl](https://github.com/drwl))
39
+ - Add integration tests [\#60](https://github.com/drwl/annotaterb/pull/60) ([drwl](https://github.com/drwl))
40
+ - Fix the default array value from being escaped [\#58](https://github.com/drwl/annotaterb/pull/58) ([drwl](https://github.com/drwl))
41
+ - Update dummyapp Rails version [\#56](https://github.com/drwl/annotaterb/pull/56) ([drwl](https://github.com/drwl))
42
+ - Bump puma from 5.6.5 to 6.3.1 in /dummyapp [\#55](https://github.com/drwl/annotaterb/pull/55) ([dependabot[bot]](https://github.com/apps/dependabot))
43
+ - Generate changelog for v4.4.0 [\#53](https://github.com/drwl/annotaterb/pull/53) ([drwl](https://github.com/drwl))
44
+ - Add CLI specs using `aruba` gem [\#43](https://github.com/drwl/annotaterb/pull/43) ([drwl](https://github.com/drwl))
45
+
3
46
  ## [v4.4.0](https://github.com/drwl/annotaterb/tree/v4.4.0) (2023-06-24)
4
47
 
5
48
  [Full Changelog](https://github.com/drwl/annotaterb/compare/v4.3.1...v4.4.0)
data/README.md CHANGED
@@ -65,6 +65,35 @@ To skip the automatic annotation that happens after a db task, pass the environm
65
65
  $ ANNOTATERB_SKIP_ON_DB_TASKS=1 bin/rails db:migrate
66
66
  ```
67
67
 
68
+ ### Added Rails generators
69
+ The following Rails generator commands get added.
70
+
71
+ ```sh
72
+ $ bin/rails generator --help
73
+
74
+ ...
75
+
76
+ AnnotateRb:
77
+ annotate_rb:config
78
+ annotate_rb:hook
79
+ annotate_rb:install
80
+ annotate_rb:update_config
81
+ ...
82
+
83
+ ```
84
+
85
+ `bin/rails g annotate_rb:config`
86
+ - Generates a new configuration file, `.annotaterb.yml`, using defaults from the gem.
87
+
88
+ `bin/rails g annotate_rb:hook`
89
+ - Installs the Rake file to automatically annotate Rails models on a database task (e.g. AnnotateRb will automatically run after running `bin/rails db:migrate`).
90
+
91
+ `bin/rails g annotate_rb:install`
92
+ - Runs the `config` and `hook` generator commands
93
+
94
+ `bin/rails g annotate_rb:update_config`
95
+ - Appends to `.annotaterb.yml` any configuration key-value pairs that are used by the Gem. This is useful when there's a drift between the config file values and the gem defaults (i.e. when new features get added).
96
+
68
97
  ## Migrating from the annotate gem
69
98
  Refer to the [migration guide](MIGRATION_GUIDE.md).
70
99
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.4.1
1
+ 4.6.0
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ class ConfigGenerator
5
+ class << self
6
+ # Returns unset configuration key-value pairs as yaml.
7
+ # Useful when a config file was generated an older version of gem and new
8
+ # settings get added.
9
+ def unset_config_defaults
10
+ user_defaults = ConfigLoader.load_config
11
+ defaults = Options.from({}, {}).to_h
12
+
13
+ differences = defaults.keys - user_defaults.keys
14
+ result = defaults.slice(*differences)
15
+
16
+ # Generates proper YAML including the leading hyphens `---` header
17
+ yml_content = YAML.dump(result, StringIO.new).string
18
+ # Remove the header
19
+ yml_content.sub("---", "")
20
+ end
21
+
22
+ def default_config_yml
23
+ defaults_hash = Options.from({}, {}).to_h
24
+ _yml_content = YAML.dump(defaults_hash, StringIO.new).string
25
+ end
26
+ end
27
+ end
28
+ end
@@ -8,8 +8,12 @@ module AnnotateRb
8
8
  options[:require].count > 0 && options[:require].each { |path| require path }
9
9
 
10
10
  if defined?(::Rails::Application)
11
- klass = ::Rails::Application.send(:subclasses).first
12
- klass.eager_load!
11
+ if defined?(::Zeitwerk)
12
+ # Delegate to Zeitwerk to load stuff as needed
13
+ else
14
+ klass = ::Rails::Application.send(:subclasses).first
15
+ klass.eager_load!
16
+ end
13
17
  else
14
18
  options[:model_dir].each do |dir|
15
19
  ::Rake::FileList["#{dir}/**/*.rb"].each do |fname|
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module AnnotatedFile
6
+ # Generates the file with fresh annotations
7
+ class Generator
8
+ def initialize(file_content, new_annotations, annotation_position, options)
9
+ @annotation_position = annotation_position
10
+ @options = options
11
+
12
+ @new_wrapped_annotations = wrapped_content(new_annotations)
13
+
14
+ @new_annotations = new_annotations
15
+ @file_content = file_content
16
+
17
+ @parsed_file = FileParser::ParsedFile.new(@file_content, @new_annotations, options).parse
18
+ end
19
+
20
+ # @return [String] Returns the annotated file content to be written back to a file
21
+ def generate
22
+ # Need to keep `.to_s` for now since the it can be either a String or Symbol
23
+ annotation_write_position = @options[@annotation_position].to_s
24
+
25
+ # New method: first remove annotations
26
+ content_without_annotations = if @parsed_file.has_annotations?
27
+ @file_content.sub(@parsed_file.annotations_with_whitespace, "")
28
+ elsif @options[:force]
29
+ @file_content.sub(@parsed_file.annotations_with_whitespace, "")
30
+ else
31
+ @file_content
32
+ end
33
+
34
+ # We need to get class start and class end depending on the position
35
+ parsed = FileParser::CustomParser.new(content_without_annotations, "", 0).tap(&:parse)
36
+
37
+ _content = if %w[after bottom].include?(annotation_write_position)
38
+ content_annotated_after(parsed, content_without_annotations)
39
+ else
40
+ content_annotated_before(parsed, content_without_annotations, annotation_write_position)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def content_annotated_before(parsed, content_without_annotations, write_position)
47
+ same_write_position = @parsed_file.has_annotations? && @parsed_file.annotation_position.to_s == write_position
48
+
49
+ # Could error if there's no class or module declaration
50
+ _constant_name, line_number_before = parsed.starts.first
51
+
52
+ content_with_annotations_written_before = []
53
+ content_with_annotations_written_before << content_without_annotations.lines[0...line_number_before]
54
+ content_with_annotations_written_before << $/ if @parsed_file.has_leading_whitespace? && same_write_position
55
+ content_with_annotations_written_before << @new_wrapped_annotations.lines
56
+ content_with_annotations_written_before << $/ if @parsed_file.has_trailing_whitespace? && same_write_position
57
+ content_with_annotations_written_before << content_without_annotations.lines[line_number_before..]
58
+
59
+ content_with_annotations_written_before.join
60
+ end
61
+
62
+ def content_annotated_after(parsed, content_without_annotations)
63
+ _constant_name, line_number_after = parsed.ends.last
64
+
65
+ content_with_annotations_written_after = []
66
+ content_with_annotations_written_after << content_without_annotations.lines[0..line_number_after]
67
+ content_with_annotations_written_after << $/
68
+ content_with_annotations_written_after << @new_wrapped_annotations.lines
69
+ content_with_annotations_written_after << content_without_annotations.lines[(line_number_after + 1)..]
70
+
71
+ content_with_annotations_written_after.join
72
+ end
73
+
74
+ def wrapped_content(content)
75
+ wrapper_open = if @options[:wrapper_open]
76
+ "# #{@options[:wrapper_open]}\n"
77
+ else
78
+ ""
79
+ end
80
+
81
+ wrapper_close = if @options[:wrapper_close]
82
+ "# #{@options[:wrapper_close]}\n"
83
+ else
84
+ ""
85
+ end
86
+
87
+ _wrapped_info_block = "#{wrapper_open}#{content}#{wrapper_close}"
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module AnnotatedFile
6
+ # Updates existing annotations
7
+ class Updater
8
+ def initialize(file_content, new_annotations, _annotation_position, options)
9
+ @options = options
10
+
11
+ @new_annotations = new_annotations
12
+ @file_content = file_content
13
+
14
+ @parsed_file = FileParser::ParsedFile.new(@file_content, @new_annotations, options).parse
15
+ end
16
+
17
+ # @return [String] Returns the annotated file content to be written back to a file
18
+ def update
19
+ return "" if !@parsed_file.has_annotations?
20
+
21
+ new_annotation = wrapped_content(@new_annotations)
22
+
23
+ _content = @file_content.sub(@parsed_file.annotations, new_annotation)
24
+ end
25
+
26
+ private
27
+
28
+ def wrapped_content(content)
29
+ wrapper_open = if @options[:wrapper_open]
30
+ "# #{@options[:wrapper_open]}\n"
31
+ else
32
+ ""
33
+ end
34
+
35
+ wrapper_close = if @options[:wrapper_close]
36
+ "# #{@options[:wrapper_close]}\n"
37
+ else
38
+ ""
39
+ end
40
+
41
+ _wrapped_info_block = "#{wrapper_open}#{content}#{wrapper_close}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module AnnotatedFile
6
+ autoload :Generator, "annotate_rb/model_annotator/annotated_file/generator"
7
+ autoload :Updater, "annotate_rb/model_annotator/annotated_file/updater"
8
+ end
9
+ end
10
+ end
@@ -4,11 +4,11 @@ module AnnotateRb
4
4
  module ModelAnnotator
5
5
  class Annotator
6
6
  class << self
7
- def do_annotations(options = {})
7
+ def do_annotations(options)
8
8
  new(options).do_annotations
9
9
  end
10
10
 
11
- def remove_annotations(options = {})
11
+ def remove_annotations(options)
12
12
  new(options).remove_annotations
13
13
  end
14
14
  end
@@ -5,8 +5,13 @@ module AnnotateRb
5
5
  class FileNameResolver
6
6
  class << self
7
7
  def call(filename_template, model_name, table_name)
8
+ # e.g. with a model file name like "app/models/collapsed/example/test_model.rb"
9
+ # and using a collapsed `model_name` such as "collapsed/test_model"
10
+ model_name_without_namespace = model_name.split("/").last
11
+
8
12
  filename_template
9
13
  .gsub("%MODEL_NAME%", model_name)
14
+ .gsub("%MODEL_NAME_WITHOUT_NS%", model_name_without_namespace)
10
15
  .gsub("%PLURALIZED_MODEL_NAME%", model_name.pluralize)
11
16
  .gsub("%TABLE_NAME%", table_name || model_name.pluralize)
12
17
  end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module FileParser
6
+ class AnnotationFinder
7
+ COMPAT_PREFIX = "== Schema Info"
8
+ COMPAT_PREFIX_MD = "## Schema Info"
9
+ DEFAULT_ANNOTATION_ENDING = "#"
10
+
11
+ class MalformedAnnotation < StandardError; end
12
+
13
+ class NoAnnotationFound < StandardError; end
14
+
15
+ # Returns the line index (not the line number) that the annotation starts.
16
+ attr_reader :annotation_start
17
+ # Returns the line index (not the line number) that the annotation ends, inclusive.
18
+ attr_reader :annotation_end
19
+
20
+ attr_reader :parser
21
+
22
+ def initialize(content, wrapper_open, wrapper_close)
23
+ @content = content
24
+ @wrapper_open = wrapper_open
25
+ @wrapper_close = wrapper_close
26
+ @annotation_start = nil
27
+ @annotation_end = nil
28
+ @parser = nil
29
+ end
30
+
31
+ # Find the annotation's line start and line end
32
+ def run
33
+ # CustomParser returns line numbers as 0-indexed
34
+ @parser = FileParser::CustomParser.new(@content, "", 0).tap(&:parse)
35
+ comments = @parser.comments
36
+
37
+ start = comments.find_index { |comment, _| comment.include?(COMPAT_PREFIX) || comment.include?(COMPAT_PREFIX_MD) }
38
+ raise NoAnnotationFound if start.nil? # Stop execution because we did not find
39
+
40
+ if @wrapper_open
41
+ prev_comment, _prev_line_number = comments[start - 1]
42
+
43
+ # Change start to the line before if wrapper_open is defined and we find the wrapper open comment
44
+ if prev_comment&.include?(@wrapper_open)
45
+ start -= 1
46
+ end
47
+ end
48
+
49
+ # Find a contiguous block of comments from the starting point
50
+ ending = start
51
+ while ending < comments.size - 1
52
+ _comment, line_number = comments[ending]
53
+ _next_comment, next_line_number = comments[ending + 1]
54
+
55
+ if next_line_number - line_number == 1
56
+ ending += 1
57
+ else
58
+ break
59
+ end
60
+ end
61
+
62
+ raise MalformedAnnotation if start == ending
63
+
64
+ if @wrapper_close
65
+ if comments[ending].first.include?(@wrapper_close)
66
+ # We can end here because it's the end of the annotation block
67
+ else
68
+ # Walk back until we find the end of the annotation comment block or the wrapper close to be flexible
69
+ # We check if @wrapper_close is a substring because `comments` contains strings with the comment character
70
+ while ending > start && comments[ending].first != DEFAULT_ANNOTATION_ENDING && !comments[ending].first.include?(@wrapper_close)
71
+ ending -= 1
72
+ end
73
+ end
74
+ else
75
+ # Walk back until we find the end of the annotation comment block
76
+ while ending > start && comments[ending].first != DEFAULT_ANNOTATION_ENDING
77
+ ending -= 1
78
+ end
79
+ end
80
+
81
+ # We want .last because we want the line indexes
82
+ @annotation_start = comments[start].last
83
+ @annotation_end = comments[ending].last
84
+
85
+ [@annotation_start, @annotation_end]
86
+ end
87
+
88
+ def annotation
89
+ @annotation ||=
90
+ begin
91
+ lines = @content.lines
92
+ lines[@annotation_start..@annotation_end].join
93
+ end
94
+ end
95
+
96
+ # Returns true if annotations are detected in the file content
97
+ def annotated?
98
+ @annotation_start && @annotation_end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ripper"
4
+
5
+ module AnnotateRb
6
+ module ModelAnnotator
7
+ module FileParser
8
+ class CustomParser < Ripper
9
+ # Overview of Ripper: https://kddnewton.com/2022/02/14/formatting-ruby-part-1.html
10
+ # Ripper API: https://kddnewton.com/ripper-docs/
11
+
12
+ class << self
13
+ def parse(string)
14
+ _parser = new(string, "", 0).tap(&:parse)
15
+ end
16
+ end
17
+
18
+ attr_reader :comments
19
+
20
+ def initialize(input, ...)
21
+ super
22
+ @_stack_code_block = []
23
+ @_input = input
24
+ @_const_event_map = {}
25
+
26
+ @comments = []
27
+ @block_starts = []
28
+ @block_ends = []
29
+ @const_type_map = {}
30
+ end
31
+
32
+ def starts
33
+ @block_starts
34
+ end
35
+
36
+ def ends
37
+ @block_ends
38
+ end
39
+
40
+ def type_map
41
+ @const_type_map
42
+ end
43
+
44
+ def on_program(...)
45
+ {
46
+ comments: @comments,
47
+ starts: @block_starts,
48
+ ends: @block_ends,
49
+ type_map: @const_type_map
50
+ }
51
+ end
52
+
53
+ def on_const_ref(const)
54
+ add_event(__method__, const, lineno)
55
+ @block_starts << [const, lineno]
56
+ super
57
+ end
58
+
59
+ # Used for `class Foo::User`
60
+ def on_const_path_ref(_left, const)
61
+ add_event(__method__, const, lineno)
62
+ @block_starts << [const, lineno]
63
+ super
64
+ end
65
+
66
+ def on_module(const, _bodystmt)
67
+ add_event(__method__, const, lineno)
68
+ @const_type_map[const] = :module unless @const_type_map[const]
69
+ @block_ends << [const, lineno]
70
+ super
71
+ end
72
+
73
+ def on_class(const, _superclass, _bodystmt)
74
+ add_event(__method__, const, lineno)
75
+ @const_type_map[const] = :class unless @const_type_map[const]
76
+ @block_ends << [const, lineno]
77
+ super
78
+ end
79
+
80
+ # Gets the `RSpec` opening in:
81
+ # ```ruby
82
+ # RSpec.describe "Collapsed::TestModel" do
83
+ # # Deliberately left empty
84
+ # end
85
+ # ```
86
+ # receiver: "RSpec", operator: ".", method: "describe"
87
+ def on_command_call(receiver, operator, method, args)
88
+ add_event(__method__, receiver, lineno)
89
+ @block_starts << [receiver, lineno]
90
+
91
+ # We keep track of blocks using a stack
92
+ @_stack_code_block << receiver
93
+ super
94
+ end
95
+
96
+ def on_method_add_block(method, block)
97
+ # When parsing a line with no explicit receiver, the method will be presented in an Array.
98
+ # It's not immediately clear why.
99
+ #
100
+ # Example:
101
+ # ```ruby
102
+ # describe "Collapsed::TestModel" do
103
+ # # Deliberately left empty
104
+ # end
105
+ # ```
106
+ #
107
+ # => method = ["describe"]
108
+ if method.is_a?(Array) && method.size == 1
109
+ method = method.first
110
+ end
111
+
112
+ add_event(__method__, method, lineno)
113
+
114
+ if @_stack_code_block.last == method
115
+ @block_ends << [method, lineno]
116
+ @_stack_code_block.pop
117
+ else
118
+ @block_starts << [method, lineno]
119
+ end
120
+ super
121
+ end
122
+
123
+ def on_method_add_arg(method, args)
124
+ add_event(__method__, method, lineno)
125
+ @block_starts << [method, lineno]
126
+
127
+ # We keep track of blocks using a stack
128
+ @_stack_code_block << method
129
+ super
130
+ end
131
+
132
+ # Gets the `FactoryBot` line in:
133
+ # ```ruby
134
+ # FactoryBot.define do
135
+ # factory :user do
136
+ # ...
137
+ # end
138
+ # end
139
+ # ```
140
+ def on_call(receiver, operator, message)
141
+ # We only want to add the parsed line if the beginning of the Ruby
142
+ if @block_starts.empty?
143
+ add_event(__method__, receiver, lineno)
144
+ @block_starts << [receiver, lineno]
145
+ end
146
+
147
+ super
148
+ end
149
+
150
+ # Gets the `factory` block start in:
151
+ # ```ruby
152
+ # factory :user, aliases: [:author, :commenter] do
153
+ # ...
154
+ # end
155
+ # ```
156
+ def on_command(message, args)
157
+ add_event(__method__, message, lineno)
158
+ @block_starts << [message, lineno]
159
+
160
+ # We keep track of blocks using a stack
161
+ @_stack_code_block << message
162
+ super
163
+ end
164
+
165
+ # Matches the `end` in:
166
+ # ```ruby
167
+ # factory :user, aliases: [:author, :commenter] do
168
+ # first_name { "John" }
169
+ # last_name { "Doe" }
170
+ # date_of_birth { 18.years.ago }
171
+ # end
172
+ # ```
173
+ def on_do_block(block_var, bodystmt)
174
+ if block_var.blank? && bodystmt.blank?
175
+ @block_ends << ["end", lineno]
176
+ add_event(__method__, "end", lineno)
177
+ end
178
+ super
179
+ end
180
+
181
+ def on_embdoc_beg(value)
182
+ add_event(__method__, value, lineno)
183
+ @comments << [value.strip, lineno]
184
+ super
185
+ end
186
+
187
+ def on_embdoc_end(value)
188
+ add_event(__method__, value, lineno)
189
+ @comments << [value.strip, lineno]
190
+ super
191
+ end
192
+
193
+ def on_embdoc(value)
194
+ add_event(__method__, value, lineno)
195
+ @comments << [value.strip, lineno]
196
+ super
197
+ end
198
+
199
+ def on_comment(value)
200
+ add_event(__method__, value, lineno)
201
+ @comments << [value.strip, lineno]
202
+ super
203
+ end
204
+
205
+ private
206
+
207
+ def add_event(event, const, lineno)
208
+ if !@_const_event_map[lineno]
209
+ @_const_event_map[lineno] = []
210
+ end
211
+
212
+ @_const_event_map[lineno] << [const, event]
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end