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.
Files changed (171) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +29 -0
  3. data/CODE_OF_CONDUCT.md +83 -0
  4. data/CONTRIBUTING.md +65 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +481 -0
  7. data/exe/codebase-console-mcp +22 -0
  8. data/exe/codebase-index-mcp +61 -0
  9. data/exe/codebase-index-mcp-http +64 -0
  10. data/exe/codebase-index-mcp-start +58 -0
  11. data/lib/codebase_index/ast/call_site_extractor.rb +106 -0
  12. data/lib/codebase_index/ast/method_extractor.rb +76 -0
  13. data/lib/codebase_index/ast/node.rb +88 -0
  14. data/lib/codebase_index/ast/parser.rb +653 -0
  15. data/lib/codebase_index/ast.rb +6 -0
  16. data/lib/codebase_index/builder.rb +137 -0
  17. data/lib/codebase_index/chunking/chunk.rb +84 -0
  18. data/lib/codebase_index/chunking/semantic_chunker.rb +290 -0
  19. data/lib/codebase_index/console/adapters/cache_adapter.rb +58 -0
  20. data/lib/codebase_index/console/adapters/good_job_adapter.rb +66 -0
  21. data/lib/codebase_index/console/adapters/sidekiq_adapter.rb +66 -0
  22. data/lib/codebase_index/console/adapters/solid_queue_adapter.rb +66 -0
  23. data/lib/codebase_index/console/audit_logger.rb +75 -0
  24. data/lib/codebase_index/console/bridge.rb +170 -0
  25. data/lib/codebase_index/console/confirmation.rb +90 -0
  26. data/lib/codebase_index/console/connection_manager.rb +173 -0
  27. data/lib/codebase_index/console/console_response_renderer.rb +78 -0
  28. data/lib/codebase_index/console/model_validator.rb +81 -0
  29. data/lib/codebase_index/console/safe_context.rb +82 -0
  30. data/lib/codebase_index/console/server.rb +557 -0
  31. data/lib/codebase_index/console/sql_validator.rb +172 -0
  32. data/lib/codebase_index/console/tools/tier1.rb +118 -0
  33. data/lib/codebase_index/console/tools/tier2.rb +117 -0
  34. data/lib/codebase_index/console/tools/tier3.rb +110 -0
  35. data/lib/codebase_index/console/tools/tier4.rb +79 -0
  36. data/lib/codebase_index/coordination/pipeline_lock.rb +109 -0
  37. data/lib/codebase_index/cost_model/embedding_cost.rb +88 -0
  38. data/lib/codebase_index/cost_model/estimator.rb +128 -0
  39. data/lib/codebase_index/cost_model/provider_pricing.rb +67 -0
  40. data/lib/codebase_index/cost_model/storage_cost.rb +52 -0
  41. data/lib/codebase_index/cost_model.rb +22 -0
  42. data/lib/codebase_index/db/migrations/001_create_units.rb +38 -0
  43. data/lib/codebase_index/db/migrations/002_create_edges.rb +35 -0
  44. data/lib/codebase_index/db/migrations/003_create_embeddings.rb +37 -0
  45. data/lib/codebase_index/db/migrations/004_create_snapshots.rb +45 -0
  46. data/lib/codebase_index/db/migrations/005_create_snapshot_units.rb +40 -0
  47. data/lib/codebase_index/db/migrator.rb +71 -0
  48. data/lib/codebase_index/db/schema_version.rb +73 -0
  49. data/lib/codebase_index/dependency_graph.rb +227 -0
  50. data/lib/codebase_index/embedding/indexer.rb +130 -0
  51. data/lib/codebase_index/embedding/openai.rb +105 -0
  52. data/lib/codebase_index/embedding/provider.rb +135 -0
  53. data/lib/codebase_index/embedding/text_preparer.rb +112 -0
  54. data/lib/codebase_index/evaluation/baseline_runner.rb +115 -0
  55. data/lib/codebase_index/evaluation/evaluator.rb +146 -0
  56. data/lib/codebase_index/evaluation/metrics.rb +79 -0
  57. data/lib/codebase_index/evaluation/query_set.rb +148 -0
  58. data/lib/codebase_index/evaluation/report_generator.rb +90 -0
  59. data/lib/codebase_index/extracted_unit.rb +145 -0
  60. data/lib/codebase_index/extractor.rb +956 -0
  61. data/lib/codebase_index/extractors/action_cable_extractor.rb +228 -0
  62. data/lib/codebase_index/extractors/ast_source_extraction.rb +46 -0
  63. data/lib/codebase_index/extractors/behavioral_profile.rb +309 -0
  64. data/lib/codebase_index/extractors/caching_extractor.rb +261 -0
  65. data/lib/codebase_index/extractors/callback_analyzer.rb +232 -0
  66. data/lib/codebase_index/extractors/concern_extractor.rb +253 -0
  67. data/lib/codebase_index/extractors/configuration_extractor.rb +219 -0
  68. data/lib/codebase_index/extractors/controller_extractor.rb +494 -0
  69. data/lib/codebase_index/extractors/database_view_extractor.rb +278 -0
  70. data/lib/codebase_index/extractors/decorator_extractor.rb +260 -0
  71. data/lib/codebase_index/extractors/engine_extractor.rb +204 -0
  72. data/lib/codebase_index/extractors/event_extractor.rb +211 -0
  73. data/lib/codebase_index/extractors/factory_extractor.rb +289 -0
  74. data/lib/codebase_index/extractors/graphql_extractor.rb +917 -0
  75. data/lib/codebase_index/extractors/i18n_extractor.rb +117 -0
  76. data/lib/codebase_index/extractors/job_extractor.rb +369 -0
  77. data/lib/codebase_index/extractors/lib_extractor.rb +249 -0
  78. data/lib/codebase_index/extractors/mailer_extractor.rb +339 -0
  79. data/lib/codebase_index/extractors/manager_extractor.rb +202 -0
  80. data/lib/codebase_index/extractors/middleware_extractor.rb +133 -0
  81. data/lib/codebase_index/extractors/migration_extractor.rb +469 -0
  82. data/lib/codebase_index/extractors/model_extractor.rb +960 -0
  83. data/lib/codebase_index/extractors/phlex_extractor.rb +252 -0
  84. data/lib/codebase_index/extractors/policy_extractor.rb +214 -0
  85. data/lib/codebase_index/extractors/poro_extractor.rb +246 -0
  86. data/lib/codebase_index/extractors/pundit_extractor.rb +223 -0
  87. data/lib/codebase_index/extractors/rails_source_extractor.rb +473 -0
  88. data/lib/codebase_index/extractors/rake_task_extractor.rb +343 -0
  89. data/lib/codebase_index/extractors/route_extractor.rb +181 -0
  90. data/lib/codebase_index/extractors/scheduled_job_extractor.rb +331 -0
  91. data/lib/codebase_index/extractors/serializer_extractor.rb +334 -0
  92. data/lib/codebase_index/extractors/service_extractor.rb +254 -0
  93. data/lib/codebase_index/extractors/shared_dependency_scanner.rb +91 -0
  94. data/lib/codebase_index/extractors/shared_utility_methods.rb +99 -0
  95. data/lib/codebase_index/extractors/state_machine_extractor.rb +398 -0
  96. data/lib/codebase_index/extractors/test_mapping_extractor.rb +225 -0
  97. data/lib/codebase_index/extractors/validator_extractor.rb +225 -0
  98. data/lib/codebase_index/extractors/view_component_extractor.rb +310 -0
  99. data/lib/codebase_index/extractors/view_template_extractor.rb +261 -0
  100. data/lib/codebase_index/feedback/gap_detector.rb +89 -0
  101. data/lib/codebase_index/feedback/store.rb +119 -0
  102. data/lib/codebase_index/flow_analysis/operation_extractor.rb +209 -0
  103. data/lib/codebase_index/flow_analysis/response_code_mapper.rb +154 -0
  104. data/lib/codebase_index/flow_assembler.rb +290 -0
  105. data/lib/codebase_index/flow_document.rb +191 -0
  106. data/lib/codebase_index/flow_precomputer.rb +102 -0
  107. data/lib/codebase_index/formatting/base.rb +40 -0
  108. data/lib/codebase_index/formatting/claude_adapter.rb +98 -0
  109. data/lib/codebase_index/formatting/generic_adapter.rb +56 -0
  110. data/lib/codebase_index/formatting/gpt_adapter.rb +64 -0
  111. data/lib/codebase_index/formatting/human_adapter.rb +78 -0
  112. data/lib/codebase_index/graph_analyzer.rb +374 -0
  113. data/lib/codebase_index/mcp/index_reader.rb +394 -0
  114. data/lib/codebase_index/mcp/renderers/claude_renderer.rb +81 -0
  115. data/lib/codebase_index/mcp/renderers/json_renderer.rb +17 -0
  116. data/lib/codebase_index/mcp/renderers/markdown_renderer.rb +352 -0
  117. data/lib/codebase_index/mcp/renderers/plain_renderer.rb +240 -0
  118. data/lib/codebase_index/mcp/server.rb +935 -0
  119. data/lib/codebase_index/mcp/tool_response_renderer.rb +62 -0
  120. data/lib/codebase_index/model_name_cache.rb +51 -0
  121. data/lib/codebase_index/notion/client.rb +217 -0
  122. data/lib/codebase_index/notion/exporter.rb +219 -0
  123. data/lib/codebase_index/notion/mapper.rb +39 -0
  124. data/lib/codebase_index/notion/mappers/column_mapper.rb +65 -0
  125. data/lib/codebase_index/notion/mappers/migration_mapper.rb +39 -0
  126. data/lib/codebase_index/notion/mappers/model_mapper.rb +164 -0
  127. data/lib/codebase_index/notion/rate_limiter.rb +68 -0
  128. data/lib/codebase_index/observability/health_check.rb +81 -0
  129. data/lib/codebase_index/observability/instrumentation.rb +34 -0
  130. data/lib/codebase_index/observability/structured_logger.rb +75 -0
  131. data/lib/codebase_index/operator/error_escalator.rb +81 -0
  132. data/lib/codebase_index/operator/pipeline_guard.rb +99 -0
  133. data/lib/codebase_index/operator/status_reporter.rb +80 -0
  134. data/lib/codebase_index/railtie.rb +26 -0
  135. data/lib/codebase_index/resilience/circuit_breaker.rb +99 -0
  136. data/lib/codebase_index/resilience/index_validator.rb +185 -0
  137. data/lib/codebase_index/resilience/retryable_provider.rb +108 -0
  138. data/lib/codebase_index/retrieval/context_assembler.rb +249 -0
  139. data/lib/codebase_index/retrieval/query_classifier.rb +131 -0
  140. data/lib/codebase_index/retrieval/ranker.rb +273 -0
  141. data/lib/codebase_index/retrieval/search_executor.rb +327 -0
  142. data/lib/codebase_index/retriever.rb +160 -0
  143. data/lib/codebase_index/ruby_analyzer/class_analyzer.rb +190 -0
  144. data/lib/codebase_index/ruby_analyzer/dataflow_analyzer.rb +78 -0
  145. data/lib/codebase_index/ruby_analyzer/fqn_builder.rb +18 -0
  146. data/lib/codebase_index/ruby_analyzer/mermaid_renderer.rb +275 -0
  147. data/lib/codebase_index/ruby_analyzer/method_analyzer.rb +143 -0
  148. data/lib/codebase_index/ruby_analyzer/trace_enricher.rb +139 -0
  149. data/lib/codebase_index/ruby_analyzer.rb +87 -0
  150. data/lib/codebase_index/session_tracer/file_store.rb +111 -0
  151. data/lib/codebase_index/session_tracer/middleware.rb +143 -0
  152. data/lib/codebase_index/session_tracer/redis_store.rb +112 -0
  153. data/lib/codebase_index/session_tracer/session_flow_assembler.rb +263 -0
  154. data/lib/codebase_index/session_tracer/session_flow_document.rb +223 -0
  155. data/lib/codebase_index/session_tracer/solid_cache_store.rb +145 -0
  156. data/lib/codebase_index/session_tracer/store.rb +67 -0
  157. data/lib/codebase_index/storage/graph_store.rb +120 -0
  158. data/lib/codebase_index/storage/metadata_store.rb +169 -0
  159. data/lib/codebase_index/storage/pgvector.rb +163 -0
  160. data/lib/codebase_index/storage/qdrant.rb +172 -0
  161. data/lib/codebase_index/storage/vector_store.rb +156 -0
  162. data/lib/codebase_index/temporal/snapshot_store.rb +341 -0
  163. data/lib/codebase_index/version.rb +5 -0
  164. data/lib/codebase_index.rb +223 -0
  165. data/lib/generators/codebase_index/install_generator.rb +32 -0
  166. data/lib/generators/codebase_index/pgvector_generator.rb +37 -0
  167. data/lib/generators/codebase_index/templates/add_pgvector_to_codebase_index.rb.erb +15 -0
  168. data/lib/generators/codebase_index/templates/create_codebase_index_tables.rb.erb +43 -0
  169. data/lib/tasks/codebase_index.rake +583 -0
  170. data/lib/tasks/codebase_index_evaluation.rake +115 -0
  171. metadata +252 -0
@@ -0,0 +1,653 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'node'
4
+
5
+ module CodebaseIndex
6
+ class Error < StandardError; end unless defined?(CodebaseIndex::Error)
7
+ class ExtractionError < Error; end unless defined?(CodebaseIndex::ExtractionError)
8
+ module Ast
9
+ # Parser adapter that normalizes Prism and parser gem ASTs to a common
10
+ # {Ast::Node} structure. Auto-detects parser availability at load time.
11
+ #
12
+ # @example Parsing Ruby source
13
+ # parser = Ast::Parser.new
14
+ # root = parser.parse("class Foo; def bar; end; end")
15
+ # root.find_all(:def).first.method_name #=> "bar"
16
+ #
17
+ class Parser
18
+ # Parse Ruby source into a normalized AST.
19
+ #
20
+ # @param source [String] Ruby source code
21
+ # @return [Ast::Node] Root node of the normalized tree
22
+ # @raise [CodebaseIndex::ExtractionError] if parsing fails
23
+ def parse(source)
24
+ if prism_available?
25
+ parse_with_prism(source)
26
+ else
27
+ parse_with_parser_gem(source)
28
+ end
29
+ rescue CodebaseIndex::ExtractionError
30
+ raise
31
+ rescue StandardError => e
32
+ raise CodebaseIndex::ExtractionError, "Failed to parse source: #{e.message}"
33
+ end
34
+
35
+ # Check if Prism is available.
36
+ #
37
+ # @return [Boolean]
38
+ def prism_available?
39
+ if @prism_available.nil?
40
+ begin
41
+ require 'prism'
42
+ @prism_available = defined?(Prism) ? true : false
43
+ rescue LoadError
44
+ @prism_available = false
45
+ end
46
+ end
47
+ @prism_available
48
+ end
49
+
50
+ private
51
+
52
+ # Parse using Prism (Ruby 3.3+ stdlib or backport gem).
53
+ def parse_with_prism(source)
54
+ require 'prism' unless defined?(Prism)
55
+
56
+ result = Prism.parse(source)
57
+
58
+ unless result.success?
59
+ errors = result.errors.map(&:message).join(', ')
60
+ raise CodebaseIndex::ExtractionError, "Parse error: #{errors}"
61
+ end
62
+
63
+ convert_prism_node(result.value, source)
64
+ end
65
+
66
+ # Parse using the parser gem (fallback for older Ruby).
67
+ def parse_with_parser_gem(source)
68
+ require 'parser/current' unless defined?(::Parser::CurrentRuby)
69
+
70
+ buffer = ::Parser::Source::Buffer.new('(source)', source: source)
71
+ ast = ::Parser::CurrentRuby.parse(buffer.source)
72
+
73
+ raise CodebaseIndex::ExtractionError, 'Parse returned nil' unless ast
74
+
75
+ convert_parser_node(ast, source)
76
+ end
77
+
78
+ # Convert a Prism node tree to Ast::Node.
79
+ #
80
+ # @param prism_node [Prism::Node] A Prism AST node
81
+ # @param source [String] Original source for extracting text spans
82
+ # @return [Ast::Node]
83
+ def convert_prism_node(prism_node, source)
84
+ case prism_node
85
+ when Prism::ProgramNode
86
+ children = convert_prism_children(prism_node.statements, source)
87
+ Node.new(type: :program, children: children, line: line_for_prism(prism_node))
88
+ when Prism::StatementsNode
89
+ children = prism_node.body.map { |child| convert_prism_node(child, source) }
90
+ Node.new(type: :begin, children: children, line: line_for_prism(prism_node))
91
+ when Prism::ClassNode
92
+ convert_prism_class(prism_node, source)
93
+ when Prism::ModuleNode
94
+ convert_prism_module(prism_node, source)
95
+ when Prism::DefNode
96
+ convert_prism_def(prism_node, source)
97
+ when Prism::CallNode
98
+ convert_prism_call(prism_node, source)
99
+ when Prism::ConstantReadNode
100
+ Node.new(
101
+ type: :const,
102
+ children: [],
103
+ line: line_for_prism(prism_node),
104
+ method_name: prism_node.name.to_s
105
+ )
106
+ when Prism::ConstantPathNode
107
+ convert_prism_constant_path(prism_node, source)
108
+ when Prism::IfNode
109
+ convert_prism_if(prism_node, source)
110
+ when Prism::UnlessNode
111
+ convert_prism_unless(prism_node, source)
112
+ when Prism::CaseNode
113
+ convert_prism_case(prism_node, source)
114
+ when Prism::BeginNode
115
+ children = []
116
+ children += prism_node.statements.body.map { |c| convert_prism_node(c, source) } if prism_node.statements
117
+ children << convert_prism_node(prism_node.rescue_clause, source) if prism_node.rescue_clause
118
+ children << convert_prism_node(prism_node.ensure_clause, source) if prism_node.ensure_clause
119
+ Node.new(type: :begin, children: children, line: line_for_prism(prism_node))
120
+ when Prism::RescueNode
121
+ children = prism_node.statements ? prism_node.statements.body.map { |c| convert_prism_node(c, source) } : []
122
+ Node.new(type: :rescue, children: children, line: line_for_prism(prism_node))
123
+ when Prism::EnsureNode
124
+ children = prism_node.statements ? prism_node.statements.body.map { |c| convert_prism_node(c, source) } : []
125
+ Node.new(type: :ensure, children: children, line: line_for_prism(prism_node))
126
+ when Prism::SymbolNode
127
+ prism_node.value.to_s
128
+ when Prism::StringNode
129
+ prism_node.unescaped
130
+ when Prism::IntegerNode, Prism::FloatNode
131
+ prism_node.value
132
+ when Prism::NilNode
133
+ nil
134
+ when Prism::TrueNode
135
+ Node.new(type: true, children: [], line: line_for_prism(prism_node))
136
+ when Prism::FalseNode
137
+ Node.new(type: false, children: [], line: line_for_prism(prism_node))
138
+ when Prism::SelfNode
139
+ Node.new(type: :self, children: [], line: line_for_prism(prism_node), source: 'self')
140
+ when Prism::LocalVariableReadNode
141
+ Node.new(
142
+ type: :lvar,
143
+ children: [],
144
+ line: line_for_prism(prism_node),
145
+ method_name: prism_node.name.to_s,
146
+ source: prism_node.name.to_s
147
+ )
148
+ when Prism::LocalVariableWriteNode
149
+ value = prism_node.value ? convert_prism_node(prism_node.value, source) : nil
150
+ Node.new(
151
+ type: :lvasgn,
152
+ children: [value].compact,
153
+ line: line_for_prism(prism_node),
154
+ method_name: prism_node.name.to_s,
155
+ source: prism_node.name.to_s
156
+ )
157
+ when Prism::InstanceVariableReadNode
158
+ Node.new(
159
+ type: :ivar,
160
+ children: [],
161
+ line: line_for_prism(prism_node),
162
+ method_name: prism_node.name.to_s,
163
+ source: prism_node.name.to_s
164
+ )
165
+ when Prism::InstanceVariableWriteNode
166
+ value = prism_node.value ? convert_prism_node(prism_node.value, source) : nil
167
+ Node.new(
168
+ type: :ivasgn,
169
+ children: [value].compact,
170
+ line: line_for_prism(prism_node),
171
+ method_name: prism_node.name.to_s,
172
+ source: prism_node.name.to_s
173
+ )
174
+ when Prism::BlockNode
175
+ children = prism_node.body ? [convert_prism_node(prism_node.body, source)] : []
176
+ Node.new(type: :block_body, children: children, line: line_for_prism(prism_node))
177
+ when Prism::LambdaNode
178
+ children = prism_node.body ? [convert_prism_node(prism_node.body, source)] : []
179
+ Node.new(type: :lambda, children: children, line: line_for_prism(prism_node))
180
+ when Prism::ReturnNode
181
+ children = if prism_node.arguments
182
+ prism_node.arguments.arguments.map do |a|
183
+ convert_prism_node(a, source)
184
+ end
185
+ else
186
+ []
187
+ end
188
+ Node.new(type: :return, children: children, line: line_for_prism(prism_node))
189
+ when Prism::YieldNode
190
+ Node.new(type: :yield, children: [], line: line_for_prism(prism_node))
191
+ when Prism::ArrayNode
192
+ children = prism_node.elements.map { |e| convert_prism_node(e, source) }
193
+ Node.new(type: :array, children: children, line: line_for_prism(prism_node))
194
+ when Prism::HashNode
195
+ Node.new(type: :hash, children: [], line: line_for_prism(prism_node))
196
+ when Prism::ParenthesesNode
197
+ convert_prism_node(prism_node.body, source)
198
+ when Prism::InterpolatedStringNode
199
+ Node.new(type: :dstr, children: [], line: line_for_prism(prism_node))
200
+ when Prism::SingletonClassNode
201
+ children = prism_node.body ? [convert_prism_node(prism_node.body, source)] : []
202
+ Node.new(type: :sclass, children: children, line: line_for_prism(prism_node))
203
+ when Prism::ConstantWriteNode
204
+ value = prism_node.value ? convert_prism_node(prism_node.value, source) : nil
205
+ Node.new(
206
+ type: :casgn,
207
+ children: [value].compact,
208
+ line: line_for_prism(prism_node),
209
+ method_name: prism_node.name.to_s
210
+ )
211
+ else
212
+ # Generic fallback: convert children we can find
213
+ children = extract_prism_generic_children(prism_node, source)
214
+ Node.new(
215
+ type: prism_node.class.name.split('::').last.sub(/Node$/, '').gsub(/([a-z])([A-Z])/,
216
+ '\1_\2').downcase.to_sym,
217
+ children: children,
218
+ line: line_for_prism(prism_node)
219
+ )
220
+ end
221
+ end
222
+
223
+ def convert_prism_class(prism_node, source)
224
+ name_node = convert_prism_node(prism_node.constant_path, source)
225
+ superclass = prism_node.superclass ? convert_prism_node(prism_node.superclass, source) : nil
226
+ body_children = if prism_node.body
227
+ if prism_node.body.is_a?(Prism::StatementsNode)
228
+ prism_node.body.body.map do |c|
229
+ convert_prism_node(c, source)
230
+ end
231
+ else
232
+ [convert_prism_node(prism_node.body, source)]
233
+ end
234
+ else
235
+ []
236
+ end
237
+
238
+ children = [name_node, superclass] + body_children
239
+
240
+ Node.new(
241
+ type: :class,
242
+ children: children,
243
+ line: line_for_prism(prism_node),
244
+ end_line: end_line_for_prism(prism_node),
245
+ method_name: extract_const_name(prism_node.constant_path)
246
+ )
247
+ end
248
+
249
+ def convert_prism_module(prism_node, source)
250
+ name_node = convert_prism_node(prism_node.constant_path, source)
251
+ body_children = if prism_node.body
252
+ if prism_node.body.is_a?(Prism::StatementsNode)
253
+ prism_node.body.body.map do |c|
254
+ convert_prism_node(c, source)
255
+ end
256
+ else
257
+ [convert_prism_node(prism_node.body, source)]
258
+ end
259
+ else
260
+ []
261
+ end
262
+
263
+ children = [name_node] + body_children
264
+
265
+ Node.new(
266
+ type: :module,
267
+ children: children,
268
+ line: line_for_prism(prism_node),
269
+ end_line: end_line_for_prism(prism_node),
270
+ method_name: extract_const_name(prism_node.constant_path)
271
+ )
272
+ end
273
+
274
+ def convert_prism_def(prism_node, source)
275
+ body_children = if prism_node.body
276
+ if prism_node.body.is_a?(Prism::StatementsNode)
277
+ prism_node.body.body.map { |c| convert_prism_node(c, source) }
278
+ else
279
+ [convert_prism_node(prism_node.body, source)]
280
+ end
281
+ else
282
+ []
283
+ end
284
+
285
+ is_class_method = prism_node.respond_to?(:receiver) && prism_node.receiver
286
+ receiver_text = if is_class_method
287
+ src = prism_node.receiver
288
+ src.is_a?(Prism::SelfNode) ? 'self' : extract_prism_source_text(src, source)
289
+ end
290
+
291
+ Node.new(
292
+ type: is_class_method ? :defs : :def,
293
+ children: body_children,
294
+ line: line_for_prism(prism_node),
295
+ end_line: end_line_for_prism(prism_node),
296
+ method_name: prism_node.name.to_s,
297
+ receiver: receiver_text,
298
+ source: extract_prism_source_span(prism_node, source)
299
+ )
300
+ end
301
+
302
+ def convert_prism_call(prism_node, source)
303
+ receiver_text = (extract_prism_receiver_text(prism_node.receiver, source) if prism_node.receiver)
304
+
305
+ args = if prism_node.arguments
306
+ prism_node.arguments.arguments.map { |a| extract_prism_source_text(a, source) }
307
+ else
308
+ []
309
+ end
310
+
311
+ # Convert receiver node so tree walking finds nested calls/constants
312
+ receiver_node = prism_node.receiver ? convert_prism_node(prism_node.receiver, source) : nil
313
+ children = [receiver_node].compact
314
+
315
+ # If there's a block, create a :block node wrapping this send
316
+ if prism_node.block.is_a?(Prism::BlockNode)
317
+ send_node = Node.new(
318
+ type: :send,
319
+ children: children,
320
+ line: line_for_prism(prism_node),
321
+ receiver: receiver_text,
322
+ method_name: prism_node.name.to_s,
323
+ arguments: args
324
+ )
325
+
326
+ block_body = (convert_prism_node(prism_node.block.body, source) if prism_node.block.body)
327
+
328
+ return Node.new(
329
+ type: :block,
330
+ children: [send_node, block_body].compact,
331
+ line: line_for_prism(prism_node),
332
+ end_line: end_line_for_prism(prism_node.block)
333
+ )
334
+ end
335
+
336
+ Node.new(
337
+ type: :send,
338
+ children: children,
339
+ line: line_for_prism(prism_node),
340
+ end_line: end_line_for_prism(prism_node),
341
+ receiver: receiver_text,
342
+ method_name: prism_node.name.to_s,
343
+ arguments: args
344
+ )
345
+ end
346
+
347
+ def convert_prism_constant_path(prism_node, _source)
348
+ parent_text = (extract_const_path_text(prism_node.parent) if prism_node.parent)
349
+
350
+ Node.new(
351
+ type: :const,
352
+ children: [],
353
+ line: line_for_prism(prism_node),
354
+ receiver: parent_text,
355
+ method_name: prism_node.name.to_s
356
+ )
357
+ end
358
+
359
+ def convert_prism_if(prism_node, source)
360
+ condition = convert_prism_node(prism_node.predicate, source)
361
+ condition_source = extract_prism_source_text(prism_node.predicate, source)
362
+ if condition.is_a?(Node) && condition.source.nil?
363
+ condition = Node.new(**condition.to_h, source: condition_source)
364
+ end
365
+
366
+ then_body = prism_node.statements ? convert_prism_node(prism_node.statements, source) : nil
367
+ else_body = prism_node.subsequent ? convert_prism_node(prism_node.subsequent, source) : nil
368
+
369
+ Node.new(
370
+ type: :if,
371
+ children: [condition, then_body, else_body].compact,
372
+ line: line_for_prism(prism_node),
373
+ end_line: end_line_for_prism(prism_node),
374
+ source: condition_source
375
+ )
376
+ end
377
+
378
+ def convert_prism_unless(prism_node, source)
379
+ condition = convert_prism_node(prism_node.predicate, source)
380
+ condition_source = extract_prism_source_text(prism_node.predicate, source)
381
+
382
+ then_body = prism_node.statements ? convert_prism_node(prism_node.statements, source) : nil
383
+ else_body = prism_node.else_clause ? convert_prism_node(prism_node.else_clause, source) : nil
384
+
385
+ Node.new(
386
+ type: :if,
387
+ children: [condition, then_body, else_body].compact,
388
+ line: line_for_prism(prism_node),
389
+ end_line: end_line_for_prism(prism_node),
390
+ source: condition_source
391
+ )
392
+ end
393
+
394
+ def convert_prism_case(prism_node, source)
395
+ children = []
396
+ children << convert_prism_node(prism_node.predicate, source) if prism_node.predicate
397
+ prism_node.conditions.each { |c| children << convert_prism_node(c, source) }
398
+ children << convert_prism_node(prism_node.else_clause, source) if prism_node.else_clause
399
+ Node.new(type: :case, children: children, line: line_for_prism(prism_node))
400
+ end
401
+
402
+ def convert_prism_children(statements_node, source)
403
+ return [] unless statements_node
404
+
405
+ if statements_node.is_a?(Prism::StatementsNode)
406
+ statements_node.body.map { |c| convert_prism_node(c, source) }
407
+ else
408
+ [convert_prism_node(statements_node, source)]
409
+ end
410
+ end
411
+
412
+ def extract_prism_generic_children(prism_node, source)
413
+ children = []
414
+ prism_node.child_nodes.compact.each do |child|
415
+ converted = convert_prism_node(child, source)
416
+ children << converted if converted
417
+ end
418
+ children
419
+ end
420
+
421
+ def line_for_prism(node)
422
+ node.location.start_line
423
+ end
424
+
425
+ def end_line_for_prism(node)
426
+ node.location.end_line
427
+ end
428
+
429
+ def extract_prism_source_span(node, source)
430
+ lines = source.lines
431
+ start_idx = node.location.start_line - 1
432
+ end_idx = node.location.end_line - 1
433
+ return nil if start_idx.negative? || end_idx >= lines.length
434
+
435
+ lines[start_idx..end_idx].join
436
+ end
437
+
438
+ def extract_prism_source_text(node, source)
439
+ source.byteslice(node.location.start_offset, node.location.length) || ''
440
+ rescue StandardError
441
+ ''
442
+ end
443
+
444
+ def extract_prism_receiver_text(receiver_node, source)
445
+ case receiver_node
446
+ when Prism::SelfNode
447
+ 'self'
448
+ when Prism::ConstantReadNode, Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode
449
+ receiver_node.name.to_s
450
+ when Prism::ConstantPathNode
451
+ extract_const_path_text(receiver_node)
452
+ when Prism::CallNode
453
+ text = extract_prism_receiver_text(receiver_node.receiver, source) if receiver_node.receiver
454
+ [text, receiver_node.name.to_s].compact.join('.')
455
+ else
456
+ extract_prism_source_text(receiver_node, source)
457
+ end
458
+ end
459
+
460
+ def extract_const_path_text(node)
461
+ case node
462
+ when Prism::ConstantReadNode
463
+ node.name.to_s
464
+ when Prism::ConstantPathNode
465
+ parent = node.parent ? extract_const_path_text(node.parent) : nil
466
+ [parent, node.name.to_s].compact.join('::')
467
+ end
468
+ end
469
+
470
+ def extract_const_name(node)
471
+ case node
472
+ when Prism::ConstantReadNode
473
+ node.name.to_s
474
+ when Prism::ConstantPathNode
475
+ extract_const_path_text(node)
476
+ end
477
+ end
478
+
479
+ # ── Parser gem fallback ──────────────────────────────────────────────
480
+
481
+ def convert_parser_node(parser_node, source)
482
+ return nil unless parser_node
483
+ return nil unless parser_node.is_a?(::Parser::AST::Node)
484
+
485
+ case parser_node.type
486
+ when :begin
487
+ children = parser_node.children.map { |c| convert_parser_node(c, source) }.compact
488
+ Node.new(type: :begin, children: children, line: parser_node.loc&.line || 1)
489
+ when :class
490
+ name_node = convert_parser_node(parser_node.children[0], source)
491
+ superclass = parser_node.children[1] ? convert_parser_node(parser_node.children[1], source) : nil
492
+ body = parser_node.children[2] ? convert_parser_node(parser_node.children[2], source) : nil
493
+ body_children = body&.type == :begin ? body.children : [body].compact
494
+ children = [name_node, superclass] + body_children
495
+ Node.new(
496
+ type: :class,
497
+ children: children,
498
+ line: parser_node.loc.line,
499
+ end_line: parser_node.loc.expression.last_line,
500
+ method_name: extract_parser_const_name(parser_node.children[0])
501
+ )
502
+ when :module
503
+ name_node = convert_parser_node(parser_node.children[0], source)
504
+ body = parser_node.children[1] ? convert_parser_node(parser_node.children[1], source) : nil
505
+ body_children = body&.type == :begin ? body.children : [body].compact
506
+ children = [name_node] + body_children
507
+ Node.new(
508
+ type: :module,
509
+ children: children,
510
+ line: parser_node.loc.line,
511
+ end_line: parser_node.loc.expression.last_line,
512
+ method_name: extract_parser_const_name(parser_node.children[0])
513
+ )
514
+ when :def
515
+ body = parser_node.children[2] ? convert_parser_node(parser_node.children[2], source) : nil
516
+ body_children = body&.type == :begin ? body.children : [body].compact
517
+ Node.new(
518
+ type: :def,
519
+ children: body_children,
520
+ line: parser_node.loc.line,
521
+ end_line: parser_node.loc.expression.last_line,
522
+ method_name: parser_node.children[0].to_s,
523
+ source: extract_parser_source_span(parser_node, source)
524
+ )
525
+ when :defs
526
+ body = parser_node.children[3] ? convert_parser_node(parser_node.children[3], source) : nil
527
+ body_children = body&.type == :begin ? body.children : [body].compact
528
+ receiver = parser_node.children[0].type == :self ? 'self' : parser_node.children[0].to_s
529
+ Node.new(
530
+ type: :defs,
531
+ children: body_children,
532
+ line: parser_node.loc.line,
533
+ end_line: parser_node.loc.expression.last_line,
534
+ method_name: parser_node.children[1].to_s,
535
+ receiver: receiver,
536
+ source: extract_parser_source_span(parser_node, source)
537
+ )
538
+ when :send
539
+ receiver_text = parser_node.children[0] ? extract_parser_receiver_text(parser_node.children[0], source) : nil
540
+ method_name = parser_node.children[1].to_s
541
+ args = parser_node.children[2..].compact.map { |a| extract_parser_source_text(a, source) }
542
+ Node.new(
543
+ type: :send,
544
+ children: [],
545
+ line: parser_node.loc.line,
546
+ end_line: parser_node.loc.expression&.last_line,
547
+ receiver: receiver_text,
548
+ method_name: method_name,
549
+ arguments: args
550
+ )
551
+ when :block
552
+ send_child = convert_parser_node(parser_node.children[0], source)
553
+ body = parser_node.children[2] ? convert_parser_node(parser_node.children[2], source) : nil
554
+ Node.new(
555
+ type: :block,
556
+ children: [send_child, body].compact,
557
+ line: parser_node.loc.line,
558
+ end_line: parser_node.loc.expression.last_line
559
+ )
560
+ when :if
561
+ condition = convert_parser_node(parser_node.children[0], source)
562
+ condition_source = extract_parser_source_text(parser_node.children[0], source)
563
+ if condition.is_a?(Node) && condition.source.nil?
564
+ condition = Node.new(**condition.to_h, source: condition_source)
565
+ end
566
+ then_body = parser_node.children[1] ? convert_parser_node(parser_node.children[1], source) : nil
567
+ else_body = parser_node.children[2] ? convert_parser_node(parser_node.children[2], source) : nil
568
+ Node.new(
569
+ type: :if,
570
+ children: [condition, then_body, else_body].compact,
571
+ line: parser_node.loc.line,
572
+ end_line: parser_node.loc.expression&.last_line,
573
+ source: condition_source
574
+ )
575
+ when :const
576
+ parent = parser_node.children[0] ? extract_parser_const_name(parser_node.children[0]) : nil
577
+ Node.new(
578
+ type: :const,
579
+ children: [],
580
+ line: parser_node.loc.line,
581
+ receiver: parent,
582
+ method_name: parser_node.children[1].to_s
583
+ )
584
+ when :sym
585
+ parser_node.children[0].to_s
586
+ when :str, :int, :float
587
+ parser_node.children[0]
588
+ when :nil
589
+ nil
590
+ when true
591
+ Node.new(type: true, children: [], line: parser_node.loc.line)
592
+ when false
593
+ Node.new(type: false, children: [], line: parser_node.loc.line)
594
+ when :self
595
+ Node.new(type: :self, children: [], line: parser_node.loc.line, source: 'self')
596
+ else
597
+ children = parser_node.children.filter_map do |child|
598
+ child.is_a?(::Parser::AST::Node) ? convert_parser_node(child, source) : nil
599
+ end
600
+ Node.new(
601
+ type: parser_node.type,
602
+ children: children,
603
+ line: parser_node.loc&.line || 1
604
+ )
605
+ end
606
+ end
607
+
608
+ def extract_parser_source_span(node, source)
609
+ lines = source.lines
610
+ start_idx = node.loc.line - 1
611
+ end_idx = node.loc.expression.last_line - 1
612
+ return nil if start_idx.negative? || end_idx >= lines.length
613
+
614
+ lines[start_idx..end_idx].join
615
+ end
616
+
617
+ def extract_parser_source_text(node, source)
618
+ return node.to_s unless node.is_a?(::Parser::AST::Node) && node.loc&.expression
619
+
620
+ loc = node.loc.expression
621
+ source[loc.begin_pos...loc.end_pos] || ''
622
+ rescue StandardError
623
+ ''
624
+ end
625
+
626
+ def extract_parser_receiver_text(node, source)
627
+ case node.type
628
+ when :self
629
+ 'self'
630
+ when :const
631
+ extract_parser_const_name(node)
632
+ when :send
633
+ recv = node.children[0] ? extract_parser_receiver_text(node.children[0], source) : nil
634
+ [recv, node.children[1].to_s].compact.join('.')
635
+ when :lvar, :ivar
636
+ node.children[0].to_s
637
+ else
638
+ extract_parser_source_text(node, source)
639
+ end
640
+ end
641
+
642
+ def extract_parser_const_name(node)
643
+ return nil unless node.is_a?(::Parser::AST::Node)
644
+
645
+ case node.type
646
+ when :const
647
+ parent = node.children[0] ? extract_parser_const_name(node.children[0]) : nil
648
+ [parent, node.children[1].to_s].compact.join('::')
649
+ end
650
+ end
651
+ end
652
+ end
653
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ast/node'
4
+ require_relative 'ast/parser'
5
+ require_relative 'ast/method_extractor'
6
+ require_relative 'ast/call_site_extractor'