annotaterb 4.4.1 → 4.6.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 +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
|