annotaterb 4.5.0 → 4.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +57 -0
- data/VERSION +1 -1
- data/lib/annotate_rb/eager_loader.rb +6 -2
- data/lib/annotate_rb/model_annotator/annotated_file/generator.rb +50 -7
- data/lib/annotate_rb/model_annotator/annotated_file/updater.rb +9 -9
- data/lib/annotate_rb/model_annotator/column_annotation/attributes_builder.rb +1 -1
- data/lib/annotate_rb/model_annotator/column_annotation/column_wrapper.rb +3 -2
- data/lib/annotate_rb/model_annotator/column_annotation/default_value_builder.rb +4 -1
- data/lib/annotate_rb/model_annotator/column_annotation/type_builder.rb +1 -1
- data/lib/annotate_rb/model_annotator/file_name_resolver.rb +5 -0
- data/lib/annotate_rb/model_annotator/file_parser/annotation_finder.rb +103 -0
- data/lib/annotate_rb/model_annotator/file_parser/custom_parser.rb +217 -0
- data/lib/annotate_rb/model_annotator/file_parser/parsed_file.rb +94 -0
- data/lib/annotate_rb/model_annotator/file_parser/parsed_file_result.rb +54 -0
- data/lib/annotate_rb/model_annotator/file_parser.rb +4 -1
- data/lib/annotate_rb/model_annotator/model_class_getter.rb +7 -0
- data/lib/annotate_rb/model_annotator/model_wrapper.rb +10 -1
- data/lib/annotate_rb/model_annotator/related_files_list_builder.rb +3 -3
- data/lib/annotate_rb/model_annotator/single_file_annotation_remover.rb +10 -12
- data/lib/annotate_rb/model_annotator/single_file_annotator.rb +14 -6
- data/lib/annotate_rb/model_annotator/zeitwerk_class_getter.rb +113 -0
- data/lib/annotate_rb/model_annotator.rb +1 -2
- data/lib/annotate_rb/options.rb +4 -2
- data/lib/annotate_rb/parser.rb +10 -1
- data/lib/annotate_rb/tasks/annotate_models_migrate.rake +5 -0
- metadata +8 -6
- data/lib/annotate_rb/model_annotator/annotation_pattern_generator.rb +0 -19
- data/lib/annotate_rb/model_annotator/file_components.rb +0 -81
- data/lib/annotate_rb/model_annotator/file_parser/magic_comment_parser.rb +0 -34
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f31e6cf08a5d57a35274e5964a5da9e4a01d006e0ddf3d4ac4467932901709ac
|
4
|
+
data.tar.gz: 020ad6bab20eb3c029173d5764050d7c62855785c2fa357750bc2fbff27cd02b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1a52c81202a634161721175021e8ee8742cad49f0eab0aab2e89319d856a8f81fe0e1e2ea3de1e78e3695f261a6d2ee5bd5b41ea1ac8c9b2d072a62bcec4b1cd
|
7
|
+
data.tar.gz: b01f86669748d083e52a0f3fef891632cff7fcf1c476ae1ce71e5e654cc028105435c4ceadcc73b23443dba28f3dd9c082ebbadeae541ad053a3cd93005d59cb
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,62 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v4.6.0](https://github.com/drwl/annotaterb/tree/v4.6.0) (2024-02-27)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/drwl/annotaterb/compare/v4.5.0...v4.6.0)
|
6
|
+
|
7
|
+
Adds two big changes: Fix annotations swallowing comments [\#72](https://github.com/drwl/annotaterb/pull/72) and
|
8
|
+
Add Zeitwerk support [\#85](https://github.com/drwl/annotaterb/pull/85).
|
9
|
+
Please report any issues you encounter.
|
10
|
+
|
11
|
+
**Closed issues:**
|
12
|
+
|
13
|
+
- Add support for `data_migrate` gem [\#89](https://github.com/drwl/annotaterb/issues/89)
|
14
|
+
|
15
|
+
**Merged pull requests:**
|
16
|
+
|
17
|
+
- Bump version to v4.6.0 [\#95](https://github.com/drwl/annotaterb/pull/95) ([drwl](https://github.com/drwl))
|
18
|
+
- Add support for parsing RSpec files [\#94](https://github.com/drwl/annotaterb/pull/94) ([drwl](https://github.com/drwl))
|
19
|
+
- Add support for model name without namespace in resolver [\#93](https://github.com/drwl/annotaterb/pull/93) ([drwl](https://github.com/drwl))
|
20
|
+
- Fixes for `RelatedFilesListBuilder` [\#92](https://github.com/drwl/annotaterb/pull/92) ([drwl](https://github.com/drwl))
|
21
|
+
- Refactor `AnnotatedFile` classes [\#91](https://github.com/drwl/annotaterb/pull/91) ([drwl](https://github.com/drwl))
|
22
|
+
- Add support for data\_migrate gem [\#90](https://github.com/drwl/annotaterb/pull/90) ([cmer](https://github.com/cmer))
|
23
|
+
- Support non-model files in `CustomParser` [\#88](https://github.com/drwl/annotaterb/pull/88) ([drwl](https://github.com/drwl))
|
24
|
+
- Fix flakey integration test [\#87](https://github.com/drwl/annotaterb/pull/87) ([drwl](https://github.com/drwl))
|
25
|
+
- Improve integration tests [\#86](https://github.com/drwl/annotaterb/pull/86) ([drwl](https://github.com/drwl))
|
26
|
+
- Add Zeitwerk support [\#85](https://github.com/drwl/annotaterb/pull/85) ([drwl](https://github.com/drwl))
|
27
|
+
- Improve annotate after adding new migration integration test [\#84](https://github.com/drwl/annotaterb/pull/84) ([drwl](https://github.com/drwl))
|
28
|
+
- Add integration test using force [\#81](https://github.com/drwl/annotaterb/pull/81) ([drwl](https://github.com/drwl))
|
29
|
+
- Generate changelog for v4.5.0 [\#80](https://github.com/drwl/annotaterb/pull/80) ([drwl](https://github.com/drwl))
|
30
|
+
- Fix annotations swallowing comments [\#72](https://github.com/drwl/annotaterb/pull/72) ([drwl](https://github.com/drwl))
|
31
|
+
|
32
|
+
## [v4.5.0](https://github.com/drwl/annotaterb/tree/v4.5.0) (2024-02-08)
|
33
|
+
|
34
|
+
[Full Changelog](https://github.com/drwl/annotaterb/compare/v4.4.1...v4.5.0)
|
35
|
+
|
36
|
+
**Closed issues:**
|
37
|
+
|
38
|
+
- Add an automated way to migrate from the old annotate gem [\#73](https://github.com/drwl/annotaterb/issues/73)
|
39
|
+
- Default array value is double-quoted/escaped [\#57](https://github.com/drwl/annotaterb/issues/57)
|
40
|
+
|
41
|
+
**Merged pull requests:**
|
42
|
+
|
43
|
+
- Bump version to v4.5.0 [\#79](https://github.com/drwl/annotaterb/pull/79) ([drwl](https://github.com/drwl))
|
44
|
+
- Update README on the new Rails generator commands [\#78](https://github.com/drwl/annotaterb/pull/78) ([drwl](https://github.com/drwl))
|
45
|
+
- Bump github/codeql-action from 2 to 3 [\#77](https://github.com/drwl/annotaterb/pull/77) ([dependabot[bot]](https://github.com/apps/dependabot))
|
46
|
+
- Add command to generate a configuration file [\#76](https://github.com/drwl/annotaterb/pull/76) ([drwl](https://github.com/drwl))
|
47
|
+
- Bump actions/checkout from 3 to 4 [\#75](https://github.com/drwl/annotaterb/pull/75) ([dependabot[bot]](https://github.com/apps/dependabot))
|
48
|
+
- CI: Configure dependabot to update GH Actions [\#74](https://github.com/drwl/annotaterb/pull/74) ([olleolleolle](https://github.com/olleolleolle))
|
49
|
+
- Refactor `FileBuilder` and `MagicCommentParser` [\#71](https://github.com/drwl/annotaterb/pull/71) ([drwl](https://github.com/drwl))
|
50
|
+
- Test running annotations after a migration [\#70](https://github.com/drwl/annotaterb/pull/70) ([drwl](https://github.com/drwl))
|
51
|
+
- Add integration test for rake task installer [\#69](https://github.com/drwl/annotaterb/pull/69) ([drwl](https://github.com/drwl))
|
52
|
+
- Add integration test for annotating routes [\#68](https://github.com/drwl/annotaterb/pull/68) ([drwl](https://github.com/drwl))
|
53
|
+
- Remove optional args [\#67](https://github.com/drwl/annotaterb/pull/67) ([drwl](https://github.com/drwl))
|
54
|
+
- Remove optional arg from `AnnotationPatternGenerator` [\#66](https://github.com/drwl/annotaterb/pull/66) ([drwl](https://github.com/drwl))
|
55
|
+
- Remove `ARGV` use during runtime [\#65](https://github.com/drwl/annotaterb/pull/65) ([drwl](https://github.com/drwl))
|
56
|
+
- Add integration test for annotating a singular file [\#64](https://github.com/drwl/annotaterb/pull/64) ([drwl](https://github.com/drwl))
|
57
|
+
- Generate changelog for v4.4.1 [\#63](https://github.com/drwl/annotaterb/pull/63) ([drwl](https://github.com/drwl))
|
58
|
+
- Add support for factory\_bot's default suffixed pattern [\#59](https://github.com/drwl/annotaterb/pull/59) ([drwl](https://github.com/drwl))
|
59
|
+
|
3
60
|
## [v4.4.1](https://github.com/drwl/annotaterb/tree/v4.4.1) (2023-09-11)
|
4
61
|
|
5
62
|
[Full Changelog](https://github.com/drwl/annotaterb/compare/v4.4.0...v4.4.1)
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
4.
|
1
|
+
4.7.0
|
@@ -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
|
-
|
12
|
-
|
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|
|
@@ -5,29 +5,72 @@ module AnnotateRb
|
|
5
5
|
module AnnotatedFile
|
6
6
|
# Generates the file with fresh annotations
|
7
7
|
class Generator
|
8
|
-
def initialize(
|
9
|
-
@file_components = file_components
|
8
|
+
def initialize(file_content, new_annotations, annotation_position, options)
|
10
9
|
@annotation_position = annotation_position
|
11
10
|
@options = options
|
12
11
|
|
13
|
-
@new_wrapped_annotations = wrapped_content(
|
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
|
14
18
|
end
|
15
19
|
|
20
|
+
# @return [String] Returns the annotated file content to be written back to a file
|
16
21
|
def generate
|
17
22
|
# Need to keep `.to_s` for now since the it can be either a String or Symbol
|
18
23
|
annotation_write_position = @options[@annotation_position].to_s
|
19
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
|
+
|
20
37
|
_content = if %w[after bottom].include?(annotation_write_position)
|
21
|
-
|
22
|
-
elsif @file_components.magic_comments.empty?
|
23
|
-
@file_components.magic_comments + @new_wrapped_annotations + @file_components.pure_file_content.lstrip
|
38
|
+
content_annotated_after(parsed, content_without_annotations)
|
24
39
|
else
|
25
|
-
|
40
|
+
content_annotated_before(parsed, content_without_annotations, annotation_write_position)
|
26
41
|
end
|
27
42
|
end
|
28
43
|
|
29
44
|
private
|
30
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
|
+
|
31
74
|
def wrapped_content(content)
|
32
75
|
wrapper_open = if @options[:wrapper_open]
|
33
76
|
"# #{@options[:wrapper_open]}\n"
|
@@ -5,22 +5,22 @@ module AnnotateRb
|
|
5
5
|
module AnnotatedFile
|
6
6
|
# Updates existing annotations
|
7
7
|
class Updater
|
8
|
-
def initialize(
|
9
|
-
@file_components = file_components
|
10
|
-
@annotation_position = annotation_position
|
8
|
+
def initialize(file_content, new_annotations, _annotation_position, options)
|
11
9
|
@options = options
|
12
10
|
|
13
|
-
@
|
11
|
+
@new_annotations = new_annotations
|
12
|
+
@file_content = file_content
|
13
|
+
|
14
|
+
@parsed_file = FileParser::ParsedFile.new(@file_content, @new_annotations, options).parse
|
14
15
|
end
|
15
16
|
|
17
|
+
# @return [String] Returns the annotated file content to be written back to a file
|
16
18
|
def update
|
17
|
-
return "" if !@
|
18
|
-
|
19
|
-
annotation_pattern = AnnotationPatternGenerator.call(@options)
|
19
|
+
return "" if !@parsed_file.has_annotations?
|
20
20
|
|
21
|
-
new_annotation = @
|
21
|
+
new_annotation = wrapped_content(@new_annotations)
|
22
22
|
|
23
|
-
_content = @
|
23
|
+
_content = @file_content.sub(@parsed_file.annotations, new_annotation)
|
24
24
|
end
|
25
25
|
|
26
26
|
private
|
@@ -8,7 +8,7 @@ module AnnotateRb
|
|
8
8
|
NO_DEFAULT_COL_TYPES = %w[json jsonb hstore].freeze
|
9
9
|
|
10
10
|
def initialize(column, options, is_primary_key, column_indices, column_defaults)
|
11
|
-
@column = ColumnWrapper.new(column, column_defaults)
|
11
|
+
@column = ColumnWrapper.new(column, column_defaults, options)
|
12
12
|
@options = options
|
13
13
|
@is_primary_key = is_primary_key
|
14
14
|
@column_indices = column_indices
|
@@ -4,9 +4,10 @@ module AnnotateRb
|
|
4
4
|
module ModelAnnotator
|
5
5
|
module ColumnAnnotation
|
6
6
|
class ColumnWrapper
|
7
|
-
def initialize(column, column_defaults)
|
7
|
+
def initialize(column, column_defaults, options)
|
8
8
|
@column = column
|
9
9
|
@column_defaults = column_defaults
|
10
|
+
@options = options
|
10
11
|
end
|
11
12
|
|
12
13
|
def raw_default
|
@@ -88,7 +89,7 @@ module AnnotateRb
|
|
88
89
|
|
89
90
|
# Simple quoting for the default column value
|
90
91
|
def quote(value)
|
91
|
-
DefaultValueBuilder.new(value).build
|
92
|
+
DefaultValueBuilder.new(value, @options).build
|
92
93
|
end
|
93
94
|
end
|
94
95
|
end
|
@@ -4,8 +4,9 @@ module AnnotateRb
|
|
4
4
|
module ModelAnnotator
|
5
5
|
module ColumnAnnotation
|
6
6
|
class DefaultValueBuilder
|
7
|
-
def initialize(value)
|
7
|
+
def initialize(value, options)
|
8
8
|
@value = value
|
9
|
+
@options = options
|
9
10
|
end
|
10
11
|
|
11
12
|
# @return [String]
|
@@ -27,6 +28,8 @@ module AnnotateRb
|
|
27
28
|
private
|
28
29
|
|
29
30
|
def quote(value)
|
31
|
+
return value.to_s.inspect if @options[:classes_default_to_s]&.include?(value.class.name)
|
32
|
+
|
30
33
|
case value
|
31
34
|
when NilClass then "NULL"
|
32
35
|
when TrueClass then "TRUE"
|
@@ -10,7 +10,7 @@ module AnnotateRb
|
|
10
10
|
|
11
11
|
def initialize(column, options, column_defaults)
|
12
12
|
# Passing `column_defaults` for posterity, don't actually need it here since it's not used
|
13
|
-
@column = ColumnWrapper.new(column, column_defaults)
|
13
|
+
@column = ColumnWrapper.new(column, column_defaults, options)
|
14
14
|
@options = options
|
15
15
|
end
|
16
16
|
|
@@ -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
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnnotateRb
|
4
|
+
module ModelAnnotator
|
5
|
+
module FileParser
|
6
|
+
class ParsedFile
|
7
|
+
SKIP_ANNOTATION_STRING = "# -*- SkipSchemaAnnotations"
|
8
|
+
|
9
|
+
def initialize(file_content, new_annotations, options)
|
10
|
+
@file_content = file_content
|
11
|
+
@file_lines = @file_content.lines
|
12
|
+
@new_annotations = new_annotations
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse
|
17
|
+
@finder = AnnotationFinder.new(@file_content, @options[:wrapper_open], @options[:wrapper_close])
|
18
|
+
has_annotations = false
|
19
|
+
|
20
|
+
begin
|
21
|
+
@finder.run
|
22
|
+
has_annotations = @finder.annotated?
|
23
|
+
rescue AnnotationFinder::NoAnnotationFound => _e
|
24
|
+
end
|
25
|
+
|
26
|
+
annotations = if has_annotations
|
27
|
+
@file_lines[(@finder.annotation_start)..(@finder.annotation_end)].join
|
28
|
+
else
|
29
|
+
""
|
30
|
+
end
|
31
|
+
|
32
|
+
@diff = AnnotationDiffGenerator.new(annotations, @new_annotations).generate
|
33
|
+
@file_parser = @finder.parser
|
34
|
+
|
35
|
+
has_skip_string = @file_parser.comments.any? { |comment, _lineno| comment.include?(SKIP_ANNOTATION_STRING) }
|
36
|
+
annotations_changed = @diff.changed?
|
37
|
+
|
38
|
+
has_leading_whitespace = false
|
39
|
+
has_trailing_whitespace = false
|
40
|
+
|
41
|
+
annotations_with_whitespace = if has_annotations
|
42
|
+
begin
|
43
|
+
annotation_start = @finder.annotation_start
|
44
|
+
annotation_end = @finder.annotation_end
|
45
|
+
|
46
|
+
if @file_lines[annotation_start - 1]&.strip&.empty?
|
47
|
+
annotation_start -= 1
|
48
|
+
has_leading_whitespace = true
|
49
|
+
end
|
50
|
+
|
51
|
+
if @file_lines[annotation_end + 1]&.strip&.empty?
|
52
|
+
annotation_end += 1
|
53
|
+
has_trailing_whitespace = true
|
54
|
+
end
|
55
|
+
|
56
|
+
@file_lines[annotation_start..annotation_end].join
|
57
|
+
end
|
58
|
+
else
|
59
|
+
""
|
60
|
+
end
|
61
|
+
|
62
|
+
# :before or :after when it's set
|
63
|
+
annotation_position = nil
|
64
|
+
|
65
|
+
if has_annotations
|
66
|
+
const_declaration = @file_parser.starts.first
|
67
|
+
|
68
|
+
# If the file does not have any class or module declaration then const_declaration can be nil
|
69
|
+
_const, line_number = const_declaration
|
70
|
+
|
71
|
+
if line_number
|
72
|
+
annotation_position = if @finder.annotation_start < line_number
|
73
|
+
:before
|
74
|
+
else
|
75
|
+
:after
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
_result = ParsedFileResult.new(
|
81
|
+
has_annotations: has_annotations,
|
82
|
+
has_skip_string: has_skip_string,
|
83
|
+
annotations_changed: annotations_changed,
|
84
|
+
annotations: annotations,
|
85
|
+
annotations_with_whitespace: annotations_with_whitespace,
|
86
|
+
has_leading_whitespace: has_leading_whitespace,
|
87
|
+
has_trailing_whitespace: has_trailing_whitespace,
|
88
|
+
annotation_position: annotation_position
|
89
|
+
)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnnotateRb
|
4
|
+
module ModelAnnotator
|
5
|
+
module FileParser
|
6
|
+
class ParsedFileResult
|
7
|
+
def initialize(
|
8
|
+
has_annotations:,
|
9
|
+
has_skip_string:,
|
10
|
+
annotations_changed:,
|
11
|
+
annotations:,
|
12
|
+
annotations_with_whitespace:,
|
13
|
+
has_leading_whitespace:,
|
14
|
+
has_trailing_whitespace:,
|
15
|
+
annotation_position:
|
16
|
+
)
|
17
|
+
@has_annotations = has_annotations
|
18
|
+
@has_skip_string = has_skip_string
|
19
|
+
@annotations_changed = annotations_changed
|
20
|
+
@annotations = annotations
|
21
|
+
@annotations_with_whitespace = annotations_with_whitespace
|
22
|
+
@has_leading_whitespace = has_leading_whitespace
|
23
|
+
@has_trailing_whitespace = has_trailing_whitespace
|
24
|
+
@annotation_position = annotation_position
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :annotations, :annotation_position
|
28
|
+
|
29
|
+
# Returns annotations with new line before and after if they exist
|
30
|
+
attr_reader :annotations_with_whitespace
|
31
|
+
|
32
|
+
def annotations_changed?
|
33
|
+
@annotations_changed
|
34
|
+
end
|
35
|
+
|
36
|
+
def has_annotations?
|
37
|
+
@has_annotations
|
38
|
+
end
|
39
|
+
|
40
|
+
def has_skip_string?
|
41
|
+
@has_skip_string
|
42
|
+
end
|
43
|
+
|
44
|
+
def has_leading_whitespace?
|
45
|
+
@has_leading_whitespace
|
46
|
+
end
|
47
|
+
|
48
|
+
def has_trailing_whitespace?
|
49
|
+
@has_trailing_whitespace
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -3,7 +3,10 @@
|
|
3
3
|
module AnnotateRb
|
4
4
|
module ModelAnnotator
|
5
5
|
module FileParser
|
6
|
-
autoload :
|
6
|
+
autoload :AnnotationFinder, "annotate_rb/model_annotator/file_parser/annotation_finder"
|
7
|
+
autoload :CustomParser, "annotate_rb/model_annotator/file_parser/custom_parser"
|
8
|
+
autoload :ParsedFile, "annotate_rb/model_annotator/file_parser/parsed_file"
|
9
|
+
autoload :ParsedFileResult, "annotate_rb/model_annotator/file_parser/parsed_file_result"
|
7
10
|
end
|
8
11
|
end
|
9
12
|
end
|
@@ -8,6 +8,13 @@ module AnnotateRb
|
|
8
8
|
# Check for namespaced models in subdirectories as well as models
|
9
9
|
# in subdirectories without namespacing.
|
10
10
|
def call(file, options)
|
11
|
+
use_zeitwerk = defined?(::Rails) && ::Rails.try(:autoloaders).try(:zeitwerk_enabled?)
|
12
|
+
|
13
|
+
if use_zeitwerk
|
14
|
+
klass = ZeitwerkClassGetter.call(file, options)
|
15
|
+
return klass if klass
|
16
|
+
end
|
17
|
+
|
11
18
|
model_path = file.gsub(/\.rb$/, "")
|
12
19
|
options[:model_dir].each { |dir| model_path = model_path.gsub(/^#{dir}/, "").gsub(/^\//, "") }
|
13
20
|
|
@@ -115,7 +115,16 @@ module AnnotateRb
|
|
115
115
|
|
116
116
|
# Try to search the table without prefix
|
117
117
|
table_name_without_prefix = table_name.to_s.sub(@klass.table_name_prefix, "")
|
118
|
-
|
118
|
+
begin
|
119
|
+
@klass.connection.indexes(table_name_without_prefix)
|
120
|
+
rescue ActiveRecord::StatementInvalid => _e
|
121
|
+
# Mysql2 adapter behaves differently than Sqlite3 and Postgres adapter.
|
122
|
+
# If `table_name_without_prefix` does not exist, Mysql2 will raise,
|
123
|
+
# the other adapters will return an empty array.
|
124
|
+
#
|
125
|
+
# See: https://github.com/rails/rails/issues/51205
|
126
|
+
[]
|
127
|
+
end
|
119
128
|
end
|
120
129
|
|
121
130
|
def with_comments?
|
@@ -24,10 +24,10 @@ module AnnotateRb
|
|
24
24
|
add_related_scaffold_files if !@options[:exclude_scaffolds]
|
25
25
|
add_related_controller_files if !@options[:exclude_controllers]
|
26
26
|
add_related_helper_files if !@options[:exclude_helpers]
|
27
|
-
add_related_admin_files if
|
28
|
-
add_additional_file_patterns if
|
27
|
+
add_related_admin_files if @options[:active_admin]
|
28
|
+
add_additional_file_patterns if @options[:additional_file_patterns].present?
|
29
29
|
|
30
|
-
@list
|
30
|
+
@list.uniq
|
31
31
|
end
|
32
32
|
|
33
33
|
private
|
@@ -12,20 +12,18 @@ module AnnotateRb
|
|
12
12
|
return false unless File.exist?(file_name)
|
13
13
|
old_content = File.read(file_name)
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
wrapper_open = if options[:wrapper_open]
|
22
|
-
"# #{options[:wrapper_open]}\n"
|
23
|
-
else
|
24
|
-
""
|
15
|
+
begin
|
16
|
+
parsed_file = FileParser::ParsedFile.new(old_content, "", options).parse
|
17
|
+
rescue FileParser::AnnotationFinder::MalformedAnnotation => e
|
18
|
+
warn "Unable to process #{file_name}: #{e.message}"
|
19
|
+
warn "\t" + e.backtrace.join("\n\t") if @options[:trace]
|
20
|
+
return false
|
25
21
|
end
|
26
22
|
|
27
|
-
|
28
|
-
|
23
|
+
return false if !parsed_file.has_annotations?
|
24
|
+
return false if parsed_file.has_skip_string?
|
25
|
+
|
26
|
+
updated_file_content = old_content.sub(parsed_file.annotations_with_whitespace, "")
|
29
27
|
|
30
28
|
File.open(file_name, "wb") { |f| f.puts updated_file_content }
|
31
29
|
|
@@ -25,17 +25,25 @@ module AnnotateRb
|
|
25
25
|
return false unless File.exist?(file_name)
|
26
26
|
old_content = File.read(file_name)
|
27
27
|
|
28
|
-
|
28
|
+
begin
|
29
|
+
parsed_file = FileParser::ParsedFile.new(old_content, annotation, options).parse
|
30
|
+
rescue FileParser::AnnotationFinder::MalformedAnnotation => e
|
31
|
+
warn "Unable to process #{file_name}: #{e.message}"
|
32
|
+
warn "\t" + e.backtrace.join("\n\t") if @options[:trace]
|
33
|
+
return false
|
34
|
+
end
|
29
35
|
|
30
|
-
return false if
|
31
|
-
return false if !
|
36
|
+
return false if parsed_file.has_skip_string?
|
37
|
+
return false if !parsed_file.annotations_changed? && !options[:force]
|
32
38
|
|
33
39
|
abort "AnnotateRb error. #{file_name} needs to be updated, but annotaterb was run with `--frozen`." if options[:frozen]
|
34
40
|
|
35
|
-
updated_file_content = if !
|
36
|
-
AnnotatedFile::Generator.new(
|
41
|
+
updated_file_content = if !parsed_file.has_annotations?
|
42
|
+
AnnotatedFile::Generator.new(old_content, annotation, annotation_position, options).generate
|
43
|
+
elsif options[:force]
|
44
|
+
AnnotatedFile::Generator.new(old_content, annotation, annotation_position, options).generate
|
37
45
|
else
|
38
|
-
AnnotatedFile::Updater.new(
|
46
|
+
AnnotatedFile::Updater.new(old_content, annotation, annotation_position, options).update
|
39
47
|
end
|
40
48
|
|
41
49
|
File.open(file_name, "wb") { |f| f.puts updated_file_content }
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnnotateRb
|
4
|
+
module ModelAnnotator
|
5
|
+
class ZeitwerkClassGetter
|
6
|
+
class << self
|
7
|
+
def call(file, options)
|
8
|
+
new(file, options).call
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(file, options)
|
13
|
+
@file = file
|
14
|
+
@options = options
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Constant, nil] Attempts to return the model class constant (e.g. User) defined in the model file
|
18
|
+
# can return `nil` if the file does not define the constant.
|
19
|
+
def call
|
20
|
+
return unless defined?(::Zeitwerk)
|
21
|
+
|
22
|
+
@absolute_file_path = File.expand_path(@file)
|
23
|
+
loader = ::Rails.autoloaders.main
|
24
|
+
|
25
|
+
if supports_cpath?
|
26
|
+
constant_using_cpath(loader)
|
27
|
+
else
|
28
|
+
constant(loader)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def constant(loader)
|
35
|
+
root_dirs = loader.dirs(namespaces: true) # or `root_dirs = loader.root_dirs` with zeitwerk < 2.6.1
|
36
|
+
expanded_file = @absolute_file_path
|
37
|
+
|
38
|
+
# root_dir: "/home/dummyapp/app/models"
|
39
|
+
root_dir, namespace = root_dirs.find do |dir, _namespace|
|
40
|
+
expanded_file.start_with?(dir)
|
41
|
+
end
|
42
|
+
|
43
|
+
# expanded_file: "/home/dummyapp/app/models/collapsed/example/test_model.rb"
|
44
|
+
# filepath_relative_to_root_dir: "/collapsed/example/test_model.rb"
|
45
|
+
_, filepath_relative_to_root_dir = expanded_file.split(root_dir)
|
46
|
+
|
47
|
+
# Remove leading / and the .rb extension
|
48
|
+
filepath_relative_to_root_dir = filepath_relative_to_root_dir[1..].sub(/\.rb$/, "")
|
49
|
+
|
50
|
+
# once we have the filepath_relative_to_root_dir, we need to see if it
|
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]) }
|
53
|
+
# if the file is within a collapsed path, we then need to, for each
|
54
|
+
# collapsed path, remove the root dir
|
55
|
+
collapsed = loader.collapse.map { |path| path.split(root_dir)[1].sub(/^\//, "") }.to_set
|
56
|
+
|
57
|
+
collapsed.each do |collapse|
|
58
|
+
# next, we split the collapsed directory, e.g. `domain_name/models`, by
|
59
|
+
# slash, and discard the domain_name
|
60
|
+
_, *collapsed_namespace = collapse.split("/")
|
61
|
+
|
62
|
+
# if there are any collapsed namespaces, e.g. `models`, we then remove
|
63
|
+
# that from `filepath_relative_to_root_dir`.
|
64
|
+
#
|
65
|
+
# This would result in:
|
66
|
+
#
|
67
|
+
# previous filepath_relative_to_root_dir: domain_name/models/model_name
|
68
|
+
# new filepath_relative_to_root_dir: domain_name/model_name
|
69
|
+
if collapsed_namespace.any?
|
70
|
+
filepath_relative_to_root_dir.sub!("/#{collapsed_namespace.last}", "")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
camelize = loader.inflector.camelize(filepath_relative_to_root_dir, nil)
|
76
|
+
namespace.const_get(camelize)
|
77
|
+
rescue NameError => e
|
78
|
+
warn e
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
def constant_using_cpath(loader)
|
83
|
+
begin
|
84
|
+
constant = loader.cpath_expected_at(@absolute_file_path)
|
85
|
+
rescue ::Zeitwerk::Error => e
|
86
|
+
# Raises when file does not exist
|
87
|
+
warn "Zeitwerk unable to find file #{@file}, error:\n#{e.message}"
|
88
|
+
return
|
89
|
+
end
|
90
|
+
|
91
|
+
begin
|
92
|
+
# This uses ActiveSupport::Inflector.constantize
|
93
|
+
klass = constant.constantize
|
94
|
+
rescue NameError => e
|
95
|
+
warn e
|
96
|
+
return
|
97
|
+
end
|
98
|
+
|
99
|
+
klass
|
100
|
+
end
|
101
|
+
|
102
|
+
def supports_cpath?
|
103
|
+
@supports_cpath ||=
|
104
|
+
begin
|
105
|
+
current_version = ::Gem::Version.new(::Zeitwerk::VERSION)
|
106
|
+
required_version = ::Gem::Version.new("2.6.9")
|
107
|
+
|
108
|
+
current_version >= required_version
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -7,7 +7,6 @@ module AnnotateRb
|
|
7
7
|
autoload :BadModelFileError, "annotate_rb/model_annotator/bad_model_file_error"
|
8
8
|
autoload :FileNameResolver, "annotate_rb/model_annotator/file_name_resolver"
|
9
9
|
autoload :SingleFileAnnotationRemover, "annotate_rb/model_annotator/single_file_annotation_remover"
|
10
|
-
autoload :AnnotationPatternGenerator, "annotate_rb/model_annotator/annotation_pattern_generator"
|
11
10
|
autoload :ModelClassGetter, "annotate_rb/model_annotator/model_class_getter"
|
12
11
|
autoload :ModelFilesGetter, "annotate_rb/model_annotator/model_files_getter"
|
13
12
|
autoload :SingleFileAnnotator, "annotate_rb/model_annotator/single_file_annotator"
|
@@ -22,10 +21,10 @@ module AnnotateRb
|
|
22
21
|
autoload :SingleFileRemoveAnnotationInstruction, "annotate_rb/model_annotator/single_file_remove_annotation_instruction"
|
23
22
|
autoload :AnnotationDiffGenerator, "annotate_rb/model_annotator/annotation_diff_generator"
|
24
23
|
autoload :AnnotationDiff, "annotate_rb/model_annotator/annotation_diff"
|
25
|
-
autoload :FileComponents, "annotate_rb/model_annotator/file_components"
|
26
24
|
autoload :ProjectAnnotator, "annotate_rb/model_annotator/project_annotator"
|
27
25
|
autoload :ProjectAnnotationRemover, "annotate_rb/model_annotator/project_annotation_remover"
|
28
26
|
autoload :AnnotatedFile, "annotate_rb/model_annotator/annotated_file"
|
29
27
|
autoload :FileParser, "annotate_rb/model_annotator/file_parser"
|
28
|
+
autoload :ZeitwerkClassGetter, "annotate_rb/model_annotator/zeitwerk_class_getter"
|
30
29
|
end
|
31
30
|
end
|
data/lib/annotate_rb/options.rb
CHANGED
@@ -76,7 +76,8 @@ module AnnotateRb
|
|
76
76
|
target_action: :do_annotations, # Core; Possible values: :do_annotations, :remove_annotations
|
77
77
|
wrapper: nil, # ModelAnnotator, RouteAnnotator
|
78
78
|
wrapper_close: nil, # ModelAnnotator, RouteAnnotator
|
79
|
-
wrapper_open: nil # ModelAnnotator, RouteAnnotator
|
79
|
+
wrapper_open: nil, # ModelAnnotator, RouteAnnotator,
|
80
|
+
classes_default_to_s: [] # ModelAnnotator
|
80
81
|
}.freeze
|
81
82
|
|
82
83
|
PATH_OPTIONS = {
|
@@ -135,7 +136,8 @@ module AnnotateRb
|
|
135
136
|
:target_action,
|
136
137
|
:wrapper,
|
137
138
|
:wrapper_close,
|
138
|
-
:wrapper_open
|
139
|
+
:wrapper_open,
|
140
|
+
:classes_default_to_s
|
139
141
|
].freeze
|
140
142
|
|
141
143
|
PATH_OPTION_KEYS = [
|
data/lib/annotate_rb/parser.rb
CHANGED
@@ -9,7 +9,7 @@ module AnnotateRb
|
|
9
9
|
|
10
10
|
BANNER_STRING = <<~BANNER.freeze
|
11
11
|
Usage: annotaterb [command] [options]
|
12
|
-
|
12
|
+
|
13
13
|
Commands:
|
14
14
|
models [options]
|
15
15
|
routes [options]
|
@@ -248,6 +248,15 @@ module AnnotateRb
|
|
248
248
|
"exclude table comments in model annotations") do
|
249
249
|
@options[:with_table_comments] = false
|
250
250
|
end
|
251
|
+
|
252
|
+
option_parser.on("--classes-default-to-s class",
|
253
|
+
"Custom classes to be represented with `to_s`, may be used multiple times") do |klass|
|
254
|
+
@options[:classes_default_to_s] = if @options[:classes_default_to_s].present?
|
255
|
+
[*@options[:classes_default_to_s], klass]
|
256
|
+
else
|
257
|
+
[klass]
|
258
|
+
end
|
259
|
+
end
|
251
260
|
end
|
252
261
|
|
253
262
|
def add_route_options_to_parser(option_parser)
|
@@ -6,6 +6,11 @@
|
|
6
6
|
|
7
7
|
# Migration tasks are tasks that we'll "hook" into
|
8
8
|
migration_tasks = %w[db:migrate db:migrate:up db:migrate:down db:migrate:reset db:migrate:redo db:rollback]
|
9
|
+
|
10
|
+
# Support for data_migrate gem (https://github.com/ilyakatz/data-migrate)
|
11
|
+
migration_tasks_with_data = migration_tasks.map { |task| "#{task}:with_data" }
|
12
|
+
migration_tasks += migration_tasks_with_data
|
13
|
+
|
9
14
|
if defined?(Rails::Application) && Rails.version.split(".").first.to_i >= 6
|
10
15
|
require "active_record"
|
11
16
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: annotaterb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew W. Lee
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-03-27 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Annotates Rails/ActiveRecord Models, routes, fixtures, and others based
|
14
14
|
on the database schema.
|
@@ -44,7 +44,6 @@ files:
|
|
44
44
|
- lib/annotate_rb/model_annotator/annotation_decider.rb
|
45
45
|
- lib/annotate_rb/model_annotator/annotation_diff.rb
|
46
46
|
- lib/annotate_rb/model_annotator/annotation_diff_generator.rb
|
47
|
-
- lib/annotate_rb/model_annotator/annotation_pattern_generator.rb
|
48
47
|
- lib/annotate_rb/model_annotator/annotator.rb
|
49
48
|
- lib/annotate_rb/model_annotator/bad_model_file_error.rb
|
50
49
|
- lib/annotate_rb/model_annotator/column_annotation.rb
|
@@ -53,10 +52,12 @@ files:
|
|
53
52
|
- lib/annotate_rb/model_annotator/column_annotation/column_wrapper.rb
|
54
53
|
- lib/annotate_rb/model_annotator/column_annotation/default_value_builder.rb
|
55
54
|
- lib/annotate_rb/model_annotator/column_annotation/type_builder.rb
|
56
|
-
- lib/annotate_rb/model_annotator/file_components.rb
|
57
55
|
- lib/annotate_rb/model_annotator/file_name_resolver.rb
|
58
56
|
- lib/annotate_rb/model_annotator/file_parser.rb
|
59
|
-
- lib/annotate_rb/model_annotator/file_parser/
|
57
|
+
- lib/annotate_rb/model_annotator/file_parser/annotation_finder.rb
|
58
|
+
- lib/annotate_rb/model_annotator/file_parser/custom_parser.rb
|
59
|
+
- lib/annotate_rb/model_annotator/file_parser/parsed_file.rb
|
60
|
+
- lib/annotate_rb/model_annotator/file_parser/parsed_file_result.rb
|
60
61
|
- lib/annotate_rb/model_annotator/foreign_key_annotation.rb
|
61
62
|
- lib/annotate_rb/model_annotator/foreign_key_annotation/annotation_builder.rb
|
62
63
|
- lib/annotate_rb/model_annotator/index_annotation.rb
|
@@ -72,6 +73,7 @@ files:
|
|
72
73
|
- lib/annotate_rb/model_annotator/single_file_annotator.rb
|
73
74
|
- lib/annotate_rb/model_annotator/single_file_annotator_instruction.rb
|
74
75
|
- lib/annotate_rb/model_annotator/single_file_remove_annotation_instruction.rb
|
76
|
+
- lib/annotate_rb/model_annotator/zeitwerk_class_getter.rb
|
75
77
|
- lib/annotate_rb/options.rb
|
76
78
|
- lib/annotate_rb/parser.rb
|
77
79
|
- lib/annotate_rb/rake_bootstrapper.rb
|
@@ -116,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
118
|
- !ruby/object:Gem::Version
|
117
119
|
version: '0'
|
118
120
|
requirements: []
|
119
|
-
rubygems_version: 3.
|
121
|
+
rubygems_version: 3.5.6
|
120
122
|
signing_key:
|
121
123
|
specification_version: 4
|
122
124
|
summary: A gem for generating annotations for Rails projects.
|
@@ -1,19 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module AnnotateRb
|
4
|
-
module ModelAnnotator
|
5
|
-
class AnnotationPatternGenerator
|
6
|
-
COMPAT_PREFIX = "== Schema Info"
|
7
|
-
COMPAT_PREFIX_MD = "## Schema Info"
|
8
|
-
|
9
|
-
class << self
|
10
|
-
def call(options)
|
11
|
-
if options[:wrapper_open]
|
12
|
-
return /(?:^(\n|\r\n)?# (?:#{options[:wrapper_open]}).*(\n|\r\n)?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?(\n|\r\n)(#.*(\n|\r\n))*(\n|\r\n)*)|^(\n|\r\n)?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?(\n|\r\n)(#.*(\n|\r\n))*(\n|\r\n)*/
|
13
|
-
end
|
14
|
-
/^(\n|\r\n)?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?(\n|\r\n)(#.*(\n|\r\n))*(\n|\r\n)*/o
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
@@ -1,81 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module AnnotateRb
|
4
|
-
module ModelAnnotator
|
5
|
-
class FileComponents
|
6
|
-
SKIP_ANNOTATION_STRING = "# -*- SkipSchemaAnnotations"
|
7
|
-
SOME_PATTERN = /\A(?<start>\s*).*?\n(?<end>\s*)\z/m # Unsure what this pattern is
|
8
|
-
|
9
|
-
attr_reader :new_annotations
|
10
|
-
|
11
|
-
def initialize(file_content, new_annotations, options)
|
12
|
-
@file_content = file_content
|
13
|
-
@diff = AnnotationDiffGenerator.new(file_content, new_annotations).generate
|
14
|
-
@options = options
|
15
|
-
@annotation_pattern = AnnotationPatternGenerator.call(options)
|
16
|
-
@new_annotations = new_annotations
|
17
|
-
end
|
18
|
-
|
19
|
-
def current_file_content
|
20
|
-
@file_content
|
21
|
-
end
|
22
|
-
|
23
|
-
# TODO: Rename method once it's clear what this actually does
|
24
|
-
def space_before_annotation
|
25
|
-
return @space_before_annotation if defined?(@space_before_annotation)
|
26
|
-
|
27
|
-
match = current_annotations.match(SOME_PATTERN)
|
28
|
-
@space_before_annotation = if match
|
29
|
-
match[:start]
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
# TODO: Rename method once it's clear what this actually does
|
34
|
-
def space_after_annotation
|
35
|
-
return @space_after_annotation if defined?(@space_after_annotation)
|
36
|
-
|
37
|
-
match = current_annotations.match(SOME_PATTERN)
|
38
|
-
@space_after_annotation = if match
|
39
|
-
match[:end]
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def pure_file_content
|
44
|
-
@pure_file_content ||=
|
45
|
-
begin
|
46
|
-
content_without_magic_comments = @file_content.gsub(FileParser::MagicCommentParser::MAGIC_COMMENTS_REGEX, "")
|
47
|
-
content_without_annotations = content_without_magic_comments.sub(@annotation_pattern, "")
|
48
|
-
|
49
|
-
content_without_annotations
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def magic_comments
|
54
|
-
@magic_comments ||= FileParser::MagicCommentParser.call(@file_content)
|
55
|
-
end
|
56
|
-
|
57
|
-
def has_skip_string?
|
58
|
-
@has_skip_string ||= @file_content.include?(SKIP_ANNOTATION_STRING)
|
59
|
-
end
|
60
|
-
|
61
|
-
def has_annotations?
|
62
|
-
@has_annotations ||= @diff.current_columns.present?
|
63
|
-
end
|
64
|
-
|
65
|
-
def annotations_changed?
|
66
|
-
@has_annotations_changed ||= @diff.changed?
|
67
|
-
end
|
68
|
-
|
69
|
-
def current_annotations
|
70
|
-
@current_annotations ||=
|
71
|
-
if has_annotations?
|
72
|
-
# `#has_annotations?` uses a different regex pattern than the one in `@annotation_pattern`,
|
73
|
-
# this could lead to unexpected behavior
|
74
|
-
@file_content.match(@annotation_pattern).to_s
|
75
|
-
else
|
76
|
-
""
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module AnnotateRb
|
4
|
-
module ModelAnnotator
|
5
|
-
module FileParser
|
6
|
-
# Extracts magic comments strings and returns them
|
7
|
-
class MagicCommentParser
|
8
|
-
MAGIC_COMMENTS = [
|
9
|
-
HASH_ENCODING = /(^#\s*encoding:.*(?:\n|r\n))/,
|
10
|
-
HASH_CODING = /(^# coding:.*(?:\n|\r\n))/,
|
11
|
-
HASH_FROZEN_STRING = /(^#\s*frozen_string_literal:.+(?:\n|\r\n))/,
|
12
|
-
STAR_ENCODING = /(^# -\*- encoding\s?:.*(?:\n|\r\n))/,
|
13
|
-
STAR_CODING = /(^# -\*- coding:.*(?:\n|\r\n))/,
|
14
|
-
STAR_FROZEN_STRING = /(^# -\*- frozen_string_literal\s*:.+-\*-(?:\n|\r\n))/,
|
15
|
-
SORBET_TYPED_STRING = /(^#\s*typed:.*(?:\n|r\n))/.freeze
|
16
|
-
].freeze
|
17
|
-
|
18
|
-
MAGIC_COMMENTS_REGEX = Regexp.union(*MAGIC_COMMENTS).freeze
|
19
|
-
|
20
|
-
class << self
|
21
|
-
def call(content)
|
22
|
-
magic_comments = content.scan(MAGIC_COMMENTS_REGEX).flatten.compact
|
23
|
-
|
24
|
-
if magic_comments.any?
|
25
|
-
magic_comments.join
|
26
|
-
else
|
27
|
-
""
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|