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