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:



154
155
156
# File 'lib/rbs/merge/file_analysis.rb', line 154

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)


136
137
138
# File 'lib/rbs/merge/file_analysis.rb', line 136

def comment_attachment_for(owner, **options)
  comment_tracker.comment_attachment_for(owner, **options)
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)


145
146
147
148
149
150
# File 'lib/rbs/merge/file_analysis.rb', line 145

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)


113
114
115
# File 'lib/rbs/merge/file_analysis.rb', line 113

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)


105
106
107
# File 'lib/rbs/merge/file_analysis.rb', line 105

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)


123
124
125
126
127
128
129
# File 'lib/rbs/merge/file_analysis.rb', line 123

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

#compute_node_signature(node) ⇒ Array?

Compute default signature for a node

Parameters:

  • node (Object)

    The declaration, NodeWrapper, or FreezeNode

Returns:

  • (Array, nil)

    Signature array



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/rbs/merge/file_analysis.rb', line 178

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



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/rbs/merge/file_analysis.rb', line 202

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)


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
265
266
267
# File 'lib/rbs/merge/file_analysis.rb', line 236

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 : ""
    if 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
  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



272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/rbs/merge/file_analysis.rb', line 272

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:



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/rbs/merge/file_analysis.rb', line 158

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

#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