codebase_index 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +29 -0
- data/CODE_OF_CONDUCT.md +83 -0
- data/CONTRIBUTING.md +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +481 -0
- data/exe/codebase-console-mcp +22 -0
- data/exe/codebase-index-mcp +61 -0
- data/exe/codebase-index-mcp-http +64 -0
- data/exe/codebase-index-mcp-start +58 -0
- data/lib/codebase_index/ast/call_site_extractor.rb +106 -0
- data/lib/codebase_index/ast/method_extractor.rb +76 -0
- data/lib/codebase_index/ast/node.rb +88 -0
- data/lib/codebase_index/ast/parser.rb +653 -0
- data/lib/codebase_index/ast.rb +6 -0
- data/lib/codebase_index/builder.rb +137 -0
- data/lib/codebase_index/chunking/chunk.rb +84 -0
- data/lib/codebase_index/chunking/semantic_chunker.rb +290 -0
- data/lib/codebase_index/console/adapters/cache_adapter.rb +58 -0
- data/lib/codebase_index/console/adapters/good_job_adapter.rb +66 -0
- data/lib/codebase_index/console/adapters/sidekiq_adapter.rb +66 -0
- data/lib/codebase_index/console/adapters/solid_queue_adapter.rb +66 -0
- data/lib/codebase_index/console/audit_logger.rb +75 -0
- data/lib/codebase_index/console/bridge.rb +170 -0
- data/lib/codebase_index/console/confirmation.rb +90 -0
- data/lib/codebase_index/console/connection_manager.rb +173 -0
- data/lib/codebase_index/console/console_response_renderer.rb +78 -0
- data/lib/codebase_index/console/model_validator.rb +81 -0
- data/lib/codebase_index/console/safe_context.rb +82 -0
- data/lib/codebase_index/console/server.rb +557 -0
- data/lib/codebase_index/console/sql_validator.rb +172 -0
- data/lib/codebase_index/console/tools/tier1.rb +118 -0
- data/lib/codebase_index/console/tools/tier2.rb +117 -0
- data/lib/codebase_index/console/tools/tier3.rb +110 -0
- data/lib/codebase_index/console/tools/tier4.rb +79 -0
- data/lib/codebase_index/coordination/pipeline_lock.rb +109 -0
- data/lib/codebase_index/cost_model/embedding_cost.rb +88 -0
- data/lib/codebase_index/cost_model/estimator.rb +128 -0
- data/lib/codebase_index/cost_model/provider_pricing.rb +67 -0
- data/lib/codebase_index/cost_model/storage_cost.rb +52 -0
- data/lib/codebase_index/cost_model.rb +22 -0
- data/lib/codebase_index/db/migrations/001_create_units.rb +38 -0
- data/lib/codebase_index/db/migrations/002_create_edges.rb +35 -0
- data/lib/codebase_index/db/migrations/003_create_embeddings.rb +37 -0
- data/lib/codebase_index/db/migrations/004_create_snapshots.rb +45 -0
- data/lib/codebase_index/db/migrations/005_create_snapshot_units.rb +40 -0
- data/lib/codebase_index/db/migrator.rb +71 -0
- data/lib/codebase_index/db/schema_version.rb +73 -0
- data/lib/codebase_index/dependency_graph.rb +227 -0
- data/lib/codebase_index/embedding/indexer.rb +130 -0
- data/lib/codebase_index/embedding/openai.rb +105 -0
- data/lib/codebase_index/embedding/provider.rb +135 -0
- data/lib/codebase_index/embedding/text_preparer.rb +112 -0
- data/lib/codebase_index/evaluation/baseline_runner.rb +115 -0
- data/lib/codebase_index/evaluation/evaluator.rb +146 -0
- data/lib/codebase_index/evaluation/metrics.rb +79 -0
- data/lib/codebase_index/evaluation/query_set.rb +148 -0
- data/lib/codebase_index/evaluation/report_generator.rb +90 -0
- data/lib/codebase_index/extracted_unit.rb +145 -0
- data/lib/codebase_index/extractor.rb +956 -0
- data/lib/codebase_index/extractors/action_cable_extractor.rb +228 -0
- data/lib/codebase_index/extractors/ast_source_extraction.rb +46 -0
- data/lib/codebase_index/extractors/behavioral_profile.rb +309 -0
- data/lib/codebase_index/extractors/caching_extractor.rb +261 -0
- data/lib/codebase_index/extractors/callback_analyzer.rb +232 -0
- data/lib/codebase_index/extractors/concern_extractor.rb +253 -0
- data/lib/codebase_index/extractors/configuration_extractor.rb +219 -0
- data/lib/codebase_index/extractors/controller_extractor.rb +494 -0
- data/lib/codebase_index/extractors/database_view_extractor.rb +278 -0
- data/lib/codebase_index/extractors/decorator_extractor.rb +260 -0
- data/lib/codebase_index/extractors/engine_extractor.rb +204 -0
- data/lib/codebase_index/extractors/event_extractor.rb +211 -0
- data/lib/codebase_index/extractors/factory_extractor.rb +289 -0
- data/lib/codebase_index/extractors/graphql_extractor.rb +917 -0
- data/lib/codebase_index/extractors/i18n_extractor.rb +117 -0
- data/lib/codebase_index/extractors/job_extractor.rb +369 -0
- data/lib/codebase_index/extractors/lib_extractor.rb +249 -0
- data/lib/codebase_index/extractors/mailer_extractor.rb +339 -0
- data/lib/codebase_index/extractors/manager_extractor.rb +202 -0
- data/lib/codebase_index/extractors/middleware_extractor.rb +133 -0
- data/lib/codebase_index/extractors/migration_extractor.rb +469 -0
- data/lib/codebase_index/extractors/model_extractor.rb +960 -0
- data/lib/codebase_index/extractors/phlex_extractor.rb +252 -0
- data/lib/codebase_index/extractors/policy_extractor.rb +214 -0
- data/lib/codebase_index/extractors/poro_extractor.rb +246 -0
- data/lib/codebase_index/extractors/pundit_extractor.rb +223 -0
- data/lib/codebase_index/extractors/rails_source_extractor.rb +473 -0
- data/lib/codebase_index/extractors/rake_task_extractor.rb +343 -0
- data/lib/codebase_index/extractors/route_extractor.rb +181 -0
- data/lib/codebase_index/extractors/scheduled_job_extractor.rb +331 -0
- data/lib/codebase_index/extractors/serializer_extractor.rb +334 -0
- data/lib/codebase_index/extractors/service_extractor.rb +254 -0
- data/lib/codebase_index/extractors/shared_dependency_scanner.rb +91 -0
- data/lib/codebase_index/extractors/shared_utility_methods.rb +99 -0
- data/lib/codebase_index/extractors/state_machine_extractor.rb +398 -0
- data/lib/codebase_index/extractors/test_mapping_extractor.rb +225 -0
- data/lib/codebase_index/extractors/validator_extractor.rb +225 -0
- data/lib/codebase_index/extractors/view_component_extractor.rb +310 -0
- data/lib/codebase_index/extractors/view_template_extractor.rb +261 -0
- data/lib/codebase_index/feedback/gap_detector.rb +89 -0
- data/lib/codebase_index/feedback/store.rb +119 -0
- data/lib/codebase_index/flow_analysis/operation_extractor.rb +209 -0
- data/lib/codebase_index/flow_analysis/response_code_mapper.rb +154 -0
- data/lib/codebase_index/flow_assembler.rb +290 -0
- data/lib/codebase_index/flow_document.rb +191 -0
- data/lib/codebase_index/flow_precomputer.rb +102 -0
- data/lib/codebase_index/formatting/base.rb +40 -0
- data/lib/codebase_index/formatting/claude_adapter.rb +98 -0
- data/lib/codebase_index/formatting/generic_adapter.rb +56 -0
- data/lib/codebase_index/formatting/gpt_adapter.rb +64 -0
- data/lib/codebase_index/formatting/human_adapter.rb +78 -0
- data/lib/codebase_index/graph_analyzer.rb +374 -0
- data/lib/codebase_index/mcp/index_reader.rb +394 -0
- data/lib/codebase_index/mcp/renderers/claude_renderer.rb +81 -0
- data/lib/codebase_index/mcp/renderers/json_renderer.rb +17 -0
- data/lib/codebase_index/mcp/renderers/markdown_renderer.rb +352 -0
- data/lib/codebase_index/mcp/renderers/plain_renderer.rb +240 -0
- data/lib/codebase_index/mcp/server.rb +935 -0
- data/lib/codebase_index/mcp/tool_response_renderer.rb +62 -0
- data/lib/codebase_index/model_name_cache.rb +51 -0
- data/lib/codebase_index/notion/client.rb +217 -0
- data/lib/codebase_index/notion/exporter.rb +219 -0
- data/lib/codebase_index/notion/mapper.rb +39 -0
- data/lib/codebase_index/notion/mappers/column_mapper.rb +65 -0
- data/lib/codebase_index/notion/mappers/migration_mapper.rb +39 -0
- data/lib/codebase_index/notion/mappers/model_mapper.rb +164 -0
- data/lib/codebase_index/notion/rate_limiter.rb +68 -0
- data/lib/codebase_index/observability/health_check.rb +81 -0
- data/lib/codebase_index/observability/instrumentation.rb +34 -0
- data/lib/codebase_index/observability/structured_logger.rb +75 -0
- data/lib/codebase_index/operator/error_escalator.rb +81 -0
- data/lib/codebase_index/operator/pipeline_guard.rb +99 -0
- data/lib/codebase_index/operator/status_reporter.rb +80 -0
- data/lib/codebase_index/railtie.rb +26 -0
- data/lib/codebase_index/resilience/circuit_breaker.rb +99 -0
- data/lib/codebase_index/resilience/index_validator.rb +185 -0
- data/lib/codebase_index/resilience/retryable_provider.rb +108 -0
- data/lib/codebase_index/retrieval/context_assembler.rb +249 -0
- data/lib/codebase_index/retrieval/query_classifier.rb +131 -0
- data/lib/codebase_index/retrieval/ranker.rb +273 -0
- data/lib/codebase_index/retrieval/search_executor.rb +327 -0
- data/lib/codebase_index/retriever.rb +160 -0
- data/lib/codebase_index/ruby_analyzer/class_analyzer.rb +190 -0
- data/lib/codebase_index/ruby_analyzer/dataflow_analyzer.rb +78 -0
- data/lib/codebase_index/ruby_analyzer/fqn_builder.rb +18 -0
- data/lib/codebase_index/ruby_analyzer/mermaid_renderer.rb +275 -0
- data/lib/codebase_index/ruby_analyzer/method_analyzer.rb +143 -0
- data/lib/codebase_index/ruby_analyzer/trace_enricher.rb +139 -0
- data/lib/codebase_index/ruby_analyzer.rb +87 -0
- data/lib/codebase_index/session_tracer/file_store.rb +111 -0
- data/lib/codebase_index/session_tracer/middleware.rb +143 -0
- data/lib/codebase_index/session_tracer/redis_store.rb +112 -0
- data/lib/codebase_index/session_tracer/session_flow_assembler.rb +263 -0
- data/lib/codebase_index/session_tracer/session_flow_document.rb +223 -0
- data/lib/codebase_index/session_tracer/solid_cache_store.rb +145 -0
- data/lib/codebase_index/session_tracer/store.rb +67 -0
- data/lib/codebase_index/storage/graph_store.rb +120 -0
- data/lib/codebase_index/storage/metadata_store.rb +169 -0
- data/lib/codebase_index/storage/pgvector.rb +163 -0
- data/lib/codebase_index/storage/qdrant.rb +172 -0
- data/lib/codebase_index/storage/vector_store.rb +156 -0
- data/lib/codebase_index/temporal/snapshot_store.rb +341 -0
- data/lib/codebase_index/version.rb +5 -0
- data/lib/codebase_index.rb +223 -0
- data/lib/generators/codebase_index/install_generator.rb +32 -0
- data/lib/generators/codebase_index/pgvector_generator.rb +37 -0
- data/lib/generators/codebase_index/templates/add_pgvector_to_codebase_index.rb.erb +15 -0
- data/lib/generators/codebase_index/templates/create_codebase_index_tables.rb.erb +43 -0
- data/lib/tasks/codebase_index.rake +583 -0
- data/lib/tasks/codebase_index_evaluation.rake +115 -0
- metadata +252 -0
|
@@ -0,0 +1,253 @@
|
|
|
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
|
+
# ConcernExtractor handles ActiveSupport::Concern module extraction.
|
|
9
|
+
#
|
|
10
|
+
# Concerns are mixins that extend model and controller behavior.
|
|
11
|
+
# They live in `app/models/concerns/` and `app/controllers/concerns/`.
|
|
12
|
+
#
|
|
13
|
+
# We extract:
|
|
14
|
+
# - Module name and namespace
|
|
15
|
+
# - Included/extended hooks and class methods block
|
|
16
|
+
# - Instance methods and class methods added by the concern
|
|
17
|
+
# - Dependencies on models and other concerns
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# extractor = ConcernExtractor.new
|
|
21
|
+
# units = extractor.extract_all
|
|
22
|
+
# searchable = units.find { |u| u.identifier == "Searchable" }
|
|
23
|
+
#
|
|
24
|
+
class ConcernExtractor
|
|
25
|
+
include SharedUtilityMethods
|
|
26
|
+
include SharedDependencyScanner
|
|
27
|
+
|
|
28
|
+
# Directories to scan for concern modules
|
|
29
|
+
CONCERN_DIRECTORIES = %w[
|
|
30
|
+
app/models/concerns
|
|
31
|
+
app/controllers/concerns
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
def initialize
|
|
35
|
+
@directories = CONCERN_DIRECTORIES.map { |d| Rails.root.join(d) }
|
|
36
|
+
.select(&:directory?)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Extract all concern modules
|
|
40
|
+
#
|
|
41
|
+
# @return [Array<ExtractedUnit>] List of concern units
|
|
42
|
+
def extract_all
|
|
43
|
+
@directories.flat_map do |dir|
|
|
44
|
+
Dir[dir.join('**/*.rb')].filter_map do |file|
|
|
45
|
+
extract_concern_file(file)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Extract a single concern file
|
|
51
|
+
#
|
|
52
|
+
# @param file_path [String] Path to the concern file
|
|
53
|
+
# @return [ExtractedUnit, nil] The extracted unit or nil if not a concern
|
|
54
|
+
def extract_concern_file(file_path)
|
|
55
|
+
source = File.read(file_path)
|
|
56
|
+
module_name = extract_module_name(file_path, source)
|
|
57
|
+
|
|
58
|
+
return nil unless module_name
|
|
59
|
+
return nil unless concern_module?(source)
|
|
60
|
+
|
|
61
|
+
unit = ExtractedUnit.new(
|
|
62
|
+
type: :concern,
|
|
63
|
+
identifier: module_name,
|
|
64
|
+
file_path: file_path
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
unit.namespace = extract_namespace(module_name)
|
|
68
|
+
unit.source_code = annotate_source(source, module_name)
|
|
69
|
+
unit.metadata = extract_metadata(source, file_path)
|
|
70
|
+
unit.dependencies = extract_dependencies(source)
|
|
71
|
+
|
|
72
|
+
unit
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
Rails.logger.error("Failed to extract concern #{file_path}: #{e.message}")
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
81
|
+
# Module Discovery
|
|
82
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
# Extract the module name from source or infer from file path.
|
|
85
|
+
#
|
|
86
|
+
# @param file_path [String] Path to the concern file
|
|
87
|
+
# @param source [String] Ruby source code
|
|
88
|
+
# @return [String, nil] The module name
|
|
89
|
+
def extract_module_name(file_path, source)
|
|
90
|
+
# Try to find the outermost module definition
|
|
91
|
+
modules = source.scan(/^\s*module\s+([\w:]+)/).flatten
|
|
92
|
+
return modules.last if modules.any?
|
|
93
|
+
|
|
94
|
+
# Infer from file path
|
|
95
|
+
relative = file_path.sub("#{Rails.root}/", '')
|
|
96
|
+
relative
|
|
97
|
+
.sub(%r{^app/(models|controllers)/concerns/}, '')
|
|
98
|
+
.sub('.rb', '')
|
|
99
|
+
.split('/')
|
|
100
|
+
.map { |segment| segment.split('_').map(&:capitalize).join }
|
|
101
|
+
.join('::')
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Detect whether source defines an ActiveSupport::Concern or a plain mixin.
|
|
105
|
+
#
|
|
106
|
+
# @param source [String] Ruby source code
|
|
107
|
+
# @return [Boolean]
|
|
108
|
+
def concern_module?(source)
|
|
109
|
+
# ActiveSupport::Concern usage or plain module with methods
|
|
110
|
+
source.match?(/^\s*module\s+/) &&
|
|
111
|
+
(source.match?(/extend\s+ActiveSupport::Concern/) ||
|
|
112
|
+
source.match?(/included\s+do/) ||
|
|
113
|
+
source.match?(/class_methods\s+do/) ||
|
|
114
|
+
source.match?(/def\s+\w+/))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
118
|
+
# Source Annotation
|
|
119
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
# @param source [String] Ruby source code
|
|
122
|
+
# @param module_name [String] The concern module name
|
|
123
|
+
# @return [String] Annotated source
|
|
124
|
+
def annotate_source(source, module_name)
|
|
125
|
+
concern_type = detect_concern_type(source)
|
|
126
|
+
instance_methods = extract_instance_method_names(source)
|
|
127
|
+
|
|
128
|
+
<<~ANNOTATION
|
|
129
|
+
# ╔═══════════════════════════════════════════════════════════════════════╗
|
|
130
|
+
# ║ Concern: #{module_name.ljust(59)}║
|
|
131
|
+
# ║ Type: #{concern_type.ljust(62)}║
|
|
132
|
+
# ║ Methods: #{instance_methods.join(', ').ljust(59)}║
|
|
133
|
+
# ╚═══════════════════════════════════════════════════════════════════════╝
|
|
134
|
+
|
|
135
|
+
#{source}
|
|
136
|
+
ANNOTATION
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
140
|
+
# Metadata Extraction
|
|
141
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
# @param source [String] Ruby source code
|
|
144
|
+
# @param file_path [String] Path to the concern file
|
|
145
|
+
# @return [Hash] Concern metadata
|
|
146
|
+
def extract_metadata(source, file_path)
|
|
147
|
+
{
|
|
148
|
+
concern_type: detect_concern_type(source),
|
|
149
|
+
concern_scope: detect_concern_scope(file_path),
|
|
150
|
+
uses_active_support: source.match?(/extend\s+ActiveSupport::Concern/),
|
|
151
|
+
has_included_block: source.match?(/included\s+do/) || false,
|
|
152
|
+
has_class_methods_block: source.match?(/class_methods\s+do/) || false,
|
|
153
|
+
included_modules: detect_included_modules(source),
|
|
154
|
+
instance_methods: extract_instance_method_names(source),
|
|
155
|
+
class_methods: extract_class_methods(source),
|
|
156
|
+
public_methods: extract_public_methods(source),
|
|
157
|
+
callbacks_defined: detect_callbacks(source),
|
|
158
|
+
scopes_defined: detect_scopes(source),
|
|
159
|
+
validations_defined: detect_validations(source),
|
|
160
|
+
loc: source.lines.count { |l| l.strip.length.positive? && !l.strip.start_with?('#') },
|
|
161
|
+
method_count: source.scan(/def\s+(?:self\.)?\w+/).size
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Detect whether this is a model concern, controller concern, or generic.
|
|
166
|
+
#
|
|
167
|
+
# @param source [String] Ruby source code
|
|
168
|
+
# @return [String] One of "active_support", "plain_mixin"
|
|
169
|
+
def detect_concern_type(source)
|
|
170
|
+
if source.match?(/extend\s+ActiveSupport::Concern/)
|
|
171
|
+
'active_support'
|
|
172
|
+
else
|
|
173
|
+
'plain_mixin'
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Detect concern scope from file path (model vs controller).
|
|
178
|
+
#
|
|
179
|
+
# @param file_path [String] Path to the concern file
|
|
180
|
+
# @return [String] One of "model", "controller", "unknown"
|
|
181
|
+
def detect_concern_scope(file_path)
|
|
182
|
+
if file_path.include?('app/models/concerns')
|
|
183
|
+
'model'
|
|
184
|
+
elsif file_path.include?('app/controllers/concerns')
|
|
185
|
+
'controller'
|
|
186
|
+
else
|
|
187
|
+
'unknown'
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Extract instance method names (not self. methods).
|
|
192
|
+
#
|
|
193
|
+
# @param source [String] Ruby source code
|
|
194
|
+
# @return [Array<String>] Instance method names
|
|
195
|
+
def extract_instance_method_names(source)
|
|
196
|
+
source.scan(/^\s*def\s+(\w+[?!=]?)/).flatten.reject { |m| m.start_with?('self.') }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Detect other modules included by this concern.
|
|
200
|
+
#
|
|
201
|
+
# @param source [String] Ruby source code
|
|
202
|
+
# @return [Array<String>] Module names
|
|
203
|
+
def detect_included_modules(source)
|
|
204
|
+
source.scan(/(?:include|extend)\s+([\w:]+)/).flatten
|
|
205
|
+
.reject { |m| m == 'ActiveSupport::Concern' }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Detect callback declarations.
|
|
209
|
+
#
|
|
210
|
+
# @param source [String] Ruby source code
|
|
211
|
+
# @return [Array<String>] Callback names
|
|
212
|
+
def detect_callbacks(source)
|
|
213
|
+
source.scan(/(before_\w+|after_\w+|around_\w+)\s/).flatten.uniq
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Detect scope declarations.
|
|
217
|
+
#
|
|
218
|
+
# @param source [String] Ruby source code
|
|
219
|
+
# @return [Array<String>] Scope names
|
|
220
|
+
def detect_scopes(source)
|
|
221
|
+
source.scan(/scope\s+:(\w+)/).flatten
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Detect validation declarations.
|
|
225
|
+
#
|
|
226
|
+
# @param source [String] Ruby source code
|
|
227
|
+
# @return [Array<String>] Validation types
|
|
228
|
+
def detect_validations(source)
|
|
229
|
+
source.scan(/(validates?(?:_\w+)?)\s/).flatten.uniq
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
233
|
+
# Dependency Extraction
|
|
234
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
# @param source [String] Ruby source code
|
|
237
|
+
# @return [Array<Hash>] Dependency hashes
|
|
238
|
+
def extract_dependencies(source)
|
|
239
|
+
# Other concerns included by this concern
|
|
240
|
+
deps = detect_included_modules(source).map do |mod|
|
|
241
|
+
{ type: :concern, target: mod, via: :include }
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Standard dependency scanning
|
|
245
|
+
deps.concat(scan_model_dependencies(source))
|
|
246
|
+
deps.concat(scan_service_dependencies(source))
|
|
247
|
+
deps.concat(scan_job_dependencies(source))
|
|
248
|
+
|
|
249
|
+
deps.uniq { |d| [d[:type], d[:target]] }
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'shared_utility_methods'
|
|
4
|
+
require_relative 'shared_dependency_scanner'
|
|
5
|
+
require_relative 'behavioral_profile'
|
|
6
|
+
|
|
7
|
+
module CodebaseIndex
|
|
8
|
+
module Extractors
|
|
9
|
+
# ConfigurationExtractor handles Rails configuration file extraction.
|
|
10
|
+
#
|
|
11
|
+
# Scans `config/initializers/` and `config/environments/` for Ruby
|
|
12
|
+
# configuration files. Each file becomes one ExtractedUnit with metadata
|
|
13
|
+
# about config type, gem references, and detected settings.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# extractor = ConfigurationExtractor.new
|
|
17
|
+
# units = extractor.extract_all
|
|
18
|
+
# devise = units.find { |u| u.identifier == "initializers/devise.rb" }
|
|
19
|
+
#
|
|
20
|
+
class ConfigurationExtractor
|
|
21
|
+
include SharedUtilityMethods
|
|
22
|
+
include SharedDependencyScanner
|
|
23
|
+
|
|
24
|
+
# Directories to scan for configuration files
|
|
25
|
+
CONFIG_DIRECTORIES = %w[
|
|
26
|
+
config/initializers
|
|
27
|
+
config/environments
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@directories = CONFIG_DIRECTORIES.map { |d| Rails.root.join(d) }
|
|
32
|
+
.select(&:directory?)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Extract all configuration files and the behavioral profile.
|
|
36
|
+
#
|
|
37
|
+
# @return [Array<ExtractedUnit>] List of configuration units
|
|
38
|
+
def extract_all
|
|
39
|
+
units = @directories.flat_map do |dir|
|
|
40
|
+
Dir[dir.join('**/*.rb')].filter_map do |file|
|
|
41
|
+
extract_configuration_file(file)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
profile = BehavioralProfile.new.extract
|
|
46
|
+
units << profile if profile
|
|
47
|
+
|
|
48
|
+
units
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
Rails.logger.error("BehavioralProfile integration failed: #{e.message}")
|
|
51
|
+
units || []
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Extract a single configuration file
|
|
55
|
+
#
|
|
56
|
+
# @param file_path [String] Path to the configuration file
|
|
57
|
+
# @return [ExtractedUnit, nil] The extracted unit or nil on failure
|
|
58
|
+
def extract_configuration_file(file_path)
|
|
59
|
+
source = File.read(file_path)
|
|
60
|
+
identifier = build_identifier(file_path)
|
|
61
|
+
config_type = detect_config_type(file_path)
|
|
62
|
+
|
|
63
|
+
unit = ExtractedUnit.new(
|
|
64
|
+
type: :configuration,
|
|
65
|
+
identifier: identifier,
|
|
66
|
+
file_path: file_path
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
unit.namespace = config_type
|
|
70
|
+
unit.source_code = annotate_source(source, identifier, config_type)
|
|
71
|
+
unit.metadata = extract_metadata(source, config_type)
|
|
72
|
+
unit.dependencies = extract_dependencies(source)
|
|
73
|
+
|
|
74
|
+
unit
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
Rails.logger.error("Failed to extract configuration #{file_path}: #{e.message}")
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
83
|
+
# Identification
|
|
84
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
# Build a readable identifier from the file path.
|
|
87
|
+
#
|
|
88
|
+
# @param file_path [String]
|
|
89
|
+
# @return [String] e.g., "initializers/devise.rb" or "environments/production.rb"
|
|
90
|
+
def build_identifier(file_path)
|
|
91
|
+
relative = file_path.sub("#{Rails.root}/", '')
|
|
92
|
+
relative.sub(%r{^config/}, '')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Detect whether this is an initializer or environment config.
|
|
96
|
+
#
|
|
97
|
+
# @param file_path [String]
|
|
98
|
+
# @return [String]
|
|
99
|
+
def detect_config_type(file_path)
|
|
100
|
+
if file_path.include?('config/initializers')
|
|
101
|
+
'initializer'
|
|
102
|
+
elsif file_path.include?('config/environments')
|
|
103
|
+
'environment'
|
|
104
|
+
else
|
|
105
|
+
'configuration'
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
110
|
+
# Source Annotation
|
|
111
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
# @param source [String]
|
|
114
|
+
# @param identifier [String]
|
|
115
|
+
# @param config_type [String]
|
|
116
|
+
# @return [String]
|
|
117
|
+
def annotate_source(source, identifier, config_type)
|
|
118
|
+
gem_refs = detect_gem_references(source)
|
|
119
|
+
|
|
120
|
+
<<~ANNOTATION
|
|
121
|
+
# ╔═══════════════════════════════════════════════════════════════════════╗
|
|
122
|
+
# ║ Configuration: #{identifier.ljust(53)}║
|
|
123
|
+
# ║ Type: #{config_type.ljust(62)}║
|
|
124
|
+
# ║ Gems: #{gem_refs.join(', ').ljust(62)}║
|
|
125
|
+
# ╚═══════════════════════════════════════════════════════════════════════╝
|
|
126
|
+
|
|
127
|
+
#{source}
|
|
128
|
+
ANNOTATION
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
132
|
+
# Metadata Extraction
|
|
133
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
# @param source [String]
|
|
136
|
+
# @param config_type [String]
|
|
137
|
+
# @return [Hash]
|
|
138
|
+
def extract_metadata(source, config_type)
|
|
139
|
+
{
|
|
140
|
+
config_type: config_type,
|
|
141
|
+
gem_references: detect_gem_references(source),
|
|
142
|
+
config_settings: detect_config_settings(source),
|
|
143
|
+
rails_config_blocks: detect_rails_config_blocks(source),
|
|
144
|
+
loc: source.lines.count { |l| l.strip.length.positive? && !l.strip.start_with?('#') },
|
|
145
|
+
method_count: source.scan(/def\s+(?:self\.)?\w+/).size
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Detect gem/library references in configuration.
|
|
150
|
+
#
|
|
151
|
+
# @param source [String]
|
|
152
|
+
# @return [Array<String>]
|
|
153
|
+
def detect_gem_references(source)
|
|
154
|
+
refs = []
|
|
155
|
+
|
|
156
|
+
# Gem.configure style: Devise.setup, Sidekiq.configure_server
|
|
157
|
+
source.scan(/(\w+)\.(setup|configure\w*|config)\b/).each do |match|
|
|
158
|
+
name = match[0]
|
|
159
|
+
refs << name unless generic_config_name?(name)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# require statements for gems
|
|
163
|
+
source.scan(/require\s+['"]([^'"]+)['"]/).each do |match|
|
|
164
|
+
refs << match[0]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
refs.uniq
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Detect configuration settings (key = value patterns).
|
|
171
|
+
#
|
|
172
|
+
# @param source [String]
|
|
173
|
+
# @return [Array<String>]
|
|
174
|
+
def detect_config_settings(source)
|
|
175
|
+
# config.something = value
|
|
176
|
+
settings = source.scan(/config\.(\w+(?:\.\w+)*)\s*=/).map { |match| match[0] }
|
|
177
|
+
|
|
178
|
+
# self.something = value (inside configure blocks)
|
|
179
|
+
settings.concat(source.scan(/(?:self|config)\.(\w+)\s*=/).map { |match| match[0] })
|
|
180
|
+
|
|
181
|
+
settings.uniq
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Detect Rails.application.configure or similar blocks.
|
|
185
|
+
#
|
|
186
|
+
# @param source [String]
|
|
187
|
+
# @return [Array<String>]
|
|
188
|
+
def detect_rails_config_blocks(source)
|
|
189
|
+
source.scan(/(Rails\.application\.configure|Rails\.application\.config\.\w+)/)
|
|
190
|
+
.map { |match| match[0] }
|
|
191
|
+
.uniq
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Check if a name is too generic to be a gem reference.
|
|
195
|
+
#
|
|
196
|
+
# @param name [String]
|
|
197
|
+
# @return [Boolean]
|
|
198
|
+
def generic_config_name?(name)
|
|
199
|
+
%w[Rails ActiveRecord ActiveJob ActionMailer ActionController ActiveStorage ActionCable].include?(name)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
203
|
+
# Dependency Extraction
|
|
204
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
# @param source [String]
|
|
207
|
+
# @return [Array<Hash>]
|
|
208
|
+
def extract_dependencies(source)
|
|
209
|
+
deps = detect_gem_references(source).map do |gem_ref|
|
|
210
|
+
{ type: :gem, target: gem_ref, via: :configuration }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
deps.concat(scan_service_dependencies(source))
|
|
214
|
+
|
|
215
|
+
deps.uniq { |d| [d[:type], d[:target]] }
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|