Module: Ast::Merge::SectionTyping

Defined in:
lib/ast/merge/section_typing.rb

Overview

AST-aware section typing for identifying logical sections within parsed trees.

Unlike text-based splitting (see ‘Ast::Merge::Text::SectionSplitter`), SectionTyping works with already-parsed AST nodes where the parser has already identified structural boundaries. This eliminates the need for regex pattern matching.

## Use Cases

  • Identifying ‘appraise` blocks in Appraisals files

  • Identifying ‘group` blocks in Gemfiles

  • Identifying method definitions in Ruby files

  • Any case where the AST parser provides structural information

## How It Works

  1. Classifier: A callable that inspects an AST node and returns section info

  2. **Typed Node**: The node wrapped with its section classification

  3. **Merge Logic**: Section-aware merging based on classifications

Examples:

Defining an Appraisals block classifier

AppraisalClassifier = ->(node) do
  return nil unless node.is_a?(Prism::CallNode)
  return nil unless node.name == :appraise

  # Extract the block name from the first argument
  block_name = node.arguments&.arguments&.first
  return nil unless block_name.is_a?(Prism::StringNode)

  {
    type: :appraise_block,
    name: block_name.unescaped,
    node: node
  }
end

Using the classifier

typing = SectionTyping.new(classifier: AppraisalClassifier)
sections = typing.classify_children(parsed_tree.statements)

sections.each do |section|
  puts "#{section.type}: #{section.name}"
end

Defined Under Namespace

Classes: CallableClassifier, Classifier, CompositeClassifier, TypedSection

Class Method Summary collapse

Class Method Details

.merge_sections(template_sections, dest_sections, preference: :destination, add_template_only: false) ⇒ Array<TypedSection>

Merge typed sections from template and destination.

Similar to ‘Text::SectionSplitter#merge_section_lists` but works with TypedSection objects wrapping AST nodes.

Parameters:

  • template_sections (Array<TypedSection>)

    Sections from template

  • dest_sections (Array<TypedSection>)

    Sections from destination

  • preference (Symbol, Hash) (defaults to: :destination)

    Merge preference (:template, :destination, or per-section Hash)

  • add_template_only (Boolean) (defaults to: false)

    Whether to add sections only in template

Returns:



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/ast/merge/section_typing.rb', line 245

def self.merge_sections(template_sections, dest_sections, preference: :destination, add_template_only: false)
  dest_by_name = dest_sections.each_with_object({}) do |section, hash|
    key = section.normalized_name
    hash[key] = section unless section.unclassified?
  end

  merged = []
  seen_names = Set.new

  template_sections.each do |template_section|
    if template_section.unclassified?
      # Unclassified sections are typically kept as-is or merged specially
      merged << template_section if add_template_only
      next
    end

    key = template_section.normalized_name
    seen_names << key

    dest_section = dest_by_name[key]

    if dest_section
      # Section exists in both - choose based on preference
      section_pref = preference_for(template_section.name, preference)
      merged << ((section_pref == :template) ? template_section : dest_section)
    elsif add_template_only
      merged << template_section
    end
  end

  # Append destination-only sections
  dest_sections.each do |dest_section|
    next if dest_section.unclassified?
    key = dest_section.normalized_name
    next if seen_names.include?(key)
    merged << dest_section
  end

  merged
end

.preference_for(section_name, preference) ⇒ Symbol

Get preference for a specific section.

Parameters:

  • section_name (String, Symbol)

    The section name

  • preference (Symbol, Hash)

    Overall preference

Returns:

  • (Symbol)

    :template or :destination



291
292
293
294
295
296
297
298
299
300
# File 'lib/ast/merge/section_typing.rb', line 291

def self.preference_for(section_name, preference)
  return preference unless preference.is_a?(Hash)

  normalized = section_name.to_s.strip.downcase
  preference.each do |key, value|
    return value if key.to_s.strip.downcase == normalized
  end

  preference.fetch(:default, :destination)
end