annotate 2.7.2 → 2.7.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 365398d9352685a831b09fe60f460131a0fbe2ab
4
- data.tar.gz: ba3fce8c6c97125af42c90310adac5272c6ef78c
2
+ SHA256:
3
+ metadata.gz: d4cd9e83172aed3fe8975a79f1cfeb076a1af786e547472c4d22130e07c559c9
4
+ data.tar.gz: a276b3e5964dd0b6fd5c033be8d1d143989abbbe3c43e0a56e399455c644cbe2
5
5
  SHA512:
6
- metadata.gz: 2f219cec844284bd213ca826a12553b862a9ec6dcfb2ccc271a2821576989dba630e5ad86951ab2fba2d6df02b77c5468b6f1a9b60ff14cf71b23c90779e9a78
7
- data.tar.gz: 9fdc058f6de547f0e3fa13f0b7ddaa931b4be5ad9ba4e041fb95002b6d9bd46631d0f3df5c1428e71774b089f7899d1e3332b5af00f0db8d1fe56dc249044bc3
6
+ metadata.gz: f44658cc7947064044bbc96fc2d9a4b2e077cc4c6b2934b71477081a0a9e42ad0d01bc8ac0e43d6707f8576cdcbf49af85562441dc7dde5f7517219ffb34902c
7
+ data.tar.gz: 5d16e2603ef7c5a3428d157f9d85a51a02d6386eeafd984d16a133dd55c1dec61dc14e15be6cf3ae349d34403b358785cba37f0b262b3cccdb9f750fd8043eb6
@@ -1,3 +1,6 @@
1
+ == 2.7.3
2
+ See https://github.com/ctran/annotate_models/releases/tag/v2.7.3
3
+
1
4
  == 2.7.2
2
5
  See https://github.com/ctran/annotate_models/releases/tag/v2.7.2
3
6
 
@@ -17,7 +17,7 @@ your...
17
17
  - Object Daddy exemplars
18
18
  - Machinist blueprints
19
19
  - Fabrication fabricators
20
- - Thoughtbot's factory_girl factories, i.e. the (spec|test)/factories/<model>_factory.rb files
20
+ - Thoughtbot's factory_bot factories, i.e. the (spec|test)/factories/<model>_factory.rb files
21
21
  - routes.rb file (for Rails projects)
22
22
 
23
23
  The schema comment looks like this:
@@ -55,11 +55,15 @@ Also, if you pass the -r option, it'll annotate routes.rb with the output of
55
55
 
56
56
  Into Gemfile from rubygems.org:
57
57
 
58
- gem 'annotate'
58
+ group :development do
59
+ gem 'annotate'
60
+ end
59
61
 
60
62
  Into Gemfile from Github:
61
63
 
62
- gem 'annotate', git: 'https://github.com/ctran/annotate_models.git'
64
+ group :development do
65
+ gem 'annotate', git: 'https://github.com/ctran/annotate_models.git'
66
+ end
63
67
 
64
68
  Into environment gems from rubygems.org:
65
69
 
@@ -154,14 +158,15 @@ If you want to run <code>rake db:migrate</code> as a one-off without running ann
154
158
  you can do so with a simple environment variable, instead of editing the
155
159
  +.rake+ file:
156
160
 
157
- skip_on_db_migrate=1 rake db:migrate
161
+ ANNOTATE_SKIP_ON_DB_MIGRATE=1 rake db:migrate
158
162
 
159
163
 
160
164
  == Options
161
165
 
162
166
  Usage: annotate [options] [model_file]*
163
167
  -d, --delete Remove annotations from all model files or the routes.rb file
164
- -p, --position [before|top|after|bottom] Place the annotations at the top (before) or the bottom (after) of the model/test/fixture/factory/routes file(s)
168
+ -p [before|top|after|bottom], Place the annotations at the top (before) or the bottom (after) of the model/test/fixture/factory/route/serializer file(s)
169
+ --position
165
170
  --pc, --position-in-class [before|top|after|bottom]
166
171
  Place the annotations at the top (before) or the bottom (after) of the model file
167
172
  --pf, --position-in-factory [before|top|after|bottom]
@@ -179,15 +184,19 @@ you can do so with a simple environment variable, instead of editing the
179
184
  --wo, --wrapper-open STR Annotation wrapper opening.
180
185
  --wc, --wrapper-close STR Annotation wrapper closing
181
186
  -r, --routes Annotate routes.rb with the output of 'rake routes'
182
- -aa, --active-admin Annotate all activeadmin models
187
+ -a, --active-admin Annotate active_admin models
183
188
  -v, --version Show the current version of this gem
184
189
  -m, --show-migration Include the migration version number in the annotation
185
- -i, --show-indexes List the table's database indexes in the annotation
186
190
  -k, --show-foreign-keys List the table's foreign key constraints in the annotation
191
+ --ck, --complete-foreign-keys
192
+ Complete foreign key names in the annotation
193
+ -i, --show-indexes List the table's database indexes in the annotation
187
194
  -s, --simple-indexes Concat the column's related indexes in the annotation
188
195
  --model-dir dir Annotate model files stored in dir rather than app/models, separate multiple dirs with commas
196
+ --root-dir dir Annotate files stored within root dir projects, separate multiple dirs with commas
189
197
  --ignore-model-subdirects Ignore subdirectories of the models directory
190
198
  --sort Sort columns alphabetically, rather than in creation order
199
+ --classified-sort Sort columns alphabetically, but first goes id, then the rest columns, then the timestamp columns and then the association columns
191
200
  -R, --require path Additional file to require before loading models, may be used multiple times
192
201
  -e [tests,fixtures,factories,serializers],
193
202
  --exclude Do not annotate fixtures, test files, factories, and/or serializers
@@ -197,6 +206,13 @@ you can do so with a simple environment variable, instead of editing the
197
206
  --timestamp Include timestamp in (routes) annotation
198
207
  --trace If unable to annotate a file, print the full stack trace, not just the exception message.
199
208
  -I, --ignore-columns REGEX don't annotate columns that match a given REGEX (i.e., `annotate -I '^(id|updated_at|created_at)'`
209
+ --ignore-routes REGEX don't annotate routes that match a given REGEX (i.e., `annotate -I '(mobile|resque|pghero)'`
210
+ --hide-limit-column-types VALUES
211
+ don't show limit for given column types, separated by commas (i.e., `integer,boolean,text`)
212
+ --hide-default-column-types VALUES
213
+ don't show default for given column types, separated by commas (i.e., `json,jsonb,hstore`)
214
+ --ignore-unknown-models don't display warnings for bad model files
215
+ --with-comment include database comments in model annotations
200
216
 
201
217
 
202
218
 
@@ -242,7 +258,7 @@ extra carefully, and consider using one.
242
258
 
243
259
  == Links
244
260
 
245
- - Factory Girl: http://github.com/thoughtbot/factory_girl
261
+ - Factory Bot: http://github.com/thoughtbot/factory_bot
246
262
  - Object Daddy: http://github.com/flogic/object_daddy
247
263
  - Machinist: http://github.com/notahat/machinist
248
264
  - Fabrication: http://github.com/paulelliott/fabrication
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
7
7
  s.name = 'annotate'
8
8
  s.version = Annotate.version
9
9
 
10
- s.required_ruby_version = '>= 1.9.3'
10
+ s.required_ruby_version = '>= 2.2.0'
11
11
  s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
12
12
  s.authors = ['Alex Chaffee', 'Cuong Tran', 'Marcos Piccinini', 'Turadg Aleahmad', 'Jon Frisby']
13
13
  s.description = 'Annotates Rails/ActiveRecord Models, routes, fixtures, and others based on the database schema.'
@@ -90,7 +90,7 @@ OptionParser.new do |opts|
90
90
  ENV['routes'] = 'true'
91
91
  end
92
92
 
93
- opts.on('-aa', '--active-admin', 'Annotate active_admin models') do
93
+ opts.on('-a', '--active-admin', 'Annotate active_admin models') do
94
94
  ENV['active_admin'] = 'true'
95
95
  end
96
96
 
@@ -107,6 +107,12 @@ OptionParser.new do |opts|
107
107
  ENV['show_foreign_keys'] = 'yes'
108
108
  end
109
109
 
110
+ opts.on('--ck',
111
+ '--complete-foreign-keys', 'Complete foreign key names in the annotation') do
112
+ ENV['show_foreign_keys'] = 'yes'
113
+ ENV['show_complete_foreign_keys'] = 'yes'
114
+ end
115
+
110
116
  opts.on('-i', '--show-indexes',
111
117
  "List the table's database indexes in the annotation") do
112
118
  ENV['show_indexes'] = 'yes'
@@ -191,12 +197,16 @@ OptionParser.new do |opts|
191
197
  opts.on('--ignore-unknown-models', "don't display warnings for bad model files") do |values|
192
198
  ENV['ignore_unknown_models'] = 'true'
193
199
  end
200
+
201
+ opts.on('--with-comment', "include database comments in model annotations") do |values|
202
+ ENV['with_comment'] = 'true'
203
+ end
194
204
  end.parse!
195
205
 
196
206
  options = Annotate.setup_options(
197
207
  is_rake: ENV['is_rake'] && !ENV['is_rake'].empty?
198
208
  )
199
- Annotate.eager_load(options)
209
+ Annotate.eager_load(options) if Annotate.include_models?
200
210
 
201
211
  AnnotateModels.send(target_action, options) if Annotate.include_models?
202
212
  AnnotateRoutes.send(target_action, options) if Annotate.include_routes?
@@ -30,9 +30,10 @@ module Annotate
30
30
  :show_indexes, :simple_indexes, :include_version, :exclude_tests,
31
31
  :exclude_fixtures, :exclude_factories, :ignore_model_sub_dir,
32
32
  :format_bare, :format_rdoc, :format_markdown, :sort, :force, :trace,
33
- :timestamp, :exclude_serializers, :classified_sort, :show_foreign_keys,
33
+ :timestamp, :exclude_serializers, :classified_sort,
34
+ :show_foreign_keys, :show_complete_foreign_keys,
34
35
  :exclude_scaffolds, :exclude_controllers, :exclude_helpers,
35
- :exclude_sti_subclasses, :ignore_unknown_models
36
+ :exclude_sti_subclasses, :ignore_unknown_models, :with_comment
36
37
  ].freeze
37
38
  OTHER_OPTIONS = [
38
39
  :ignore_columns, :skip_on_db_migrate, :wrapper_open, :wrapper_close,
@@ -105,7 +106,7 @@ module Annotate
105
106
  end
106
107
 
107
108
  def self.skip_on_migration?
108
- ENV['skip_on_db_migrate'] =~ TRUE_RE
109
+ ENV['ANNOTATE_SKIP_ON_DB_MIGRATE'] =~ TRUE_RE || ENV['skip_on_db_migrate'] =~ TRUE_RE
109
110
  end
110
111
 
111
112
  def self.include_routes?
@@ -113,7 +114,7 @@ module Annotate
113
114
  end
114
115
 
115
116
  def self.include_models?
116
- true
117
+ ENV['routes'] !~ TRUE_RE
117
118
  end
118
119
 
119
120
  def self.loaded_tasks=(val)
@@ -168,7 +169,7 @@ module Annotate
168
169
  require 'rake/dsl_definition'
169
170
  rescue StandardError => e
170
171
  # We might just be on an old version of Rake...
171
- puts e.message
172
+ $stderr.puts e.message
172
173
  exit e.status_code
173
174
  end
174
175
  require 'rake'
@@ -12,6 +12,8 @@ module AnnotateModels
12
12
  PREFIX_MD = '## Schema Information'.freeze
13
13
  END_MARK = '== Schema Information End'.freeze
14
14
 
15
+ SKIP_ANNOTATION_PREFIX = '# -\*- SkipSchemaAnnotations'.freeze
16
+
15
17
  MATCHED_TYPES = %w(test fixture factory serializer scaffold controller helper).freeze
16
18
 
17
19
  # File.join for windows reverse bar compat?
@@ -65,12 +67,27 @@ module AnnotateModels
65
67
  # Don't show default value for these column types
66
68
  NO_DEFAULT_COL_TYPES = %w(json jsonb hstore).freeze
67
69
 
70
+ INDEX_CLAUSES = {
71
+ unique: {
72
+ default: 'UNIQUE',
73
+ markdown: '_unique_'
74
+ },
75
+ where: {
76
+ default: 'WHERE',
77
+ markdown: '_where_'
78
+ },
79
+ using: {
80
+ default: 'USING',
81
+ markdown: '_using_'
82
+ }
83
+ }.freeze
84
+
68
85
  class << self
69
86
  def annotate_pattern(options = {})
70
87
  if options[:wrapper_open]
71
- return /(?:^\n?# (?:#{options[:wrapper_open]}).*\n?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?\n(#.*\n)*\n*)|^\n?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?\n(#.*\n)*\n*/
88
+ 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)*/
72
89
  end
73
- /^\n?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?\n(#.*\n)*\n*/
90
+ /^(\n|\r\n)?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?(\n|\r\n)(#.*(\n|\r\n))*(\n|\r\n)*/
74
91
  end
75
92
 
76
93
  def model_dir
@@ -127,6 +144,8 @@ module AnnotateModels
127
144
  File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%MODEL_NAME%_factory.rb"), # (old style)
128
145
  File.join(root_directory, FACTORY_GIRL_TEST_DIR, "%TABLE_NAME%.rb"), # (new style)
129
146
  File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%TABLE_NAME%.rb"), # (new style)
147
+ File.join(root_directory, FACTORY_GIRL_TEST_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
148
+ File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
130
149
  File.join(root_directory, FABRICATORS_TEST_DIR, "%MODEL_NAME%_fabricator.rb"),
131
150
  File.join(root_directory, FABRICATORS_SPEC_DIR, "%MODEL_NAME%_fabricator.rb")
132
151
  ]
@@ -195,8 +214,8 @@ module AnnotateModels
195
214
  return indexes if indexes.any? || !klass.table_name_prefix
196
215
 
197
216
  # Try to search the table without prefix
198
- table_name.to_s.slice!(klass.table_name_prefix)
199
- klass.connection.indexes(table_name)
217
+ table_name_without_prefix = table_name.to_s.sub(klass.table_name_prefix, '')
218
+ klass.connection.indexes(table_name_without_prefix)
200
219
  end
201
220
 
202
221
  # Use the column information in an ActiveRecord class
@@ -207,11 +226,7 @@ module AnnotateModels
207
226
  info = "# #{header}\n"
208
227
  info << get_schema_header_text(klass, options)
209
228
 
210
- max_size = klass.column_names.map(&:size).max || 0
211
- with_comment = options[:with_comment] && klass.columns.first.respond_to?(:comment)
212
- max_size = klass.columns.map{|col| col.name.size + col.comment.size }.max || 0 if with_comment
213
- max_size += 2 if with_comment
214
- max_size += options[:format_rdoc] ? 5 : 1
229
+ max_size = max_schema_info_width(klass, options)
215
230
  md_names_overhead = 6
216
231
  md_type_allowance = 18
217
232
  bare_type_allowance = 16
@@ -232,7 +247,7 @@ module AnnotateModels
232
247
  cols = cols.sort_by(&:name) if options[:sort]
233
248
  cols = classified_sort(cols) if options[:classified_sort]
234
249
  cols.each do |col|
235
- col_type = (col.type || col.sql_type).to_s
250
+ col_type = get_col_type(col)
236
251
  attrs = []
237
252
  attrs << "default(#{schema_default(klass, col)})" unless col.default.nil? || hide_default?(col_type, options)
238
253
  attrs << 'unsigned' if col.respond_to?(:unsigned?) && col.unsigned?
@@ -274,7 +289,7 @@ module AnnotateModels
274
289
  end
275
290
  end
276
291
  end
277
- col_name = if with_comment
292
+ col_name = if with_comments?(klass, options) && col.comment
278
293
  "#{col.name}(#{col.comment})"
279
294
  else
280
295
  col.name
@@ -337,15 +352,83 @@ module AnnotateModels
337
352
  max_size = indexes.collect{|index| index.name.size}.max + 1
338
353
  indexes.sort_by(&:name).each do |index|
339
354
  index_info << if options[:format_markdown]
340
- sprintf("# * `%s`%s:\n# * **`%s`**\n", index.name, index.unique ? " (_unique_)" : "", Array(index.columns).join("`**\n# * **`"))
355
+ final_index_string_in_markdown(index)
341
356
  else
342
- sprintf("# %-#{max_size}.#{max_size}s %s %s", index.name, "(#{Array(index.columns).join(",")})", index.unique ? "UNIQUE" : "").rstrip + "\n"
357
+ final_index_string(index, max_size)
343
358
  end
344
359
  end
345
360
 
346
361
  index_info
347
362
  end
348
363
 
364
+ def get_col_type(col)
365
+ if col.respond_to?(:bigint?) && col.bigint?
366
+ 'bigint'
367
+ else
368
+ (col.type || col.sql_type).to_s
369
+ end
370
+ end
371
+
372
+ def index_columns_info(index)
373
+ Array(index.columns).map do |col|
374
+ if index.try(:orders) && index.orders[col.to_s]
375
+ "#{col} #{index.orders[col.to_s].upcase}"
376
+ else
377
+ col.to_s.gsub("\r", '\r').gsub("\n", '\n')
378
+ end
379
+ end
380
+ end
381
+
382
+ def index_unique_info(index, format = :default)
383
+ index.unique ? " #{INDEX_CLAUSES[:unique][format]}" : ''
384
+ end
385
+
386
+ def index_where_info(index, format = :default)
387
+ value = index.try(:where).try(:to_s)
388
+ if value.blank?
389
+ ''
390
+ else
391
+ " #{INDEX_CLAUSES[:where][format]} #{value}"
392
+ end
393
+ end
394
+
395
+ def index_using_info(index, format = :default)
396
+ value = index.try(:using) && index.using.try(:to_sym)
397
+ if !value.blank? && value != :btree
398
+ " #{INDEX_CLAUSES[:using][format]} #{value}"
399
+ else
400
+ ''
401
+ end
402
+ end
403
+
404
+ def final_index_string_in_markdown(index)
405
+ details = sprintf(
406
+ "%s%s%s",
407
+ index_unique_info(index, :markdown),
408
+ index_where_info(index, :markdown),
409
+ index_using_info(index, :markdown)
410
+ ).strip
411
+ details = " (#{details})" unless details.blank?
412
+
413
+ sprintf(
414
+ "# * `%s`%s:\n# * **`%s`**\n",
415
+ index.name,
416
+ details,
417
+ index_columns_info(index).join("`**\n# * **`")
418
+ )
419
+ end
420
+
421
+ def final_index_string(index, max_size)
422
+ sprintf(
423
+ "# %-#{max_size}.#{max_size}s %s%s%s%s",
424
+ index.name,
425
+ "(#{index_columns_info(index).join(',')})",
426
+ index_unique_info(index),
427
+ index_where_info(index),
428
+ index_using_info(index)
429
+ ).rstrip + "\n"
430
+ end
431
+
349
432
  def hide_limit?(col_type, options)
350
433
  excludes =
351
434
  if options[:hide_limit_column_types].blank?
@@ -417,7 +500,7 @@ module AnnotateModels
417
500
  def annotate_one_file(file_name, info_block, position, options = {})
418
501
  if File.exist?(file_name)
419
502
  old_content = File.read(file_name)
420
- return false if old_content =~ /# -\*- SkipSchemaAnnotations.*\n/
503
+ return false if old_content =~ /#{SKIP_ANNOTATION_PREFIX}.*\n/
421
504
 
422
505
  # Ignore the Schema version line because it changes with each migration
423
506
  header_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?)/
@@ -428,8 +511,7 @@ module AnnotateModels
428
511
  old_columns = old_header && old_header.scan(column_pattern).sort
429
512
  new_columns = new_header && new_header.scan(column_pattern).sort
430
513
 
431
- magic_comment_matcher = Regexp.new(/(^#\s*encoding:.*\n)|(^# coding:.*\n)|(^# -\*- coding:.*\n)|(^# -\*- encoding\s?:.*\n)|(^#\s*frozen_string_literal:.+\n)|(^# -\*- frozen_string_literal\s*:.+-\*-\n)/)
432
- magic_comments = old_content.scan(magic_comment_matcher).flatten.compact
514
+ magic_comments_block = magic_comments_as_string(old_content)
433
515
 
434
516
  if old_columns == new_columns && !options[:force]
435
517
  return false
@@ -447,13 +529,13 @@ module AnnotateModels
447
529
  # if there *was* no old schema info (no substitution happened) or :force was passed,
448
530
  # we simply need to insert it in correct position
449
531
  if new_content == old_content || options[:force]
450
- old_content.sub!(magic_comment_matcher, '')
532
+ old_content.gsub!(magic_comment_matcher, '')
451
533
  old_content.sub!(annotate_pattern(options), '')
452
534
 
453
535
  new_content = if %w(after bottom).include?(options[position].to_s)
454
- magic_comments.join + (old_content.rstrip + "\n\n" + wrapped_info_block)
536
+ magic_comments_block + (old_content.rstrip + "\n\n" + wrapped_info_block)
455
537
  else
456
- magic_comments.join + wrapped_info_block + "\n" + old_content
538
+ magic_comments_block + wrapped_info_block + "\n" + old_content
457
539
  end
458
540
  end
459
541
 
@@ -465,9 +547,25 @@ module AnnotateModels
465
547
  end
466
548
  end
467
549
 
550
+ def magic_comment_matcher
551
+ Regexp.new(/(^#\s*encoding:.*(?:\n|r\n))|(^# coding:.*(?:\n|\r\n))|(^# -\*- coding:.*(?:\n|\r\n))|(^# -\*- encoding\s?:.*(?:\n|\r\n))|(^#\s*frozen_string_literal:.+(?:\n|\r\n))|(^# -\*- frozen_string_literal\s*:.+-\*-(?:\n|\r\n))/)
552
+ end
553
+
554
+ def magic_comments_as_string(content)
555
+ magic_comments = content.scan(magic_comment_matcher).flatten.compact
556
+
557
+ if magic_comments.any?
558
+ magic_comments.join + "\n"
559
+ else
560
+ ''
561
+ end
562
+ end
563
+
468
564
  def remove_annotation_of_file(file_name, options = {})
469
565
  if File.exist?(file_name)
470
566
  content = File.read(file_name)
567
+ return false if content =~ /#{SKIP_ANNOTATION_PREFIX}.*\n/
568
+
471
569
  wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ''
472
570
  content.sub!(/(#{wrapper_open})?#{annotate_pattern(options)}/, '')
473
571
 
@@ -542,8 +640,8 @@ module AnnotateModels
542
640
  end
543
641
  end
544
642
  rescue StandardError => e
545
- puts "Unable to annotate #{file}: #{e.message}"
546
- puts "\t" + e.backtrace.join("\n\t") if options[:trace]
643
+ $stderr.puts "Unable to annotate #{file}: #{e.message}"
644
+ $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
547
645
  end
548
646
 
549
647
  annotated
@@ -559,35 +657,53 @@ module AnnotateModels
559
657
  # of model files from root dir. Otherwise we take all the model files
560
658
  # in the model_dir directory.
561
659
  def get_model_files(options)
562
- models = []
563
- unless options[:is_rake]
564
- models = ARGV.dup.reject { |m| m.match(/^(.*)=/) }
565
- end
660
+ model_files = []
566
661
 
567
- if models.empty?
568
- begin
569
- model_dir.each do |dir|
570
- Dir.chdir(dir) do
571
- lst =
572
- if options[:ignore_model_sub_dir]
573
- Dir["*.rb"].map{ |f| [dir, f] }
574
- else
575
- Dir["**/*.rb"].reject{ |f| f["concerns/"] }.map{ |f| [dir, f] }
576
- end
577
- models.concat(lst)
578
- end
579
- end
580
- rescue SystemCallError
581
- puts "No models found in directory '#{model_dir.join("', '")}'."
582
- puts "Either specify models on the command line, or use the --model-dir option."
583
- puts "Call 'annotate --help' for more info."
584
- exit 1
662
+ model_files = list_model_files_from_argument unless options[:is_rake]
663
+
664
+ return model_files unless model_files.empty?
665
+
666
+ model_dir.each do |dir|
667
+ Dir.chdir(dir) do
668
+ list = if options[:ignore_model_sub_dir]
669
+ Dir["*.rb"].map { |f| [dir, f] }
670
+ else
671
+ Dir["**/*.rb"].reject { |f| f["concerns/"] }.map { |f| [dir, f] }
672
+ end
673
+ model_files.concat(list)
585
674
  end
586
675
  end
587
676
 
588
- models
677
+ model_files
678
+ rescue SystemCallError
679
+ $stderr.puts "No models found in directory '#{model_dir.join("', '")}'."
680
+ $stderr.puts "Either specify models on the command line, or use the --model-dir option."
681
+ $stderr.puts "Call 'annotate --help' for more info."
682
+ exit 1
589
683
  end
590
684
 
685
+ def list_model_files_from_argument
686
+ return [] if ARGV.empty?
687
+
688
+ specified_files = ARGV.map { |file| File.expand_path(file) }
689
+
690
+ model_files = model_dir.flat_map do |dir|
691
+ absolute_dir_path = File.expand_path(dir)
692
+ specified_files
693
+ .find_all { |file| file.start_with?(absolute_dir_path) }
694
+ .map { |file| [dir, file.sub("#{absolute_dir_path}/", '')] }
695
+ end
696
+
697
+ if model_files.size != specified_files.size
698
+ puts "The specified file could not be found in directory '#{model_dir.join("', '")}'."
699
+ puts "Call 'annotate --help' for more info."
700
+ exit 1
701
+ end
702
+
703
+ model_files
704
+ end
705
+ private :list_model_files_from_argument
706
+
591
707
  # Retrieve the classes belonging to the model names we're asked to process
592
708
  # Check for namespaced models in subdirectories as well as models
593
709
  # in subdirectories without namespacing.
@@ -595,11 +711,11 @@ module AnnotateModels
595
711
  model_path = file.gsub(/\.rb$/, '')
596
712
  model_dir.each { |dir| model_path = model_path.gsub(/^#{dir}/, '').gsub(/^\//, '') }
597
713
  begin
598
- get_loaded_model(model_path) || raise(BadModelFileError.new)
714
+ get_loaded_model(model_path, file) || raise(BadModelFileError.new)
599
715
  rescue LoadError
600
716
  # this is for non-rails projects, which don't get Rails auto-require magic
601
717
  file_path = File.expand_path(file)
602
- if File.file?(file_path) && silence_warnings { Kernel.require(file_path) }
718
+ if File.file?(file_path) && Kernel.require(file_path)
603
719
  retry
604
720
  elsif model_path =~ /\//
605
721
  model_path = model_path.split('/')[1..-1].join('/').to_s
@@ -610,10 +726,26 @@ module AnnotateModels
610
726
  end
611
727
  end
612
728
 
729
+ # Retrieve loaded model class
730
+ def get_loaded_model(model_path, file)
731
+ loaded_model_class = get_loaded_model_by_path(model_path)
732
+ return loaded_model_class if loaded_model_class
733
+
734
+ # We cannot get loaded model when `model_path` is loaded by Rails
735
+ # auto_load/eager_load paths. Try all possible model paths one by one.
736
+ absolute_file = File.expand_path(file)
737
+ model_paths =
738
+ $LOAD_PATH.select { |path| absolute_file.include?(path) }
739
+ .map { |path| absolute_file.sub(path, '').sub(/\.rb$/, '').sub(/^\//, '') }
740
+ model_paths
741
+ .map { |path| get_loaded_model_by_path(path) }
742
+ .find { |loaded_model| !loaded_model.nil? }
743
+ end
744
+
613
745
  # Retrieve loaded model class by path to the file where it's supposed to be defined.
614
- def get_loaded_model(model_path)
746
+ def get_loaded_model_by_path(model_path)
615
747
  ActiveSupport::Inflector.constantize(ActiveSupport::Inflector.camelize(model_path))
616
- rescue
748
+ rescue StandardError, LoadError
617
749
  # Revert to the old way but it is not really robust
618
750
  ObjectSpace.each_object(::Class)
619
751
  .select do |c|
@@ -624,10 +756,15 @@ module AnnotateModels
624
756
  end
625
757
 
626
758
  def parse_options(options = {})
627
- self.model_dir = options[:model_dir] if options[:model_dir]
759
+ self.model_dir = split_model_dir(options[:model_dir]) if options[:model_dir]
628
760
  self.root_dir = options[:root_dir] if options[:root_dir]
629
761
  end
630
762
 
763
+ def split_model_dir(option_value)
764
+ option_value = option_value.is_a?(Array) ? option_value : option_value.split(',')
765
+ option_value.map(&:strip).reject(&:empty?)
766
+ end
767
+
631
768
  # We're passed a name of things that might be
632
769
  # ActiveRecord models. If we can find the class, and
633
770
  # if its a subclass of ActiveRecord::Base,
@@ -655,7 +792,7 @@ module AnnotateModels
655
792
 
656
793
  def annotate_model_file(annotated, file, header, options)
657
794
  begin
658
- return false if /# -\*- SkipSchemaAnnotations.*/ =~ (File.exist?(file) ? File.read(file) : '')
795
+ return false if /#{SKIP_ANNOTATION_PREFIX}.*/ =~ (File.exist?(file) ? File.read(file) : '')
659
796
  klass = get_model_class(file)
660
797
  do_annotate = klass &&
661
798
  klass < ActiveRecord::Base &&
@@ -666,12 +803,12 @@ module AnnotateModels
666
803
  annotated.concat(annotate(klass, file, header, options)) if do_annotate
667
804
  rescue BadModelFileError => e
668
805
  unless options[:ignore_unknown_models]
669
- puts "Unable to annotate #{file}: #{e.message}"
670
- puts "\t" + e.backtrace.join("\n\t") if options[:trace]
806
+ $stderr.puts "Unable to annotate #{file}: #{e.message}"
807
+ $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
671
808
  end
672
809
  rescue StandardError => e
673
- puts "Unable to annotate #{file}: #{e.message}"
674
- puts "\t" + e.backtrace.join("\n\t") if options[:trace]
810
+ $stderr.puts "Unable to annotate #{file}: #{e.message}"
811
+ $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
675
812
  end
676
813
  end
677
814
 
@@ -701,8 +838,8 @@ module AnnotateModels
701
838
  end
702
839
  deannotated << klass if deannotated_klass
703
840
  rescue StandardError => e
704
- puts "Unable to deannotate #{File.join(file)}: #{e.message}"
705
- puts "\t" + e.backtrace.join("\n\t") if options[:trace]
841
+ $stderr.puts "Unable to deannotate #{File.join(file)}: #{e.message}"
842
+ $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
706
843
  end
707
844
  end
708
845
  puts "Removed annotations from: #{deannotated.join(', ')}"
@@ -737,13 +874,26 @@ module AnnotateModels
737
874
  ([id] << rest_cols << timestamps << associations).flatten.compact
738
875
  end
739
876
 
740
- # Ignore warnings for the duration of the block ()
741
- def silence_warnings
742
- old_verbose = $VERBOSE
743
- $VERBOSE = nil
744
- yield
745
- ensure
746
- $VERBOSE = old_verbose
877
+ private
878
+
879
+ def with_comments?(klass, options)
880
+ options[:with_comment] &&
881
+ klass.columns.first.respond_to?(:comment) &&
882
+ klass.columns.any? { |col| !col.comment.nil? }
883
+ end
884
+
885
+ def max_schema_info_width(klass, options)
886
+ if with_comments?(klass, options)
887
+ max_size = klass.columns.map do |column|
888
+ column.name.size + (column.comment ? column.comment.size : 0)
889
+ end.max || 0
890
+ max_size += 2
891
+ else
892
+ max_size = klass.column_names.map(&:size).max
893
+ end
894
+ max_size += options[:format_rdoc] ? 5 : 1
895
+
896
+ max_size
747
897
  end
748
898
  end
749
899
 
@@ -1,3 +1,5 @@
1
+ # rubocop:disable Metrics/ModuleLength
2
+
1
3
  # == Annotate Routes
2
4
  #
3
5
  # Based on:
@@ -36,7 +38,18 @@ module AnnotateRoutes
36
38
  def header(options = {})
37
39
  routes_map = app_routes_map(options)
38
40
 
39
- out = ["# #{options[:format_markdown] ? PREFIX_MD : PREFIX}" + (options[:timestamp] ? " (Updated #{Time.now.strftime('%Y-%m-%d %H:%M')})" : '')]
41
+ magic_comments_map, routes_map = extract_magic_comments_from_array(routes_map)
42
+
43
+ out = []
44
+
45
+ magic_comments_map.each do |magic_comment|
46
+ out << magic_comment
47
+ end
48
+ out << '' if magic_comments_map.any?
49
+
50
+ out += ["# #{options[:wrapper_open]}"] if options[:wrapper_open]
51
+
52
+ out += ["# #{options[:format_markdown] ? PREFIX_MD : PREFIX}" + (options[:timestamp] ? " (Updated #{Time.now.strftime('%Y-%m-%d %H:%M')})" : '')]
40
53
  out += ['#']
41
54
  return out if routes_map.size.zero?
42
55
 
@@ -51,35 +64,56 @@ module AnnotateRoutes
51
64
  out += ["# #{content(routes_map[0], maxs, options)}"]
52
65
  end
53
66
 
54
- out + routes_map[1..-1].map { |line| "# #{content(options[:format_markdown] ? line.split(' ') : line, maxs, options)}" }
67
+ out += routes_map[1..-1].map { |line| "# #{content(options[:format_markdown] ? line.split(' ') : line, maxs, options)}" }
68
+ out += ["# #{options[:wrapper_close]}"] if options[:wrapper_close]
69
+
70
+ out
55
71
  end
56
72
 
57
73
  def do_annotations(options = {})
58
74
  return unless routes_exists?
59
75
  existing_text = File.read(routes_file)
60
76
 
61
- if write_contents(existing_text, header(options), options)
77
+ if rewrite_contents_with_header(existing_text, header(options), options)
62
78
  puts "#{routes_file} annotated."
63
79
  end
64
80
  end
65
81
 
66
- def remove_annotations(options={})
82
+ def remove_annotations(_options={})
67
83
  return unless routes_exists?
68
84
  existing_text = File.read(routes_file)
69
85
  content, where_header_found = strip_annotations(existing_text)
70
-
71
- content = strip_on_removal(content, where_header_found)
72
-
73
- if write_contents(existing_text, content, options)
86
+ new_content = strip_on_removal(content, where_header_found)
87
+ if rewrite_contents(existing_text, new_content)
74
88
  puts "Removed annotations from #{routes_file}."
75
89
  end
76
90
  end
77
91
  end
78
92
 
79
- private
93
+ def self.magic_comment_matcher
94
+ Regexp.new(/(^#\s*encoding:.*)|(^# coding:.*)|(^# -\*- coding:.*)|(^# -\*- encoding\s?:.*)|(^#\s*frozen_string_literal:.+)|(^# -\*- frozen_string_literal\s*:.+-\*-)/)
95
+ end
96
+
97
+ # @param [Array<String>] content
98
+ # @return [Array<String>] all found magic comments
99
+ # @return [Array<String>] content without magic comments
100
+ def self.extract_magic_comments_from_array(content_array)
101
+ magic_comments = []
102
+ new_content = []
103
+
104
+ content_array.map do |row|
105
+ if row =~ magic_comment_matcher
106
+ magic_comments << row.strip
107
+ else
108
+ new_content << row
109
+ end
110
+ end
111
+
112
+ [magic_comments, new_content]
113
+ end
80
114
 
81
115
  def self.app_routes_map(options)
82
- routes_map = `rake routes`.split(/\n/, -1)
116
+ routes_map = `rake routes`.chomp("\n").split(/\n/, -1)
83
117
 
84
118
  # In old versions of Rake, the first line of output was the cwd. Not so
85
119
  # much in newer ones. We ditch that line if it exists, and if not, we
@@ -106,7 +140,22 @@ module AnnotateRoutes
106
140
  routes_exists
107
141
  end
108
142
 
109
- def self.write_contents(existing_text, header, options = {})
143
+ # @param [String, Array<String>]
144
+ def self.rewrite_contents(existing_text, new_content)
145
+ # Make sure we end on a trailing newline.
146
+ new_content << '' unless new_content.last == ''
147
+ new_text = new_content.join("\n")
148
+
149
+ if existing_text == new_text
150
+ puts "#{routes_file} unchanged."
151
+ false
152
+ else
153
+ File.open(routes_file, 'wb') { |f| f.puts(new_text) }
154
+ true
155
+ end
156
+ end
157
+
158
+ def self.rewrite_contents_with_header(existing_text, header, options = {})
110
159
  content, where_header_found = strip_annotations(existing_text)
111
160
  new_content = annotate_routes(header, content, where_header_found, options)
112
161
 
@@ -124,9 +173,11 @@ module AnnotateRoutes
124
173
  end
125
174
 
126
175
  def self.annotate_routes(header, content, where_header_found, options = {})
176
+ magic_comments_map, content = extract_magic_comments_from_array(content)
127
177
  if %w(before top).include?(options[:position_in_routes])
128
178
  header = header << '' if content.first != ''
129
- new_content = header + content
179
+ magic_comments_map << '' if magic_comments_map.any?
180
+ new_content = magic_comments_map + header + content
130
181
  else
131
182
  # Ensure we have adequate trailing newlines at the end of the file to
132
183
  # ensure a blank line separating the content from the annotation.
@@ -136,7 +187,7 @@ module AnnotateRoutes
136
187
  # the spacer we put in the first time around.
137
188
  content.shift if where_header_found == :before && content.first == ''
138
189
 
139
- new_content = content + header
190
+ new_content = magic_comments_map + content + header
140
191
  end
141
192
 
142
193
  new_content
@@ -156,7 +207,7 @@ module AnnotateRoutes
156
207
  content.split(/\n/, -1).each_with_index do |line, line_number|
157
208
  if mode == :header && line !~ /\s*#/
158
209
  mode = :content
159
- next unless line == ''
210
+ real_content << line unless line.blank?
160
211
  elsif mode == :content
161
212
  if line =~ /^\s*#\s*== Route.*$/
162
213
  header_found_at = line_number + 1 # index start's at 0
@@ -1,5 +1,5 @@
1
1
  module Annotate
2
2
  def self.version
3
- '2.7.2'
3
+ '2.7.3'
4
4
  end
5
5
  end
@@ -1,8 +1,10 @@
1
+ require 'annotate'
2
+
1
3
  module Annotate
2
4
  module Generators
3
5
  class InstallGenerator < Rails::Generators::Base
4
6
  desc 'Copy annotate_models rakefiles for automatic annotation'
5
- source_root File.expand_path('../templates', __FILE__)
7
+ source_root File.expand_path('templates', __dir__)
6
8
 
7
9
  # copy rake tasks
8
10
  def copy_tasks
@@ -42,6 +42,7 @@ if Rails.env.development?
42
42
  'format_markdown' => 'false',
43
43
  'sort' => 'false',
44
44
  'force' => 'false',
45
+ 'classified_sort' => 'true',
45
46
  'trace' => 'false',
46
47
  'wrapper_open' => nil,
47
48
  'wrapper_close' => nil,
@@ -47,6 +47,7 @@ task annotate_models: :environment do
47
47
  options[:ignore_routes] = ENV.fetch('ignore_routes', nil)
48
48
  options[:hide_limit_column_types] = Annotate.fallback(ENV['hide_limit_column_types'], '')
49
49
  options[:hide_default_column_types] = Annotate.fallback(ENV['hide_default_column_types'], '')
50
+ options[:with_comment] = Annotate.fallback(ENV['with_comment'], '')
50
51
 
51
52
  AnnotateModels.do_annotations(options)
52
53
  end
@@ -8,6 +8,8 @@ task :annotate_routes => :environment do
8
8
  options[:position_in_routes] = Annotate.fallback(ENV['position_in_routes'], ENV['position'])
9
9
  options[:ignore_routes] = Annotate.fallback(ENV['ignore_routes'], nil)
10
10
  options[:require] = ENV['require'] ? ENV['require'].split(',') : []
11
+ options[:wrapper_open] = Annotate.fallback(ENV['wrapper_open'], ENV['wrapper'])
12
+ options[:wrapper_close] = Annotate.fallback(ENV['wrapper_close'], ENV['wrapper'])
11
13
  AnnotateRoutes.do_annotations(options)
12
14
  end
13
15
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: annotate
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.2
4
+ version: 2.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Chaffee
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2017-06-02 00:00:00.000000000 Z
15
+ date: 2018-04-21 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: rake
@@ -101,7 +101,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
101
101
  requirements:
102
102
  - - ">="
103
103
  - !ruby/object:Gem::Version
104
- version: 1.9.3
104
+ version: 2.2.0
105
105
  required_rubygems_version: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - ">="
@@ -109,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
109
  version: '0'
110
110
  requirements: []
111
111
  rubyforge_project: annotate
112
- rubygems_version: 2.6.8
112
+ rubygems_version: 2.7.6
113
113
  signing_key:
114
114
  specification_version: 4
115
115
  summary: Annotates Rails Models, routes, fixtures, and others based on the database