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 (forward compatibility)



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

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

  # 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

#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:



93
94
95
# File 'lib/rbs/merge/file_analysis.rb', line 93

def statements
  @statements
end

Instance Method Details

#compute_node_signature(node) ⇒ Array?

Compute default signature for a node

Parameters:

  • node (Object)

    The declaration, NodeWrapper, or FreezeNode

Returns:

  • (Array, nil)

    Signature array



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/rbs/merge/file_analysis.rb', line 117

def compute_node_signature(node)
  return if node.nil?

  case node
  when FreezeNode
    node.signature
  when NodeWrapper
    node.signature
  else
    # For raw TreeHaver::Node (tree-sitter) or RBS gem nodes
    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



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/rbs/merge/file_analysis.rb', line 138

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 :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)


168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/rbs/merge/file_analysis.rb', line 168

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



204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/rbs/merge/file_analysis.rb', line 204

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:



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/rbs/merge/file_analysis.rb', line 97

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)


85
86
87
88
89
# File 'lib/rbs/merge/file_analysis.rb', line 85

def valid?
  return false unless @errors.empty?

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