annotaterb 4.4.1 → 4.6.0

Sign up to get free protection for your applications and to get access to all the features.
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