annotaterb 4.22.0 → 4.23.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README.md +40 -4
  4. data/VERSION +1 -1
  5. data/lib/annotate_rb/helper.rb +24 -1
  6. data/lib/annotate_rb/model_annotator/annotated_file/generator.rb +34 -12
  7. data/lib/annotate_rb/model_annotator/annotated_file/updater.rb +14 -1
  8. data/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb +1 -0
  9. data/lib/annotate_rb/model_annotator/annotation_decider.rb +3 -0
  10. data/lib/annotate_rb/model_annotator/annotation_diff_generator.rb +9 -4
  11. data/lib/annotate_rb/model_annotator/column_annotation/attributes_builder.rb +7 -0
  12. data/lib/annotate_rb/model_annotator/column_annotation/column_component.rb +2 -2
  13. data/lib/annotate_rb/model_annotator/column_annotation/column_wrapper.rb +4 -0
  14. data/lib/annotate_rb/model_annotator/enum_annotation/annotation.rb +40 -0
  15. data/lib/annotate_rb/model_annotator/enum_annotation/annotation_builder.rb +38 -0
  16. data/lib/annotate_rb/model_annotator/enum_annotation/enum_component.rb +27 -0
  17. data/lib/annotate_rb/model_annotator/enum_annotation.rb +11 -0
  18. data/lib/annotate_rb/model_annotator/file_parser/annotation_finder.rb +39 -4
  19. data/lib/annotate_rb/model_annotator/file_parser/annotation_target.rb +31 -0
  20. data/lib/annotate_rb/model_annotator/file_parser/custom_parser.rb +11 -4
  21. data/lib/annotate_rb/model_annotator/file_parser/magic_comment.rb +32 -0
  22. data/lib/annotate_rb/model_annotator/file_parser/parsed_file.rb +5 -6
  23. data/lib/annotate_rb/model_annotator/file_parser.rb +2 -0
  24. data/lib/annotate_rb/model_annotator/index_annotation/index_component.rb +26 -4
  25. data/lib/annotate_rb/model_annotator/model_wrapper.rb +25 -1
  26. data/lib/annotate_rb/model_annotator/project_annotator.rb +2 -1
  27. data/lib/annotate_rb/model_annotator/single_file_annotator.rb +5 -5
  28. data/lib/annotate_rb/model_annotator/single_file_annotator_instruction.rb +3 -2
  29. data/lib/annotate_rb/model_annotator/zeitwerk_class_getter.rb +1 -1
  30. data/lib/annotate_rb/model_annotator.rb +1 -0
  31. data/lib/annotate_rb/options.rb +6 -2
  32. data/lib/annotate_rb/parser.rb +7 -5
  33. data/lib/annotate_rb/route_annotator/base_processor.rb +1 -1
  34. data/lib/annotate_rb/route_annotator/helper.rb +1 -1
  35. data/lib/annotate_rb/runner.rb +12 -4
  36. data/lib/annotate_rb/tasks/annotate_models_migrate.rake +8 -3
  37. data/lib/generators/annotate_rb/update_config/update_config_generator.rb +4 -1
  38. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2be17a236402d5406e2f4d1f68a567bf51de51f924f66591adf9460bfacabe18
4
- data.tar.gz: 48743c136f9a195bb1dbaa564bd8047c16411394a991b4c86b57a70844d0e121
3
+ metadata.gz: 249172f3c37299ac55df233909754c48456b629d11517ce03c84fae62232677d
4
+ data.tar.gz: 9fa726c0abbb3bc7508257d24e7e0f0e4f9a79c94e255909b8bf7162a2f25d9f
5
5
  SHA512:
6
- metadata.gz: a203a269dbcf4c42a082a2088c6c38d8c4b97c3bbf9ab760762e3149294fb38dc31799aea2f3ece425504782cd2027187bd161ccf4a9c006aaa7a9a791094311
7
- data.tar.gz: 8cc55f09f193f5f428db97afc1270386a06eb8185773cfd339830e1ee8e49631a1f203cced676fdb2f4d7d5c0b690f5aadc9108f63d3de588c019d7585c0011b
6
+ metadata.gz: f695ccb014c329b7c897f1832c4bb053df9fbec718198d68466884585b17f17fbfb6805a053024a38133196f7d9934aaff56d89c83598160966b1fc4dcaaaa3c
7
+ data.tar.gz: 73994c40f74a0c793cd974cf1369b59403e3b3a49ce6c643b661f9dfdffb7425dcfbb96c98ba2a23c3347861fa4cd759be380613d92627cb4046956af249dea3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [v4.22.0](https://github.com/drwl/annotaterb/tree/v4.22.0) (2026-02-12)
4
+
5
+ [Full Changelog](https://github.com/drwl/annotaterb/compare/v4.21.0...v4.22.0)
6
+
7
+ **Fixed bugs:**
8
+
9
+ - Yardoc formatting for comments on database attributes [\#162](https://github.com/drwl/annotaterb/issues/162)
10
+
11
+ **Closed issues:**
12
+
13
+ - New `ignore_multi_database_name` option seems to be non-functional [\#303](https://github.com/drwl/annotaterb/issues/303)
14
+ - Changing sort options does not change annotations [\#294](https://github.com/drwl/annotaterb/issues/294)
15
+ - CLI script for annotaterb not installed or runnable [\#290](https://github.com/drwl/annotaterb/issues/290)
16
+ - Feature: ruby-lsp addon [\#175](https://github.com/drwl/annotaterb/issues/175)
17
+ - Mounting ActionCable leads to weird annotation [\#161](https://github.com/drwl/annotaterb/issues/161)
18
+
19
+ **Merged pull requests:**
20
+
21
+ - Bump version to v4.22.0 [\#310](https://github.com/drwl/annotaterb/pull/310) ([drwl](https://github.com/drwl))
22
+ - Run CI on CRuby 4.0 [\#308](https://github.com/drwl/annotaterb/pull/308) ([viralpraxis](https://github.com/viralpraxis))
23
+ - Generate changelog for v4.21.0 [\#307](https://github.com/drwl/annotaterb/pull/307) ([drwl](https://github.com/drwl))
24
+ - fix NoMethodError when using nested\_position with fixture files [\#298](https://github.com/drwl/annotaterb/pull/298) ([OdenTakashi](https://github.com/OdenTakashi))
25
+ - fix: Respect configured sort [\#295](https://github.com/drwl/annotaterb/pull/295) ([patrickarnett](https://github.com/patrickarnett)) **(Maintainer note: this could result in annotations shifting depending on configuration, please create an issue if it is a breaking change)**
26
+ - Use `#lease_connection` if available [\#292](https://github.com/drwl/annotaterb/pull/292) ([viralpraxis](https://github.com/viralpraxis))
27
+ - refactor: simplify primary key check logic \(no functional changes\) [\#285](https://github.com/drwl/annotaterb/pull/285) ([OdenTakashi](https://github.com/OdenTakashi))
28
+ - Honor skip\_on\_db\_migrate config option when runnig migrate tasks [\#274](https://github.com/drwl/annotaterb/pull/274) ([martinechtner](https://github.com/martinechtner))
29
+
3
30
  ## [v4.21.0](https://github.com/drwl/annotaterb/tree/v4.21.0) (2026-01-30)
4
31
 
5
32
  [Full Changelog](https://github.com/drwl/annotaterb/compare/v4.20.0...v4.21.0)
data/README.md CHANGED
@@ -64,6 +64,13 @@ $ bin/rails g annotate_rb:install
64
64
 
65
65
  This will copy a rake task into your Rails project's `lib/tasks` directory that will hook into the Rails project rake tasks, automatically running AnnotateRb after database migration rake tasks.
66
66
 
67
+ ```sh
68
+ $ bin/rails db:migrate
69
+ # ...
70
+ # Annotating models
71
+ # Annotated (1): app/models/task.rb
72
+ ```
73
+
67
74
  To skip the automatic annotation that happens after a db task, pass the environment variable `ANNOTATERB_SKIP_ON_DB_TASKS=1` before your command.
68
75
 
69
76
  ```sh
@@ -184,10 +191,10 @@ Additional options that work for annotating models and routes
184
191
  -f [bare|rdoc|yard|markdown], Render Schema Information as plain/RDoc/Yard/Markdown
185
192
  --format
186
193
  --config_path [path] Path to configuration file (by default, .annotaterb.yml in the root of the project)
187
- -p [before|top|after|bottom], Place the annotations at the top (before) or the bottom (after) of the model/test/fixture/factory/route/serializer file(s)
188
- --position
189
- --pc, --position-in-class [before|top|after|bottom]
190
- Place the annotations at the top (before) or the bottom (after) of the model file
194
+ -p [before|top|after|bottom|before_doc],
195
+ --position Place the annotations at the top (before), bottom (after), or above the class documentation (before_doc) of the model/test/fixture/factory/route/serializer file(s)
196
+ --pc, --position-in-class [before|top|after|bottom|before_doc]
197
+ Place the annotations at the top (before), bottom (after), or above the class documentation (before_doc) of the model file
191
198
  --pf, --position-in-factory [before|top|after|bottom]
192
199
  Place the annotations at the top (before) or the bottom (after) of any factory files
193
200
  --px, --position-in-fixture [before|top|after|bottom]
@@ -230,6 +237,35 @@ Annotaterb reads first the configuration file, if it exists, passes its content
230
237
 
231
238
  For further details visit the [section in the migration guide](MIGRATION_GUIDE.md#automatic-annotations-after-running-database-migration-commands).
232
239
 
240
+ ### Preserving class documentation comments
241
+
242
+ By default, when `position_in_class` is `before` (or `top`), AnnotateRb places the schema annotation immediately before the class declaration line. Any human-written documentation comment that was directly above the class is therefore pushed above the annotation.
243
+
244
+ If you prefer to keep the documentation comment adjacent to the class, set `position_in_class` to `before_doc`. The schema annotation is then inserted above the documentation comment block, leaving the comment directly before the class.
245
+
246
+ ```ruby
247
+ # Source file:
248
+ # Doc about User
249
+ class User < ApplicationRecord
250
+ end
251
+
252
+ # With position_in_class: before (default)
253
+ # Doc about User
254
+ # == Schema Information
255
+ # ...
256
+ class User < ApplicationRecord
257
+ end
258
+
259
+ # With position_in_class: before_doc
260
+ # == Schema Information
261
+ # ...
262
+ # Doc about User
263
+ class User < ApplicationRecord
264
+ end
265
+ ```
266
+
267
+ A "documentation comment" is the contiguous comment block immediately above the class declaration, with no blank line between the comments and the class. Recognized magic comments (`encoding`, `frozen_string_literal`, `shareable_constant_value`, `warn_indent`, `typed`, `rbs_inline`, plus Emacs/Vim style modelines) are excluded so the annotation can still be inserted between magic comments and the class doc.
268
+
233
269
  ### How to skip annotating a particular model
234
270
 
235
271
  If you want to always skip annotations on a particular model, add this string
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.22.0
1
+ 4.23.0
@@ -2,15 +2,38 @@
2
2
 
3
3
  module AnnotateRb
4
4
  module Helper
5
+ # Unicode ranges that typically occupy 2 columns in monospace terminals.
6
+ # This is a simplified wcwidth implementation.
7
+ DOUBLE_WIDTH_RANGES = [
8
+ 0x1100..0x115F, # Hangul Jamo
9
+ 0x2E80..0x303E, # CJK Radicals, Kangxi, CJK Symbols
10
+ 0x3040..0x4DBF, # Hiragana, Katakana, Bopomofo, CJK Ext A
11
+ 0x4E00..0x9FFF, # CJK Unified Ideographs
12
+ 0xAC00..0xD7AF, # Hangul Syllables
13
+ 0xF900..0xFAFF, # CJK Compatibility Ideographs
14
+ 0xFE30..0xFE6F, # CJK Compatibility Forms
15
+ 0xFF01..0xFF60, # Fullwidth Forms
16
+ 0xFFE0..0xFFE6, # Fullwidth Signs
17
+ 0x20000..0x2FA1F, # CJK Extensions B-F
18
+ 0x30000..0x3134F # CJK Extensions G-I
19
+ ].freeze
20
+
5
21
  class << self
6
22
  def width(string)
7
- string.chars.inject(0) { |acc, elem| acc + ((elem.bytesize == 3) ? 2 : 1) }
23
+ string.each_char.sum { |char| double_width?(char) ? 2 : 1 }
8
24
  end
9
25
 
10
26
  # TODO: Find another implementation that doesn't depend on ActiveSupport
11
27
  def fallback(*args)
12
28
  args.compact.detect(&:present?)
13
29
  end
30
+
31
+ private
32
+
33
+ def double_width?(char)
34
+ cp = char.ord
35
+ DOUBLE_WIDTH_RANGES.any? { |range| range.cover?(cp) }
36
+ end
14
37
  end
15
38
  end
16
39
  end
@@ -5,7 +5,7 @@ module AnnotateRb
5
5
  module AnnotatedFile
6
6
  # Generates the file with fresh annotations
7
7
  class Generator
8
- def initialize(file_content, new_annotations, annotation_position, parser_klass, parsed_file, options)
8
+ def initialize(file_content, new_annotations, annotation_position, parser_klass, parsed_file, options, model_class_name: nil)
9
9
  @annotation_position = annotation_position
10
10
  @options = options
11
11
 
@@ -16,6 +16,7 @@ module AnnotateRb
16
16
 
17
17
  @parser = parser_klass
18
18
  @parsed_file = parsed_file
19
+ @model_class_name = model_class_name
19
20
  end
20
21
 
21
22
  # @return [String] Returns the annotated file content to be written back to a file
@@ -35,7 +36,7 @@ module AnnotateRb
35
36
  # We need to get class start and class end depending on the position
36
37
  parsed = @parser.parse(content_without_annotations)
37
38
 
38
- _content = if %w[after bottom].include?(annotation_write_position)
39
+ _content = if Parser::AFTER_POSITIONS.include?(annotation_write_position)
39
40
  content_annotated_after(parsed, content_without_annotations)
40
41
  else
41
42
  content_annotated_before(parsed, content_without_annotations, annotation_write_position)
@@ -45,10 +46,14 @@ module AnnotateRb
45
46
  private
46
47
 
47
48
  def content_annotated_before(parsed, content_without_annotations, write_position)
48
- same_write_position = @parsed_file.has_annotations? && @parsed_file.annotation_position.to_s == write_position
49
+ same_write_position = same_side?(write_position)
49
50
 
50
51
  _constant_name, line_number_before = determine_annotation_position(parsed)
51
52
 
53
+ if Parser::DOC_AWARE_POSITIONS.include?(write_position)
54
+ line_number_before = class_doc_start_line(content_without_annotations.lines, line_number_before)
55
+ end
56
+
52
57
  content_with_annotations_written_before = []
53
58
  content_with_annotations_written_before << content_without_annotations.lines[0...line_number_before]
54
59
  content_with_annotations_written_before << $/ if @parsed_file.has_leading_whitespace? && same_write_position
@@ -59,17 +64,34 @@ module AnnotateRb
59
64
  content_with_annotations_written_before.join
60
65
  end
61
66
 
62
- # Determines where to place the annotation based on the nested_position option.
63
- # When nested_position is enabled, finds the most deeply nested class declaration
64
- # to place annotations directly above nested classes instead of at the file top.
65
- def determine_annotation_position(parsed)
66
- # Handle empty files where no classes/modules are found
67
- return [nil, 0] if parsed.starts.empty?
67
+ def same_side?(write_position)
68
+ return false unless @parsed_file.has_annotations?
69
+
70
+ new_side = Parser::AFTER_POSITIONS.include?(write_position) ? :after : :before
71
+ @parsed_file.annotation_position == new_side
72
+ end
68
73
 
69
- return parsed.starts.first unless @options[:nested_position] && parsed.respond_to?(:type_map)
74
+ # Magic comments are excluded so annotations slot between them and
75
+ # the class doc rather than being treated as part of the doc.
76
+ def class_doc_start_line(content_lines, line_number_before)
77
+ doc_start = line_number_before
78
+ cursor = line_number_before - 1
70
79
 
71
- class_entries = parsed.starts.select { |name, _line| parsed.type_map[name] == :class }
72
- class_entries.last || parsed.starts.first
80
+ while cursor >= 0
81
+ line = content_lines[cursor]
82
+ break if line.match?(/\A\s*\z/)
83
+ break unless line.match?(/\A\s*#/)
84
+ break if FileParser::MagicComment.match?(line)
85
+
86
+ doc_start = cursor
87
+ cursor -= 1
88
+ end
89
+
90
+ doc_start
91
+ end
92
+
93
+ def determine_annotation_position(parsed)
94
+ FileParser::AnnotationTarget.find(parsed, @options, model_class_name: @model_class_name) || [nil, 0]
73
95
  end
74
96
 
75
97
  # Formats annotations with appropriate indentation for consistent code structure.
@@ -18,13 +18,26 @@ module AnnotateRb
18
18
  def update
19
19
  return "" if !@parsed_file.has_annotations?
20
20
 
21
- new_annotation = wrapped_content(@new_annotations)
21
+ new_annotation = indent(wrapped_content(@new_annotations), existing_indentation)
22
22
 
23
23
  _content = @file_content.sub(@parsed_file.annotations) { new_annotation }
24
24
  end
25
25
 
26
26
  private
27
27
 
28
+ # Returns the leading whitespace of the existing annotation block so
29
+ # nested-position annotations keep their indentation across re-runs.
30
+ def existing_indentation
31
+ first_line = @parsed_file.annotations.lines.first
32
+ first_line&.match(/\A([ \t]*)/)&.[](1) || ""
33
+ end
34
+
35
+ def indent(content, indentation)
36
+ return content if indentation.empty?
37
+
38
+ content.lines.map { |line| (line == "\n") ? line : "#{indentation}#{line}" }.join
39
+ end
40
+
28
41
  def wrapped_content(content)
29
42
  wrapper_open = if @options[:wrapper_open]
30
43
  "# #{@options[:wrapper_open]}\n"
@@ -27,6 +27,7 @@ module AnnotateRb
27
27
  IndexAnnotation::AnnotationBuilder.new(@model, @options).build,
28
28
  ForeignKeyAnnotation::AnnotationBuilder.new(@model, @options).build,
29
29
  CheckConstraintAnnotation::AnnotationBuilder.new(@model, @options).build,
30
+ EnumAnnotation::AnnotationBuilder.new(@model, @options).build,
30
31
  SchemaFooter.new
31
32
  ]
32
33
  end
@@ -33,6 +33,9 @@ module AnnotateRb
33
33
  warn "Unable to process #{@file}: #{e.message}"
34
34
  warn "\t#{e.backtrace.join("\n\t")}" if @options[:trace]
35
35
  end
36
+ rescue ActiveRecord::ConnectionNotEstablished,
37
+ ActiveRecord::NoDatabaseError => e
38
+ abort "AnnotateRb: Database connection error - #{e.message}"
36
39
  rescue => e
37
40
  warn "Unable to process #{@file}: #{e.message}"
38
41
  warn "\t#{e.backtrace.join("\n\t")}" if @options[:trace]
@@ -4,14 +4,17 @@ module AnnotateRb
4
4
  module ModelAnnotator
5
5
  # Compares the current file content and new annotation block and generates the column annotation differences
6
6
  class AnnotationDiffGenerator
7
- HEADER_PATTERN = /(^# Table name:.*?\n(#.*\r?\n)*\r?)/
7
+ # Leading whitespace is tolerated so that annotations placed inside a
8
+ # nested module/class (i.e. with --nested-position, or hand-aligned)
9
+ # still match.
10
+ HEADER_PATTERN = /(^[ \t]*# Table name:.*?\n([ \t]*#.*\r?\n)*\r?)/
8
11
  # Example matches:
9
12
  # - "# id :uuid not null, primary key"
10
13
  # - "# title(length 255) :string not null"
11
14
  # - "# status(a/b/c) :string not null"
12
15
  # - "# created_at :datetime not null"
13
16
  # - "# updated_at :datetime not null"
14
- COLUMN_PATTERN = /^#[\t ]+[[\p{L}\p{N}_]*.`\[\]():]+(?:\(.*?\))?[\t ]+.+$/
17
+ COLUMN_PATTERN = /^[ \t]*#[\t ]+[[\p{L}\p{N}_]*.`\[\]():]+(?:\(.*?\))?[\t ]+.+$/
15
18
  class << self
16
19
  def call(file_content, annotation_block)
17
20
  new(file_content, annotation_block).generate
@@ -30,14 +33,16 @@ module AnnotateRb
30
33
  current_annotations = @file_content.match(HEADER_PATTERN).to_s
31
34
  new_annotations = @annotation_block.match(HEADER_PATTERN).to_s
32
35
 
36
+ # Strip leading whitespace from each scanned column so that an
37
+ # indented existing block compares equal to a flush-left new block.
33
38
  current_annotation_columns = if current_annotations.present?
34
- current_annotations.scan(COLUMN_PATTERN)
39
+ current_annotations.scan(COLUMN_PATTERN).map(&:lstrip)
35
40
  else
36
41
  []
37
42
  end
38
43
 
39
44
  new_annotation_columns = if new_annotations.present?
40
- new_annotations.scan(COLUMN_PATTERN)
45
+ new_annotations.scan(COLUMN_PATTERN).map(&:lstrip)
41
46
  else
42
47
  []
43
48
  end
@@ -84,6 +84,13 @@ module AnnotateRb
84
84
  end
85
85
  end
86
86
 
87
+ if column_type == "enum" && @options[:show_enums]
88
+ enum_type_name = @column.sql_type
89
+ if enum_type_name.present? && enum_type_name != "enum"
90
+ attrs << "enum_type: #{enum_type_name}"
91
+ end
92
+ end
93
+
87
94
  # Check if the column is a virtual column and print the function
88
95
  if @options[:show_virtual_columns] && @column.virtual?
89
96
  # Any whitespace in the function gets reduced to a single space
@@ -42,7 +42,7 @@ module AnnotateRb
42
42
 
43
43
  def to_yard
44
44
  res = ""
45
- res += sprintf("# @!attribute #{name}") + "\n"
45
+ res += "# @!attribute #{name}\n"
46
46
 
47
47
  ruby_class = if @column.respond_to?(:array) && @column.array
48
48
  "Array<#{map_col_type_to_ruby_classes(type)}>"
@@ -50,7 +50,7 @@ module AnnotateRb
50
50
  map_col_type_to_ruby_classes(type)
51
51
  end
52
52
 
53
- res += sprintf("# @return [#{ruby_class}]")
53
+ res += "# @return [#{ruby_class}]"
54
54
 
55
55
  res
56
56
  end
@@ -26,6 +26,10 @@ module AnnotateRb
26
26
  @column.type
27
27
  end
28
28
 
29
+ def sql_type
30
+ @column.sql_type.to_s
31
+ end
32
+
29
33
  def column_type_string
30
34
  if (@column.respond_to?(:bigint?) && @column.bigint?) || /\Abigint\b/ =~ @column.sql_type
31
35
  "bigint"
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module EnumAnnotation
6
+ class Annotation
7
+ HEADER_TEXT = "Enums"
8
+
9
+ def initialize(enums)
10
+ @enums = enums
11
+ end
12
+
13
+ def body
14
+ [
15
+ Components::BlankCommentLine.new,
16
+ Components::Header.new(HEADER_TEXT),
17
+ Components::BlankCommentLine.new,
18
+ *@enums
19
+ ]
20
+ end
21
+
22
+ def to_markdown
23
+ body.map(&:to_markdown).join("\n")
24
+ end
25
+
26
+ def to_rdoc
27
+ body.map(&:to_rdoc).join("\n")
28
+ end
29
+
30
+ def to_yard
31
+ body.map(&:to_yard).join("\n")
32
+ end
33
+
34
+ def to_default
35
+ body.map(&:to_default).join("\n")
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module EnumAnnotation
6
+ class AnnotationBuilder
7
+ def initialize(model, options)
8
+ @model = model
9
+ @options = options
10
+ end
11
+
12
+ def build
13
+ return Components::NilComponent.new unless @options[:show_enums]
14
+
15
+ enum_types = @model.enum_types
16
+ return Components::NilComponent.new if enum_types.empty?
17
+
18
+ # Filter to only enum types used by this table's columns
19
+ table_enum_types = @model.columns.select { |col| col.type == :enum }
20
+ .map { |col| col.sql_type.to_s }
21
+ .uniq
22
+
23
+ relevant_enums = enum_types
24
+ .filter_map { |name, values| [name.to_s, values] if table_enum_types.include?(name.to_s) }
25
+ return Components::NilComponent.new if relevant_enums.empty?
26
+
27
+ max_size = relevant_enums.map { |name, _values| name.size }.max + 1
28
+
29
+ components = relevant_enums.sort_by { |name, _values| name }.map do |name, values|
30
+ EnumComponent.new(name, values, max_size)
31
+ end
32
+
33
+ Annotation.new(components)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module EnumAnnotation
6
+ class EnumComponent < Components::Base
7
+ attr_reader :name, :values, :max_size
8
+
9
+ def initialize(name, values, max_size)
10
+ @name = name
11
+ @values = values
12
+ @max_size = max_size
13
+ end
14
+
15
+ def to_default
16
+ # standard:disable Lint/FormatParameterMismatch
17
+ sprintf("# %-#{max_size}.#{max_size}s %s", name, values.join(", ")).rstrip
18
+ # standard:enable Lint/FormatParameterMismatch
19
+ end
20
+
21
+ def to_markdown
22
+ sprintf("# * `%s`: `%s`", name, values.join(", "))
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module EnumAnnotation
6
+ autoload :AnnotationBuilder, "annotate_rb/model_annotator/enum_annotation/annotation_builder"
7
+ autoload :Annotation, "annotate_rb/model_annotator/enum_annotation/annotation"
8
+ autoload :EnumComponent, "annotate_rb/model_annotator/enum_annotation/enum_component"
9
+ end
10
+ end
11
+ end
@@ -8,6 +8,20 @@ module AnnotateRb
8
8
  COMPAT_PREFIX_MD = "## Schema Info"
9
9
  DEFAULT_ANNOTATION_ENDING = "#"
10
10
 
11
+ SCHEMA_HEADER_PREFIXES = [
12
+ COMPAT_PREFIX,
13
+ COMPAT_PREFIX_MD,
14
+ "Table name:",
15
+ "Database name:",
16
+ "Schema version:"
17
+ ].freeze
18
+
19
+ SCHEMA_HEADER_EXACT = [
20
+ IndexAnnotation::Annotation::HEADER_TEXT,
21
+ ForeignKeyAnnotation::Annotation::HEADER_TEXT,
22
+ CheckConstraintAnnotation::Annotation::HEADER_TEXT
23
+ ].freeze
24
+
11
25
  class MalformedAnnotation < StandardError; end
12
26
 
13
27
  class NoAnnotationFound < StandardError; end
@@ -70,10 +84,7 @@ module AnnotateRb
70
84
  end
71
85
  end
72
86
  else
73
- # Walk back until we find the end of the annotation comment block
74
- while ending > start && comments[ending].first != DEFAULT_ANNOTATION_ENDING
75
- ending -= 1
76
- end
87
+ ending = walk_forward_through_schema(comments, start, ending)
77
88
  end
78
89
 
79
90
  # We want .last because we want the line indexes
@@ -95,6 +106,30 @@ module AnnotateRb
95
106
  def annotated?
96
107
  @annotation_start && @annotation_end
97
108
  end
109
+
110
+ private
111
+
112
+ def walk_forward_through_schema(comments, start, block_end)
113
+ ending = start
114
+ while ending < block_end
115
+ break unless schema_like?(comments[ending + 1].first)
116
+ ending += 1
117
+ end
118
+ ending
119
+ end
120
+
121
+ # Tabular rows have ≥2 leading spaces after `#`; prose has at most one.
122
+ def schema_like?(comment)
123
+ return true if comment == DEFAULT_ANNOTATION_ENDING
124
+
125
+ text = comment.sub(/\A#\s?/, "")
126
+ return false if text.empty?
127
+
128
+ return true if SCHEMA_HEADER_PREFIXES.any? { |p| text.start_with?(p) }
129
+ return true if SCHEMA_HEADER_EXACT.include?(text)
130
+
131
+ comment.match?(/\A#\s{2,}\S/)
132
+ end
98
133
  end
99
134
  end
100
135
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module FileParser
6
+ # When `model_class_name` is given, matches that class declaration directly
7
+ # so inner classes inside the model body cannot be mistaken for the target.
8
+ module AnnotationTarget
9
+ SKIP_NAMES = %w[require require_relative load].freeze
10
+
11
+ def self.find(parser, options, model_class_name: nil)
12
+ starts = parser.starts.reject { |entry| SKIP_NAMES.include?(entry.first) }
13
+ return nil if starts.empty?
14
+
15
+ return starts.first unless options[:nested_position] && parser.respond_to?(:type_map)
16
+
17
+ if model_class_name && parser.type_map[model_class_name] == :class
18
+ match = starts.find { |name, _line| name == model_class_name }
19
+ return match if match
20
+ end
21
+
22
+ class_entries = starts
23
+ .select { |name, _line| parser.type_map[name] == :class }
24
+ .uniq { |name, _line| name }
25
+
26
+ class_entries.last || starts.first
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -50,16 +50,23 @@ module AnnotateRb
50
50
  }
51
51
  end
52
52
 
53
+ def on_const(const)
54
+ @_last_const_lineno = lineno
55
+ super
56
+ end
57
+
53
58
  def on_const_ref(const)
54
- add_event(__method__, const, lineno)
55
- @block_starts << [const, lineno]
59
+ declaration_lineno = @_last_const_lineno || lineno
60
+ add_event(__method__, const, declaration_lineno)
61
+ @block_starts << [const, declaration_lineno]
56
62
  super
57
63
  end
58
64
 
59
65
  # Used for `class Foo::User`
60
66
  def on_const_path_ref(_left, const)
61
- add_event(__method__, const, lineno)
62
- @block_starts << [const, lineno]
67
+ declaration_lineno = @_last_const_lineno || lineno
68
+ add_event(__method__, const, declaration_lineno)
69
+ @block_starts << [const, declaration_lineno]
63
70
  super
64
71
  end
65
72
 
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module FileParser
6
+ module MagicComment
7
+ DIRECTIVE_KEYS = %w[
8
+ encoding
9
+ coding
10
+ frozen_string_literal
11
+ warn_indent
12
+ shareable_constant_value
13
+ typed
14
+ rbs_inline
15
+ ].freeze
16
+
17
+ SIMPLE = /\A\s*#\s*(?<key>[A-Za-z][A-Za-z0-9_-]*)\s*:\s*\S/
18
+ EMACS = /\A\s*#\s*-\*-.*-\*-\s*\z/
19
+ VIM = /\A\s*#\s*vim:\s/
20
+
21
+ def self.match?(line)
22
+ if (m = SIMPLE.match(line))
23
+ key = m[:key].downcase.tr("-", "_")
24
+ return true if DIRECTIVE_KEYS.include?(key)
25
+ end
26
+
27
+ EMACS.match?(line) || VIM.match?(line)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -6,12 +6,13 @@ module AnnotateRb
6
6
  class ParsedFile
7
7
  SKIP_ANNOTATION_STRING = "# -*- SkipSchemaAnnotations"
8
8
 
9
- def initialize(file_content, new_annotations, parser_klass, options)
9
+ def initialize(file_content, new_annotations, parser_klass, options, model_class_name: nil)
10
10
  @file_content = file_content
11
11
  @file_lines = @file_content.lines
12
12
  @new_annotations = new_annotations
13
13
  @parser_klass = parser_klass
14
14
  @options = options
15
+ @model_class_name = model_class_name
15
16
  end
16
17
 
17
18
  def parse
@@ -44,7 +45,7 @@ module AnnotateRb
44
45
  annotation_start = @finder.annotation_start
45
46
  annotation_end = @finder.annotation_end
46
47
 
47
- if @file_lines[annotation_start - 1]&.strip&.empty?
48
+ if annotation_start > 0 && @file_lines[annotation_start - 1]&.strip&.empty?
48
49
  annotation_start -= 1
49
50
  has_leading_whitespace = true
50
51
  end
@@ -64,10 +65,8 @@ module AnnotateRb
64
65
  annotation_position = nil
65
66
 
66
67
  if has_annotations
67
- const_declaration = @file_parser.starts.first
68
-
69
- # If the file does not have any class or module declaration then const_declaration can be nil
70
- _const, line_number = const_declaration
68
+ target = AnnotationTarget.find(@file_parser, @options, model_class_name: @model_class_name)
69
+ _const, line_number = target if target
71
70
 
72
71
  if line_number
73
72
  annotation_position = if @finder.annotation_start < line_number
@@ -4,7 +4,9 @@ module AnnotateRb
4
4
  module ModelAnnotator
5
5
  module FileParser
6
6
  autoload :AnnotationFinder, "annotate_rb/model_annotator/file_parser/annotation_finder"
7
+ autoload :AnnotationTarget, "annotate_rb/model_annotator/file_parser/annotation_target"
7
8
  autoload :CustomParser, "annotate_rb/model_annotator/file_parser/custom_parser"
9
+ autoload :MagicComment, "annotate_rb/model_annotator/file_parser/magic_comment"
8
10
  autoload :ParsedFile, "annotate_rb/model_annotator/file_parser/parsed_file"
9
11
  autoload :ParsedFileResult, "annotate_rb/model_annotator/file_parser/parsed_file_result"
10
12
  autoload :YmlParser, "annotate_rb/model_annotator/file_parser/yml_parser"
@@ -45,16 +45,27 @@ module AnnotateRb
45
45
  end
46
46
  end
47
47
 
48
+ comment_info = ""
49
+ if options[:show_indexes_comments]
50
+ value = index.try(:comment).try(:to_s)
51
+ comment_info = if value.present?
52
+ " COMMENT #{value}"
53
+ else
54
+ ""
55
+ end
56
+ end
57
+
48
58
  # standard:disable Lint/FormatParameterMismatch
49
59
  sprintf(
50
- "# %-#{max_size}.#{max_size}s %s%s%s%s%s%s",
60
+ "# %-#{max_size}.#{max_size}s %s%s%s%s%s%s%s",
51
61
  index.name,
52
62
  "(#{columns_info.join(",")})",
53
63
  include_info,
54
64
  unique_info,
55
65
  nulls_not_distinct_info,
56
66
  where_info,
57
- using_info
67
+ using_info,
68
+ comment_info
58
69
  ).rstrip
59
70
  # standard:enable Lint/FormatParameterMismatch
60
71
  end
@@ -92,13 +103,24 @@ module AnnotateRb
92
103
  end
93
104
  end
94
105
 
106
+ comment_info = ""
107
+ if options[:show_indexes_comments]
108
+ value = index.try(:comment).try(:to_s)
109
+ comment_info = if value.present?
110
+ " _comment_ #{value}"
111
+ else
112
+ ""
113
+ end
114
+ end
115
+
95
116
  details = sprintf(
96
- "%s%s%s%s%s",
117
+ "%s%s%s%s%s%s",
97
118
  include_info,
98
119
  unique_info,
99
120
  nulls_not_distinct_info,
100
121
  where_info,
101
- using_info
122
+ using_info,
123
+ comment_info
102
124
  ).strip
103
125
  details = " (#{details})" unless details.blank?
104
126
 
@@ -219,6 +219,23 @@ module AnnotateRb
219
219
  ]
220
220
  end
221
221
 
222
+ def enum_types
223
+ @enum_types ||=
224
+ if connection.respond_to?(:enum_types)
225
+ begin
226
+ # enum values may be a String or an Array depending on the Rails version.
227
+ # See: https://github.com/rails/rails/pull/54141
228
+ connection.enum_types.map do |name, values|
229
+ [name, values.is_a?(Array) ? values : values.to_s.split(",")]
230
+ end
231
+ rescue ActiveRecord::StatementInvalid
232
+ []
233
+ end
234
+ else
235
+ []
236
+ end
237
+ end
238
+
222
239
  def migration_version
223
240
  return 0 unless @options[:include_version]
224
241
 
@@ -229,7 +246,14 @@ module AnnotateRb
229
246
 
230
247
  if @options.get_state(cache_key).nil?
231
248
  migration_version = begin
232
- connection.migration_context.current_version
249
+ # Rails 7.1+ moved migration_context from ConnectionAdapter to ConnectionPool.
250
+ # ConnectionAdapter#migration_context was removed in Rails 7.2.
251
+ # See: https://github.com/rails/rails/pull/51162
252
+ if connection.pool.respond_to?(:migration_context)
253
+ connection.pool.migration_context.current_version
254
+ else
255
+ connection.migration_context.current_version
256
+ end
233
257
  rescue
234
258
  0
235
259
  end
@@ -42,13 +42,14 @@ module AnnotateRb
42
42
  klass.reset_column_information
43
43
  annotation = Annotation::AnnotationBuilder.new(klass, @options).build
44
44
  model_name = klass.name.underscore
45
+ model_class_name = klass.name.demodulize
45
46
 
46
47
  # In multi-database configurations, it is possible for different models to have the same table name but live
47
48
  # in different databases. Here we are opting to use the table name to find related files only when the model
48
49
  # is using the primary connection.
49
50
  table_name = klass.table_name if klass.connection_specification_name == ActiveRecord::Base.name
50
51
 
51
- model_instruction = SingleFileAnnotatorInstruction.new(file, annotation, :position_in_class, @options)
52
+ model_instruction = SingleFileAnnotatorInstruction.new(file, annotation, :position_in_class, @options, model_class_name: model_class_name)
52
53
  instructions << model_instruction
53
54
 
54
55
  related_files = RelatedFilesListBuilder.new(file, model_name, table_name, @options).build
@@ -5,7 +5,7 @@ module AnnotateRb
5
5
  class SingleFileAnnotator
6
6
  class << self
7
7
  def call_with_instructions(instruction)
8
- call(instruction.file, instruction.annotation, instruction.position, instruction.options)
8
+ call(instruction.file, instruction.annotation, instruction.position, instruction.options, model_class_name: instruction.model_class_name)
9
9
  end
10
10
 
11
11
  # Add a schema block to a file. If the file already contains
@@ -21,14 +21,14 @@ module AnnotateRb
21
21
  # :position_in_*<Symbol>:: where to place the annotated section in fixture or model file,
22
22
  # :before, :top, :after or :bottom. Default is :before.
23
23
  #
24
- def call(file_name, annotation, annotation_position, options)
24
+ def call(file_name, annotation, annotation_position, options, model_class_name: nil)
25
25
  return false unless File.exist?(file_name)
26
26
  old_content = File.read(file_name)
27
27
 
28
28
  parser_klass = FileToParserMapper.map(file_name)
29
29
 
30
30
  begin
31
- parsed_file = FileParser::ParsedFile.new(old_content, annotation, parser_klass, options).parse
31
+ parsed_file = FileParser::ParsedFile.new(old_content, annotation, parser_klass, options, model_class_name: model_class_name).parse
32
32
  rescue FileParser::AnnotationFinder::MalformedAnnotation => e
33
33
  warn "Unable to process #{file_name}: #{e.message}"
34
34
  warn "\t" + e.backtrace.join("\n\t") if @options[:trace]
@@ -41,9 +41,9 @@ module AnnotateRb
41
41
  abort "AnnotateRb error. #{file_name} needs to be updated, but annotaterb was run with `--frozen`." if options[:frozen]
42
42
 
43
43
  updated_file_content = if !parsed_file.has_annotations?
44
- AnnotatedFile::Generator.new(old_content, annotation, annotation_position, parser_klass, parsed_file, options).generate
44
+ AnnotatedFile::Generator.new(old_content, annotation, annotation_position, parser_klass, parsed_file, options, model_class_name: model_class_name).generate
45
45
  elsif options[:force]
46
- AnnotatedFile::Generator.new(old_content, annotation, annotation_position, parser_klass, parsed_file, options).generate
46
+ AnnotatedFile::Generator.new(old_content, annotation, annotation_position, parser_klass, parsed_file, options, model_class_name: model_class_name).generate
47
47
  else
48
48
  AnnotatedFile::Updater.new(old_content, annotation, annotation_position, parsed_file, options).update
49
49
  end
@@ -4,14 +4,15 @@ module AnnotateRb
4
4
  module ModelAnnotator
5
5
  # A plain old Ruby object (PORO) that contains all necessary information for SingleFileAnnotator
6
6
  class SingleFileAnnotatorInstruction
7
- def initialize(file, annotation, position, options)
7
+ def initialize(file, annotation, position, options, model_class_name: nil)
8
8
  @file = file # Path to file
9
9
  @annotation = annotation # Annotation string
10
10
  @position = position # Position in the file where to write the annotation to
11
11
  @options = options
12
+ @model_class_name = model_class_name # Short class name; set for the model file itself, nil for related files
12
13
  end
13
14
 
14
- attr_reader :file, :annotation, :position, :options
15
+ attr_reader :file, :annotation, :position, :options, :model_class_name
15
16
  end
16
17
  end
17
18
  end
@@ -49,7 +49,7 @@ module AnnotateRb
49
49
 
50
50
  # once we have the filepath_relative_to_root_dir, we need to see if it
51
51
  # falls within one of our Zeitwerk "collapsed" paths.
52
- if loader.collapse.any? { |path| path.include?(root_dir) && file.include?(path.split(root_dir)[1]) }
52
+ if loader.collapse.any? { |path| path.include?(root_dir) && @file.include?(path.split(root_dir)[1]) }
53
53
  # if the file is within a collapsed path, we then need to, for each
54
54
  # collapsed path, remove the root dir
55
55
  collapsed = loader.collapse.map { |path| path.split(root_dir)[1].sub(/^\//, "") }.to_set
@@ -27,6 +27,7 @@ module AnnotateRb
27
27
  autoload :FileParser, "annotate_rb/model_annotator/file_parser"
28
28
  autoload :ZeitwerkClassGetter, "annotate_rb/model_annotator/zeitwerk_class_getter"
29
29
  autoload :CheckConstraintAnnotation, "annotate_rb/model_annotator/check_constraint_annotation"
30
+ autoload :EnumAnnotation, "annotate_rb/model_annotator/enum_annotation"
30
31
  autoload :FileToParserMapper, "annotate_rb/model_annotator/file_to_parser_mapper"
31
32
  autoload :Components, "annotate_rb/model_annotator/components"
32
33
  autoload :Annotation, "annotate_rb/model_annotator/annotation"
@@ -47,8 +47,10 @@ module AnnotateRb
47
47
  include_version: false, # ModelAnnotator
48
48
  show_complete_foreign_keys: false, # ModelAnnotator
49
49
  show_check_constraints: false, # ModelAnnotator
50
+ show_enums: false, # ModelAnnotator
50
51
  show_foreign_keys: true, # ModelAnnotator
51
52
  show_indexes: true, # ModelAnnotator
53
+ show_indexes_comments: false, # ModelAnnotator
52
54
  show_indexes_include: false, # ModelAnnotator
53
55
  show_virtual_columns: false, # ModelAnnotator
54
56
  simple_indexes: false, # ModelAnnotator
@@ -78,10 +80,10 @@ module AnnotateRb
78
80
  ignore_columns: nil, # ModelAnnotator
79
81
  ignore_multi_database_name: false, # ModelAnnotator
80
82
  ignore_routes: nil, # RouteAnnotator
81
- ignore_unknown_models: false, # ModelAnnotator
82
83
  models: true, # Core
83
84
  routes: false, # Core
84
85
  skip_on_db_migrate: false, # Core
86
+ auto_annotate_routes_after_migrate: false, # Core
85
87
  target_action: :do_annotations, # Core; Possible values: :do_annotations, :remove_annotations
86
88
  wrapper: nil, # ModelAnnotator, RouteAnnotator
87
89
  wrapper_close: nil, # ModelAnnotator, RouteAnnotator
@@ -120,9 +122,11 @@ module AnnotateRb
120
122
  :ignore_unknown_models,
121
123
  :include_version,
122
124
  :show_check_constraints,
125
+ :show_enums,
123
126
  :show_complete_foreign_keys,
124
127
  :show_foreign_keys,
125
128
  :show_indexes,
129
+ :show_indexes_comments,
126
130
  :show_indexes_include,
127
131
  :simple_indexes,
128
132
  :sort,
@@ -143,11 +147,11 @@ module AnnotateRb
143
147
  :timestamp_columns,
144
148
  :ignore_columns,
145
149
  :ignore_routes,
146
- :ignore_unknown_models,
147
150
  :ignore_multi_database_name,
148
151
  :models,
149
152
  :routes,
150
153
  :skip_on_db_migrate,
154
+ :auto_annotate_routes_after_migrate,
151
155
  :target_action,
152
156
  :wrapper,
153
157
  :wrapper_close,
@@ -22,7 +22,9 @@ module AnnotateRb
22
22
  exit: false
23
23
  }.freeze
24
24
 
25
- ANNOTATION_POSITIONS = %w[before top after bottom].freeze
25
+ ANNOTATION_POSITIONS = %w[before top after bottom before_doc].freeze
26
+ AFTER_POSITIONS = %w[after bottom].freeze
27
+ DOC_AWARE_POSITIONS = %w[before_doc].freeze
26
28
  FILE_TYPE_POSITIONS = %w[position_in_class position_in_factory position_in_fixture position_in_test position_in_routes position_in_serializer].freeze
27
29
  EXCLUSION_LIST = %w[tests fixtures factories serializers].freeze
28
30
  FORMAT_TYPES = %w[bare rdoc yard markdown].freeze
@@ -297,9 +299,9 @@ module AnnotateRb
297
299
  has_set_position = {}
298
300
 
299
301
  option_parser.on("-p",
300
- "--position [before|top|after|bottom]",
302
+ "--position [before|top|after|bottom|before_doc]",
301
303
  ANNOTATION_POSITIONS,
302
- "Place the annotations at the top (before) or the bottom (after) of the model/test/fixture/factory/route/serializer file(s)") do |position|
304
+ "Place the annotations at the top (before), bottom (after), or above the class documentation (before_doc) of the model/test/fixture/factory/route/serializer file(s)") do |position|
303
305
  @options[:position] = position
304
306
 
305
307
  FILE_TYPE_POSITIONS.each do |key|
@@ -308,9 +310,9 @@ module AnnotateRb
308
310
  end
309
311
 
310
312
  option_parser.on("--pc",
311
- "--position-in-class [before|top|after|bottom]",
313
+ "--position-in-class [before|top|after|bottom|before_doc]",
312
314
  ANNOTATION_POSITIONS,
313
- "Place the annotations at the top (before) or the bottom (after) of the model file") do |position_in_class|
315
+ "Place the annotations at the top (before), bottom (after), or above the class documentation (before_doc) of the model file") do |position_in_class|
314
316
  @options[:position_in_class] = position_in_class
315
317
  has_set_position["position_in_class"] = true
316
318
  end
@@ -75,7 +75,7 @@ module AnnotateRb
75
75
  header_position = 0
76
76
 
77
77
  content.split(/\n/, -1).each_with_index do |line, line_number|
78
- if mode == :header && line !~ /\s*#/
78
+ if mode == :header && line !~ /\A\s*#/
79
79
  mode = :content
80
80
  real_content << line unless line.blank?
81
81
  elsif mode == :content
@@ -70,7 +70,7 @@ module AnnotateRb
70
70
  header_position = 0
71
71
 
72
72
  content.split(/\n/, -1).each_with_index do |line, line_number|
73
- if mode == :header && line !~ /\s*#/
73
+ if mode == :header && line !~ /\A\s*#/
74
74
  mode = :content
75
75
  real_content << line unless line.blank?
76
76
  elsif mode == :content
@@ -5,10 +5,18 @@ module AnnotateRb
5
5
  class << self
6
6
  attr_reader :runner
7
7
 
8
- def run(args)
8
+ def run_after_migration
9
+ config_file_options = ConfigLoader.load_config
10
+ options = Options.from(config_file_options)
11
+
12
+ commands = ["models", *(options[:auto_annotate_routes_after_migrate] ? ["routes"] : [])]
13
+ commands.each { |cmd| run([cmd], config_file_options: config_file_options) }
14
+ end
15
+
16
+ def run(args, config_file_options: nil)
9
17
  self.runner = new
10
18
 
11
- runner.run(args)
19
+ runner.run(args, config_file_options: config_file_options)
12
20
 
13
21
  self.runner = nil
14
22
  end
@@ -22,14 +30,14 @@ module AnnotateRb
22
30
  attr_writer :runner
23
31
  end
24
32
 
25
- def run(args)
33
+ def run(args, config_file_options: nil)
26
34
  parser = Parser.new(args, {})
27
35
 
28
36
  parsed_options = parser.parse
29
37
  remaining_args = parser.remaining_args
30
38
 
31
39
  AnnotateRb::ConfigFinder.config_path = parsed_options[:config_path] if parsed_options[:config_path]
32
- config_file_options = ConfigLoader.load_config
40
+ config_file_options = ConfigLoader.load_config if config_file_options.nil?
33
41
  options = config_file_options.merge(parsed_options)
34
42
 
35
43
  @options = Options.from(options, {working_args: remaining_args})
@@ -28,11 +28,16 @@ migration_tasks.each do |task|
28
28
  next unless Rake::Task.task_defined?(task)
29
29
  next if config[:skip_on_db_migrate]
30
30
 
31
- Rake::Task[task].enhance do # This block is ran after `task` completes
32
- task_name = Rake.application.top_level_tasks.last # The name of the task that was run, e.g. "db:migrate"
31
+ Rake::Task[task].enhance do |current_task| # This block is ran after `task` completes
32
+ # Prefer the top-level task (the one invoked from the CLI, e.g. "db:migrate") so that
33
+ # when sub-tasks chain (e.g. db:migrate:redo invokes db:rollback then db:migrate), we
34
+ # defer the annotation run to after everything completes. Fall back to the currently
35
+ # enhanced task when there is no top-level task (e.g. when the task is invoked
36
+ # programmatically from application code rather than from the Rake CLI).
37
+ task_name = Rake.application.top_level_tasks.last || current_task.name
33
38
 
34
39
  Rake::Task[task_name].enhance do
35
- ::AnnotateRb::Runner.run(["models"])
40
+ ::AnnotateRb::Runner.run_after_migration
36
41
  end
37
42
  end
38
43
  end
@@ -6,7 +6,10 @@ module AnnotateRb
6
6
  module Generators
7
7
  class UpdateConfigGenerator < ::Rails::Generators::Base
8
8
  def generate_config
9
- insert_into_file ::AnnotateRb::ConfigFinder::DOTFILE do
9
+ parsed_options = AnnotateRb::Parser.parse(ARGV, {})
10
+ AnnotateRb::ConfigFinder.config_path = parsed_options[:config_path] if parsed_options[:config_path]
11
+
12
+ insert_into_file ::AnnotateRb::ConfigFinder.find_project_dotfile do
10
13
  ::AnnotateRb::ConfigGenerator.unset_config_defaults
11
14
  end
12
15
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: annotaterb
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.22.0
4
+ version: 4.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew W. Lee
@@ -90,10 +90,16 @@ files:
90
90
  - lib/annotate_rb/model_annotator/column_annotation/default_value_builder.rb
91
91
  - lib/annotate_rb/model_annotator/column_annotation/type_builder.rb
92
92
  - lib/annotate_rb/model_annotator/components.rb
93
+ - lib/annotate_rb/model_annotator/enum_annotation.rb
94
+ - lib/annotate_rb/model_annotator/enum_annotation/annotation.rb
95
+ - lib/annotate_rb/model_annotator/enum_annotation/annotation_builder.rb
96
+ - lib/annotate_rb/model_annotator/enum_annotation/enum_component.rb
93
97
  - lib/annotate_rb/model_annotator/file_name_resolver.rb
94
98
  - lib/annotate_rb/model_annotator/file_parser.rb
95
99
  - lib/annotate_rb/model_annotator/file_parser/annotation_finder.rb
100
+ - lib/annotate_rb/model_annotator/file_parser/annotation_target.rb
96
101
  - lib/annotate_rb/model_annotator/file_parser/custom_parser.rb
102
+ - lib/annotate_rb/model_annotator/file_parser/magic_comment.rb
97
103
  - lib/annotate_rb/model_annotator/file_parser/parsed_file.rb
98
104
  - lib/annotate_rb/model_annotator/file_parser/parsed_file_result.rb
99
105
  - lib/annotate_rb/model_annotator/file_parser/yml_parser.rb
@@ -164,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
170
  - !ruby/object:Gem::Version
165
171
  version: '0'
166
172
  requirements: []
167
- rubygems_version: 4.0.4
173
+ rubygems_version: 3.6.9
168
174
  specification_version: 4
169
175
  summary: A gem for generating annotations for Rails projects.
170
176
  test_files: []