annotaterb 4.22.0 → 4.24.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -0
  3. data/README.md +156 -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 +9 -1
  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/exclusion_constraint_annotation/annotation.rb +40 -0
  19. data/lib/annotate_rb/model_annotator/exclusion_constraint_annotation/annotation_builder.rb +38 -0
  20. data/lib/annotate_rb/model_annotator/exclusion_constraint_annotation/exclusion_constraint_component.rb +27 -0
  21. data/lib/annotate_rb/model_annotator/exclusion_constraint_annotation.rb +11 -0
  22. data/lib/annotate_rb/model_annotator/file_parser/annotation_finder.rb +75 -4
  23. data/lib/annotate_rb/model_annotator/file_parser/annotation_target.rb +31 -0
  24. data/lib/annotate_rb/model_annotator/file_parser/custom_parser.rb +11 -4
  25. data/lib/annotate_rb/model_annotator/file_parser/magic_comment.rb +32 -0
  26. data/lib/annotate_rb/model_annotator/file_parser/parsed_file.rb +5 -6
  27. data/lib/annotate_rb/model_annotator/file_parser/yml_parser.rb +43 -4
  28. data/lib/annotate_rb/model_annotator/file_parser.rb +2 -0
  29. data/lib/annotate_rb/model_annotator/foreign_key_annotation/foreign_key_component_builder.rb +6 -0
  30. data/lib/annotate_rb/model_annotator/index_annotation/annotation_builder.rb +29 -0
  31. data/lib/annotate_rb/model_annotator/index_annotation/index_component.rb +26 -4
  32. data/lib/annotate_rb/model_annotator/model_wrapper.rb +58 -2
  33. data/lib/annotate_rb/model_annotator/project_annotator.rb +2 -1
  34. data/lib/annotate_rb/model_annotator/single_file_annotation_remover.rb +1 -1
  35. data/lib/annotate_rb/model_annotator/single_file_annotator.rb +6 -6
  36. data/lib/annotate_rb/model_annotator/single_file_annotator_instruction.rb +3 -2
  37. data/lib/annotate_rb/model_annotator/unique_constraint_annotation/annotation.rb +40 -0
  38. data/lib/annotate_rb/model_annotator/unique_constraint_annotation/annotation_builder.rb +37 -0
  39. data/lib/annotate_rb/model_annotator/unique_constraint_annotation/unique_constraint_component.rb +27 -0
  40. data/lib/annotate_rb/model_annotator/unique_constraint_annotation.rb +11 -0
  41. data/lib/annotate_rb/model_annotator/zeitwerk_class_getter.rb +1 -1
  42. data/lib/annotate_rb/model_annotator.rb +3 -0
  43. data/lib/annotate_rb/options.rb +12 -2
  44. data/lib/annotate_rb/parser.rb +23 -6
  45. data/lib/annotate_rb/route_annotator/base_processor.rb +1 -1
  46. data/lib/annotate_rb/route_annotator/helper.rb +1 -1
  47. data/lib/annotate_rb/runner.rb +12 -6
  48. data/lib/annotate_rb/tasks/annotate_models_migrate.rake +8 -3
  49. data/lib/generators/annotate_rb/update_config/update_config_generator.rb +4 -1
  50. metadata +16 -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: ff11e0177eddc61744806ec5749c7a5d234023e0e7d75d46bd5fc21e4d0998fc
4
+ data.tar.gz: f8c87d260bf6cafbeb120568ecda6131f6bd335ef2c3882630a45cc2e6631a05
5
5
  SHA512:
6
- metadata.gz: a203a269dbcf4c42a082a2088c6c38d8c4b97c3bbf9ab760762e3149294fb38dc31799aea2f3ece425504782cd2027187bd161ccf4a9c006aaa7a9a791094311
7
- data.tar.gz: 8cc55f09f193f5f428db97afc1270386a06eb8185773cfd339830e1ee8e49631a1f203cced676fdb2f4d7d5c0b690f5aadc9108f63d3de588c019d7585c0011b
6
+ metadata.gz: 6e2edbfc22db4f8b1bd150313e795993e3f5366ac4594084fec2f7ee9f1d2650d98cef0933bdc92a0ff0a4e52af4bb77b35672cbd0049fed9c8e6c256743e00e
7
+ data.tar.gz: ddf3161a49b9c041083ce42c9264823da16e23703a8f013b98098f24dc4f7d46642a659621f0d0612572865fec1099a91e7622139acc3a1c689e7df1d96e81bf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,88 @@
1
1
  # Changelog
2
2
 
3
+ ## [v4.23.0](https://github.com/drwl/annotaterb/tree/v4.23.0) (2026-06-25)
4
+
5
+ [Full Changelog](https://github.com/drwl/annotaterb/compare/v4.22.0...v4.23.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - \[Request\] Support for multiple serializers for one model [\#329](https://github.com/drwl/annotaterb/issues/329)
10
+ - Support COMMENT on an INDEX [\#258](https://github.com/drwl/annotaterb/issues/258)
11
+
12
+ **Fixed bugs:**
13
+
14
+ - Re-running annotaterb strips indentation from nested \(indented\) schema annotations [\#332](https://github.com/drwl/annotaterb/issues/332)
15
+ - Setting all sort options to `false` sometimes doesn't respect migration/DB order [\#237](https://github.com/drwl/annotaterb/issues/237)
16
+
17
+ **Closed issues:**
18
+
19
+ - mistake post please delete this [\#314](https://github.com/drwl/annotaterb/issues/314)
20
+ - Removal of final empty comment line after foreign keys results in duplicate `fk_rails_...` line [\#297](https://github.com/drwl/annotaterb/issues/297)
21
+ - Default settings does not indent correctly inside a module [\#271](https://github.com/drwl/annotaterb/issues/271)
22
+ - Top does not do what I think it should? And annotator fails to indent. [\#202](https://github.com/drwl/annotaterb/issues/202)
23
+ - annotaterb is moving my class doc [\#201](https://github.com/drwl/annotaterb/issues/201)
24
+
25
+ **Merged pull requests:**
26
+
27
+ - Bump version to v4.23.0 [\#347](https://github.com/drwl/annotaterb/pull/347) ([drwl](https://github.com/drwl))
28
+ - Bump actions/checkout from 6 to 7 [\#346](https://github.com/drwl/annotaterb/pull/346) ([dependabot[bot]](https://github.com/apps/dependabot))
29
+ - Fix duplicate annotations when a file ends with a trailing blank line [\#342](https://github.com/drwl/annotaterb/pull/342) ([OdenTakashi](https://github.com/OdenTakashi))
30
+ - Fix update\_config for non-default config locations [\#341](https://github.com/drwl/annotaterb/pull/341) ([OdenTakashi](https://github.com/OdenTakashi))
31
+ - Fix NameError in ZeitwerkClassGetter for collapsed Zeitwerk paths [\#339](https://github.com/drwl/annotaterb/pull/339) ([KaiyuanMa](https://github.com/KaiyuanMa))
32
+ - Fix annotation inserted inside module when doc comment is present [\#338](https://github.com/drwl/annotaterb/pull/338) ([OdenTakashi](https://github.com/OdenTakashi))
33
+ - Add `position_in_class: after_doc` to keep class doc adjacent to class [\#337](https://github.com/drwl/annotaterb/pull/337) ([yamat47](https://github.com/yamat47))
34
+ - Fix annotation placement above inner class declarations under `nested_position` [\#336](https://github.com/drwl/annotaterb/pull/336) ([yamat47](https://github.com/yamat47))
35
+ - Drop Ruby \< 3.3 from CI matrix [\#335](https://github.com/drwl/annotaterb/pull/335) ([drwl](https://github.com/drwl))
36
+ - Fix annotation placement in nested files and around trailing user comments [\#334](https://github.com/drwl/annotaterb/pull/334) ([yamat47](https://github.com/yamat47))
37
+ - Preserve indentation when re-running annotaterb on nested-position annotations. [\#333](https://github.com/drwl/annotaterb/pull/333) ([yamat47](https://github.com/yamat47))
38
+ - Respect `--config-path` during config loading [\#331](https://github.com/drwl/annotaterb/pull/331) ([OdenTakashi](https://github.com/OdenTakashi))
39
+ - Fall back to current task when no top-level task is set [\#328](https://github.com/drwl/annotaterb/pull/328) ([jordan-brough](https://github.com/jordan-brough))
40
+ - Fix --show-migration option not working on Rails 7.2+. [\#327](https://github.com/drwl/annotaterb/pull/327) ([OdenTakashi](https://github.com/OdenTakashi))
41
+ - Exit with non-zero code when database is not accessible [\#326](https://github.com/drwl/annotaterb/pull/326) ([OdenTakashi](https://github.com/OdenTakashi))
42
+ - fix potential ReDoS in route annotator regex [\#325](https://github.com/drwl/annotaterb/pull/325) ([OdenTakashi](https://github.com/OdenTakashi))
43
+ - Support opt-in display of PostgreSQL enum types in annotations [\#324](https://github.com/drwl/annotaterb/pull/324) ([OdenTakashi](https://github.com/OdenTakashi))
44
+ - Fix error when column comments contain `%` [\#322](https://github.com/drwl/annotaterb/pull/322) ([willnet](https://github.com/willnet))
45
+ - Add unit tests for AnnotateRb::Helper [\#321](https://github.com/drwl/annotaterb/pull/321) ([OdenTakashi](https://github.com/OdenTakashi))
46
+ - Migrate issue templates to folder-based format [\#320](https://github.com/drwl/annotaterb/pull/320) ([OdenTakashi](https://github.com/OdenTakashi))
47
+ - Add opt-in show\_indexes\_comments option to display index comments [\#319](https://github.com/drwl/annotaterb/pull/319) ([olleolleolle](https://github.com/olleolleolle))
48
+ - Only connect SecondaryRecord to secondary DB when MULTI\_DB\_TEST is set [\#318](https://github.com/drwl/annotaterb/pull/318) ([OdenTakashi](https://github.com/OdenTakashi))
49
+ - Correct secondary database setup in dummy app [\#317](https://github.com/drwl/annotaterb/pull/317) ([OdenTakashi](https://github.com/OdenTakashi))
50
+ - Remove duplicate ignore\_unknown\_models option definition [\#316](https://github.com/drwl/annotaterb/pull/316) ([OdenTakashi](https://github.com/OdenTakashi))
51
+ - Fix column annotation alignment for CJK and fullwidth characters [\#315](https://github.com/drwl/annotaterb/pull/315) ([SergeyGildenshtern](https://github.com/SergeyGildenshtern))
52
+ - Added command example for applying annotations to README [\#313](https://github.com/drwl/annotaterb/pull/313) ([zackerms](https://github.com/zackerms))
53
+ - Generate changelog for v4.22.0 [\#311](https://github.com/drwl/annotaterb/pull/311) ([drwl](https://github.com/drwl))
54
+ - Add opt-in support for automatically annotating routes after migration tasks [\#293](https://github.com/drwl/annotaterb/pull/293) ([OdenTakashi](https://github.com/OdenTakashi))
55
+
56
+ ## [v4.22.0](https://github.com/drwl/annotaterb/tree/v4.22.0) (2026-02-12)
57
+
58
+ [Full Changelog](https://github.com/drwl/annotaterb/compare/v4.21.0...v4.22.0)
59
+
60
+ **Implemented enhancements:**
61
+
62
+ - Feature: ruby-lsp addon [\#175](https://github.com/drwl/annotaterb/issues/175)
63
+ - Mounting ActionCable leads to weird annotation [\#161](https://github.com/drwl/annotaterb/issues/161)
64
+
65
+ **Fixed bugs:**
66
+
67
+ - Yardoc formatting for comments on database attributes [\#162](https://github.com/drwl/annotaterb/issues/162)
68
+
69
+ **Closed issues:**
70
+
71
+ - New `ignore_multi_database_name` option seems to be non-functional [\#303](https://github.com/drwl/annotaterb/issues/303)
72
+ - Changing sort options does not change annotations [\#294](https://github.com/drwl/annotaterb/issues/294)
73
+ - CLI script for annotaterb not installed or runnable [\#290](https://github.com/drwl/annotaterb/issues/290)
74
+
75
+ **Merged pull requests:**
76
+
77
+ - Bump version to v4.22.0 [\#310](https://github.com/drwl/annotaterb/pull/310) ([drwl](https://github.com/drwl))
78
+ - Run CI on CRuby 4.0 [\#308](https://github.com/drwl/annotaterb/pull/308) ([viralpraxis](https://github.com/viralpraxis))
79
+ - Generate changelog for v4.21.0 [\#307](https://github.com/drwl/annotaterb/pull/307) ([drwl](https://github.com/drwl))
80
+ - fix NoMethodError when using nested\_position with fixture files [\#298](https://github.com/drwl/annotaterb/pull/298) ([OdenTakashi](https://github.com/OdenTakashi))
81
+ - fix: Respect configured sort [\#295](https://github.com/drwl/annotaterb/pull/295) ([patrickarnett](https://github.com/patrickarnett))
82
+ - Use `#lease_connection` if available [\#292](https://github.com/drwl/annotaterb/pull/292) ([viralpraxis](https://github.com/viralpraxis))
83
+ - refactor: simplify primary key check logic \(no functional changes\) [\#285](https://github.com/drwl/annotaterb/pull/285) ([OdenTakashi](https://github.com/OdenTakashi))
84
+ - Honor skip\_on\_db\_migrate config option when runnig migrate tasks [\#274](https://github.com/drwl/annotaterb/pull/274) ([martinechtner](https://github.com/martinechtner))
85
+
3
86
  ## [v4.21.0](https://github.com/drwl/annotaterb/tree/v4.21.0) (2026-01-30)
4
87
 
5
88
  [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,151 @@ 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
+ ### Configuration options
241
+
242
+ Keys use snake_case and match the gem defaults in `AnnotateRb::Options`. CLI flags override values from the config file.
243
+
244
+ #### Position
245
+
246
+ | Option | Default | Description |
247
+ | --- | --- | --- |
248
+ | `position` | `before` | Fallback position for all `position_in_*` options. One of `before`, `top`, `after`, `bottom`, `before_doc`. |
249
+ | `position_in_class` | `before` | Position in model files. `before_doc` keeps class documentation adjacent to the class. |
250
+ | `position_in_factory` | `before` | Position in FactoryBot factory files. |
251
+ | `position_in_fixture` | `before` | Position in fixture files. |
252
+ | `position_in_test` | `before` | Position in test/spec files. |
253
+ | `position_in_routes` | `before` | Position in `config/routes.rb`. |
254
+ | `position_in_serializer` | `before` | Position in serializer files. |
255
+ | `position_in_additional_file_patterns` | `before` | Position in files matched by `additional_file_patterns`. |
256
+ | `nested_position` | `false` | Place annotations directly above nested classes/modules instead of at the top of the file. |
257
+
258
+ #### Schema annotation content
259
+
260
+ | Option | Default | Description |
261
+ | --- | --- | --- |
262
+ | `show_foreign_keys` | `true` | List foreign key constraints. |
263
+ | `show_complete_foreign_keys` | `false` | Use complete foreign key names. |
264
+ | `show_indexes` | `true` | List table indexes. |
265
+ | `show_indexes_comments` | `false` | Include index comments. |
266
+ | `show_indexes_include` | `false` | Include `INCLUDE` columns on indexes. |
267
+ | `simple_indexes` | `false` | Concat related indexes onto each column line. |
268
+ | `show_check_constraints` | `false` | List check constraints. |
269
+ | `show_enums` | `false` | Show PostgreSQL enum types. |
270
+ | `show_virtual_columns` | `false` | Show virtual/generated columns. |
271
+ | `include_version` | `false` | Include the migration version number. |
272
+ | `with_comment` | `true` | Include database comments (fallback for column/table comment flags). |
273
+ | `with_column_comments` | `true` | Include column comments. |
274
+ | `with_table_comments` | `true` | Include table comments. |
275
+ | `position_of_column_comment` | `with_name` | Column comment placement: `with_name` or `rightmost_column`. |
276
+ | `hide_default_column_types` | `""` | Comma-separated column types that omit defaults (e.g. `json,jsonb,hstore`). |
277
+ | `hide_limit_column_types` | `""` | Comma-separated column types that omit limits (e.g. `integer,boolean,text`). |
278
+ | `ignore_columns` | `null` | Regex of column names to skip. |
279
+ | `ignore_database_name` | `false` | Omit the database name from annotations. |
280
+ | `ignore_multi_database_name` | `false` | Omit the database name in multi-database setups. |
281
+ | `timestamp_columns` | `[created_at, updated_at]` | Column names treated as timestamps for classified sorting. |
282
+ | `classes_default_to_s` | `[]` | Class names whose default values are rendered with `to_s`. |
283
+
284
+ #### Sorting and format
285
+
286
+ | Option | Default | Description |
287
+ | --- | --- | --- |
288
+ | `sort` | `false` | Sort columns alphabetically. |
289
+ | `classified_sort` | `true` | Sort as id → other columns → timestamps → associations. |
290
+ | `grouped_polymorphic` | `false` | Group polymorphic associations when using `classified_sort`. |
291
+ | `format_markdown` | `false` | Render annotations as Markdown. |
292
+ | `format_rdoc` | `false` | Render annotations as RDoc. |
293
+ | `format_yard` | `false` | Render annotations as YARD. |
294
+
295
+ #### What to annotate / exclude
296
+
297
+ | Option | Default | Description |
298
+ | --- | --- | --- |
299
+ | `active_admin` | `false` | Annotate ActiveAdmin models. |
300
+ | `exclude_controllers` | `true` | Skip controller files. |
301
+ | `exclude_factories` | `false` | Skip factory files. |
302
+ | `exclude_fixtures` | `false` | Skip fixture files. |
303
+ | `exclude_helpers` | `true` | Skip helper files. |
304
+ | `exclude_scaffolds` | `true` | Skip scaffold files. |
305
+ | `exclude_serializers` | `false` | Skip serializer files. |
306
+ | `exclude_sti_subclasses` | `false` | Skip STI subclasses. |
307
+ | `exclude_tests` | `false` | Skip test/spec files. Can also be an array of symbols. |
308
+ | `ignore_model_sub_dir` | `false` | Ignore subdirectories under `model_dir`. |
309
+ | `ignore_unknown_models` | `false` | Suppress warnings for bad model files. |
310
+
311
+ #### Paths
312
+
313
+ | Option | Default | Description |
314
+ | --- | --- | --- |
315
+ | `model_dir` | `[app/models]` | Directories containing models. |
316
+ | `root_dir` | `[""]` | Root directories for multi-project layouts. |
317
+ | `additional_file_patterns` | `[]` | Extra paths/globs to annotate (supports `%MODEL_NAME%`). |
318
+ | `require` | `[]` | Extra files to require before loading models. |
319
+
320
+ #### Routes
321
+
322
+ | Option | Default | Description |
323
+ | --- | --- | --- |
324
+ | `ignore_routes` | `null` | Regex of routes to skip. |
325
+ | `timestamp` | `false` | Include a timestamp in route annotations. |
326
+ | `auto_annotate_routes_after_migrate` | `false` | Also annotate routes after DB migrate tasks. |
327
+
328
+ #### Wrappers and behavior
329
+
330
+ | Option | Default | Description |
331
+ | --- | --- | --- |
332
+ | `wrapper` | `null` | Text used for both opening and closing wrappers. |
333
+ | `wrapper_open` | `null` | Opening wrapper text (falls back to `wrapper`). |
334
+ | `wrapper_close` | `null` | Closing wrapper text (falls back to `wrapper`). |
335
+ | `force` | `false` | Rewrite annotations even when unchanged. |
336
+ | `frozen` | `false` | Exit non-zero if annotations would change. |
337
+ | `skip_on_db_migrate` | `false` | Skip automatic annotation after DB migrate tasks. |
338
+ | `debug` | `false` | Print resolved options for debugging. |
339
+ | `trace` | `false` | Print full stack traces when annotation fails. |
340
+
341
+ Example:
342
+
343
+ ```yml
344
+ # .annotaterb.yml
345
+ position: after
346
+ show_foreign_keys: true
347
+ show_indexes: true
348
+ show_enums: true
349
+ classified_sort: true
350
+ model_dir:
351
+ - app/models
352
+ - app/models/concerns
353
+ skip_on_db_migrate: false
354
+ ```
355
+
356
+ ### Preserving class documentation comments
357
+
358
+ 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.
359
+
360
+ 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.
361
+
362
+ ```ruby
363
+ # Source file:
364
+ # Doc about User
365
+ class User < ApplicationRecord
366
+ end
367
+
368
+ # With position_in_class: before (default)
369
+ # Doc about User
370
+ # == Schema Information
371
+ # ...
372
+ class User < ApplicationRecord
373
+ end
374
+
375
+ # With position_in_class: before_doc
376
+ # == Schema Information
377
+ # ...
378
+ # Doc about User
379
+ class User < ApplicationRecord
380
+ end
381
+ ```
382
+
383
+ 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.
384
+
233
385
  ### How to skip annotating a particular model
234
386
 
235
387
  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.24.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,9 @@ 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
+ UniqueConstraintAnnotation::AnnotationBuilder.new(@model, @options).build,
31
+ ExclusionConstraintAnnotation::AnnotationBuilder.new(@model, @options).build,
32
+ EnumAnnotation::AnnotationBuilder.new(@model, @options).build,
30
33
  SchemaFooter.new
31
34
  ]
32
35
  end
@@ -64,7 +67,7 @@ module AnnotateRb
64
67
  table_name = @model.table_name
65
68
  table_comment = @model.connection.try(:table_comment, @model.table_name)
66
69
  max_size = @model.max_schema_info_width
67
- database_name = @model.database_name if multi_db_environment?
70
+ database_name = @model.database_name if show_database_name?
68
71
 
69
72
  _annotation = Annotation.new(@options,
70
73
  version: version, table_name: table_name, table_comment: table_comment,
@@ -73,6 +76,11 @@ module AnnotateRb
73
76
 
74
77
  private
75
78
 
79
+ # TODO: Consolidate ignore_database_name and ignore_multi_database_name; they serve the same purpose
80
+ def show_database_name?
81
+ !@options[:ignore_database_name] && multi_db_environment?
82
+ end
83
+
76
84
  def multi_db_environment?
77
85
  return false if @options[:ignore_multi_database_name]
78
86
 
@@ -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