codebase_index 0.2.1 → 0.3.1
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 +4 -4
- data/CHANGELOG.md +60 -0
- data/README.md +95 -300
- data/exe/codebase-index-mcp +3 -31
- data/exe/codebase-index-mcp-http +3 -31
- data/lib/codebase_index/ast/method_extractor.rb +3 -8
- data/lib/codebase_index/ast/node.rb +28 -0
- data/lib/codebase_index/ast/parser.rb +53 -92
- data/lib/codebase_index/builder.rb +67 -4
- data/lib/codebase_index/cache/cache_middleware.rb +199 -0
- data/lib/codebase_index/cache/cache_store.rb +264 -0
- data/lib/codebase_index/cache/redis_cache_store.rb +116 -0
- data/lib/codebase_index/cache/solid_cache_store.rb +111 -0
- data/lib/codebase_index/chunking/semantic_chunker.rb +29 -24
- data/lib/codebase_index/console/adapters/good_job_adapter.rb +7 -40
- data/lib/codebase_index/console/adapters/job_adapter.rb +68 -0
- data/lib/codebase_index/console/adapters/sidekiq_adapter.rb +7 -40
- data/lib/codebase_index/console/adapters/solid_queue_adapter.rb +7 -40
- data/lib/codebase_index/console/bridge.rb +7 -0
- data/lib/codebase_index/console/console_response_renderer.rb +3 -7
- data/lib/codebase_index/console/embedded_executor.rb +2 -1
- data/lib/codebase_index/console/server.rb +1 -4
- data/lib/codebase_index/dependency_graph.rb +28 -19
- data/lib/codebase_index/embedding/indexer.rb +18 -8
- data/lib/codebase_index/embedding/openai.rb +27 -6
- data/lib/codebase_index/embedding/provider.rb +29 -2
- data/lib/codebase_index/evaluation/evaluator.rb +5 -12
- data/lib/codebase_index/extractor.rb +40 -44
- data/lib/codebase_index/extractors/action_cable_extractor.rb +9 -36
- data/lib/codebase_index/extractors/callback_analyzer.rb +22 -8
- data/lib/codebase_index/extractors/controller_extractor.rb +3 -93
- data/lib/codebase_index/extractors/decorator_extractor.rb +7 -14
- data/lib/codebase_index/extractors/engine_extractor.rb +20 -1
- data/lib/codebase_index/extractors/graphql_extractor.rb +4 -29
- data/lib/codebase_index/extractors/job_extractor.rb +11 -6
- data/lib/codebase_index/extractors/lib_extractor.rb +0 -31
- data/lib/codebase_index/extractors/mailer_extractor.rb +15 -85
- data/lib/codebase_index/extractors/manager_extractor.rb +1 -15
- data/lib/codebase_index/extractors/model_extractor.rb +20 -53
- data/lib/codebase_index/extractors/phlex_extractor.rb +8 -8
- data/lib/codebase_index/extractors/policy_extractor.rb +1 -24
- data/lib/codebase_index/extractors/poro_extractor.rb +0 -17
- data/lib/codebase_index/extractors/serializer_extractor.rb +12 -7
- data/lib/codebase_index/extractors/service_extractor.rb +1 -38
- data/lib/codebase_index/extractors/shared_utility_methods.rb +183 -1
- data/lib/codebase_index/extractors/validator_extractor.rb +3 -17
- data/lib/codebase_index/extractors/view_component_extractor.rb +10 -9
- data/lib/codebase_index/filename_utils.rb +32 -0
- data/lib/codebase_index/flow_analysis/operation_extractor.rb +1 -4
- data/lib/codebase_index/formatting/base.rb +0 -10
- data/lib/codebase_index/graph_analyzer.rb +1 -1
- data/lib/codebase_index/mcp/bootstrapper.rb +58 -0
- data/lib/codebase_index/mcp/renderers/markdown_renderer.rb +35 -34
- data/lib/codebase_index/mcp/renderers/plain_renderer.rb +29 -29
- data/lib/codebase_index/mcp/server.rb +59 -68
- data/lib/codebase_index/mcp/tool_response_renderer.rb +23 -0
- data/lib/codebase_index/notion/client.rb +2 -2
- data/lib/codebase_index/notion/mapper.rb +1 -0
- data/lib/codebase_index/notion/mappers/column_mapper.rb +3 -11
- data/lib/codebase_index/notion/mappers/model_mapper.rb +20 -23
- data/lib/codebase_index/notion/mappers/shared.rb +22 -0
- data/lib/codebase_index/observability/health_check.rb +0 -2
- data/lib/codebase_index/observability/structured_logger.rb +12 -30
- data/lib/codebase_index/operator/pipeline_guard.rb +0 -7
- data/lib/codebase_index/resilience/index_validator.rb +3 -21
- data/lib/codebase_index/retrieval/context_assembler.rb +19 -7
- data/lib/codebase_index/retrieval/query_classifier.rb +14 -12
- data/lib/codebase_index/retrieval/ranker.rb +6 -2
- data/lib/codebase_index/retrieval/search_executor.rb +8 -19
- data/lib/codebase_index/retriever.rb +1 -9
- data/lib/codebase_index/ruby_analyzer/class_analyzer.rb +5 -25
- data/lib/codebase_index/ruby_analyzer/dataflow_analyzer.rb +6 -7
- data/lib/codebase_index/ruby_analyzer/mermaid_renderer.rb +58 -53
- data/lib/codebase_index/ruby_analyzer/trace_enricher.rb +11 -7
- data/lib/codebase_index/session_tracer/file_store.rb +1 -8
- data/lib/codebase_index/session_tracer/redis_store.rb +1 -7
- data/lib/codebase_index/session_tracer/session_flow_assembler.rb +4 -13
- data/lib/codebase_index/session_tracer/solid_cache_store.rb +1 -7
- data/lib/codebase_index/session_tracer/store.rb +14 -0
- data/lib/codebase_index/storage/metadata_store.rb +37 -10
- data/lib/codebase_index/storage/pgvector.rb +37 -5
- data/lib/codebase_index/storage/qdrant.rb +39 -6
- data/lib/codebase_index/storage/vector_store.rb +11 -0
- data/lib/codebase_index/temporal/snapshot_store.rb +14 -10
- data/lib/codebase_index/token_utils.rb +19 -0
- data/lib/codebase_index/version.rb +1 -1
- data/lib/codebase_index.rb +25 -6
- data/lib/tasks/codebase_index.rake +2 -2
- metadata +11 -2
|
@@ -7,6 +7,7 @@ require 'open3'
|
|
|
7
7
|
require 'pathname'
|
|
8
8
|
require 'set'
|
|
9
9
|
|
|
10
|
+
require_relative 'filename_utils'
|
|
10
11
|
require_relative 'extracted_unit'
|
|
11
12
|
require_relative 'dependency_graph'
|
|
12
13
|
require_relative 'extractors/model_extractor'
|
|
@@ -62,6 +63,8 @@ module CodebaseIndex
|
|
|
62
63
|
# extractor.extract_changed(["app/models/user.rb", "app/services/checkout.rb"])
|
|
63
64
|
#
|
|
64
65
|
class Extractor
|
|
66
|
+
include FilenameUtils
|
|
67
|
+
|
|
65
68
|
# Directories under app/ that contain classes we need to extract.
|
|
66
69
|
# Used by eager_load_extraction_directories as a fallback when
|
|
67
70
|
# Rails.application.eager_load! fails (e.g., NameError from graphql/).
|
|
@@ -443,19 +446,24 @@ module CodebaseIndex
|
|
|
443
446
|
# ──────────────────────────────────────────────────────────────────────
|
|
444
447
|
|
|
445
448
|
def resolve_dependents
|
|
446
|
-
|
|
447
|
-
unit_map =
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
449
|
+
# Build complete unit map first (cross-type dependencies require all units indexed).
|
|
450
|
+
unit_map = @results.each_with_object({}) do |(_type, units), map|
|
|
451
|
+
units.each { |u| map[u.identifier] = u }
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Resolve dependents using the complete map.
|
|
455
|
+
@results.each_value do |units|
|
|
456
|
+
units.each do |unit|
|
|
457
|
+
unit.dependencies.each do |dep|
|
|
458
|
+
target_unit = unit_map[dep[:target]]
|
|
459
|
+
next unless target_unit
|
|
460
|
+
|
|
461
|
+
target_unit.dependents ||= []
|
|
462
|
+
target_unit.dependents << {
|
|
463
|
+
type: unit.type,
|
|
464
|
+
identifier: unit.identifier
|
|
465
|
+
}
|
|
466
|
+
end
|
|
459
467
|
end
|
|
460
468
|
end
|
|
461
469
|
end
|
|
@@ -480,7 +488,7 @@ module CodebaseIndex
|
|
|
480
488
|
# ──────────────────────────────────────────────────────────────────────
|
|
481
489
|
|
|
482
490
|
def precompute_flows
|
|
483
|
-
all_units = @results.values.flatten
|
|
491
|
+
all_units = @results.values.flatten(1)
|
|
484
492
|
precomputer = FlowPrecomputer.new(units: all_units, graph: @dependency_graph, output_dir: @output_dir.to_s)
|
|
485
493
|
flow_map = precomputer.precompute
|
|
486
494
|
Rails.logger.info "[CodebaseIndex] Precomputed #{flow_map.size} request flows"
|
|
@@ -585,6 +593,7 @@ module CodebaseIndex
|
|
|
585
593
|
result = {}
|
|
586
594
|
relative_paths.each { |rp| result[rp] = {} }
|
|
587
595
|
|
|
596
|
+
path_set = relative_paths.to_set
|
|
588
597
|
relative_paths.each_slice(500) do |batch|
|
|
589
598
|
log_output = run_git(
|
|
590
599
|
'log', '--all', '--name-only',
|
|
@@ -592,7 +601,7 @@ module CodebaseIndex
|
|
|
592
601
|
'--since=365 days ago',
|
|
593
602
|
'--', *batch
|
|
594
603
|
)
|
|
595
|
-
parse_git_log_output(log_output,
|
|
604
|
+
parse_git_log_output(log_output, path_set, result)
|
|
596
605
|
end
|
|
597
606
|
|
|
598
607
|
ninety_days_ago = (Time.current - 90.days).iso8601
|
|
@@ -708,9 +717,16 @@ module CodebaseIndex
|
|
|
708
717
|
def write_graph_analysis
|
|
709
718
|
return unless @graph_analysis
|
|
710
719
|
|
|
720
|
+
enriched = @graph_analysis.merge(
|
|
721
|
+
generated_at: Time.current.iso8601,
|
|
722
|
+
graph_sha: Digest::SHA256.hexdigest(
|
|
723
|
+
File.read(@output_dir.join('dependency_graph.json'))
|
|
724
|
+
)
|
|
725
|
+
)
|
|
726
|
+
|
|
711
727
|
File.write(
|
|
712
728
|
@output_dir.join('graph_analysis.json'),
|
|
713
|
-
json_serialize(
|
|
729
|
+
json_serialize(enriched)
|
|
714
730
|
)
|
|
715
731
|
end
|
|
716
732
|
|
|
@@ -725,7 +741,7 @@ module CodebaseIndex
|
|
|
725
741
|
|
|
726
742
|
# Total stats
|
|
727
743
|
total_units: @results.values.sum(&:size),
|
|
728
|
-
total_chunks: @results.
|
|
744
|
+
total_chunks: @results.sum { |_, units| units.sum { |u| u.chunks.size } },
|
|
729
745
|
|
|
730
746
|
# Git info
|
|
731
747
|
git_sha: run_git('rev-parse', 'HEAD').presence,
|
|
@@ -753,7 +769,7 @@ module CodebaseIndex
|
|
|
753
769
|
return if @results.empty?
|
|
754
770
|
|
|
755
771
|
total_units = @results.values.sum(&:size)
|
|
756
|
-
total_chunks = @results.
|
|
772
|
+
total_chunks = @results.sum { |_, units| units.sum { |u| [u.chunks.size, 1].max } }
|
|
757
773
|
category_count = @results.count { |_, units| units.any? }
|
|
758
774
|
|
|
759
775
|
summary = []
|
|
@@ -841,31 +857,11 @@ module CodebaseIndex
|
|
|
841
857
|
end
|
|
842
858
|
|
|
843
859
|
def schema_sha
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
# Generate a safe JSON filename from a unit identifier.
|
|
851
|
-
#
|
|
852
|
-
# @param identifier [String] Unit identifier (e.g., "Admin::UsersController")
|
|
853
|
-
# @return [String] Safe filename (e.g., "Admin__UsersController.json")
|
|
854
|
-
def safe_filename(identifier)
|
|
855
|
-
"#{identifier.gsub('::', '__').gsub(/[^a-zA-Z0-9_-]/, '_')}.json"
|
|
856
|
-
end
|
|
857
|
-
|
|
858
|
-
# Generate a collision-safe JSON filename by appending a short digest.
|
|
859
|
-
# Unlike safe_filename, this guarantees distinct filenames even when two
|
|
860
|
-
# identifiers differ only in characters that safe_filename normalizes
|
|
861
|
-
# (e.g., "GET /foo/bar" vs "GET /foo_bar" both become "GET__foo_bar.json").
|
|
862
|
-
#
|
|
863
|
-
# @param identifier [String] Unit identifier
|
|
864
|
-
# @return [String] Collision-safe filename (e.g., "GET__foo_bar_a1b2c3d4.json")
|
|
865
|
-
def collision_safe_filename(identifier)
|
|
866
|
-
base = identifier.gsub('::', '__').gsub(/[^a-zA-Z0-9_-]/, '_')
|
|
867
|
-
digest = ::Digest::SHA256.hexdigest(identifier)[0, 8]
|
|
868
|
-
"#{base}_#{digest}.json"
|
|
860
|
+
%w[db/schema.rb db/structure.sql].each do |path|
|
|
861
|
+
full = Rails.root.join(path)
|
|
862
|
+
return Digest::SHA256.file(full).hexdigest if full.exist?
|
|
863
|
+
end
|
|
864
|
+
nil
|
|
869
865
|
end
|
|
870
866
|
|
|
871
867
|
def json_serialize(data)
|
|
@@ -878,7 +874,7 @@ module CodebaseIndex
|
|
|
878
874
|
|
|
879
875
|
def log_summary
|
|
880
876
|
total = @results.values.sum(&:size)
|
|
881
|
-
chunks = @results.
|
|
877
|
+
chunks = @results.sum { |_, units| units.sum { |u| u.chunks.size } }
|
|
882
878
|
|
|
883
879
|
Rails.logger.info '[CodebaseIndex] ═══════════════════════════════════════════'
|
|
884
880
|
Rails.logger.info '[CodebaseIndex] Extraction Complete'
|
|
@@ -50,7 +50,7 @@ module CodebaseIndex
|
|
|
50
50
|
# @return [ExtractedUnit, nil]
|
|
51
51
|
def extract_channel(klass)
|
|
52
52
|
name = klass.name
|
|
53
|
-
file_path =
|
|
53
|
+
file_path = source_file_for(klass, name)
|
|
54
54
|
source = read_source(file_path)
|
|
55
55
|
own_methods = klass.instance_methods(false)
|
|
56
56
|
|
|
@@ -89,48 +89,21 @@ module CodebaseIndex
|
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
#
|
|
92
|
+
# Locate the source file for a channel class.
|
|
93
93
|
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
94
|
+
# Convention path first, then introspection via {#resolve_source_location}
|
|
95
|
+
# which filters out vendor/node_modules paths.
|
|
96
96
|
#
|
|
97
97
|
# @param klass [Class] The channel class
|
|
98
98
|
# @param name [String] The channel class name
|
|
99
99
|
# @return [String, nil]
|
|
100
|
-
def
|
|
101
|
-
path = source_location_from_methods(klass)
|
|
102
|
-
return path if path
|
|
103
|
-
|
|
104
|
-
convention_fallback(name)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Try to get source_location from the channel's instance methods.
|
|
108
|
-
# Tries subscribed first, then any other instance method.
|
|
109
|
-
#
|
|
110
|
-
# @param klass [Class] The channel class
|
|
111
|
-
# @return [String, nil]
|
|
112
|
-
def source_location_from_methods(klass)
|
|
113
|
-
try_methods = [:subscribed] + (klass.instance_methods(false) - [:subscribed])
|
|
114
|
-
try_methods.each do |method_name|
|
|
115
|
-
location = klass.instance_method(method_name).source_location
|
|
116
|
-
return location[0] if location
|
|
117
|
-
rescue NameError, TypeError
|
|
118
|
-
next
|
|
119
|
-
end
|
|
120
|
-
nil
|
|
121
|
-
rescue StandardError
|
|
122
|
-
nil
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Fall back to Rails convention path for channel files.
|
|
126
|
-
#
|
|
127
|
-
# @param name [String] Channel class name
|
|
128
|
-
# @return [String, nil]
|
|
129
|
-
def convention_fallback(name)
|
|
100
|
+
def source_file_for(klass, name)
|
|
130
101
|
return nil unless defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
131
102
|
|
|
132
|
-
|
|
133
|
-
File.exist?(
|
|
103
|
+
convention_path = Rails.root.join('app', 'channels', "#{name.underscore}.rb").to_s
|
|
104
|
+
return convention_path if File.exist?(convention_path)
|
|
105
|
+
|
|
106
|
+
resolve_source_location(klass, app_root: Rails.root.to_s, fallback: nil)
|
|
134
107
|
end
|
|
135
108
|
|
|
136
109
|
# Read source code from a file path.
|
|
@@ -34,6 +34,21 @@ module CodebaseIndex
|
|
|
34
34
|
# Async enqueue methods that indicate a job is being dispatched.
|
|
35
35
|
ASYNC_METHODS = %w[perform_later perform_async perform_in perform_at].freeze
|
|
36
36
|
|
|
37
|
+
# Pre-compiled regex patterns (avoid dynamic regex construction in hot loops)
|
|
38
|
+
SINGLE_COLUMN_WRITER_PATTERNS = SINGLE_COLUMN_WRITERS.to_h do |w|
|
|
39
|
+
[w, /\b#{Regexp.escape(w)}\s*\(?\s*[:'"](\w+)/]
|
|
40
|
+
end.freeze
|
|
41
|
+
|
|
42
|
+
MULTI_COLUMN_WRITER_PATTERNS = MULTI_COLUMN_WRITERS.to_h do |w|
|
|
43
|
+
[w, /\b#{Regexp.escape(w)}\s*\(([^)]+)\)/m]
|
|
44
|
+
end.freeze
|
|
45
|
+
|
|
46
|
+
ASYNC_PATTERN = /(\w+(?:Job|Worker))\.(?:#{ASYNC_METHODS.map { |m| Regexp.escape(m) }.join('|')})/
|
|
47
|
+
|
|
48
|
+
DB_READ_PATTERNS = DB_READ_METHODS.to_h do |m|
|
|
49
|
+
[m, /\.#{Regexp.escape(m)}\b/]
|
|
50
|
+
end.freeze
|
|
51
|
+
|
|
37
52
|
# @param source_code [String] Composite model source (with inlined concerns)
|
|
38
53
|
# @param column_names [Array<String>] Model's database column names
|
|
39
54
|
def initialize(source_code:, column_names: [])
|
|
@@ -141,15 +156,15 @@ module CodebaseIndex
|
|
|
141
156
|
end
|
|
142
157
|
|
|
143
158
|
# Pattern: update_column(:col, ...) / write_attribute(:col, ...)
|
|
144
|
-
|
|
145
|
-
method_source.scan(
|
|
159
|
+
SINGLE_COLUMN_WRITER_PATTERNS.each_value do |pattern|
|
|
160
|
+
method_source.scan(pattern).flatten.each do |col|
|
|
146
161
|
columns << col if @column_names.include?(col)
|
|
147
162
|
end
|
|
148
163
|
end
|
|
149
164
|
|
|
150
165
|
# Pattern: update_columns(col: ...) / assign_attributes(col: ...)
|
|
151
|
-
|
|
152
|
-
method_source.scan(
|
|
166
|
+
MULTI_COLUMN_WRITER_PATTERNS.each_value do |pattern|
|
|
167
|
+
method_source.scan(pattern).each do |match|
|
|
153
168
|
match[0].scan(/\b(\w+)\s*:(?!:)/).flatten.each do |col|
|
|
154
169
|
columns << col if @column_names.include?(col)
|
|
155
170
|
end
|
|
@@ -166,8 +181,7 @@ module CodebaseIndex
|
|
|
166
181
|
# @param method_source [String]
|
|
167
182
|
# @return [Array<String>]
|
|
168
183
|
def detect_jobs_enqueued(method_source)
|
|
169
|
-
|
|
170
|
-
method_source.scan(/(\w+(?:Job|Worker))\.(?:#{async_pattern})/).flatten.uniq.sort
|
|
184
|
+
method_source.scan(ASYNC_PATTERN).flatten.uniq.sort
|
|
171
185
|
end
|
|
172
186
|
|
|
173
187
|
# Detect service objects called by the callback method.
|
|
@@ -197,8 +211,8 @@ module CodebaseIndex
|
|
|
197
211
|
# @param method_source [String]
|
|
198
212
|
# @return [Array<String>]
|
|
199
213
|
def detect_database_reads(method_source)
|
|
200
|
-
|
|
201
|
-
method_source.match?(
|
|
214
|
+
DB_READ_PATTERNS.filter_map do |method, pattern|
|
|
215
|
+
method if method_source.match?(pattern)
|
|
202
216
|
end
|
|
203
217
|
end
|
|
204
218
|
|
|
@@ -115,40 +115,16 @@ module CodebaseIndex
|
|
|
115
115
|
|
|
116
116
|
# Find the source file for a controller, validating paths are within Rails.root.
|
|
117
117
|
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
118
|
+
# Convention path first, then introspection via {#resolve_source_location}
|
|
119
|
+
# which filters out vendor/node_modules paths.
|
|
120
120
|
#
|
|
121
121
|
# @param controller [Class] The controller class
|
|
122
122
|
# @return [String] Absolute path to the controller source file
|
|
123
123
|
def source_file_for(controller)
|
|
124
|
-
app_root = Rails.root.to_s
|
|
125
124
|
convention_path = Rails.root.join("app/controllers/#{controller.name.underscore}.rb").to_s
|
|
126
|
-
|
|
127
|
-
# Tier 1: Instance methods defined directly on this controller
|
|
128
|
-
controller.instance_methods(false).each do |method_name|
|
|
129
|
-
loc = controller.instance_method(method_name).source_location&.first
|
|
130
|
-
return loc if loc&.start_with?(app_root)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Tier 2: Class/singleton methods defined on this controller
|
|
134
|
-
controller.methods(false).each do |method_name|
|
|
135
|
-
loc = controller.method(method_name).source_location&.first
|
|
136
|
-
return loc if loc&.start_with?(app_root)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Tier 3: Convention path if file exists
|
|
140
125
|
return convention_path if File.exist?(convention_path)
|
|
141
126
|
|
|
142
|
-
|
|
143
|
-
if Object.respond_to?(:const_source_location)
|
|
144
|
-
loc = Object.const_source_location(controller.name)&.first
|
|
145
|
-
return loc if loc&.start_with?(app_root)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# Tier 5: Always return convention path — never a gem path
|
|
149
|
-
convention_path
|
|
150
|
-
rescue StandardError
|
|
151
|
-
Rails.root.join("app/controllers/#{controller.name.underscore}.rb").to_s
|
|
127
|
+
resolve_source_location(controller, app_root: Rails.root.to_s, fallback: convention_path)
|
|
152
128
|
end
|
|
153
129
|
|
|
154
130
|
# Build composite source with routes and filters as headers
|
|
@@ -228,72 +204,6 @@ module CodebaseIndex
|
|
|
228
204
|
end
|
|
229
205
|
end
|
|
230
206
|
|
|
231
|
-
# Extract :only/:except action lists and :if/:unless conditions from a callback.
|
|
232
|
-
#
|
|
233
|
-
# Modern Rails (4.2+) stores conditions in @if/@unless ivar arrays.
|
|
234
|
-
# ActionFilter objects hold action Sets; other conditions are procs/symbols.
|
|
235
|
-
#
|
|
236
|
-
# @param callback [ActiveSupport::Callbacks::Callback]
|
|
237
|
-
# @return [Array(Array<String>, Array<String>, Array<String>, Array<String>)]
|
|
238
|
-
# [only_actions, except_actions, if_labels, unless_labels]
|
|
239
|
-
def extract_callback_conditions(callback)
|
|
240
|
-
if_conditions = callback.instance_variable_get(:@if) || []
|
|
241
|
-
unless_conditions = callback.instance_variable_get(:@unless) || []
|
|
242
|
-
|
|
243
|
-
only = []
|
|
244
|
-
except = []
|
|
245
|
-
if_labels = []
|
|
246
|
-
unless_labels = []
|
|
247
|
-
|
|
248
|
-
if_conditions.each do |cond|
|
|
249
|
-
actions = extract_action_filter_actions(cond)
|
|
250
|
-
if actions
|
|
251
|
-
only.concat(actions)
|
|
252
|
-
else
|
|
253
|
-
if_labels << condition_label(cond)
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
unless_conditions.each do |cond|
|
|
258
|
-
actions = extract_action_filter_actions(cond)
|
|
259
|
-
if actions
|
|
260
|
-
except.concat(actions)
|
|
261
|
-
else
|
|
262
|
-
unless_labels << condition_label(cond)
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
[only, except, if_labels, unless_labels]
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
# Extract action names from an ActionFilter-like condition object.
|
|
270
|
-
# Duck-types on the @actions ivar being a Set, avoiding dependence
|
|
271
|
-
# on private class names across Rails versions.
|
|
272
|
-
#
|
|
273
|
-
# @param condition [Object] A condition from the callback's @if/@unless array
|
|
274
|
-
# @return [Array<String>, nil] Action names, or nil if not an ActionFilter
|
|
275
|
-
def extract_action_filter_actions(condition)
|
|
276
|
-
return nil unless condition.instance_variable_defined?(:@actions)
|
|
277
|
-
|
|
278
|
-
actions = condition.instance_variable_get(:@actions)
|
|
279
|
-
return nil unless actions.is_a?(Set)
|
|
280
|
-
|
|
281
|
-
actions.to_a
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
# Human-readable label for a non-ActionFilter condition.
|
|
285
|
-
#
|
|
286
|
-
# @param condition [Object] A proc, symbol, or other condition
|
|
287
|
-
# @return [String]
|
|
288
|
-
def condition_label(condition)
|
|
289
|
-
case condition
|
|
290
|
-
when Symbol then ":#{condition}"
|
|
291
|
-
when Proc then 'Proc'
|
|
292
|
-
when String then condition
|
|
293
|
-
else condition.class.name
|
|
294
|
-
end
|
|
295
|
-
end
|
|
296
|
-
|
|
297
207
|
# ──────────────────────────────────────────────────────────────────────
|
|
298
208
|
# Metadata Extraction
|
|
299
209
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -90,11 +90,10 @@ module CodebaseIndex
|
|
|
90
90
|
# Class Discovery
|
|
91
91
|
# ──────────────────────────────────────────────────────────────────────
|
|
92
92
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
# UserDecorator → "Admin::UserDecorator").
|
|
93
|
+
# Override SharedUtilityMethods#extract_class_name for decorator-specific
|
|
94
|
+
# namespace resolution. The shared version only matches `class Foo::Bar`
|
|
95
|
+
# (inline namespacing); this version also handles `module Admin / class
|
|
96
|
+
# UserDecorator` (block namespacing) by scanning for enclosing modules.
|
|
98
97
|
#
|
|
99
98
|
# @param file_path [String] Path to the file
|
|
100
99
|
# @param source [String] Ruby source code
|
|
@@ -119,14 +118,6 @@ module CodebaseIndex
|
|
|
119
118
|
end
|
|
120
119
|
end
|
|
121
120
|
|
|
122
|
-
# Skip module-only files (concerns, base modules without a class).
|
|
123
|
-
#
|
|
124
|
-
# @param source [String] Ruby source code
|
|
125
|
-
# @return [Boolean]
|
|
126
|
-
def skip_file?(source)
|
|
127
|
-
source.match?(/^\s*module\s+\w+\s*$/) && !source.match?(/^\s*class\s+/)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
121
|
# ──────────────────────────────────────────────────────────────────────
|
|
131
122
|
# Source Annotation
|
|
132
123
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -220,7 +211,9 @@ module CodebaseIndex
|
|
|
220
211
|
methods.uniq
|
|
221
212
|
end
|
|
222
213
|
|
|
223
|
-
#
|
|
214
|
+
# Override SharedUtilityMethods#detect_entry_points with decorator-specific
|
|
215
|
+
# entry points (decorate, present, to_partial_path) instead of the generic
|
|
216
|
+
# service-oriented ones (perform, execute, run, process).
|
|
224
217
|
#
|
|
225
218
|
# @param source [String] Ruby source code
|
|
226
219
|
# @return [Array<String>] Entry point method names
|
|
@@ -132,7 +132,8 @@ module CodebaseIndex
|
|
|
132
132
|
mounted_path: mounted_path,
|
|
133
133
|
route_count: route_count,
|
|
134
134
|
isolate_namespace: isolated,
|
|
135
|
-
controllers: controllers
|
|
135
|
+
controllers: controllers,
|
|
136
|
+
engine_source: framework_engine?(engine) ? :framework : :application
|
|
136
137
|
}
|
|
137
138
|
unit.dependencies = build_engine_dependencies(controllers)
|
|
138
139
|
|
|
@@ -142,6 +143,24 @@ module CodebaseIndex
|
|
|
142
143
|
nil
|
|
143
144
|
end
|
|
144
145
|
|
|
146
|
+
# Check if an engine is a framework gem rather than an application engine.
|
|
147
|
+
#
|
|
148
|
+
# An engine is framework if it lives outside Rails.root, or inside
|
|
149
|
+
# Rails.root but under vendor/bundle or bundler/gems (common in Docker
|
|
150
|
+
# where Rails.root is /app and gems install to /app/vendor/bundle).
|
|
151
|
+
#
|
|
152
|
+
# @param engine [Class] A Rails::Engine subclass
|
|
153
|
+
# @return [Boolean]
|
|
154
|
+
def framework_engine?(engine)
|
|
155
|
+
root = engine.root.to_s
|
|
156
|
+
|
|
157
|
+
# Engine outside Rails.root is definitely framework
|
|
158
|
+
return true unless root.start_with?(Rails.root.to_s)
|
|
159
|
+
|
|
160
|
+
# Engine inside Rails.root but in vendor/bundler paths is framework
|
|
161
|
+
root.include?('/vendor/') || root.include?('/bundler/gems/')
|
|
162
|
+
end
|
|
163
|
+
|
|
145
164
|
# Count routes defined by an engine.
|
|
146
165
|
#
|
|
147
166
|
# @param engine [Class] A Rails::Engine subclass
|
|
@@ -202,39 +202,16 @@ module CodebaseIndex
|
|
|
202
202
|
# Determine the source file for a runtime-loaded class, validating that
|
|
203
203
|
# paths are within Rails.root to avoid returning graphql gem internals.
|
|
204
204
|
#
|
|
205
|
-
#
|
|
205
|
+
# Convention path first, then introspection via {#resolve_source_location}
|
|
206
|
+
# which filters out vendor/node_modules paths.
|
|
206
207
|
#
|
|
207
208
|
# @param klass [Class]
|
|
208
209
|
# @return [String] Absolute path to the source file
|
|
209
210
|
def source_file_for_class(klass)
|
|
210
|
-
app_root = Rails.root.to_s
|
|
211
211
|
convention_path = Rails.root.join("#{GRAPHQL_DIRECTORY}/#{klass.name.underscore}.rb").to_s
|
|
212
|
-
|
|
213
|
-
# Tier 1: Instance methods defined directly on this class
|
|
214
|
-
klass.instance_methods(false).each do |method_name|
|
|
215
|
-
loc = klass.instance_method(method_name).source_location&.first
|
|
216
|
-
return loc if loc&.start_with?(app_root)
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
# Tier 2: Singleton methods defined on this class
|
|
220
|
-
klass.singleton_methods(false).each do |method_name|
|
|
221
|
-
loc = klass.method(method_name).source_location&.first
|
|
222
|
-
return loc if loc&.start_with?(app_root)
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
# Tier 3: Convention path if file exists
|
|
226
212
|
return convention_path if File.exist?(convention_path)
|
|
227
213
|
|
|
228
|
-
|
|
229
|
-
if Object.respond_to?(:const_source_location)
|
|
230
|
-
loc = Object.const_source_location(klass.name)&.first
|
|
231
|
-
return loc if loc&.start_with?(app_root)
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
# Tier 5: Always return convention path — never a gem path
|
|
235
|
-
convention_path
|
|
236
|
-
rescue StandardError
|
|
237
|
-
Rails.root.join("#{GRAPHQL_DIRECTORY}/#{klass.name.underscore}.rb").to_s
|
|
214
|
+
resolve_source_location(klass, app_root: Rails.root.to_s, fallback: convention_path)
|
|
238
215
|
end
|
|
239
216
|
|
|
240
217
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -619,9 +596,7 @@ module CodebaseIndex
|
|
|
619
596
|
# @return [Array<String>]
|
|
620
597
|
def extract_connections(source)
|
|
621
598
|
# field :items, Types::ItemType.connection_type
|
|
622
|
-
connections = source.scan(/([\w:]+)\.connection_type/).flatten
|
|
623
|
-
type
|
|
624
|
-
end
|
|
599
|
+
connections = source.scan(/([\w:]+)\.connection_type/).flatten
|
|
625
600
|
|
|
626
601
|
# connection_type_class ConnectionType
|
|
627
602
|
source.scan(/connection_type_class\s+([\w:]+)/).flatten.each do |type|
|
|
@@ -152,13 +152,18 @@ module CodebaseIndex
|
|
|
152
152
|
source.match?(/def perform/)
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
+
# Locate the source file for a job class.
|
|
156
|
+
#
|
|
157
|
+
# Convention path first, then introspection via {#resolve_source_location}
|
|
158
|
+
# which filters out vendor/node_modules paths.
|
|
159
|
+
#
|
|
160
|
+
# @param job_class [Class]
|
|
161
|
+
# @return [String, nil]
|
|
155
162
|
def source_file_for(job_class)
|
|
156
|
-
|
|
157
|
-
if
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
rescue StandardError
|
|
161
|
-
nil
|
|
163
|
+
convention_path = Rails.root.join("app/jobs/#{job_class.name.underscore}.rb").to_s
|
|
164
|
+
return convention_path if File.exist?(convention_path)
|
|
165
|
+
|
|
166
|
+
resolve_source_location(job_class, app_root: Rails.root.to_s, fallback: convention_path)
|
|
162
167
|
end
|
|
163
168
|
|
|
164
169
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -201,37 +201,6 @@ module CodebaseIndex
|
|
|
201
201
|
}
|
|
202
202
|
end
|
|
203
203
|
|
|
204
|
-
# Extract the parent class name from a class definition.
|
|
205
|
-
#
|
|
206
|
-
# @param source [String] Ruby source code
|
|
207
|
-
# @return [String, nil] Parent class name or nil
|
|
208
|
-
def extract_parent_class(source)
|
|
209
|
-
match = source.match(/^\s*class\s+[\w:]+\s*<\s*([\w:]+)/)
|
|
210
|
-
match ? match[1] : nil
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# Count non-blank, non-comment lines of code.
|
|
214
|
-
#
|
|
215
|
-
# @param source [String] Ruby source code
|
|
216
|
-
# @return [Integer] LOC count
|
|
217
|
-
def count_loc(source)
|
|
218
|
-
source.lines.count { |l| l.strip.length.positive? && !l.strip.start_with?('#') }
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
# Detect common entry point methods.
|
|
222
|
-
#
|
|
223
|
-
# @param source [String] Ruby source code
|
|
224
|
-
# @return [Array<String>] Entry point method names
|
|
225
|
-
def detect_entry_points(source)
|
|
226
|
-
points = []
|
|
227
|
-
points << 'call' if source.match?(/def (self\.)?call\b/)
|
|
228
|
-
points << 'perform' if source.match?(/def (self\.)?perform\b/)
|
|
229
|
-
points << 'execute' if source.match?(/def (self\.)?execute\b/)
|
|
230
|
-
points << 'run' if source.match?(/def (self\.)?run\b/)
|
|
231
|
-
points << 'process' if source.match?(/def (self\.)?process\b/)
|
|
232
|
-
points.empty? ? ['unknown'] : points
|
|
233
|
-
end
|
|
234
|
-
|
|
235
204
|
# ──────────────────────────────────────────────────────────────────────
|
|
236
205
|
# Dependency Extraction
|
|
237
206
|
# ──────────────────────────────────────────────────────────────────────
|