codebase_index 0.3.2 → 0.4.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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/lib/codebase_index.rb +3 -243
  3. metadata +28 -223
  4. data/CHANGELOG.md +0 -89
  5. data/CODE_OF_CONDUCT.md +0 -83
  6. data/CONTRIBUTING.md +0 -65
  7. data/LICENSE.txt +0 -21
  8. data/README.md +0 -325
  9. data/exe/codebase-console +0 -59
  10. data/exe/codebase-console-mcp +0 -22
  11. data/exe/codebase-index-mcp +0 -34
  12. data/exe/codebase-index-mcp-http +0 -37
  13. data/exe/codebase-index-mcp-start +0 -58
  14. data/lib/codebase_index/ast/call_site_extractor.rb +0 -106
  15. data/lib/codebase_index/ast/method_extractor.rb +0 -71
  16. data/lib/codebase_index/ast/node.rb +0 -116
  17. data/lib/codebase_index/ast/parser.rb +0 -614
  18. data/lib/codebase_index/ast.rb +0 -6
  19. data/lib/codebase_index/builder.rb +0 -200
  20. data/lib/codebase_index/cache/cache_middleware.rb +0 -199
  21. data/lib/codebase_index/cache/cache_store.rb +0 -264
  22. data/lib/codebase_index/cache/redis_cache_store.rb +0 -116
  23. data/lib/codebase_index/cache/solid_cache_store.rb +0 -111
  24. data/lib/codebase_index/chunking/chunk.rb +0 -84
  25. data/lib/codebase_index/chunking/semantic_chunker.rb +0 -295
  26. data/lib/codebase_index/console/adapters/cache_adapter.rb +0 -58
  27. data/lib/codebase_index/console/adapters/good_job_adapter.rb +0 -33
  28. data/lib/codebase_index/console/adapters/job_adapter.rb +0 -68
  29. data/lib/codebase_index/console/adapters/sidekiq_adapter.rb +0 -33
  30. data/lib/codebase_index/console/adapters/solid_queue_adapter.rb +0 -33
  31. data/lib/codebase_index/console/audit_logger.rb +0 -75
  32. data/lib/codebase_index/console/bridge.rb +0 -177
  33. data/lib/codebase_index/console/confirmation.rb +0 -90
  34. data/lib/codebase_index/console/connection_manager.rb +0 -173
  35. data/lib/codebase_index/console/console_response_renderer.rb +0 -74
  36. data/lib/codebase_index/console/embedded_executor.rb +0 -373
  37. data/lib/codebase_index/console/model_validator.rb +0 -81
  38. data/lib/codebase_index/console/rack_middleware.rb +0 -87
  39. data/lib/codebase_index/console/safe_context.rb +0 -82
  40. data/lib/codebase_index/console/server.rb +0 -612
  41. data/lib/codebase_index/console/sql_validator.rb +0 -172
  42. data/lib/codebase_index/console/tools/tier1.rb +0 -118
  43. data/lib/codebase_index/console/tools/tier2.rb +0 -117
  44. data/lib/codebase_index/console/tools/tier3.rb +0 -110
  45. data/lib/codebase_index/console/tools/tier4.rb +0 -79
  46. data/lib/codebase_index/coordination/pipeline_lock.rb +0 -109
  47. data/lib/codebase_index/cost_model/embedding_cost.rb +0 -88
  48. data/lib/codebase_index/cost_model/estimator.rb +0 -128
  49. data/lib/codebase_index/cost_model/provider_pricing.rb +0 -67
  50. data/lib/codebase_index/cost_model/storage_cost.rb +0 -52
  51. data/lib/codebase_index/cost_model.rb +0 -22
  52. data/lib/codebase_index/db/migrations/001_create_units.rb +0 -38
  53. data/lib/codebase_index/db/migrations/002_create_edges.rb +0 -35
  54. data/lib/codebase_index/db/migrations/003_create_embeddings.rb +0 -37
  55. data/lib/codebase_index/db/migrations/004_create_snapshots.rb +0 -45
  56. data/lib/codebase_index/db/migrations/005_create_snapshot_units.rb +0 -40
  57. data/lib/codebase_index/db/migrator.rb +0 -71
  58. data/lib/codebase_index/db/schema_version.rb +0 -73
  59. data/lib/codebase_index/dependency_graph.rb +0 -236
  60. data/lib/codebase_index/embedding/indexer.rb +0 -140
  61. data/lib/codebase_index/embedding/openai.rb +0 -126
  62. data/lib/codebase_index/embedding/provider.rb +0 -162
  63. data/lib/codebase_index/embedding/text_preparer.rb +0 -112
  64. data/lib/codebase_index/evaluation/baseline_runner.rb +0 -115
  65. data/lib/codebase_index/evaluation/evaluator.rb +0 -139
  66. data/lib/codebase_index/evaluation/metrics.rb +0 -79
  67. data/lib/codebase_index/evaluation/query_set.rb +0 -148
  68. data/lib/codebase_index/evaluation/report_generator.rb +0 -90
  69. data/lib/codebase_index/extracted_unit.rb +0 -145
  70. data/lib/codebase_index/extractor.rb +0 -1028
  71. data/lib/codebase_index/extractors/action_cable_extractor.rb +0 -201
  72. data/lib/codebase_index/extractors/ast_source_extraction.rb +0 -46
  73. data/lib/codebase_index/extractors/behavioral_profile.rb +0 -309
  74. data/lib/codebase_index/extractors/caching_extractor.rb +0 -261
  75. data/lib/codebase_index/extractors/callback_analyzer.rb +0 -246
  76. data/lib/codebase_index/extractors/concern_extractor.rb +0 -292
  77. data/lib/codebase_index/extractors/configuration_extractor.rb +0 -219
  78. data/lib/codebase_index/extractors/controller_extractor.rb +0 -404
  79. data/lib/codebase_index/extractors/database_view_extractor.rb +0 -278
  80. data/lib/codebase_index/extractors/decorator_extractor.rb +0 -253
  81. data/lib/codebase_index/extractors/engine_extractor.rb +0 -223
  82. data/lib/codebase_index/extractors/event_extractor.rb +0 -211
  83. data/lib/codebase_index/extractors/factory_extractor.rb +0 -289
  84. data/lib/codebase_index/extractors/graphql_extractor.rb +0 -892
  85. data/lib/codebase_index/extractors/i18n_extractor.rb +0 -117
  86. data/lib/codebase_index/extractors/job_extractor.rb +0 -374
  87. data/lib/codebase_index/extractors/lib_extractor.rb +0 -218
  88. data/lib/codebase_index/extractors/mailer_extractor.rb +0 -269
  89. data/lib/codebase_index/extractors/manager_extractor.rb +0 -188
  90. data/lib/codebase_index/extractors/middleware_extractor.rb +0 -133
  91. data/lib/codebase_index/extractors/migration_extractor.rb +0 -469
  92. data/lib/codebase_index/extractors/model_extractor.rb +0 -988
  93. data/lib/codebase_index/extractors/phlex_extractor.rb +0 -252
  94. data/lib/codebase_index/extractors/policy_extractor.rb +0 -191
  95. data/lib/codebase_index/extractors/poro_extractor.rb +0 -229
  96. data/lib/codebase_index/extractors/pundit_extractor.rb +0 -223
  97. data/lib/codebase_index/extractors/rails_source_extractor.rb +0 -473
  98. data/lib/codebase_index/extractors/rake_task_extractor.rb +0 -343
  99. data/lib/codebase_index/extractors/route_extractor.rb +0 -181
  100. data/lib/codebase_index/extractors/scheduled_job_extractor.rb +0 -331
  101. data/lib/codebase_index/extractors/serializer_extractor.rb +0 -339
  102. data/lib/codebase_index/extractors/service_extractor.rb +0 -217
  103. data/lib/codebase_index/extractors/shared_dependency_scanner.rb +0 -91
  104. data/lib/codebase_index/extractors/shared_utility_methods.rb +0 -281
  105. data/lib/codebase_index/extractors/state_machine_extractor.rb +0 -398
  106. data/lib/codebase_index/extractors/test_mapping_extractor.rb +0 -225
  107. data/lib/codebase_index/extractors/validator_extractor.rb +0 -211
  108. data/lib/codebase_index/extractors/view_component_extractor.rb +0 -311
  109. data/lib/codebase_index/extractors/view_template_extractor.rb +0 -261
  110. data/lib/codebase_index/feedback/gap_detector.rb +0 -89
  111. data/lib/codebase_index/feedback/store.rb +0 -119
  112. data/lib/codebase_index/filename_utils.rb +0 -32
  113. data/lib/codebase_index/flow_analysis/operation_extractor.rb +0 -206
  114. data/lib/codebase_index/flow_analysis/response_code_mapper.rb +0 -154
  115. data/lib/codebase_index/flow_assembler.rb +0 -290
  116. data/lib/codebase_index/flow_document.rb +0 -191
  117. data/lib/codebase_index/flow_precomputer.rb +0 -102
  118. data/lib/codebase_index/formatting/base.rb +0 -30
  119. data/lib/codebase_index/formatting/claude_adapter.rb +0 -98
  120. data/lib/codebase_index/formatting/generic_adapter.rb +0 -56
  121. data/lib/codebase_index/formatting/gpt_adapter.rb +0 -64
  122. data/lib/codebase_index/formatting/human_adapter.rb +0 -78
  123. data/lib/codebase_index/graph_analyzer.rb +0 -374
  124. data/lib/codebase_index/mcp/bootstrapper.rb +0 -96
  125. data/lib/codebase_index/mcp/index_reader.rb +0 -394
  126. data/lib/codebase_index/mcp/renderers/claude_renderer.rb +0 -81
  127. data/lib/codebase_index/mcp/renderers/json_renderer.rb +0 -17
  128. data/lib/codebase_index/mcp/renderers/markdown_renderer.rb +0 -353
  129. data/lib/codebase_index/mcp/renderers/plain_renderer.rb +0 -240
  130. data/lib/codebase_index/mcp/server.rb +0 -961
  131. data/lib/codebase_index/mcp/tool_response_renderer.rb +0 -85
  132. data/lib/codebase_index/model_name_cache.rb +0 -51
  133. data/lib/codebase_index/notion/client.rb +0 -217
  134. data/lib/codebase_index/notion/exporter.rb +0 -219
  135. data/lib/codebase_index/notion/mapper.rb +0 -40
  136. data/lib/codebase_index/notion/mappers/column_mapper.rb +0 -57
  137. data/lib/codebase_index/notion/mappers/migration_mapper.rb +0 -39
  138. data/lib/codebase_index/notion/mappers/model_mapper.rb +0 -161
  139. data/lib/codebase_index/notion/mappers/shared.rb +0 -22
  140. data/lib/codebase_index/notion/rate_limiter.rb +0 -68
  141. data/lib/codebase_index/observability/health_check.rb +0 -79
  142. data/lib/codebase_index/observability/instrumentation.rb +0 -34
  143. data/lib/codebase_index/observability/structured_logger.rb +0 -57
  144. data/lib/codebase_index/operator/error_escalator.rb +0 -81
  145. data/lib/codebase_index/operator/pipeline_guard.rb +0 -92
  146. data/lib/codebase_index/operator/status_reporter.rb +0 -80
  147. data/lib/codebase_index/railtie.rb +0 -38
  148. data/lib/codebase_index/resilience/circuit_breaker.rb +0 -99
  149. data/lib/codebase_index/resilience/index_validator.rb +0 -167
  150. data/lib/codebase_index/resilience/retryable_provider.rb +0 -108
  151. data/lib/codebase_index/retrieval/context_assembler.rb +0 -261
  152. data/lib/codebase_index/retrieval/query_classifier.rb +0 -133
  153. data/lib/codebase_index/retrieval/ranker.rb +0 -277
  154. data/lib/codebase_index/retrieval/search_executor.rb +0 -316
  155. data/lib/codebase_index/retriever.rb +0 -152
  156. data/lib/codebase_index/ruby_analyzer/class_analyzer.rb +0 -170
  157. data/lib/codebase_index/ruby_analyzer/dataflow_analyzer.rb +0 -77
  158. data/lib/codebase_index/ruby_analyzer/fqn_builder.rb +0 -18
  159. data/lib/codebase_index/ruby_analyzer/mermaid_renderer.rb +0 -280
  160. data/lib/codebase_index/ruby_analyzer/method_analyzer.rb +0 -143
  161. data/lib/codebase_index/ruby_analyzer/trace_enricher.rb +0 -143
  162. data/lib/codebase_index/ruby_analyzer.rb +0 -87
  163. data/lib/codebase_index/session_tracer/file_store.rb +0 -104
  164. data/lib/codebase_index/session_tracer/middleware.rb +0 -143
  165. data/lib/codebase_index/session_tracer/redis_store.rb +0 -106
  166. data/lib/codebase_index/session_tracer/session_flow_assembler.rb +0 -254
  167. data/lib/codebase_index/session_tracer/session_flow_document.rb +0 -223
  168. data/lib/codebase_index/session_tracer/solid_cache_store.rb +0 -139
  169. data/lib/codebase_index/session_tracer/store.rb +0 -81
  170. data/lib/codebase_index/storage/graph_store.rb +0 -120
  171. data/lib/codebase_index/storage/metadata_store.rb +0 -196
  172. data/lib/codebase_index/storage/pgvector.rb +0 -195
  173. data/lib/codebase_index/storage/qdrant.rb +0 -205
  174. data/lib/codebase_index/storage/vector_store.rb +0 -167
  175. data/lib/codebase_index/temporal/json_snapshot_store.rb +0 -245
  176. data/lib/codebase_index/temporal/snapshot_store.rb +0 -345
  177. data/lib/codebase_index/token_utils.rb +0 -19
  178. data/lib/codebase_index/version.rb +0 -5
  179. data/lib/generators/codebase_index/install_generator.rb +0 -32
  180. data/lib/generators/codebase_index/pgvector_generator.rb +0 -37
  181. data/lib/generators/codebase_index/templates/add_pgvector_to_codebase_index.rb.erb +0 -15
  182. data/lib/generators/codebase_index/templates/create_codebase_index_tables.rb.erb +0 -43
  183. data/lib/tasks/codebase_index.rake +0 -597
  184. data/lib/tasks/codebase_index_evaluation.rake +0 -115
@@ -1,211 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'shared_utility_methods'
4
- require_relative 'shared_dependency_scanner'
5
-
6
- module CodebaseIndex
7
- module Extractors
8
- # ValidatorExtractor handles custom validator class extraction.
9
- #
10
- # Custom validators encapsulate reusable validation logic that applies
11
- # across multiple models. They inherit from `ActiveModel::Validator`
12
- # or `ActiveModel::EachValidator` and live in `app/validators/`.
13
- #
14
- # We extract:
15
- # - Validator name and namespace
16
- # - Base class (Validator vs EachValidator)
17
- # - Validation rules (what they check)
18
- # - Models they operate on (from source references)
19
- # - Dependencies (what models/services they reference)
20
- #
21
- # @example
22
- # extractor = ValidatorExtractor.new
23
- # units = extractor.extract_all
24
- # email = units.find { |u| u.identifier == "EmailFormatValidator" }
25
- #
26
- class ValidatorExtractor
27
- include SharedUtilityMethods
28
- include SharedDependencyScanner
29
-
30
- # Directories to scan for custom validators
31
- VALIDATOR_DIRECTORIES = %w[
32
- app/validators
33
- ].freeze
34
-
35
- def initialize
36
- @directories = VALIDATOR_DIRECTORIES.map { |d| Rails.root.join(d) }
37
- .select(&:directory?)
38
- end
39
-
40
- # Extract all custom validators
41
- #
42
- # @return [Array<ExtractedUnit>] List of validator units
43
- def extract_all
44
- @directories.flat_map do |dir|
45
- Dir[dir.join('**/*.rb')].filter_map do |file|
46
- extract_validator_file(file)
47
- end
48
- end
49
- end
50
-
51
- # Extract a single validator file
52
- #
53
- # @param file_path [String] Path to the validator file
54
- # @return [ExtractedUnit, nil] The extracted unit or nil if not a validator
55
- def extract_validator_file(file_path)
56
- source = File.read(file_path)
57
- class_name = extract_class_name(file_path, source, 'validators')
58
-
59
- return nil unless class_name
60
- return nil unless validator_file?(source)
61
-
62
- unit = ExtractedUnit.new(
63
- type: :validator,
64
- identifier: class_name,
65
- file_path: file_path
66
- )
67
-
68
- unit.namespace = extract_namespace(class_name)
69
- unit.source_code = annotate_source(source, class_name)
70
- unit.metadata = extract_metadata(source, class_name)
71
- unit.dependencies = extract_dependencies(source)
72
-
73
- unit
74
- rescue StandardError => e
75
- Rails.logger.error("Failed to extract validator #{file_path}: #{e.message}")
76
- nil
77
- end
78
-
79
- private
80
-
81
- # ──────────────────────────────────────────────────────────────────────
82
- # Class Discovery
83
- # ──────────────────────────────────────────────────────────────────────
84
-
85
- def validator_file?(source)
86
- source.match?(/< ActiveModel::Validator/) ||
87
- source.match?(/< ActiveModel::EachValidator/) ||
88
- source.match?(/def\s+validate_each\b/) ||
89
- source.match?(/def\s+validate\(/)
90
- end
91
-
92
- # ──────────────────────────────────────────────────────────────────────
93
- # Source Annotation
94
- # ──────────────────────────────────────────────────────────────────────
95
-
96
- def annotate_source(source, class_name)
97
- validator_type = detect_validator_type(source)
98
- validated_attrs = extract_validated_attributes(source)
99
-
100
- <<~ANNOTATION
101
- # ╔═══════════════════════════════════════════════════════════════════════╗
102
- # ║ Validator: #{class_name.ljust(57)}║
103
- # ║ Type: #{validator_type.to_s.ljust(62)}║
104
- # ║ Attributes: #{validated_attrs.join(', ').ljust(56)}║
105
- # ╚═══════════════════════════════════════════════════════════════════════╝
106
-
107
- #{source}
108
- ANNOTATION
109
- end
110
-
111
- # ──────────────────────────────────────────────────────────────────────
112
- # Metadata Extraction
113
- # ──────────────────────────────────────────────────────────────────────
114
-
115
- def extract_metadata(source, class_name)
116
- {
117
- validator_type: detect_validator_type(source),
118
- validated_attributes: extract_validated_attributes(source),
119
- validation_rules: extract_validation_rules(source),
120
- error_messages: extract_error_messages(source),
121
- public_methods: extract_public_methods(source),
122
- class_methods: extract_class_methods(source),
123
- options_used: extract_options(source),
124
- inferred_models: infer_models_from_name(class_name),
125
- custom_errors: extract_custom_errors(source),
126
- loc: source.lines.count { |l| l.strip.length.positive? && !l.strip.start_with?('#') },
127
- method_count: source.scan(/def\s+(?:self\.)?\w+/).size
128
- }
129
- end
130
-
131
- def detect_validator_type(source)
132
- return :each_validator if source.match?(/< ActiveModel::EachValidator/)
133
- return :validator if source.match?(/< ActiveModel::Validator/)
134
- return :each_validator if source.match?(/def\s+validate_each\b/)
135
- return :validator if source.match?(/def\s+validate\(/)
136
-
137
- :unknown
138
- end
139
-
140
- def extract_validated_attributes(source)
141
- attrs = []
142
-
143
- # EachValidator: the attribute param in validate_each
144
- attrs << ::Regexp.last_match(1) if source =~ /def\s+validate_each\s*\(\s*\w+\s*,\s*(\w+)/
145
-
146
- # From error.add calls: record.errors.add(:attribute, ...)
147
- source.scan(/errors\.add\s*\(\s*:(\w+)/).flatten.each { |a| attrs << a }
148
-
149
- # From validates_each blocks
150
- source.scan(/validates_each\s*\(\s*:(\w+)/).flatten.each { |a| attrs << a }
151
-
152
- attrs.uniq
153
- end
154
-
155
- def extract_validation_rules(source)
156
- # Conditional checks in validate/validate_each body
157
- rules = source.scan(/unless\s+(.+)$/).flatten.map(&:strip)
158
- source.scan(/if\s+(.+?)(?:\s*$|\s*then)/).flatten.each { |r| rules << r.strip }
159
-
160
- # Regex validations
161
- source.scan(%r{=~\s*(/[^/]+/)}).flatten.each { |r| rules << "matches #{r}" }
162
- source.scan(%r{match\?\s*\((/[^/]+/)\)}).flatten.each { |r| rules << "matches #{r}" }
163
-
164
- rules.first(10) # Cap at 10 to avoid noise
165
- end
166
-
167
- def extract_error_messages(source)
168
- # errors.add(:attr, "message") or errors.add(variable, "message")
169
- messages = source.scan(/errors\.add\s*\(\s*:?\w+\s*,\s*["']([^"']+)["']/).flatten
170
-
171
- # errors.add(:attr, :symbol) or errors.add(variable, :symbol)
172
- source.scan(/errors\.add\s*\(\s*:?\w+\s*,\s*:(\w+)/).flatten.each { |m| messages << ":#{m}" }
173
-
174
- messages
175
- end
176
-
177
- def extract_options(source)
178
- # options[:key] access
179
- options = source.scan(/options\[:(\w+)\]/).flatten
180
-
181
- options.uniq
182
- end
183
-
184
- def infer_models_from_name(class_name)
185
- # EmailFormatValidator -> might validate email on many models
186
- # No reliable way to infer specific models from name alone
187
- # Return the validator's conceptual domain
188
- stripped = class_name.split('::').last
189
- inferred = stripped.sub(/Validator\z/, '')
190
- inferred.empty? ? [] : [inferred]
191
- end
192
-
193
- # ──────────────────────────────────────────────────────────────────────
194
- # Dependency Extraction
195
- # ──────────────────────────────────────────────────────────────────────
196
-
197
- def extract_dependencies(source)
198
- deps = []
199
- deps.concat(scan_model_dependencies(source, via: :validation))
200
- deps.concat(scan_service_dependencies(source))
201
-
202
- # Other validators referenced
203
- source.scan(/(\w+Validator)(?:\.|::new)/).flatten.uniq.each do |validator|
204
- deps << { type: :validator, target: validator, via: :code_reference }
205
- end
206
-
207
- deps.uniq { |d| [d[:type], d[:target]] }
208
- end
209
- end
210
- end
211
- end
@@ -1,311 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'shared_utility_methods'
4
- require_relative 'shared_dependency_scanner'
5
-
6
- module CodebaseIndex
7
- module Extractors
8
- # ViewComponentExtractor handles ViewComponent extraction.
9
- #
10
- # ViewComponent components are Ruby classes that encapsulate view logic.
11
- # We can extract:
12
- # - Slot definitions (renders_one, renders_many)
13
- # - Sidecar template paths (.html.erb files next to the .rb file)
14
- # - Initialize parameters (the component's API)
15
- # - Preview classes (ViewComponent::Preview subclasses)
16
- # - Collection support
17
- # - Callbacks (before_render, after_render)
18
- # - Content areas (legacy API)
19
- # - Component dependencies (rendered sub-components, model references)
20
- #
21
- # @example
22
- # extractor = ViewComponentExtractor.new
23
- # units = extractor.extract_all
24
- # card = units.find { |u| u.identifier == "CardComponent" }
25
- #
26
- class ViewComponentExtractor
27
- include SharedUtilityMethods
28
- include SharedDependencyScanner
29
-
30
- def initialize
31
- @component_base = find_component_base
32
- end
33
-
34
- # Extract all ViewComponent components
35
- #
36
- # @return [Array<ExtractedUnit>] List of view component units
37
- def extract_all
38
- return [] unless @component_base
39
-
40
- @component_base.descendants.map do |component|
41
- extract_component(component)
42
- end.compact
43
- end
44
-
45
- # Extract a single ViewComponent component
46
- #
47
- # @param component [Class] The component class
48
- # @return [ExtractedUnit, nil] The extracted unit, or nil on failure
49
- def extract_component(component)
50
- return nil if component.name.nil?
51
- return nil if preview_class?(component)
52
-
53
- unit = ExtractedUnit.new(
54
- type: :view_component,
55
- identifier: component.name,
56
- file_path: source_file_for(component)
57
- )
58
-
59
- # Skip components with no resolvable source file (framework/internal)
60
- return nil unless unit.file_path
61
-
62
- unit.source_code = read_source(unit.file_path)
63
-
64
- unit.namespace = extract_namespace(component)
65
- unit.metadata = extract_metadata(component, unit.source_code)
66
- unit.dependencies = extract_dependencies(component, unit.source_code)
67
-
68
- unit
69
- rescue StandardError => e
70
- Rails.logger.error("Failed to extract view component #{component.name}: #{e.message}")
71
- nil
72
- end
73
-
74
- private
75
-
76
- # Find the ViewComponent::Base class if the gem is loaded
77
- #
78
- # @return [Class, nil]
79
- def find_component_base
80
- return nil unless defined?(ViewComponent::Base)
81
-
82
- ViewComponent::Base
83
- end
84
-
85
- # Check if a class is a preview class (not a component itself)
86
- #
87
- # @param klass [Class]
88
- # @return [Boolean]
89
- def preview_class?(klass)
90
- defined?(ViewComponent::Preview) && klass < ViewComponent::Preview
91
- end
92
-
93
- # Locate the source file for a component class.
94
- #
95
- # Convention paths first, then introspection via {#resolve_source_location}
96
- # which filters out vendor/node_modules paths.
97
- #
98
- # @param component [Class]
99
- # @return [String, nil]
100
- def source_file_for(component)
101
- possible_paths = [
102
- Rails.root.join("app/components/#{component.name.underscore}.rb"),
103
- Rails.root.join("app/views/components/#{component.name.underscore}.rb")
104
- ]
105
-
106
- found = possible_paths.find { |p| File.exist?(p) }
107
- return found.to_s if found
108
-
109
- resolve_source_location(component, app_root: Rails.root.to_s, fallback: nil)
110
- end
111
-
112
- # @param file_path [String, nil]
113
- # @return [String]
114
- def read_source(file_path)
115
- return '' unless file_path && File.exist?(file_path)
116
-
117
- File.read(file_path)
118
- end
119
-
120
- # ──────────────────────────────────────────────────────────────────────
121
- # Metadata Extraction
122
- # ──────────────────────────────────────────────────────────────────────
123
-
124
- def extract_metadata(component, source)
125
- {
126
- slots: extract_slots(source),
127
- initialize_params: extract_initialize_params(component),
128
- public_methods: component.public_instance_methods(false),
129
- parent_component: component.superclass.name,
130
- sidecar_template: detect_sidecar_template(component),
131
- preview_class: detect_preview_class(component),
132
- collection_support: detect_collection_support(source),
133
- callbacks: extract_callbacks(source),
134
- content_areas: extract_content_areas(source),
135
- renders_many: extract_renders_many(source),
136
- renders_one: extract_renders_one(source),
137
- loc: source.lines.count { |l| l.strip.length.positive? && !l.strip.start_with?('#') }
138
- }
139
- end
140
-
141
- # Extract slot definitions from renders_one / renders_many
142
- #
143
- # @param source [String]
144
- # @return [Array<Hash>]
145
- def extract_slots(source)
146
- slots = []
147
-
148
- source.scan(/renders_one\s+:(\w+)(?:,\s*(\w+(?:::\w+)*))?/) do |name, klass|
149
- slots << { name: name, type: :one, class: klass }
150
- end
151
-
152
- source.scan(/renders_many\s+:(\w+)(?:,\s*(\w+(?:::\w+)*))?/) do |name, klass|
153
- slots << { name: name, type: :many, class: klass }
154
- end
155
-
156
- slots
157
- end
158
-
159
- def extract_renders_many(source)
160
- source.scan(/renders_many\s+:(\w+)/).flatten
161
- end
162
-
163
- def extract_renders_one(source)
164
- source.scan(/renders_one\s+:(\w+)/).flatten
165
- end
166
-
167
- # Extract initialize parameters to understand the component's data requirements
168
- #
169
- # @param component [Class]
170
- # @return [Array<Hash>]
171
- def extract_initialize_params(component)
172
- method = component.instance_method(:initialize)
173
- params = method.parameters
174
-
175
- params.map do |type, name|
176
- param_type = case type
177
- when :req then :required
178
- when :opt then :optional
179
- when :keyreq then :keyword_required
180
- when :key then :keyword_optional
181
- when :rest then :splat
182
- when :keyrest then :double_splat
183
- when :block then :block
184
- else type
185
- end
186
- { name: name, type: param_type }
187
- end
188
- rescue StandardError
189
- []
190
- end
191
-
192
- # Detect sidecar template file (.html.erb next to the .rb file)
193
- #
194
- # @param component [Class]
195
- # @return [String, nil] Path to sidecar template if found
196
- def detect_sidecar_template(component)
197
- base_path = Rails.root.join("app/components/#{component.name.underscore}")
198
-
199
- # Check common sidecar template patterns
200
- candidates = [
201
- "#{base_path}.html.erb",
202
- "#{base_path}.html.haml",
203
- "#{base_path}.html.slim",
204
- "#{base_path}/#{component.name.demodulize.underscore}.html.erb"
205
- ]
206
-
207
- candidates.find { |path| File.exist?(path) }
208
- rescue StandardError
209
- nil
210
- end
211
-
212
- # Detect if a preview class exists for this component
213
- #
214
- # @param component [Class]
215
- # @return [String, nil] Preview class name if found
216
- def detect_preview_class(component)
217
- return nil unless defined?(ViewComponent::Preview)
218
-
219
- preview_name = "#{component.name}Preview"
220
- klass = preview_name.safe_constantize
221
- klass&.name if klass && klass < ViewComponent::Preview
222
- rescue StandardError
223
- nil
224
- end
225
-
226
- # Detect if the component supports collection rendering
227
- #
228
- # @param source [String]
229
- # @return [Boolean]
230
- def detect_collection_support(source)
231
- source.match?(/with_collection_parameter/) ||
232
- source.match?(/def\s+self\.collection_parameter/)
233
- end
234
-
235
- # Extract before_render / after_render callbacks
236
- #
237
- # @param source [String]
238
- # @return [Array<Hash>]
239
- def extract_callbacks(source)
240
- callbacks = []
241
-
242
- source.scan(/before_render\s+:(\w+)/) do |name|
243
- callbacks << { kind: :before_render, method: name[0] }
244
- end
245
-
246
- source.scan(/after_render\s+:(\w+)/) do |name|
247
- callbacks << { kind: :after_render, method: name[0] }
248
- end
249
-
250
- # Also detect inline before_render method override
251
- callbacks << { kind: :before_render, method: :inline } if source.match?(/def\s+before_render\b/)
252
-
253
- callbacks
254
- end
255
-
256
- # Extract legacy content_areas definitions
257
- #
258
- # @param source [String]
259
- # @return [Array<String>]
260
- def extract_content_areas(source)
261
- source.scan(/with_content_areas\s+(.+)$/).flatten.flat_map do |area_list|
262
- area_list.scan(/:(\w+)/).flatten
263
- end
264
- end
265
-
266
- # ──────────────────────────────────────────────────────────────────────
267
- # Dependency Extraction
268
- # ──────────────────────────────────────────────────────────────────────
269
-
270
- def extract_dependencies(component, source)
271
- deps = []
272
-
273
- # Other components rendered via render()
274
- source.scan(/render\s*\(?\s*(\w+(?:::\w+)*)\.new/).flatten.uniq.each do |comp|
275
- next if comp == component.name
276
-
277
- deps << { type: :component, target: comp, via: :render }
278
- end
279
-
280
- # Components rendered via slot classes
281
- source.scan(/renders_one\s+:\w+,\s*(\w+(?:::\w+)*)/).flatten.uniq.each do |comp|
282
- deps << { type: :component, target: comp, via: :slot }
283
- end
284
-
285
- source.scan(/renders_many\s+:\w+,\s*(\w+(?:::\w+)*)/).flatten.uniq.each do |comp|
286
- deps << { type: :component, target: comp, via: :slot }
287
- end
288
-
289
- # Model references
290
- deps.concat(scan_model_dependencies(source, via: :data_dependency))
291
-
292
- # Helper modules
293
- source.scan(/include\s+(\w+Helper)/).flatten.uniq.each do |helper|
294
- deps << { type: :helper, target: helper, via: :include }
295
- end
296
-
297
- # Stimulus controllers (from data-controller attributes in templates/source)
298
- source.scan(/data[_-]controller[=:]\s*["']([^"']+)["']/).flatten.uniq.each do |controller|
299
- deps << { type: :stimulus_controller, target: controller, via: :html_attribute }
300
- end
301
-
302
- # URL helpers
303
- source.scan(/(\w+)_(?:path|url)/).flatten.uniq.each do |route|
304
- deps << { type: :route, target: route, via: :url_helper }
305
- end
306
-
307
- deps.uniq { |d| [d[:type], d[:target]] }
308
- end
309
- end
310
- end
311
- end