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,254 @@
|
|
|
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
|
+
# ServiceExtractor handles service object extraction.
|
|
9
|
+
#
|
|
10
|
+
# Service objects often contain the most important business logic.
|
|
11
|
+
# Unlike models (which are discovered via ActiveRecord), services
|
|
12
|
+
# are discovered by scanning conventional directories.
|
|
13
|
+
#
|
|
14
|
+
# We extract:
|
|
15
|
+
# - Public interface (call/perform/execute methods)
|
|
16
|
+
# - Dependencies (what models/services/jobs they use)
|
|
17
|
+
# - Error classes (custom exceptions defined)
|
|
18
|
+
# - Input/output patterns
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# extractor = ServiceExtractor.new
|
|
22
|
+
# units = extractor.extract_all
|
|
23
|
+
# checkout = units.find { |u| u.identifier == "CheckoutService" }
|
|
24
|
+
#
|
|
25
|
+
class ServiceExtractor
|
|
26
|
+
include SharedUtilityMethods
|
|
27
|
+
include SharedDependencyScanner
|
|
28
|
+
|
|
29
|
+
# Directories to scan for service objects
|
|
30
|
+
SERVICE_DIRECTORIES = %w[
|
|
31
|
+
app/services
|
|
32
|
+
app/interactors
|
|
33
|
+
app/operations
|
|
34
|
+
app/commands
|
|
35
|
+
app/use_cases
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
def initialize
|
|
39
|
+
@directories = SERVICE_DIRECTORIES.map { |d| Rails.root.join(d) }
|
|
40
|
+
.select(&:directory?)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Extract all service objects
|
|
44
|
+
#
|
|
45
|
+
# @return [Array<ExtractedUnit>] List of service units
|
|
46
|
+
def extract_all
|
|
47
|
+
@directories.flat_map do |dir|
|
|
48
|
+
Dir[dir.join('**/*.rb')].filter_map do |file|
|
|
49
|
+
extract_service_file(file)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Extract a single service file
|
|
55
|
+
#
|
|
56
|
+
# @param file_path [String] Path to the service file
|
|
57
|
+
# @return [ExtractedUnit, nil] The extracted unit or nil if not a service
|
|
58
|
+
def extract_service_file(file_path)
|
|
59
|
+
source = File.read(file_path)
|
|
60
|
+
class_name = extract_class_name(file_path, source)
|
|
61
|
+
|
|
62
|
+
return nil unless class_name
|
|
63
|
+
return nil if skip_file?(source)
|
|
64
|
+
|
|
65
|
+
unit = ExtractedUnit.new(
|
|
66
|
+
type: :service,
|
|
67
|
+
identifier: class_name,
|
|
68
|
+
file_path: file_path
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
unit.namespace = extract_namespace(class_name)
|
|
72
|
+
unit.source_code = annotate_source(source, class_name)
|
|
73
|
+
unit.metadata = extract_metadata(source, class_name, file_path)
|
|
74
|
+
unit.dependencies = extract_dependencies(source)
|
|
75
|
+
|
|
76
|
+
unit
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
Rails.logger.error("Failed to extract service #{file_path}: #{e.message}")
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
85
|
+
# Class Discovery
|
|
86
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
def extract_class_name(file_path, source)
|
|
89
|
+
# Try to extract from source first (handles nested modules)
|
|
90
|
+
return ::Regexp.last_match(1) if source =~ /^\s*class\s+([\w:]+)/
|
|
91
|
+
|
|
92
|
+
# Fall back to convention
|
|
93
|
+
relative_path = file_path.sub("#{Rails.root}/", '')
|
|
94
|
+
|
|
95
|
+
# app/services/payments/stripe_service.rb -> Payments::StripeService
|
|
96
|
+
relative_path
|
|
97
|
+
.sub(%r{^app/(services|interactors|operations|commands|use_cases)/}, '')
|
|
98
|
+
.sub('.rb', '')
|
|
99
|
+
.camelize
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def skip_file?(source)
|
|
103
|
+
# Skip module-only files (concerns, base modules)
|
|
104
|
+
source.match?(/^\s*module\s+\w+\s*$/) && !source.match?(/^\s*class\s+/)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
108
|
+
# Source Annotation
|
|
109
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
# Add annotations to help with retrieval
|
|
112
|
+
def annotate_source(source, class_name)
|
|
113
|
+
entry_points = detect_entry_points(source)
|
|
114
|
+
|
|
115
|
+
annotation = <<~ANNOTATION
|
|
116
|
+
# ╔═══════════════════════════════════════════════════════════════════════╗
|
|
117
|
+
# ║ Service: #{class_name.ljust(60)}║
|
|
118
|
+
# ║ Entry Points: #{entry_points.join(', ').ljust(55)}║
|
|
119
|
+
# ╚═══════════════════════════════════════════════════════════════════════╝
|
|
120
|
+
|
|
121
|
+
ANNOTATION
|
|
122
|
+
|
|
123
|
+
annotation + source
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def detect_entry_points(source)
|
|
127
|
+
points = []
|
|
128
|
+
points << 'call' if source.match?(/def (self\.)?call\b/)
|
|
129
|
+
points << 'perform' if source.match?(/def (self\.)?perform\b/)
|
|
130
|
+
points << 'execute' if source.match?(/def (self\.)?execute\b/)
|
|
131
|
+
points << 'run' if source.match?(/def (self\.)?run\b/)
|
|
132
|
+
points << 'process' if source.match?(/def (self\.)?process\b/)
|
|
133
|
+
points.empty? ? ['unknown'] : points
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
137
|
+
# Metadata Extraction
|
|
138
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
def extract_metadata(source, _class_name, file_path)
|
|
141
|
+
{
|
|
142
|
+
# Entry points
|
|
143
|
+
public_methods: extract_public_methods(source),
|
|
144
|
+
entry_points: detect_entry_points(source),
|
|
145
|
+
class_methods: extract_class_methods(source),
|
|
146
|
+
|
|
147
|
+
# Patterns
|
|
148
|
+
is_callable: source.match?(/def (self\.)?call\b/),
|
|
149
|
+
is_interactor: source.match?(/include\s+Interactor/),
|
|
150
|
+
uses_dry_monads: source.match?(/include\s+Dry::Monads/),
|
|
151
|
+
|
|
152
|
+
# Dependency injection
|
|
153
|
+
initialize_params: extract_initialize_params(source),
|
|
154
|
+
injected_dependencies: extract_injected_deps(source),
|
|
155
|
+
|
|
156
|
+
# Error handling
|
|
157
|
+
custom_errors: extract_custom_errors(source),
|
|
158
|
+
rescues: extract_rescue_handlers(source),
|
|
159
|
+
|
|
160
|
+
# Return patterns
|
|
161
|
+
return_type: infer_return_type(source),
|
|
162
|
+
|
|
163
|
+
# Metrics
|
|
164
|
+
loc: source.lines.count { |l| l.strip.present? && !l.strip.start_with?('#') },
|
|
165
|
+
method_count: source.scan(/def\s+(?:self\.)?\w+/).size,
|
|
166
|
+
complexity: estimate_complexity(source),
|
|
167
|
+
|
|
168
|
+
# Directory context (what kind of service pattern)
|
|
169
|
+
service_type: infer_service_type(file_path)
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def extract_injected_deps(source)
|
|
174
|
+
# Look for attr_reader/accessor that match common dependency patterns
|
|
175
|
+
deps = []
|
|
176
|
+
|
|
177
|
+
source.scan(/attr_(?:reader|accessor)\s+(.+)/) do |match|
|
|
178
|
+
match[0].scan(/:(\w+)/).flatten.each do |attr|
|
|
179
|
+
deps << attr if attr.match?(/service|repository|client|adapter|gateway|notifier|mailer/)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Also look for initialize assignments
|
|
184
|
+
source.scan(/@(\w+)\s*=\s*(\w+)/) do |ivar, value|
|
|
185
|
+
deps << ivar if value.match?(/Service|Client|Repository|Adapter|Gateway/)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
deps.uniq
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def extract_custom_errors(source)
|
|
192
|
+
source.scan(/class\s+(\w+(?:Error|Exception))\s*</).flatten
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def extract_rescue_handlers(source)
|
|
196
|
+
source.scan(/rescue\s+([\w:]+)/).flatten.uniq
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def infer_return_type(source)
|
|
200
|
+
return :dry_monad if source.match?(/Success\(|Failure\(/)
|
|
201
|
+
return :result_object if source.match?(/Result\.new|OpenStruct\.new/)
|
|
202
|
+
return :boolean if source.match?(/def call.*?(?:true|false)\s*$/m)
|
|
203
|
+
|
|
204
|
+
:unknown
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def estimate_complexity(source)
|
|
208
|
+
# Simple cyclomatic complexity estimate
|
|
209
|
+
branches = source.scan(/\b(?:if|unless|elsif|when|while|until|for|rescue|&&|\|\|)\b/).size
|
|
210
|
+
branches + 1
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def infer_service_type(file_path)
|
|
214
|
+
case file_path
|
|
215
|
+
when /interactors/ then :interactor
|
|
216
|
+
when /operations/ then :operation
|
|
217
|
+
when /commands/ then :command
|
|
218
|
+
when /use_cases/ then :use_case
|
|
219
|
+
else :service
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
224
|
+
# Dependency Extraction
|
|
225
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
def extract_dependencies(source)
|
|
228
|
+
deps = scan_common_dependencies(source)
|
|
229
|
+
|
|
230
|
+
# Interactors
|
|
231
|
+
source.scan(/(\w+Interactor)(?:\.|::)/).flatten.uniq.each do |interactor|
|
|
232
|
+
deps << { type: :interactor, target: interactor, via: :code_reference }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# External API clients
|
|
236
|
+
source.scan(/(\w+Client)(?:\.|::new)/).flatten.uniq.each do |client|
|
|
237
|
+
deps << { type: :api_client, target: client, via: :code_reference }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# HTTP calls
|
|
241
|
+
if source.match?(/HTTParty|Faraday|RestClient|Net::HTTP/)
|
|
242
|
+
deps << { type: :external, target: :http_api, via: :code_reference }
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Redis
|
|
246
|
+
if source.match?(/Redis\.current|REDIS|Sidekiq\.redis/)
|
|
247
|
+
deps << { type: :infrastructure, target: :redis, via: :code_reference }
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
deps.uniq { |d| [d[:type], d[:target]] }
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../model_name_cache'
|
|
4
|
+
|
|
5
|
+
module CodebaseIndex
|
|
6
|
+
module Extractors
|
|
7
|
+
# Common dependency scanning patterns shared across extractors.
|
|
8
|
+
#
|
|
9
|
+
# Most extractors scan source code for the same four dependency types:
|
|
10
|
+
# model references (via ModelNameCache), service objects, background jobs,
|
|
11
|
+
# and mailers. This module centralizes those scanning patterns.
|
|
12
|
+
#
|
|
13
|
+
# Individual scan methods accept an optional +:via+ parameter so
|
|
14
|
+
# extractors can customize the relationship label (e.g., +:serialization+
|
|
15
|
+
# instead of the default +:code_reference+).
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# class FooExtractor
|
|
19
|
+
# include SharedDependencyScanner
|
|
20
|
+
#
|
|
21
|
+
# def extract_dependencies(source)
|
|
22
|
+
# deps = scan_common_dependencies(source)
|
|
23
|
+
# deps << { type: :custom, target: "Bar", via: :special }
|
|
24
|
+
# deps.uniq { |d| [d[:type], d[:target]] }
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
module SharedDependencyScanner
|
|
29
|
+
# Scan for ActiveRecord model references using the precomputed regex.
|
|
30
|
+
#
|
|
31
|
+
# @param source [String] Ruby source code to scan
|
|
32
|
+
# @param via [Symbol] Relationship label (default: :code_reference)
|
|
33
|
+
# @return [Array<Hash>] Dependency hashes with :type, :target, :via
|
|
34
|
+
def scan_model_dependencies(source, via: :code_reference)
|
|
35
|
+
source.scan(ModelNameCache.model_names_regex).uniq.map do |model_name|
|
|
36
|
+
{ type: :model, target: model_name, via: via }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Scan for service object references (e.g., FooService.call, FooService::new).
|
|
41
|
+
#
|
|
42
|
+
# @param source [String] Ruby source code to scan
|
|
43
|
+
# @param via [Symbol] Relationship label (default: :code_reference)
|
|
44
|
+
# @return [Array<Hash>] Dependency hashes
|
|
45
|
+
def scan_service_dependencies(source, via: :code_reference)
|
|
46
|
+
source.scan(/(\w+Service)(?:\.|::)/).flatten.uniq.map do |service|
|
|
47
|
+
{ type: :service, target: service, via: via }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Scan for background job references (e.g., FooJob.perform_later).
|
|
52
|
+
#
|
|
53
|
+
# @param source [String] Ruby source code to scan
|
|
54
|
+
# @param via [Symbol] Relationship label (default: :code_reference)
|
|
55
|
+
# @return [Array<Hash>] Dependency hashes
|
|
56
|
+
def scan_job_dependencies(source, via: :code_reference)
|
|
57
|
+
source.scan(/(\w+Job)\.perform/).flatten.uniq.map do |job|
|
|
58
|
+
{ type: :job, target: job, via: via }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Scan for mailer references (e.g., UserMailer.welcome_email).
|
|
63
|
+
#
|
|
64
|
+
# @param source [String] Ruby source code to scan
|
|
65
|
+
# @param via [Symbol] Relationship label (default: :code_reference)
|
|
66
|
+
# @return [Array<Hash>] Dependency hashes
|
|
67
|
+
def scan_mailer_dependencies(source, via: :code_reference)
|
|
68
|
+
source.scan(/(\w+Mailer)\./).flatten.uniq.map do |mailer|
|
|
69
|
+
{ type: :mailer, target: mailer, via: via }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Scan for all common dependency types and return a deduplicated array.
|
|
74
|
+
#
|
|
75
|
+
# Combines model, service, job, and mailer scans. Use this when an
|
|
76
|
+
# extractor needs all four standard dependency types with the default
|
|
77
|
+
# +:code_reference+ via label.
|
|
78
|
+
#
|
|
79
|
+
# @param source [String] Ruby source code to scan
|
|
80
|
+
# @return [Array<Hash>] Deduplicated dependency hashes
|
|
81
|
+
def scan_common_dependencies(source)
|
|
82
|
+
deps = []
|
|
83
|
+
deps.concat(scan_model_dependencies(source))
|
|
84
|
+
deps.concat(scan_service_dependencies(source))
|
|
85
|
+
deps.concat(scan_job_dependencies(source))
|
|
86
|
+
deps.concat(scan_mailer_dependencies(source))
|
|
87
|
+
deps.uniq { |d| [d[:type], d[:target]] }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CodebaseIndex
|
|
4
|
+
module Extractors
|
|
5
|
+
# Utility methods shared across multiple extractors.
|
|
6
|
+
#
|
|
7
|
+
# Provides common helpers for namespace extraction, public method
|
|
8
|
+
# scanning, class method scanning, and initialize parameter parsing.
|
|
9
|
+
# These methods are duplicated across 4-11 extractors; this module
|
|
10
|
+
# centralizes them.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# class FooExtractor
|
|
14
|
+
# include SharedUtilityMethods
|
|
15
|
+
#
|
|
16
|
+
# def extract_foo(klass)
|
|
17
|
+
# namespace = extract_namespace(klass)
|
|
18
|
+
# # ...
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
module SharedUtilityMethods
|
|
23
|
+
# Extract namespace from a class name string or class object.
|
|
24
|
+
#
|
|
25
|
+
# Handles both string input (e.g., "Payments::StripeService")
|
|
26
|
+
# and class object input (e.g., a Controller class).
|
|
27
|
+
#
|
|
28
|
+
# @param name_or_object [String, Class, Module] A class name or class object
|
|
29
|
+
# @return [String, nil] The namespace, or nil if top-level
|
|
30
|
+
def extract_namespace(name_or_object)
|
|
31
|
+
name = name_or_object.is_a?(String) ? name_or_object : name_or_object.name
|
|
32
|
+
parts = name.split('::')
|
|
33
|
+
parts.size > 1 ? parts[0..-2].join('::') : nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Extract public instance and class methods from source code.
|
|
37
|
+
#
|
|
38
|
+
# Walks source line-by-line tracking private/protected visibility.
|
|
39
|
+
# Returns method names that are in public scope and don't start with underscore.
|
|
40
|
+
#
|
|
41
|
+
# @param source [String] Ruby source code
|
|
42
|
+
# @return [Array<String>] Public method names
|
|
43
|
+
def extract_public_methods(source)
|
|
44
|
+
methods = []
|
|
45
|
+
in_private = false
|
|
46
|
+
in_protected = false
|
|
47
|
+
|
|
48
|
+
source.each_line do |line|
|
|
49
|
+
stripped = line.strip
|
|
50
|
+
|
|
51
|
+
in_private = true if stripped == 'private'
|
|
52
|
+
in_protected = true if stripped == 'protected'
|
|
53
|
+
in_private = false if stripped == 'public'
|
|
54
|
+
in_protected = false if stripped == 'public'
|
|
55
|
+
|
|
56
|
+
if !in_private && !in_protected && stripped =~ /def\s+((?:self\.)?\w+[?!=]?)/
|
|
57
|
+
method_name = ::Regexp.last_match(1)
|
|
58
|
+
methods << method_name unless method_name.start_with?('_')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
methods
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Extract class-level (self.) method names from source code.
|
|
66
|
+
#
|
|
67
|
+
# @param source [String] Ruby source code
|
|
68
|
+
# @return [Array<String>] Class method names
|
|
69
|
+
def extract_class_methods(source)
|
|
70
|
+
source.scan(/def\s+self\.(\w+[?!=]?)/).flatten
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Extract initialize parameters from source code.
|
|
74
|
+
#
|
|
75
|
+
# Parses the parameter list of the initialize method to determine
|
|
76
|
+
# parameter names, defaults, and whether they are keyword arguments.
|
|
77
|
+
#
|
|
78
|
+
# @param source [String] Ruby source code
|
|
79
|
+
# @return [Array<Hash>] Parameter info hashes with :name, :has_default, :keyword
|
|
80
|
+
def extract_initialize_params(source)
|
|
81
|
+
init_match = source.match(/def\s+initialize\s*\((.*?)\)/m)
|
|
82
|
+
return [] unless init_match
|
|
83
|
+
|
|
84
|
+
params_str = init_match[1]
|
|
85
|
+
params = []
|
|
86
|
+
|
|
87
|
+
params_str.scan(/(\w+)(?::\s*([^,\n]+))?/) do |name, default|
|
|
88
|
+
params << {
|
|
89
|
+
name: name,
|
|
90
|
+
has_default: !default.nil?,
|
|
91
|
+
keyword: params_str.include?("#{name}:")
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
params
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|