Class: Ast::Merge::Comment::HashTrackerBase

Inherits:
Object
  • Object
show all
Defined in:
lib/ast/merge/comment/hash_tracker_base.rb

Overview

Shared base class for hash-comment (#) trackers across the merge family.

This base provides the common lookup, query, region-building, and attachment API that every #-syntax format shares. Format-specific subclasses override:

  • #extract_comments — scanning/parsing logic for the format
  • #owner_line_num — how to resolve a structural owner to a line number

The tracked-comment hash shape is the same as TrackedHashAdapter expects:

{ line: Integer,     # 1-based line number
indent: Integer,   # leading whitespace width
text: String,      # comment text without prefix
full_line: Boolean, # true if the comment is the entire line
raw: String }      # original source line

Examples:

Subclassing

class MyFormat::CommentTracker < Ast::Merge::Comment::HashTrackerBase
  private
  def extract_comments
    # scan @lines, return Array<Hash> in tracked-hash shape
  end
end

See Also:

Defined Under Namespace

Classes: MissingTrackedLineError

Constant Summary collapse

FULL_LINE_COMMENT_REGEX =

Matches a full-line hash-style comment.

Returns:

  • (Regexp)
/\A(?<indent>\s*)#\s?(?<text>.*)\z/

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(lines, tree_haver_comments: nil) ⇒ HashTrackerBase

Initialize the tracker. Subclasses may accept additional arguments but should call super or reproduce the setup here.

When tree_haver_comments is provided the tracker skips its own text-scanning path (+extract_comments+) and converts the tree_haver comment objects into the internal hash format instead. This is the preferred input when the calling backend has already obtained a flat, deduplicated list from TreeHaver::Base::Parser#comments.

Parameters:

  • lines (Array<String>)

    Source lines (already chomped/split). Always required; used for raw line lookup and blank-line queries even when tree_haver_comments is supplied.

  • tree_haver_comments (Array<TreeHaver::Base::Comment>, nil) (defaults to: nil)

    Optional pre-parsed comment objects from a tree_haver backend.



60
61
62
63
64
65
66
67
68
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 60

def initialize(lines, tree_haver_comments: nil)
  @lines = Array(lines)
  @comments = if tree_haver_comments
                comments_from_tree_haver(tree_haver_comments)
              else
                extract_comments
              end
  @comments_by_line = @comments.group_by { |c| c[:line] }
end

Instance Attribute Details

#commentsArray<Hash> (readonly)

Returns All extracted comments with metadata.

Returns:

  • (Array<Hash>)

    All extracted comments with metadata



41
42
43
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 41

def comments
  @comments
end

#linesArray<String> (readonly)

Returns Source lines (chomped).

Returns:

  • (Array<String>)

    Source lines (chomped)



44
45
46
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 44

def lines
  @lines
end

Instance Method Details

#augment(owners: [], **options) ⇒ Ast::Merge::Comment::Augmenter

Build a passive shared comment augmenter for this source.

Parameters:

  • owners (Array) (defaults to: [])

    Structural owners for attachment inference

  • options (Hash)

    Additional augmenter options

Returns:



389
390
391
392
393
394
395
396
397
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 389

def augment(owners: [], **options)
  Augmenter.new(
    lines: @lines,
    comments: @comments,
    owners: owners,
    style: comment_style,
    **options
  )
end

#blank_line?(line_num) ⇒ Boolean

Check if a line is blank.

Parameters:

  • line_num (Integer)

    1-based line number

Returns:

  • (Boolean)


321
322
323
324
325
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 321

def blank_line?(line_num)
  return false if line_num < 1 || line_num > @lines.length

  @lines[line_num - 1].to_s.strip.empty?
end

#comment_at(line_num) ⇒ Hash?

Get comment hash at a specific line.

Parameters:

  • line_num (Integer)

    1-based line number

Returns:

  • (Hash, nil)


78
79
80
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 78

def comment_at(line_num)
  @comments_by_line[line_num]&.first
end

#comment_attachment_for(owner, line_num: nil, leading_comments: nil, inline_comment: nil, trailing_comments: nil, **metadata) ⇒ Ast::Merge::Comment::Attachment

Build a passive shared comment attachment for an owner.

Parameters:

  • owner (Object)

    Structural owner for the attachment

  • line_num (Integer, nil) (defaults to: nil)

    Line number to use for leading/inline lookup

  • leading_comments (Array<Hash>, nil) (defaults to: nil)

    Optional preselected leading comments

  • inline_comment (Hash, nil) (defaults to: nil)

    Optional preselected inline comment

  • trailing_comments (Array<Hash>, nil) (defaults to: nil)

    Optional preselected trailing comments

  • metadata (Hash)

    Additional metadata preserved on the attachment

Returns:



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 278

def comment_attachment_for(owner, line_num: nil, leading_comments: nil, inline_comment: nil,
                           trailing_comments: nil, **)
  resolved_line_num = line_num || owner_line_num(owner)
  resolved_end_line = owner_end_line(owner) || resolved_line_num
  leading_region = if resolved_line_num
                     leading_comment_region_before(resolved_line_num, comments: leading_comments)
                   end
  inline_region = (inline_comment_region_at(resolved_line_num, comment: inline_comment) if resolved_line_num)
  trailing_region = if resolved_end_line
                      trailing_comment_region_after(resolved_end_line, comments: trailing_comments,
                                                                       owner: owner)
                    end

  Attachment.new(
    owner: owner,
    leading_region: leading_region,
    inline_region: inline_region,
    trailing_region: trailing_region,
    metadata: .merge(
      line_num: resolved_line_num,
      end_line: resolved_end_line,
      source: :comment_tracker
    )
  )
end

#comment_node_at(line_num) ⇒ Ast::Merge::Comment::Line?

Get a shared comment node at a specific line.

Parameters:

  • line_num (Integer)

    1-based line number

Returns:



93
94
95
96
97
98
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 93

def comment_node_at(line_num)
  comment = comment_at(line_num)
  return unless comment

  build_comment_node(comment)
end

#comment_nodesArray<Ast::Merge::Comment::Line>

Get all comments converted to shared comment nodes.

Returns:



85
86
87
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 85

def comment_nodes
  @comment_nodes ||= @comments.map { |c| build_comment_node(c) }
end

#comment_region_for_range(range, kind:, full_line_only: false) ⇒ Ast::Merge::Comment::Region

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 (:leading, :inline, :orphan, etc.)

  • full_line_only (Boolean) (defaults to: false)

    Whether to keep only full-line comments

Returns:



118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 118

def comment_region_for_range(range, kind:, full_line_only: false)
  selected = comments_in_range(range)
  selected = selected.select { |c| c[:full_line] } if full_line_only

  build_region(
    kind: kind,
    comments: selected,
    metadata: {
      range: range,
      full_line_only: full_line_only,
      source: :comment_tracker
    }
  )
end

#comments_in_range(range) ⇒ Array<Hash>

Get all comments in a line range.

Parameters:

  • range (Range)

    Range of 1-based line numbers

Returns:

  • (Array<Hash>)


108
109
110
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 108

def comments_in_range(range)
  @comments.select { |c| range.cover?(c[:line]) }
end

#full_line_comment?(line_num) ⇒ Boolean

Check if a line is a full-line comment.

Parameters:

  • line_num (Integer)

    1-based line number

Returns:

  • (Boolean)


312
313
314
315
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 312

def full_line_comment?(line_num)
  comment = comment_at(line_num)
  comment&.dig(:full_line) || false
end

#inline_comment_at(line_num) ⇒ Hash?

Get trailing comment on the same line (inline comment).

Parameters:

  • line_num (Integer)

    1-based line number

Returns:

  • (Hash, nil)


191
192
193
194
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 191

def inline_comment_at(line_num)
  comment = comment_at(line_num)
  comment if comment && !comment[:full_line]
end

#inline_comment_region_at(line_num, comment: nil) ⇒ Ast::Merge::Comment::Region?

Get a shared inline comment region at a line.

Parameters:

  • line_num (Integer)

    1-based line number

  • comment (Hash, nil) (defaults to: nil)

    Optional preselected inline comment hash

Returns:



201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 201

def inline_comment_region_at(line_num, comment: nil)
  selected = [comment || inline_comment_at(line_num)].compact
  return if selected.empty?

  build_region(
    kind: :inline,
    comments: selected,
    metadata: {
      line_num: line_num,
      source: :comment_tracker
    }
  )
end

#leading_comment_region_before(line_num, comments: nil) ⇒ Ast::Merge::Comment::Region?

Get a shared leading comment region before a line.

Parameters:

  • line_num (Integer)

    1-based line number

  • comments (Array<Hash>, nil) (defaults to: nil)

    Optional preselected comment hashes

Returns:



172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 172

def leading_comment_region_before(line_num, comments: nil)
  selected = comments || leading_comments_before(line_num)
  selected = selected.select { |c| c[:full_line] }
  return if selected.empty?

  build_region(
    kind: :leading,
    comments: selected,
    metadata: {
      line_num: line_num,
      source: :comment_tracker
    }
  )
end

#leading_comments_before(line_num) ⇒ Array<Hash>

Get leading full-line comments before a line, walking backward and skipping blank lines between consecutive comment blocks.

Parameters:

  • line_num (Integer)

    1-based line number

Returns:

  • (Array<Hash>)


142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 142

def leading_comments_before(line_num)
  leading = []
  current = line_num - 1

  # Skip blank lines between the node and its leading comments
  current -= 1 while current >= 1 && blank_line?(current)

  while current >= 1
    comment = comment_at(current)
    break unless comment && comment[:full_line]

    leading.unshift(comment)
    current -= 1

    # Skip blank lines between consecutive comments
    current -= 1 while current >= 1 && blank_line?(current)
  end

  # If the collected comments extend all the way to the file's first
  # line, they are a preamble/header comment — not semantically owned
  # by this particular node.  Strip preamble lines that are separated
  # from the node-specific comment block by a blank line.
  strip_preamble(leading, line_num)
end

#line_at(line_num) ⇒ String?

Get raw line content.

Parameters:

  • line_num (Integer)

    1-based line number

Returns:

  • (String, nil)


361
362
363
364
365
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 361

def line_at(line_num)
  return if line_num < 1 || line_num > @lines.length

  @lines[line_num - 1]
end

#owner_end_line(owner) ⇒ Object



367
368
369
370
371
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 367

def owner_end_line(owner)
  return owner.end_line if owner.respond_to?(:end_line) && owner.end_line

  owner_line_num(owner)
end

#strip_preamble(comments, node_line) ⇒ Array<Hash>

Strip file-preamble comments from a leading-comment collection.

Any comment block that starts at line 1 and is followed by a blank-line gap is a file header/preamble — it belongs to the file, not to any particular key. The gap is the definitive signal: it separates the preamble from node-specific comments (if any).

Unclaimed preamble comments are later picked up by the Augmenter as a preamble_region and emitted once at the top of the merged output.

Parameters:

  • comments (Array<Hash>)

    collected leading comments (ascending line order)

  • node_line (Integer)

    1-based line of the node these comments precede

Returns:

  • (Array<Hash>)

    pruned leading comments (may be empty)



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 341

def strip_preamble(comments, node_line)
  return comments if comments.empty?
  return comments unless comments.first[:line] == 1

  # Find blank-line gaps in the range from the first comment to the node.
  gaps = []
  ((comments.first[:line])..node_line).each do |ln|
    gaps << ln if blank_line?(ln)
  end
  return comments if gaps.empty?

  # Everything at or before the first gap is preamble.
  # Keep only comments that appear after the first gap.
  comments.select { |c| c[:line] > gaps.first }
end

#trailing_comment_owned_by?(comment, owner) ⇒ Boolean

Returns:

  • (Boolean)


373
374
375
376
377
378
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 373

def trailing_comment_owned_by?(comment, owner)
  return true unless owner.respond_to?(:indent) && !owner.indent.nil?
  return true unless comment.key?(:indent)

  comment[:indent].to_i == owner.indent.to_i
end

#trailing_comment_region_after(line_num, upper_bound: nil, comments: nil, owner: nil) ⇒ Ast::Merge::Comment::Region?

Get a shared trailing comment region after a line/span.

Parameters:

  • line_num (Integer)

    1-based end line of the owner

  • upper_bound (Integer, nil) (defaults to: nil)

    exclusive upper bound before the next owner

  • comments (Array<Hash>, nil) (defaults to: nil)

    Optional preselected trailing comments

Returns:



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 249

def trailing_comment_region_after(line_num, upper_bound: nil, comments: nil, owner: nil)
  selected = comments || trailing_comments_after(line_num, upper_bound: upper_bound, owner: owner)
  selected = selected.select { |c| c[:full_line] }
  return if selected.empty?

  build_region(
    kind: :trailing,
    comments: selected,
    metadata: {
      line_num: line_num,
      upper_bound: upper_bound,
      source: :comment_tracker
    }
  )
end

#trailing_comments_after(line_num, upper_bound: nil, owner: nil) ⇒ Array<Hash>

Get adjacent full-line trailing comments after an owner line/span.

Trailing ownership is stricter than leading ownership: the first trailing comment must begin immediately on the next line after the owner. Once attached, blank lines between later comment lines are preserved inside the trailing region.

Parameters:

  • line_num (Integer)

    1-based end line of the owner

  • upper_bound (Integer, nil) (defaults to: nil)

    exclusive upper bound before the next owner

Returns:

  • (Array<Hash>)


225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/ast/merge/comment/hash_tracker_base.rb', line 225

def trailing_comments_after(line_num, upper_bound: nil, owner: nil)
  trailing = []
  current = line_num + 1
  max_line = upper_bound ? upper_bound - 1 : @lines.length
  return trailing if current > max_line || blank_line?(current)

  while current <= max_line
    comment = comment_at(current)
    break unless comment && comment[:full_line] && trailing_comment_owned_by?(comment, owner)

    trailing << comment
    current += 1
    current += 1 while current <= max_line && blank_line?(current)
  end

  trailing
end