Module: Ast::Merge::DebugLogger

Extended by:
DebugLogger
Included in:
DebugLogger
Defined in:
lib/ast/merge/debug_logger.rb

Overview

Note:

Shared examples require silent_stream and rspec-stubbed_env gems.

Base debug logging utility for AST merge libraries. Provides conditional debug output based on environment configuration.

This module is designed to be extended by file-type-specific merge libraries (e.g., Prism::Merge, Psych::Merge) which configure their own environment variable and log prefix.

Minimal Integration

Simply extend this module and configure your environment variable and log prefix:

Overriding Methods

When you extend a module, its instance methods become singleton methods on your module. To override inherited behavior, you must define *singleton methods* (+def self.method_name+), not instance methods (+def method_name+).

Testing with Shared Examples

Use the provided shared examples to validate your integration:

require "ast/merge/rspec/shared_examples"

RSpec.describe MyMerge::DebugLogger do
  it_behaves_like "Ast::Merge::DebugLogger" do
    let(:described_logger) { MyMerge::DebugLogger }
    let(:env_var_name) { "MY_MERGE_DEBUG" }
    let(:log_prefix) { "[MyMerge]" }
  end
end

Examples:

Creating a custom debug logger (minimal integration)

module MyMerge
  module DebugLogger
    extend Ast::Merge::DebugLogger

    self.env_var_name = "MY_MERGE_DEBUG"
    self.log_prefix = "[MyMerge]"
  end
end

Overriding a method (correct - singleton method)

module MyMerge
  module DebugLogger
    extend Ast::Merge::DebugLogger

    self.env_var_name = "MY_MERGE_DEBUG"
    self.log_prefix = "[MyMerge]"

    # Override extract_node_info for custom node types
    def self.extract_node_info(node)
      case node
      when MyMerge::CustomNode
        {type: "CustomNode", lines: "#{node.start_line}..#{node.end_line}"}
      else
        # Delegate to base implementation
        Ast::Merge::DebugLogger.extract_node_info(node)
      end
    end
  end
end

Enable debug logging

ENV['AST_MERGE_DEBUG'] = '1'
Ast::Merge::DebugLogger.debug("Processing node", {type: "mapping", line: 5})

Constant Summary collapse

BENCHMARK_AVAILABLE =

Benchmark is optional - gracefully degrade if not available

begin
  require "benchmark"
  true
rescue LoadError
  # :nocov:
  # Platform-specific: benchmark is part of Ruby stdlib, LoadError only on unusual Ruby builds
  false
  # :nocov:
end

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.env_var_nameString

Returns Environment variable name to check for debug mode.

Returns:

  • (String)

    Environment variable name to check for debug mode



86
87
88
# File 'lib/ast/merge/debug_logger.rb', line 86

def env_var_name
  @env_var_name
end

.log_prefixString

Returns Prefix for log messages.

Returns:

  • (String)

    Prefix for log messages



89
90
91
# File 'lib/ast/merge/debug_logger.rb', line 89

def log_prefix
  @log_prefix
end

Class Method Details

.extended(base) ⇒ Object

Hook called when a module extends Ast::Merge::DebugLogger. Sets up attr_accessor for env_var_name and log_prefix on the extending module, and copies the BENCHMARK_AVAILABLE constant.

Parameters:

  • base (Module)

    The module that is extending this module



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

def extended(base)
  # Create a module with the accessors and prepend it to the singleton class.
  # This avoids "method redefined" warnings when extending multiple times.
  accessors_module = Module.new do
    attr_accessor :env_var_name
    attr_accessor :log_prefix
  end
  base.singleton_class.prepend(accessors_module)

  # Set default values (inherit from Ast::Merge::DebugLogger)
  base.env_var_name = env_var_name
  base.log_prefix = log_prefix

  # Copy the BENCHMARK_AVAILABLE constant
  base.const_set(:BENCHMARK_AVAILABLE, BENCHMARK_AVAILABLE) unless base.const_defined?(:BENCHMARK_AVAILABLE)
end

Instance Method Details

#debug(message, context = {}) ⇒ Object

Log a debug message with optional context

Parameters:

  • message (String)

    The debug message

  • context (Hash) (defaults to: {})

    Optional context to include



162
163
164
165
166
167
168
# File 'lib/ast/merge/debug_logger.rb', line 162

def debug(message, context = {})
  return unless enabled?

  output = "#{log_prefix} #{message}"
  output += " #{context.inspect}" unless context.empty?
  warn(output)
end

#enabled?Boolean

Check if debug mode is enabled

Returns:

  • (Boolean)


121
122
123
124
# File 'lib/ast/merge/debug_logger.rb', line 121

def enabled?
  val = ENV[env_var_name]
  %w[1 true].include?(val)
end

#env_var_nameString

Get the environment variable name. When called as a module method (via extend self), returns own config. When called as instance method, checks class first, then falls back to base.

Returns:

  • (String)


131
132
133
134
135
136
137
138
139
140
# File 'lib/ast/merge/debug_logger.rb', line 131

def env_var_name
  if is_a?(Module) && singleton_class.method_defined?(:env_var_name)
    # Called as module method on a module that extended us
    (self.class.superclass == Module) ? @env_var_name : self.class.env_var_name
  elsif self.class.respond_to?(:env_var_name)
    self.class.env_var_name
  else
    Ast::Merge::DebugLogger.env_var_name
  end
end

#extract_lines(node) ⇒ String?

Extract line information from a node if available

Parameters:

  • node (Object)

    Node to extract lines from

Returns:

  • (String, nil)

    Line range string or nil



254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/ast/merge/debug_logger.rb', line 254

def extract_lines(node)
  if node.respond_to?(:location)
    loc = node.location
    if loc.respond_to?(:start_line) && loc.respond_to?(:end_line)
      "#{loc.start_line}..#{loc.end_line}"
    elsif loc.respond_to?(:start_line)
      loc.start_line.to_s
    end
  elsif node.respond_to?(:start_line) && node.respond_to?(:end_line)
    "#{node.start_line}..#{node.end_line}"
  end
end

#extract_node_info(node) ⇒ Hash

Extract information from a node for logging. Override in submodules for file-type-specific node types.

Parameters:

  • node (Object)

    Node to extract info from

Returns:

  • (Hash)

    Node information



226
227
228
229
230
231
232
233
# File 'lib/ast/merge/debug_logger.rb', line 226

def extract_node_info(node)
  type_name = safe_type_name(node)
  lines = extract_lines(node)

  info = {type: type_name}
  info[:lines] = lines if lines
  info
end

#info(message) ⇒ Object

Log an info message (always shown when debug is enabled)

Parameters:

  • message (String)

    The info message



173
174
175
176
177
# File 'lib/ast/merge/debug_logger.rb', line 173

def info(message)
  return unless enabled?

  warn("#{log_prefix} INFO] #{message}")
end

#log_node(node, label: "Node") ⇒ Object

Log node information - override in submodules for file-type-specific logging

Parameters:

  • node (Object)

    Node to log information about

  • label (String) (defaults to: "Node")

    Label for the node



214
215
216
217
218
219
# File 'lib/ast/merge/debug_logger.rb', line 214

def log_node(node, label: "Node")
  return unless enabled?

  info = extract_node_info(node)
  debug(label, info)
end

#log_prefixString

Get the log prefix. When called as a module method (via extend self), returns own config. When called as instance method, checks class first, then falls back to base.

Returns:

  • (String)


147
148
149
150
151
152
153
154
155
156
# File 'lib/ast/merge/debug_logger.rb', line 147

def log_prefix
  if is_a?(Module) && singleton_class.method_defined?(:log_prefix)
    # Called as module method on a module that extended us
    (self.class.superclass == Module) ? @log_prefix : self.class.log_prefix
  elsif self.class.respond_to?(:log_prefix)
    self.class.log_prefix
  else
    Ast::Merge::DebugLogger.log_prefix
  end
end

#safe_type_name(node) ⇒ String

Safely extract the type name from a node

Parameters:

  • node (Object)

    Node to get type from

Returns:

  • (String)

    Type name



239
240
241
242
243
244
245
246
247
248
# File 'lib/ast/merge/debug_logger.rb', line 239

def safe_type_name(node)
  klass = node.class
  if klass.respond_to?(:name) && klass.name
    klass.name.split("::").last
  else
    klass.to_s
  end
rescue StandardError
  node.class.to_s
end

#time(operation) { ... } ⇒ Object

Time a block and log the duration

Parameters:

  • operation (String)

    Name of the operation

Yields:

  • The block to time

Returns:

  • (Object)

    The result of the block



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/ast/merge/debug_logger.rb', line 191

def time(operation)
  return yield unless enabled?

  unless BENCHMARK_AVAILABLE
    warning("Benchmark gem not available - timing disabled for: #{operation}")
    return yield
  end

  debug("Starting: #{operation}")
  result = nil
  timing = Benchmark.measure { result = yield }
  debug("Completed: #{operation}", {
    real_ms: (timing.real * 1000).round(2),
    user_ms: (timing.utime * 1000).round(2),
    system_ms: (timing.stime * 1000).round(2),
  })
  result
end

#warning(message) ⇒ Object

Log a warning message (always shown)

Parameters:

  • message (String)

    The warning message



182
183
184
# File 'lib/ast/merge/debug_logger.rb', line 182

def warning(message)
  warn("#{log_prefix} WARNING] #{message}")
end