rails_lens 0.2.12 → 0.5.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +88 -72
  4. data/lib/rails_lens/analyzers/association_analyzer.rb +3 -10
  5. data/lib/rails_lens/analyzers/best_practices_analyzer.rb +11 -36
  6. data/lib/rails_lens/analyzers/callbacks.rb +302 -0
  7. data/lib/rails_lens/analyzers/column_analyzer.rb +6 -6
  8. data/lib/rails_lens/analyzers/composite_keys.rb +2 -5
  9. data/lib/rails_lens/analyzers/database_constraints.rb +4 -6
  10. data/lib/rails_lens/analyzers/delegated_types.rb +4 -7
  11. data/lib/rails_lens/analyzers/enums.rb +5 -11
  12. data/lib/rails_lens/analyzers/foreign_key_analyzer.rb +2 -2
  13. data/lib/rails_lens/analyzers/generated_columns.rb +4 -6
  14. data/lib/rails_lens/analyzers/index_analyzer.rb +4 -10
  15. data/lib/rails_lens/analyzers/inheritance.rb +30 -31
  16. data/lib/rails_lens/analyzers/notes.rb +29 -39
  17. data/lib/rails_lens/analyzers/performance_analyzer.rb +3 -26
  18. data/lib/rails_lens/annotation_pipeline.rb +1 -0
  19. data/lib/rails_lens/cli.rb +1 -0
  20. data/lib/rails_lens/commands.rb +26 -4
  21. data/lib/rails_lens/configuration.rb +10 -2
  22. data/lib/rails_lens/erd/visualizer.rb +0 -1
  23. data/lib/rails_lens/extension_loader.rb +5 -4
  24. data/lib/rails_lens/extensions/closure_tree_ext.rb +11 -11
  25. data/lib/rails_lens/mailer/annotator.rb +3 -3
  26. data/lib/rails_lens/model_detector.rb +49 -3
  27. data/lib/rails_lens/model_source.rb +72 -0
  28. data/lib/rails_lens/model_source_loader.rb +117 -0
  29. data/lib/rails_lens/model_sources/active_record_source.rb +89 -0
  30. data/lib/rails_lens/note_codes.rb +59 -0
  31. data/lib/rails_lens/providers/callbacks_provider.rb +24 -0
  32. data/lib/rails_lens/providers/extensions_provider.rb +1 -1
  33. data/lib/rails_lens/providers/view_provider.rb +6 -20
  34. data/lib/rails_lens/schema/adapters/base.rb +39 -2
  35. data/lib/rails_lens/schema/adapters/database_info.rb +11 -17
  36. data/lib/rails_lens/schema/adapters/mysql.rb +75 -0
  37. data/lib/rails_lens/schema/adapters/postgresql.rb +123 -3
  38. data/lib/rails_lens/schema/annotation_manager.rb +105 -50
  39. data/lib/rails_lens/schema/database_annotator.rb +197 -0
  40. data/lib/rails_lens/tasks/annotate.rake +42 -1
  41. data/lib/rails_lens/version.rb +1 -1
  42. data/lib/rails_lens.rb +1 -1
  43. metadata +8 -1
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../errors'
4
- require_relative 'error_handling'
5
-
6
3
  module RailsLens
7
4
  module Analyzers
8
5
  class Inheritance < Base
@@ -42,23 +39,23 @@ module RailsLens
42
39
 
43
40
  def analyze_sti
44
41
  lines = []
45
- lines << '== Inheritance (STI)'
46
- lines << "Type Column: #{model_class.inheritance_column}"
42
+ lines << '[sti]'
43
+ lines << "type_column = \"#{model_class.inheritance_column}\""
47
44
 
48
45
  # Check if this is a base class or subclass
49
46
  if model_class.base_class == model_class
50
47
  # This is the base class
51
48
  subclasses = find_sti_subclasses
52
- lines << "Known Subclasses: #{subclasses.join(', ')}" if subclasses.any?
53
- lines << 'Base Class: Yes'
49
+ lines << "subclasses = [#{subclasses.map { |s| "\"#{s}\"" }.join(', ')}]" if subclasses.any?
50
+ lines << 'base = true'
54
51
  else
55
52
  # This is a subclass
56
- lines << "Base Class: #{model_class.base_class.name}"
57
- lines << "Type Value: #{model_class.sti_name}"
53
+ lines << "base_class = \"#{model_class.base_class.name}\""
54
+ lines << "type_value = \"#{model_class.sti_name}\""
58
55
 
59
56
  # Find siblings
60
57
  siblings = find_sti_siblings
61
- lines << "Sibling Classes: #{siblings.join(', ')}" if siblings.any?
58
+ lines << "siblings = [#{siblings.map { |s| "\"#{s}\"" }.join(', ')}]" if siblings.any?
62
59
  end
63
60
 
64
61
  lines.join("\n")
@@ -69,14 +66,14 @@ module RailsLens
69
66
  return nil unless reflection
70
67
 
71
68
  lines = []
72
- lines << '== Delegated Type'
73
- lines << "Delegate: #{reflection.name}"
74
- lines << "Type Column: #{reflection.foreign_type}"
75
- lines << "ID Column: #{reflection.foreign_key}"
69
+ lines << '[delegated_type]'
70
+ lines << "delegate = \"#{reflection.name}\""
71
+ lines << "type_column = \"#{reflection.foreign_type}\""
72
+ lines << "id_column = \"#{reflection.foreign_key}\""
76
73
 
77
74
  # Try to find known types
78
75
  types = find_delegated_types(reflection)
79
- lines << "Known Types: #{types.join(', ')}" if types.any?
76
+ lines << "types = [#{types.map { |t| "\"#{t}\"" }.join(', ')}]" if types.any?
80
77
 
81
78
  lines.join("\n")
82
79
  end
@@ -151,38 +148,40 @@ module RailsLens
151
148
 
152
149
  def analyze_polymorphic
153
150
  lines = []
154
- lines << '== Polymorphic Associations'
151
+ lines << '[polymorphic]'
155
152
 
156
- # Find polymorphic belongs_to associations
153
+ # Find polymorphic belongs_to associations (references)
157
154
  polymorphic_belongs_to = model_class.reflect_on_all_associations(:belongs_to).select do |r|
158
155
  r.options[:polymorphic]
159
156
  end
160
157
 
161
158
  if polymorphic_belongs_to.any?
162
- lines << 'Polymorphic References:'
163
- polymorphic_belongs_to.each do |reflection|
164
- lines << "- #{reflection.name} (#{reflection.foreign_type}/#{reflection.foreign_key})"
165
-
166
- # Try to find what types are actually used
167
- next unless model_class.table_exists? && model_class.columns_hash[reflection.foreign_type.to_s]
168
-
169
- types = find_polymorphic_types(reflection)
170
- lines << " Types: #{types.join(', ')}" if types.any?
159
+ refs = polymorphic_belongs_to.map do |reflection|
160
+ types = if model_class.table_exists? && model_class.columns_hash[reflection.foreign_type.to_s]
161
+ find_polymorphic_types(reflection)
162
+ else
163
+ []
164
+ end
165
+ if types.any?
166
+ "{ name = \"#{reflection.name}\", type_col = \"#{reflection.foreign_type}\", id_col = \"#{reflection.foreign_key}\", types = [#{types.map { |t| "\"#{t}\"" }.join(', ')}] }"
167
+ else
168
+ "{ name = \"#{reflection.name}\", type_col = \"#{reflection.foreign_type}\", id_col = \"#{reflection.foreign_key}\" }"
169
+ end
171
170
  end
171
+ lines << "references = [#{refs.join(', ')}]"
172
172
  end
173
173
 
174
- # Find associations that reference this model polymorphically
174
+ # Find associations that reference this model polymorphically (targets)
175
175
  polymorphic_has_many = model_class.reflect_on_all_associations.select do |r|
176
176
  r.options[:as]
177
177
  end
178
178
 
179
179
  if polymorphic_has_many.any?
180
- lines << '' if polymorphic_belongs_to.any?
181
- lines << 'Polymorphic Targets:'
182
- polymorphic_has_many.each do |reflection|
180
+ targets = polymorphic_has_many.map do |reflection|
183
181
  as_name = reflection.options[:as]
184
- lines << "- #{reflection.name} (as: :#{as_name})"
182
+ "{ name = \"#{reflection.name}\", as = \"#{as_name}\" }"
185
183
  end
184
+ lines << "targets = [#{targets.join(', ')}]"
186
185
  end
187
186
 
188
187
  return nil if lines.size == 1 # Only header
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../errors'
4
- require_relative 'error_handling'
5
-
6
3
  module RailsLens
7
4
  module Analyzers
8
5
  class Notes < Base
@@ -61,13 +58,9 @@ module RailsLens
61
58
  notes = []
62
59
 
63
60
  # Check if this model is backed by a database view
64
- if ModelDetector.view_exists?(model_class)
65
- notes << '👁️ View-backed model: read-only'
66
-
67
- # Check if model has readonly implementation
68
- unless has_readonly_implementation?
69
- notes << 'Add readonly? method'
70
- end
61
+ # Only note if readonly protection is missing (view status is already obvious)
62
+ if ModelDetector.view_exists?(model_class) && !has_readonly_implementation?
63
+ notes << NoteCodes::ADD_READONLY
71
64
  end
72
65
 
73
66
  notes
@@ -81,22 +74,14 @@ module RailsLens
81
74
  view_metadata = ViewMetadata.new(model_class)
82
75
 
83
76
  # Check for materialized view specific issues
84
- if view_metadata.materialized_view?
85
- notes << '🔄 Materialized view: data may be stale until refreshed'
86
- unless has_refresh_methods?
87
- notes << 'Add refresh! method for manual updates'
88
- end
77
+ if view_metadata.materialized_view? && !has_refresh_methods?
78
+ notes << NoteCodes::ADD_REFRESH
89
79
  end
90
80
 
91
81
  # Check for nested views (view depending on other views)
92
82
  dependencies = view_metadata.dependencies
93
83
  if dependencies.any? { |dep| view_exists_by_name?(dep) }
94
- notes << '⚠️ Nested views detected: may impact query performance'
95
- end
96
-
97
- # Check for readonly implementation
98
- unless has_readonly_implementation?
99
- notes << '🔒 Add readonly protection to prevent write operations'
84
+ notes << NoteCodes::NESTED_VIEW
100
85
  end
101
86
 
102
87
  notes
@@ -110,19 +95,19 @@ module RailsLens
110
95
 
111
96
  # Check for missing indexes on foreign keys
112
97
  foreign_key_columns.each do |column|
113
- notes << "Missing index on foreign key '#{column}'" unless has_index?(column)
98
+ notes << NoteCodes.note(column, NoteCodes::INDEX) unless has_index?(column)
114
99
  end
115
100
 
116
101
  # Check for missing composite indexes
117
102
  common_query_patterns.each do |columns|
118
103
  unless has_composite_index?(columns)
119
- notes << "Consider composite index on (#{columns.join(', ')}) for common queries"
104
+ notes << NoteCodes.note(columns.join('+'), NoteCodes::COMP_INDEX)
120
105
  end
121
106
  end
122
107
 
123
108
  # Check for redundant indexes
124
109
  redundant_indexes.each do |index|
125
- notes << "Index '#{index.name}' might be redundant"
110
+ notes << NoteCodes.note(index.name, NoteCodes::REDUND_IDX)
126
111
  end
127
112
 
128
113
  notes
@@ -137,7 +122,7 @@ module RailsLens
137
122
  next unless column_exists?(column)
138
123
 
139
124
  unless has_foreign_key_constraint?(column)
140
- notes << "Missing foreign key constraint for '#{column}' (#{association.name})"
125
+ notes << NoteCodes.note(column, NoteCodes::FK_CONSTRAINT)
141
126
  end
142
127
  end
143
128
 
@@ -147,19 +132,19 @@ module RailsLens
147
132
  def analyze_associations
148
133
  # Check for missing inverse_of
149
134
  notes = associations_needing_inverse.map do |association|
150
- "Association '#{association.name}' should specify inverse_of"
135
+ NoteCodes.note(association.name.to_s, NoteCodes::INVERSE_OF)
151
136
  end
152
137
 
153
138
  # Check for N+1 query risks
154
139
  has_many_associations.each do |association|
155
140
  if likely_n_plus_one?(association)
156
- notes << "Association '#{association.name}' has N+1 query risk - consider includes/preload"
141
+ notes << NoteCodes.note(association.name.to_s, NoteCodes::N_PLUS_ONE)
157
142
  end
158
143
  end
159
144
 
160
145
  # Check for missing counter caches
161
146
  associations_needing_counter_cache.each do |association|
162
- notes << "Consider adding counter cache for '#{association.name}'"
147
+ notes << NoteCodes.note(association.name.to_s, NoteCodes::COUNTER_CACHE)
163
148
  end
164
149
 
165
150
  notes
@@ -168,22 +153,22 @@ module RailsLens
168
153
  def analyze_columns
169
154
  # Check for missing NOT NULL constraints
170
155
  notes = columns_needing_not_null.map do |column|
171
- "Column '#{column.name}' should probably have NOT NULL constraint"
156
+ NoteCodes.note(column.name, NoteCodes::NOT_NULL)
172
157
  end
173
158
 
174
159
  # Check for missing defaults
175
160
  columns_needing_defaults.each do |column|
176
- notes << "Column '#{column.name}' should have a default value"
161
+ notes << NoteCodes.note(column.name, NoteCodes::DEFAULT)
177
162
  end
178
163
 
179
164
  # Check for inappropriate column types
180
165
  columns.each do |column|
181
166
  if column.name.end_with?('_count') && column.type != :integer
182
- notes << "Counter column '#{column.name}' should be integer type"
167
+ notes << NoteCodes.note(column.name, NoteCodes::USE_INTEGER)
183
168
  end
184
169
 
185
170
  if column.name.match?(/price|amount|cost/) && column.type == :float
186
- notes << "Monetary column '#{column.name}' should use decimal type, not float"
171
+ notes << NoteCodes.note(column.name, NoteCodes::USE_DECIMAL)
187
172
  end
188
173
  end
189
174
 
@@ -193,7 +178,7 @@ module RailsLens
193
178
  def analyze_performance
194
179
  # Large text columns without separate storage
195
180
  notes = large_text_columns.map do |column|
196
- "Large text column '#{column.name}' might benefit from separate storage"
181
+ NoteCodes.note(column.name, NoteCodes::STORAGE)
197
182
  end
198
183
 
199
184
  # Polymorphic associations without indexes
@@ -204,14 +189,14 @@ module RailsLens
204
189
  foreign_key = association.foreign_key.to_s
205
190
  type_column = "#{association.foreign_type || association.name}_type"
206
191
  unless has_composite_index?([foreign_key, type_column])
207
- notes << "Polymorphic association '#{association.name}' needs composite index on (#{foreign_key}, #{type_column})"
192
+ notes << NoteCodes.note(association.name.to_s, NoteCodes::POLY_INDEX)
208
193
  end
209
194
  end
210
195
 
211
196
  # UUID columns without proper indexes
212
197
  uuid_columns.each do |column|
213
198
  if column.name.end_with?('_id') && !has_index?(column.name)
214
- notes << "UUID column '#{column.name}' needs an index"
199
+ notes << NoteCodes.note(column.name, NoteCodes::INDEX)
215
200
  end
216
201
  end
217
202
 
@@ -222,18 +207,23 @@ module RailsLens
222
207
  notes = []
223
208
 
224
209
  # Check for updated_at/created_at
225
- notes << "Missing 'created_at' timestamp column" unless column_exists?('created_at')
210
+ has_created = column_exists?('created_at')
211
+ has_updated = column_exists?('updated_at')
226
212
 
227
- notes << "Missing 'updated_at' timestamp column" unless column_exists?('updated_at')
213
+ if !has_created && !has_updated
214
+ notes << NoteCodes::NO_TIMESTAMPS
215
+ elsif !has_created || !has_updated
216
+ notes << NoteCodes::PARTIAL_TS
217
+ end
228
218
 
229
219
  # Check for soft deletes without index
230
220
  if column_exists?('deleted_at') && !has_index?('deleted_at')
231
- notes << "Soft delete column 'deleted_at' needs an index"
221
+ notes << NoteCodes.note('deleted_at', NoteCodes::INDEX)
232
222
  end
233
223
 
234
224
  # Check for STI without index
235
225
  if model_class.inheritance_column && column_exists?(model_class.inheritance_column) && !has_index?(model_class.inheritance_column)
236
- notes << "STI column '#{model_class.inheritance_column}' needs an index"
226
+ notes << NoteCodes.note(model_class.inheritance_column, NoteCodes::STI_INDEX)
237
227
  end
238
228
 
239
229
  notes
@@ -5,7 +5,6 @@ module RailsLens
5
5
  class PerformanceAnalyzer < Base
6
6
  def analyze
7
7
  notes = []
8
- notes.concat(analyze_large_text_columns)
9
8
  notes.concat(analyze_uuid_indexes)
10
9
  notes.concat(analyze_query_performance)
11
10
  notes
@@ -13,18 +12,6 @@ module RailsLens
13
12
 
14
13
  private
15
14
 
16
- def analyze_large_text_columns
17
- notes = []
18
-
19
- text_columns.each do |column|
20
- if frequently_queried_column?(column)
21
- notes << "Large text column '#{column.name}' is frequently queried - consider separate storage"
22
- end
23
- end
24
-
25
- notes
26
- end
27
-
28
15
  def analyze_uuid_indexes
29
16
  notes = []
30
17
 
@@ -32,7 +19,7 @@ module RailsLens
32
19
  next if column.name == 'id' # Primary keys are already indexed
33
20
 
34
21
  if should_be_indexed?(column) && !indexed?(column)
35
- notes << "UUID column '#{column.name}' should be indexed for better query performance"
22
+ notes << NoteCodes.note(column.name, NoteCodes::INDEX)
36
23
  end
37
24
  end
38
25
 
@@ -46,34 +33,24 @@ module RailsLens
46
33
  commonly_queried_columns.each do |column|
47
34
  next if indexed?(column)
48
35
 
49
- notes << "Column '#{column.name}' is commonly used in queries - consider adding an index"
36
+ notes << NoteCodes.note(column.name, NoteCodes::INDEX)
50
37
  end
51
38
 
52
39
  # Check for missing indexes on scoped columns
53
40
  scoped_columns.each do |column|
54
41
  next if indexed?(column)
55
42
 
56
- notes << "Scope column '#{column.name}' should be indexed"
43
+ notes << NoteCodes.note(column.name, NoteCodes::INDEX)
57
44
  end
58
45
 
59
46
  notes
60
47
  end
61
48
 
62
- def text_columns
63
- model_class.columns.select { |c| c.type == :text }
64
- end
65
-
66
49
  def uuid_columns
67
50
  model_class.columns.select { |c| c.type == :uuid || (c.type == :string && c.name.match?(/uuid|guid/i)) }
68
51
  end
69
52
 
70
- def frequently_queried_column?(column)
71
- # Heuristic: columns with certain names are likely to be queried frequently
72
- column.name.match?(/title|name|slug|description|summary|content|body/i)
73
- end
74
-
75
53
  def should_be_indexed?(column)
76
- # UUID columns used as foreign keys or identifiers should be indexed
77
54
  column.name.end_with?('_id', '_uuid', '_guid') ||
78
55
  column.name.match?(/identifier|reference|token/i)
79
56
  end
@@ -75,6 +75,7 @@ module RailsLens
75
75
  register(Providers::CompositeKeysProvider.new)
76
76
  register(Providers::DatabaseConstraintsProvider.new)
77
77
  register(Providers::GeneratedColumnsProvider.new)
78
+ register(Providers::CallbacksProvider.new)
78
79
 
79
80
  # Notes providers (analysis and recommendations)
80
81
  return unless RailsLens.config.schema[:include_notes]
@@ -21,6 +21,7 @@ module RailsLens
21
21
  desc 'annotate', 'Annotate Rails models with schema information'
22
22
  option :models, type: :array, desc: 'Specific models to annotate'
23
23
  option :include_abstract, type: :boolean, desc: 'Include abstract classes'
24
+ option :include_database_objects, type: :boolean, default: true, desc: 'Include database objects (functions, etc.) in abstract classes'
24
25
  option :position, type: :string, enum: %w[before after top bottom], desc: 'Annotation position'
25
26
  option :routes, type: :boolean, desc: 'Annotate controller routes'
26
27
  option :mailers, type: :boolean, desc: 'Annotate mailer methods'
@@ -24,6 +24,28 @@ module RailsLens
24
24
  end
25
25
  end
26
26
 
27
+ # Also annotate database-level objects (functions, etc.)
28
+ if options[:include_database_objects]
29
+ db_results = annotate_database_objects(options)
30
+ results.merge!(database_objects: db_results)
31
+ end
32
+
33
+ results
34
+ end
35
+
36
+ def annotate_database_objects(options = {})
37
+ results = Schema::DatabaseAnnotator.annotate_all(options)
38
+
39
+ output.say "Annotated #{results[:annotated].length} abstract base classes with database objects", :green
40
+ output.say "Skipped #{results[:skipped].length} abstract classes", :yellow if results[:skipped].any?
41
+
42
+ if results[:failed].any?
43
+ output.say "Failed to annotate #{results[:failed].length} abstract classes:", :red
44
+ results[:failed].each do |failure|
45
+ output.say " - #{failure[:model]}: #{failure[:error]}", :red
46
+ end
47
+ end
48
+
27
49
  results
28
50
  end
29
51
 
@@ -183,20 +205,20 @@ module RailsLens
183
205
  end
184
206
 
185
207
  # Create lib/tasks directory if it doesn't exist
186
- FileUtils.mkdir_p(tasks_dir) unless Dir.exist?(tasks_dir)
208
+ FileUtils.mkdir_p(tasks_dir)
187
209
 
188
210
  # Write the rake task
189
211
  File.write(rake_file, rake_task_template)
190
212
 
191
213
  output.say "Created rake task at #{rake_file}", :green
192
- output.say '', :reset
214
+ output.say ''
193
215
  output.say 'The following task has been installed:', :blue
194
216
  output.say ' • rails_lens:annotate - Annotate models after migrations', :green
195
- output.say '', :reset
217
+ output.say ''
196
218
  output.say 'Configuration options in lib/tasks/rails_lens.rake:', :blue
197
219
  output.say ' • AUTO_ANNOTATE (default: true in development)', :cyan
198
220
  output.say ' • RAILS_LENS_ENV (default: development)', :cyan
199
- output.say '', :reset
221
+ output.say ''
200
222
  output.say 'Disable auto-annotation:', :blue
201
223
  output.say ' export AUTO_ANNOTATE=false', :cyan
202
224
 
@@ -3,7 +3,7 @@
3
3
  module RailsLens
4
4
  class Config
5
5
  attr_accessor :verbose, :debug, :raise_on_error, :logger,
6
- :annotations, :erd, :schema, :extensions, :routes, :mailers
6
+ :annotations, :erd, :schema, :extensions, :routes, :mailers, :model_sources
7
7
 
8
8
  def initialize
9
9
  @verbose = false
@@ -40,7 +40,10 @@ module RailsLens
40
40
  show_comments: true,
41
41
  show_foreign_keys: true,
42
42
  show_indexes: true,
43
- show_check_constraints: true
43
+ show_check_constraints: true,
44
+ show_triggers: true,
45
+ show_functions: true,
46
+ show_callbacks: true
44
47
  }
45
48
  }
46
49
 
@@ -73,6 +76,11 @@ module RailsLens
73
76
  pattern: '**/*_mailer.rb',
74
77
  exclusion_pattern: 'vendor/**/*_mailer.rb'
75
78
  }
79
+
80
+ @model_sources = {
81
+ enabled: true, # Enable gem-provided model sources
82
+ error_reporting: :warn # :silent, :warn, :verbose
83
+ }
76
84
  end
77
85
  end
78
86
 
@@ -3,7 +3,6 @@
3
3
  module RailsLens
4
4
  module ERD
5
5
  class Visualizer
6
- require_relative 'domain_color_mapper'
7
6
  attr_reader :options, :config
8
7
 
9
8
  def initialize(options: {})
@@ -138,13 +138,14 @@ module RailsLens
138
138
  # Check each loaded gem for RailsLens extensions
139
139
 
140
140
  Gem.loaded_specs.each_key do |gem_name|
141
- # Try to find extension in the gem
142
- gem_constant_name = gem_name.gsub('-', '_').split('_').map(&:capitalize).join
143
- extension_constant_name = "#{gem_constant_name}::RailsLensExtension"
144
-
145
141
  # Skip gems that are likely to cause autoload issues
146
142
  next if %w[digest openssl uri net json].include?(gem_name)
147
143
 
144
+ # Try to find extension in the gem
145
+ # Use ActiveSupport's camelize for proper Rails-style conversion (e.g., 'activecypher' -> 'ActiveCypher')
146
+ gem_constant_name = gem_name.gsub('-', '_').camelize
147
+ extension_constant_name = "#{gem_constant_name}::RailsLensExtension"
148
+
148
149
  # First check if the gem constant exists without triggering autoload
149
150
  next unless Object.const_defined?(gem_constant_name, false)
150
151
 
@@ -20,13 +20,13 @@ module RailsLens
20
20
  return nil unless model_uses_closure_tree?
21
21
 
22
22
  lines = []
23
- lines << '== Hierarchy (ClosureTree)'
24
- lines << "Parent Column: #{parent_column_name}"
25
- lines << "Hierarchy Table: #{hierarchy_table_name}"
23
+ lines << '[closure_tree]'
24
+ lines << "parent_column = \"#{parent_column_name}\""
25
+ lines << "hierarchy_table = \"#{hierarchy_table_name}\""
26
26
 
27
- lines << "Order Column: #{order_column}" if order_column
27
+ lines << "order_column = \"#{order_column}\"" if order_column
28
28
 
29
- lines << "Depth Column: #{depth_column}" if depth_column && has_column?(depth_column)
29
+ lines << "depth_column = \"#{depth_column}\"" if depth_column && has_column?(depth_column)
30
30
 
31
31
  lines.join("\n")
32
32
  end
@@ -37,29 +37,29 @@ module RailsLens
37
37
  notes = []
38
38
 
39
39
  # Check parent column index
40
- notes << "Missing index on parent column '#{parent_column_name}'" unless has_index?(parent_column_name)
40
+ notes << NoteCodes.note(parent_column_name, NoteCodes::INDEX) unless has_index?(parent_column_name)
41
41
 
42
42
  # Check hierarchy table existence and indexes
43
43
  if hierarchy_table_exists?
44
44
  unless has_hierarchy_indexes?
45
- notes << "Hierarchy table '#{hierarchy_table_name}' needs compound index on (ancestor_id, descendant_id)"
45
+ notes << NoteCodes.note(hierarchy_table_name, NoteCodes::COMP_INDEX)
46
46
  end
47
47
 
48
48
  unless has_hierarchy_depth_index?
49
- notes << 'Consider adding index on generations column in hierarchy table for depth queries'
49
+ notes << NoteCodes.note('generations', NoteCodes::INDEX)
50
50
  end
51
51
  else
52
- notes << "Hierarchy table '#{hierarchy_table_name}' does not exist - run migrations"
52
+ notes << NoteCodes.note(hierarchy_table_name, NoteCodes::MISSING)
53
53
  end
54
54
 
55
55
  # Check for counter cache
56
56
  if should_have_counter_cache? && !has_counter_cache?
57
- notes << "Consider adding counter cache '#{suggested_counter_cache_name}' for children count"
57
+ notes << NoteCodes.note('children', NoteCodes::COUNTER_CACHE)
58
58
  end
59
59
 
60
60
  # Check depth column
61
61
  if model_class.respond_to?(:cache_depth?) && model_class.cache_depth? && !has_column?(depth_column)
62
- notes << "Depth caching enabled but column '#{depth_column}' is missing"
62
+ notes << NoteCodes.note(depth_column, NoteCodes::DEPTH_CACHE)
63
63
  end
64
64
 
65
65
  notes
@@ -7,14 +7,14 @@ module RailsLens
7
7
  def initialize(dry_run: false)
8
8
  @dry_run = dry_run
9
9
  @mailers = RailsLens::Mailer::Extractor.call
10
- @changed_files = []
10
+ @changed_files = Set.new
11
11
  end
12
12
 
13
13
  # Annotate all mailer files with mailer information
14
14
  #
15
15
  # @param pattern [String] Glob pattern for mailer files
16
16
  # @param exclusion [String] Glob pattern for files to exclude
17
- # @return [Array<String>] List of changed files
17
+ # @return [Set<String>] Set of changed files
18
18
  def annotate_all(pattern: '**/*_mailer.rb', exclusion: 'vendor/**/*_mailer.rb')
19
19
  # Simply annotate all mailer files we found via their source locations
20
20
  source_paths_map.each do |source_path, methods|
@@ -31,7 +31,7 @@ module RailsLens
31
31
  #
32
32
  # @param pattern [String] Glob pattern for mailer files
33
33
  # @param exclusion [String] Glob pattern for files to exclude
34
- # @return [Array<String>] List of changed files
34
+ # @return [Set<String>] Set of changed files
35
35
  def remove_all(pattern: '**/*_mailer.rb', exclusion: 'vendor/**/*_mailer.rb')
36
36
  # Remove annotations from all mailer files we found via their source locations
37
37
  source_paths_map.each_key do |source_path|