annotaterb 4.4.1 → 4.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/README.md +29 -0
  4. data/VERSION +1 -1
  5. data/lib/annotate_rb/config_generator.rb +28 -0
  6. data/lib/annotate_rb/eager_loader.rb +6 -2
  7. data/lib/annotate_rb/model_annotator/annotated_file/generator.rb +92 -0
  8. data/lib/annotate_rb/model_annotator/annotated_file/updater.rb +46 -0
  9. data/lib/annotate_rb/model_annotator/annotated_file.rb +10 -0
  10. data/lib/annotate_rb/model_annotator/annotator.rb +2 -2
  11. data/lib/annotate_rb/model_annotator/file_name_resolver.rb +5 -0
  12. data/lib/annotate_rb/model_annotator/file_parser/annotation_finder.rb +103 -0
  13. data/lib/annotate_rb/model_annotator/file_parser/custom_parser.rb +217 -0
  14. data/lib/annotate_rb/model_annotator/file_parser/parsed_file.rb +94 -0
  15. data/lib/annotate_rb/model_annotator/file_parser/parsed_file_result.rb +54 -0
  16. data/lib/annotate_rb/model_annotator/file_parser.rb +12 -0
  17. data/lib/annotate_rb/model_annotator/model_class_getter.rb +7 -0
  18. data/lib/annotate_rb/model_annotator/model_files_getter.rb +4 -8
  19. data/lib/annotate_rb/model_annotator/model_wrapper.rb +11 -2
  20. data/lib/annotate_rb/model_annotator/pattern_getter.rb +2 -0
  21. data/lib/annotate_rb/model_annotator/related_files_list_builder.rb +3 -3
  22. data/lib/annotate_rb/model_annotator/single_file_annotation_remover.rb +10 -12
  23. data/lib/annotate_rb/model_annotator/single_file_annotator.rb +15 -8
  24. data/lib/annotate_rb/model_annotator/single_file_annotator_instruction.rb +1 -1
  25. data/lib/annotate_rb/model_annotator/single_file_remove_annotation_instruction.rb +1 -1
  26. data/lib/annotate_rb/model_annotator/zeitwerk_class_getter.rb +113 -0
  27. data/lib/annotate_rb/model_annotator.rb +3 -4
  28. data/lib/annotate_rb/options.rb +4 -0
  29. data/lib/annotate_rb/parser.rb +9 -3
  30. data/lib/annotate_rb/runner.rb +5 -4
  31. data/lib/annotate_rb/tasks/annotate_models_migrate.rake +5 -0
  32. data/lib/annotate_rb.rb +1 -0
  33. data/lib/generators/annotate_rb/config/USAGE +6 -0
  34. data/lib/generators/annotate_rb/config/config_generator.rb +15 -0
  35. data/lib/generators/annotate_rb/hook/USAGE +7 -0
  36. data/lib/generators/annotate_rb/hook/hook_generator.rb +15 -0
  37. data/lib/generators/annotate_rb/install/install_generator.rb +3 -4
  38. data/lib/generators/annotate_rb/update_config/USAGE +6 -0
  39. data/lib/generators/annotate_rb/update_config/update_config_generator.rb +15 -0
  40. metadata +20 -8
  41. data/lib/annotate_rb/model_annotator/annotation_pattern_generator.rb +0 -19
  42. data/lib/annotate_rb/model_annotator/file_builder.rb +0 -57
  43. data/lib/annotate_rb/model_annotator/file_components.rb +0 -81
  44. data/lib/annotate_rb/model_annotator/magic_comment_parser.rb +0 -32
  45. /data/lib/generators/annotate_rb/{install → hook}/templates/annotate_rb.rake +0 -0
@@ -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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module FileParser
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"
10
+ end
11
+ end
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
 
@@ -9,13 +9,9 @@ module AnnotateRb
9
9
  # of model files from root dir. Otherwise we take all the model files
10
10
  # in the model_dir directory.
11
11
  def call(options)
12
- model_files = []
12
+ model_files = list_model_files_from_argument(options)
13
13
 
14
- # Note: This is currently broken as we don't set `is_rake` anywhere.
15
- # It's an artifact from the old Annotate gem and how it did control flow.
16
- model_files = list_model_files_from_argument(options) if !options[:is_rake]
17
-
18
- return model_files if !model_files.empty?
14
+ return model_files if model_files.any?
19
15
 
20
16
  options[:model_dir].each do |dir|
21
17
  Dir.chdir(dir) do
@@ -41,9 +37,9 @@ module AnnotateRb
41
37
  private
42
38
 
43
39
  def list_model_files_from_argument(options)
44
- return [] if ARGV.empty?
40
+ return [] if options.get_state(:working_args).empty?
45
41
 
46
- specified_files = ARGV.map { |file| File.expand_path(file) }
42
+ specified_files = options.get_state(:working_args).map { |file| File.expand_path(file) }
47
43
 
48
44
  model_files = options[:model_dir].flat_map do |dir|
49
45
  absolute_dir_path = File.expand_path(dir)
@@ -6,7 +6,7 @@ module AnnotateRb
6
6
  # Should be the wrapper for an ActiveRecord model that serves as the source of truth of the model
7
7
  # of the model that we're annotating
8
8
 
9
- def initialize(klass, options = {})
9
+ def initialize(klass, options)
10
10
  @klass = klass
11
11
  @options = options
12
12
  end
@@ -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
- @klass.connection.indexes(table_name_without_prefix)
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?
@@ -142,6 +142,8 @@ module AnnotateRb
142
142
  File.join(root_directory, FilePatterns::FACTORY_BOT_SPEC_DIR, "%TABLE_NAME%.rb"), # (new style)
143
143
  File.join(root_directory, FilePatterns::FACTORY_BOT_TEST_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
144
144
  File.join(root_directory, FilePatterns::FACTORY_BOT_SPEC_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
145
+ File.join(root_directory, FilePatterns::FACTORY_BOT_TEST_DIR, "%PLURALIZED_MODEL_NAME%_factory.rb"), # (new style)
146
+ File.join(root_directory, FilePatterns::FACTORY_BOT_SPEC_DIR, "%PLURALIZED_MODEL_NAME%_factory.rb"), # (new style)
145
147
  File.join(root_directory, FilePatterns::FABRICATORS_TEST_DIR, "%MODEL_NAME%_fabricator.rb"),
146
148
  File.join(root_directory, FilePatterns::FABRICATORS_SPEC_DIR, "%MODEL_NAME%_fabricator.rb")
147
149
  ]
@@ -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 !@options[:active_admin]
28
- add_additional_file_patterns if !@options[:additional_file_patterns].present?
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
- file_components = FileComponents.new(old_content, "", options)
16
-
17
- return false if file_components.has_skip_string?
18
- # TODO: Uncomment below after tests are fixed
19
- # return false if !file_components.has_annotations?
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
- generated_pattern = AnnotationPatternGenerator.call(options)
28
- updated_file_content = old_content.sub!(/(#{wrapper_open})?#{generated_pattern}/, "")
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
 
@@ -21,22 +21,29 @@ module AnnotateRb
21
21
  # :position_in_*<Symbol>:: where to place the annotated section in fixture or model file,
22
22
  # :before, :top, :after or :bottom. Default is :before.
23
23
  #
24
- def call(file_name, annotation, annotation_position, options = {})
24
+ def call(file_name, annotation, annotation_position, options)
25
25
  return false unless File.exist?(file_name)
26
26
  old_content = File.read(file_name)
27
27
 
28
- file_components = FileComponents.new(old_content, annotation, options)
29
- builder = FileBuilder.new(file_components, annotation_position, options)
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
30
35
 
31
- return false if file_components.has_skip_string?
32
- return false if !file_components.annotations_changed? && !options[:force]
36
+ return false if parsed_file.has_skip_string?
37
+ return false if !parsed_file.annotations_changed? && !options[:force]
33
38
 
34
39
  abort "AnnotateRb error. #{file_name} needs to be updated, but annotaterb was run with `--frozen`." if options[:frozen]
35
40
 
36
- updated_file_content = if !file_components.has_annotations? || options[:force]
37
- builder.generate_content_with_new_annotations
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
38
45
  else
39
- builder.update_existing_annotations
46
+ AnnotatedFile::Updater.new(old_content, annotation, annotation_position, options).update
40
47
  end
41
48
 
42
49
  File.open(file_name, "wb") { |f| f.puts updated_file_content }
@@ -4,7 +4,7 @@ module AnnotateRb
4
4
  module ModelAnnotator
5
5
  # A plain old Ruby object (PORO) that contains all necessary information for SingleFileAnnotator
6
6
  class SingleFileAnnotatorInstruction
7
- def initialize(file, annotation, position, options = {})
7
+ def initialize(file, annotation, position, options)
8
8
  @file = file # Path to file
9
9
  @annotation = annotation # Annotation string
10
10
  @position = position # Position in the file where to write the annotation to
@@ -4,7 +4,7 @@ module AnnotateRb
4
4
  module ModelAnnotator
5
5
  # A plain old Ruby object (PORO) that contains all necessary information for SingleFileAnnotationRemover
6
6
  class SingleFileRemoveAnnotationInstruction
7
- def initialize(file, options = {})
7
+ def initialize(file, options)
8
8
  @file = file # Path to file
9
9
  @options = options
10
10
  end
@@ -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 :FileBuilder, "annotate_rb/model_annotator/file_builder"
26
- autoload :MagicCommentParser, "annotate_rb/model_annotator/magic_comment_parser"
27
- autoload :FileComponents, "annotate_rb/model_annotator/file_components"
28
24
  autoload :ProjectAnnotator, "annotate_rb/model_annotator/project_annotator"
29
25
  autoload :ProjectAnnotationRemover, "annotate_rb/model_annotator/project_annotation_remover"
26
+ autoload :AnnotatedFile, "annotate_rb/model_annotator/annotated_file"
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
@@ -210,5 +210,9 @@ module AnnotateRb
210
210
  def get_state(key)
211
211
  @state[key]
212
212
  end
213
+
214
+ def print
215
+ # TODO: prints options and state
216
+ end
213
217
  end
214
218
  end
@@ -3,7 +3,7 @@ require "optparse"
3
3
  module AnnotateRb
4
4
  # Class for handling command line arguments
5
5
  class Parser # rubocop:disable Metrics/ClassLength
6
- def self.parse(args, existing_options = {})
6
+ def self.parse(args, existing_options)
7
7
  new(args, existing_options).parse
8
8
  end
9
9
 
@@ -35,11 +35,11 @@ module AnnotateRb
35
35
  }.freeze
36
36
 
37
37
  def initialize(args, existing_options)
38
- @args = args
38
+ @args = args.clone
39
39
  base_options = DEFAULT_OPTIONS.dup
40
40
  @options = base_options.merge(existing_options)
41
41
  @commands = []
42
- @options[:original_args] = args.dup
42
+ @options[:original_args] = args.clone
43
43
  end
44
44
 
45
45
  def parse
@@ -52,6 +52,12 @@ module AnnotateRb
52
52
  @options
53
53
  end
54
54
 
55
+ def remaining_args
56
+ # `@args` gets modified throughout the lifecycle of this class.
57
+ # It starts as a shallow clone of ARGV, then arguments matching commands and options are removed in #parse
58
+ @args
59
+ end
60
+
55
61
  private
56
62
 
57
63
  def parse_command(args)
@@ -9,14 +9,15 @@ module AnnotateRb
9
9
  end
10
10
 
11
11
  def run(args)
12
- _original_args = args.dup
13
-
14
12
  config_file_options = ConfigLoader.load_config
15
- parsed_options = Parser.parse(args)
13
+ parser = Parser.new(args, {})
14
+
15
+ parsed_options = parser.parse
16
+ remaining_args = parser.remaining_args
16
17
 
17
18
  options = config_file_options.merge(parsed_options)
18
19
 
19
- @options = Options.from(options, {})
20
+ @options = Options.from(options, {working_args: remaining_args})
20
21
  AnnotateRb::RakeBootstrapper.call(@options)
21
22
 
22
23
  if @options[:command]
@@ -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
 
data/lib/annotate_rb.rb CHANGED
@@ -22,6 +22,7 @@ require_relative "annotate_rb/eager_loader"
22
22
  require_relative "annotate_rb/rake_bootstrapper"
23
23
  require_relative "annotate_rb/config_finder"
24
24
  require_relative "annotate_rb/config_loader"
25
+ require_relative "annotate_rb/config_generator"
25
26
 
26
27
  module AnnotateRb
27
28
  end
@@ -0,0 +1,6 @@
1
+ Description:
2
+ Generates a default configuration file, `.annotaterb.yml` in your
3
+ Rails app project root.
4
+
5
+ Example:
6
+ `rails generate annotate_rb:config`
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "annotate_rb"
4
+
5
+ module AnnotateRb
6
+ module Generators
7
+ class ConfigGenerator < ::Rails::Generators::Base
8
+ def generate_config
9
+ create_file ::AnnotateRb::ConfigFinder::DOTFILE do
10
+ ::AnnotateRb::ConfigGenerator.default_config_yml
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ Description:
2
+ Adds a rake task into your Rails app's lib/tasks directory, that
3
+ automatically annotates models when you do a db:migrate in
4
+ development mode.
5
+
6
+ Example:
7
+ `rails generate annotate_rb:hook`
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "annotate_rb"
4
+
5
+ module AnnotateRb
6
+ module Generators
7
+ class HookGenerator < ::Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ def copy_hook_file
11
+ copy_file "annotate_rb.rake", "lib/tasks/annotate_rb.rake"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -5,10 +5,9 @@ require "annotate_rb"
5
5
  module AnnotateRb
6
6
  module Generators
7
7
  class InstallGenerator < ::Rails::Generators::Base
8
- source_root File.expand_path("templates", __dir__)
9
-
10
- def copy_task
11
- copy_file "annotate_rb.rake", "lib/tasks/annotate_rb.rake"
8
+ def install_hook_and_generate_defaults
9
+ generate "annotate_rb:hook"
10
+ generate "annotate_rb:config"
12
11
  end
13
12
  end
14
13
  end
@@ -0,0 +1,6 @@
1
+ Description:
2
+ Appends to .annotaterb.yml any missing default configuration
3
+ key-value pairs.
4
+
5
+ Example:
6
+ `rails generate annotate_rb:update_config`
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "annotate_rb"
4
+
5
+ module AnnotateRb
6
+ module Generators
7
+ class UpdateConfigGenerator < ::Rails::Generators::Base
8
+ def generate_config
9
+ insert_into_file ::AnnotateRb::ConfigFinder::DOTFILE do
10
+ ::AnnotateRb::ConfigGenerator.unset_config_defaults
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end