Class: Prism::Merge::ScaffoldChunkRemover

Inherits:
Object
  • Object
show all
Defined in:
lib/prism/merge/scaffold_chunk_remover.rb

Overview

Surgically removes bundle-gem scaffold boilerplate from Rakefiles.

Each ChunkSpec describes an anchor (exact require/task call) and optional satellite nodes (matched fuzzily via Jaccard similarity). The remover finds all matching nodes in the top-level statements and removes them by line range, leaving user-added content intact.

Examples:

specs = [ScaffoldChunkRemover::RSPEC_SPEC, ScaffoldChunkRemover::RUBOCOP_SPEC]
clean = ScaffoldChunkRemover.remove(source, specs)

Constant Summary collapse

ChunkSpec =
Ruby::Merge::ScaffoldChunkSupport::ChunkSpec
BUNDLER_GEM_TASKS_SPEC =
Ruby::Merge::ScaffoldChunkSupport::BUNDLER_GEM_TASKS_SPEC
RSPEC_SPEC =
Ruby::Merge::ScaffoldChunkSupport::RSPEC_SPEC
RUBOCOP_SPEC =
Ruby::Merge::ScaffoldChunkSupport::RUBOCOP_SPEC
DEFAULT_TASK_SPEC =
Ruby::Merge::ScaffoldChunkSupport::DEFAULT_TASK_SPEC
ALL_SPECS =
Ruby::Merge::ScaffoldChunkSupport::ALL_SPECS

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source, specs = ALL_SPECS) ⇒ ScaffoldChunkRemover

Returns a new instance of ScaffoldChunkRemover.



29
30
31
32
# File 'lib/prism/merge/scaffold_chunk_remover.rb', line 29

def initialize(source, specs = ALL_SPECS)
  @source = source
  @specs = specs
end

Class Method Details

.remove(source, specs = ALL_SPECS) ⇒ Object



24
25
26
# File 'lib/prism/merge/scaffold_chunk_remover.rb', line 24

def remove(source, specs = ALL_SPECS)
  new(source, specs).call
end

Instance Method Details

#callObject



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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
82
83
84
85
86
87
88
# File 'lib/prism/merge/scaffold_chunk_remover.rb', line 34

def call
  return @source if @source.empty?

  parse_result = TreeHaver.with_backend(Prism::Merge::BACKEND_REFERENCE.id) do
    TreeHaver.parser_for(:ruby, backend_type: :prism).parse(@source).parse_result
  end
  statements = parse_result.value.statements&.body&.compact || []
  return @source if statements.empty?

  lines_to_remove = Set.new

  @specs.each do |spec|
    anchor_idx = find_anchor(statements, spec)
    next unless anchor_idx

    nodes_to_remove = [statements[anchor_idx]]

    if spec.satellite_patterns.any?
      start_idx = [0, anchor_idx - spec.max_lookbehind].max
      end_idx = [statements.size - 1, anchor_idx + spec.max_lookahead].min

      spec.satellite_patterns.each do |pattern|
        p_tokens = Ruby::Merge::ScaffoldChunkSupport.jaccard_tokens(pattern)
        (start_idx..end_idx).each do |i|
          next if i == anchor_idx

          candidate = statements[i]
          c_tokens = Ruby::Merge::ScaffoldChunkSupport.jaccard_tokens(candidate.slice.to_s)
          score = Ruby::Merge::ScaffoldChunkSupport.jaccard(p_tokens, c_tokens)
          nodes_to_remove << candidate if score >= spec.jaccard_threshold
        end
      end
    end

    nodes_to_remove.each do |node|
      (node.location.start_line..node.location.end_line).each { |ln| lines_to_remove << ln }
    end
  end

  return @source if lines_to_remove.empty?

  lines = @source.lines

  # Remove up to 1 trailing blank line per contiguous removed block
  trailing = Set.new
  lines_to_remove.to_a.sort.each do |ln|
    next if lines_to_remove.include?(ln + 1)

    next_line = lines[ln] # 0-indexed: lines[ln] is line number ln+1
    trailing << (ln + 1) if next_line && next_line.strip.empty?
  end
  lines_to_remove.merge(trailing)

  lines.reject.with_index { |_, i| lines_to_remove.include?(i + 1) }.join
end