Class: Rbs::Merge::FileAnalysis

Inherits:
Object
  • Object
show all
Includes:
Ast::Merge::FileAnalyzable
Defined in:
lib/rbs/merge/file_analysis.rb

Overview

File analysis for RBS type signature files. Supports multiple backends: RBS gem (MRI only) and tree-sitter-rbs (cross-platform).

This class provides the foundation for intelligent merging by:

  • Parsing RBS files using TreeHaver's backend system
  • Extracting top-level declarations (classes, modules, interfaces, type aliases, constants)
  • Detecting freeze blocks marked with comment directives
  • Generating signatures for matching declarations between files

Examples:

Basic usage (auto-selects backend)

analysis = FileAnalysis.new(rbs_source)
analysis.statements.each do |stmt|
  puts stmt.canonical_type
end

With custom freeze token

analysis = FileAnalysis.new(source, freeze_token: "my-merge")
# Looks for: # my-merge:freeze / # my-merge:unfreeze

Constant Summary collapse

DEFAULT_FREEZE_TOKEN =

Default freeze token for identifying freeze blocks

'rbs-merge'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil, **_options) ⇒ FileAnalysis

Note:

Backend selection is handled by TreeHaver. To force a specific backend:

  • Use TreeHaver.with_backend(:mri) { ... } for tree-sitter via MRI
  • Use TreeHaver.with_backend(:rbs) { ... } for RBS gem (MRI only)
  • Set TREE_HAVER_BACKEND=rbs or TREE_HAVER_BACKEND=mri env var

Initialize file analysis



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/rbs/merge/file_analysis.rb', line 59

def initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil, **_options)
  @source = source
  @lines = source.split("\n", -1)
  @freeze_token = freeze_token
  @signature_generator = signature_generator
  @errors = []
  @backend = nil # Will be set during parsing
  @directives = []
  @declarations = []
  @ast = nil
  @comment_tracker = CommentTracker.new(@lines, freeze_token: freeze_token)

  # Parse the RBS source
  DebugLogger.time('FileAnalysis#parse') { parse_rbs }

  # Extract and integrate all nodes including freeze blocks
  @statements = integrate_nodes

  DebugLogger.debug('FileAnalysis initialized', {
                      signature_generator: signature_generator ? 'custom' : 'default',
                      backend: @backend,
                      declarations_count: @declarations.size,
                      statements_count: @statements.size,
                      freeze_blocks: freeze_blocks.size,
                      valid: valid?
                    })
end

Instance Attribute Details

#astTreeHaver::Tree? (readonly)



31
32
33
# File 'lib/rbs/merge/file_analysis.rb', line 31

def ast
  @ast
end

#backendSymbol (readonly)



37
38
39
# File 'lib/rbs/merge/file_analysis.rb', line 37

def backend
  @backend
end

#comment_trackerCommentTracker (readonly)



46
47
48
# File 'lib/rbs/merge/file_analysis.rb', line 46

def comment_tracker
  @comment_tracker
end

#declarationsArray (readonly)



43
44
45
# File 'lib/rbs/merge/file_analysis.rb', line 43

def declarations
  @declarations
end

#directivesArray (readonly)



40
41
42
# File 'lib/rbs/merge/file_analysis.rb', line 40

def directives
  @directives
end

#errorsArray (readonly)



34
35
36
# File 'lib/rbs/merge/file_analysis.rb', line 34

def errors
  @errors
end

#statementsArray<NodeWrapper, FreezeNode> (readonly)

Get all statements (declarations outside freeze blocks + FreezeNodes)



187
188
189
# File 'lib/rbs/merge/file_analysis.rb', line 187

def statements
  @statements
end

Instance Method Details

#comment_attachment_for(owner, **options) ⇒ Object

Build a passive shared comment attachment for an owner.



150
151
152
153
154
155
156
157
158
# File 'lib/rbs/merge/file_analysis.rb', line 150

def comment_attachment_for(owner, **options)
  augmented_attachment = comment_augmenter(**options).attachment_for(owner)

  shared_comment_attachment_for(
    owner,
    tracker_attachment: augmented_attachment || comment_tracker.comment_attachment_for(owner, **options),
    **options
  )
end

#comment_attachment_strategySymbol



161
162
163
# File 'lib/rbs/merge/file_analysis.rb', line 161

def comment_attachment_strategy
  :normalize_tracked_layout_merge
end

#comment_augmenter(owners: nil, **options) ⇒ Object

Build a passive shared comment augmenter for this analysis.



178
179
180
181
182
183
# File 'lib/rbs/merge/file_analysis.rb', line 178

def comment_augmenter(owners: nil, **options)
  comment_tracker.augment(
    owners: owners || comment_augmenter_default_owners,
    **options
  )
end

#comment_capabilityObject

Get shared comment capability information for this analysis.



98
99
100
# File 'lib/rbs/merge/file_analysis.rb', line 98

def comment_capability
  @comment_capability ||= comment_tracker.augment(owners: []).capability
end

#comment_node_at(line_num) ⇒ Object?

Get a shared comment node at a specific line.



127
128
129
# File 'lib/rbs/merge/file_analysis.rb', line 127

def comment_node_at(line_num)
  comment_tracker.comment_node_at(line_num)
end

#comment_nodesArray

Get all tracked comments converted to shared comment nodes.



119
120
121
# File 'lib/rbs/merge/file_analysis.rb', line 119

def comment_nodes
  comment_tracker.comment_nodes
end

#comment_region_for_range(range, kind:, full_line_only: false) ⇒ Object

Get comments in a line range converted to a shared comment region.



137
138
139
140
141
142
143
# File 'lib/rbs/merge/file_analysis.rb', line 137

def comment_region_for_range(range, kind:, full_line_only: false)
  comment_tracker.comment_region_for_range(
    range,
    kind: kind,
    full_line_only: full_line_only
  )
end

#comment_support_styleAst::Merge::Comment::SupportStyle

Describe how RBS merges currently own and emit comments.

RBS comment tracking is source-augmented and emitted through the shared synthetic merge layer rather than via editable parser-native comment AST.



108
109
110
111
112
113
114
# File 'lib/rbs/merge/file_analysis.rb', line 108

def comment_support_style
  @comment_support_style ||= shared_comment_support_style(
    source: :rbs_source,
    style: :hash_comment,
    read_strategy: :source_augmented_portable_write
  )
end

#compute_node_signature(node) ⇒ Array?

Compute default signature for a node



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/rbs/merge/file_analysis.rb', line 211

def compute_node_signature(node)
  return if node.nil?

  case node
  when FreezeNode
    node.signature
  when NodeWrapper
    node.signature
  else
    # Raw declarations/members from the RBS gem frequently respond to #type
    # as part of their own AST API, so backend selection must stay authoritative
    # here. Otherwise frozen contained RBS declarations are misrouted through the
    # tree-sitter signature path and become unmatchable.
    if @backend == :tree_sitter && node.respond_to?(:type)
      compute_tree_sitter_signature(node)
    else
      compute_rbs_gem_signature(node)
    end
  end
end

#compute_tree_sitter_signature(node) ⇒ Array?

Compute signature for a tree-sitter node



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/rbs/merge/file_analysis.rb', line 235

def compute_tree_sitter_signature(node)
  node_type = node.respond_to?(:type) ? node.type.to_s : nil
  return unless node_type

  canonical = NodeTypeNormalizer.canonical_type(node_type, :tree_sitter)
  name = extract_tree_sitter_node_name(node)

  case canonical
  when :class
    [:class, name || 'anonymous']
  when :module
    [:module, name || 'anonymous']
  when :interface
    [:interface, name || 'anonymous']
  when :type_alias
    [:type_alias, name || 'anonymous']
  when :constant
    [:constant, name || 'anonymous']
  when :global
    [:global, name || 'anonymous']
  when :class_alias
    [:class_alias, name || 'anonymous']
  when :module_alias
    [:module_alias, name || 'anonymous']
  when :method
    [:method, name || 'anonymous']
  else
    [canonical, name || node_type]
  end
end

#extract_tree_sitter_node_name(node) ⇒ String?

Extract name from a tree-sitter node



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/rbs/merge/file_analysis.rb', line 269

def extract_tree_sitter_node_name(node)
  return unless node.respond_to?(:each)

  name_node_types = %w[
    class_name
    module_name
    interface_name
    const_name
    global_name
    alias_name
    method_name
  ]

  node.each do |child|
    child_type = child.respond_to?(:type) ? child.type.to_s : ''
    next unless name_node_types.include?(child_type)

    # Name nodes often have a constant or identifier child
    if child.respond_to?(:each)
      child.each do |inner|
        inner_type = inner.respond_to?(:type) ? inner.type.to_s : ''
        if %w[constant identifier].include?(inner_type)
          return inner.respond_to?(:text) ? inner.text : nil
        end
      end
    end
    # If no inner constant/identifier, try the name node itself
    return child.respond_to?(:text) ? child.text : nil
  end

  nil
end

#fallthrough_node?(value) ⇒ Boolean

Override to detect RBS nodes for signature generator fallthrough



305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/rbs/merge/file_analysis.rb', line 305

def fallthrough_node?(value)
  return true if value.is_a?(NodeWrapper)
  return true if value.is_a?(FreezeNode)

  # Check for RBS gem AST types (when rbs gem is loaded)
  if @backend == :rbs && defined?(::RBS::AST)
    return true if value.is_a?(::RBS::AST::Declarations::Base)
    return true if value.is_a?(::RBS::AST::Members::Base)
  end

  super
end

#root_nodeNodeWrapper?

Get the root node of the parse tree



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/rbs/merge/file_analysis.rb', line 191

def root_node
  return unless valid?

  if @backend == :rbs
    # For RBS gem, create a synthetic document wrapper
    nil # RBS gem doesn't have a single root node
  else
    root = @ast.root_node
    NodeWrapper.new(
      root,
      lines: @lines,
      source: @source,
      backend: @backend
    )
  end
end

#ruleset_owner_selectorObject



165
166
167
# File 'lib/rbs/merge/file_analysis.rb', line 165

def ruleset_owner_selector
  :rbs_declarations
end

#ruleset_render_familyObject



169
170
171
# File 'lib/rbs/merge/file_analysis.rb', line 169

def ruleset_render_family
  :rbs_declarations
end

#valid?Boolean

Check if parse was successful



89
90
91
92
93
# File 'lib/rbs/merge/file_analysis.rb', line 89

def valid?
  return false unless @errors.empty?

  !@ast.nil? || @declarations.any?
end