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

Returns:

  • (String)
'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

Parameters:

  • source (String)

    RBS source code to analyze

  • freeze_token (String) (defaults to: DEFAULT_FREEZE_TOKEN)

    Token for freeze block markers (default: "rbs-merge")

  • signature_generator (Proc, nil) (defaults to: nil)

    Custom signature generator

  • options (Hash)

    Additional options



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)

Returns Parsed AST (for tree-sitter backend).

Returns:

  • (TreeHaver::Tree, nil)

    Parsed AST (for tree-sitter backend)



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

def ast
  @ast
end

#backendSymbol (readonly)

Returns The backend used for parsing (:rbs or :tree_sitter).

Returns:

  • (Symbol)

    The backend used for parsing (:rbs or :tree_sitter)



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

def backend
  @backend
end

#comment_trackerCommentTracker (readonly)

Returns Comment tracker for this file.

Returns:



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

def comment_tracker
  @comment_tracker
end

#declarationsArray (readonly)

Returns Raw declarations from parser.

Returns:

  • (Array)

    Raw declarations from parser



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

def declarations
  @declarations
end

#directivesArray (readonly)

Returns RBS directives (for RBS gem backend only).

Returns:

  • (Array)

    RBS directives (for RBS gem backend only)



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

def directives
  @directives
end

#errorsArray (readonly)

Returns Parse errors if any.

Returns:

  • (Array)

    Parse errors if any



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)

Returns:



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.

Parameters:

  • owner (Object)

    Structural owner for the attachment

  • options (Hash)

    Additional metadata / lookup overrides

Returns:

  • (Object)


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

Returns:

  • (Symbol)


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.

Parameters:

  • owners (Array<#start_line,#end_line>, nil) (defaults to: nil)

    Owners used for attachment inference

  • options (Hash)

    Additional augmenter options

Returns:

  • (Object)


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.

Returns:

  • (Object)


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.

Parameters:

  • line_num (Integer)

    1-based line number

Returns:

  • (Object, nil)


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.

Returns:

  • (Array)


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.

Parameters:

  • range (Range)

    Range of 1-based line numbers

  • kind (Symbol)

    Region kind

  • full_line_only (Boolean) (defaults to: false)

    Whether to keep only full-line comments

Returns:

  • (Object)


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.

Returns:

  • (Ast::Merge::Comment::SupportStyle)


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

Parameters:

  • node (Object)

    The declaration, NodeWrapper, or FreezeNode

Returns:

  • (Array, nil)

    Signature array



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

Parameters:

  • node (Object)

    TreeHaver::Node

Returns:

  • (Array, nil)

    Signature array



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

Parameters:

  • node (Object)

    TreeHaver::Node

Returns:

  • (String, nil)


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

Parameters:

  • value (Object)

    The value to check

Returns:

  • (Boolean)

    true if this is a fallthrough node



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

Returns:



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

Returns:

  • (Boolean)


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