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

rubocop:disable ThreadSafety/ClassAndModuleAttributes - Configuration attribute, set once at load time

Returns:

  • (String)

    Environment variable name to check for debug mode



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

def env_var_name
  @env_var_name
end

.log_prefixString

rubocop:disable ThreadSafety/ClassAndModuleAttributes - Configuration attribute, set once at load time

Returns:

  • (String)

    Prefix for log messages



92
93
94
# File 'lib/ast/merge/debug_logger.rb', line 92

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



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/ast/merge/debug_logger.rb', line 100

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



166
167
168
169
170
171
172
# File 'lib/ast/merge/debug_logger.rb', line 166

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)


125
126
127
128
# File 'lib/ast/merge/debug_logger.rb', line 125

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)


135
136
137
138
139
140
141
142
143
144
# File 'lib/ast/merge/debug_logger.rb', line 135

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



258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/ast/merge/debug_logger.rb', line 258

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



230
231
232
233
234
235
236
237
# File 'lib/ast/merge/debug_logger.rb', line 230

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



177
178
179
180
181
# File 'lib/ast/merge/debug_logger.rb', line 177

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



218
219
220
221
222
223
# File 'lib/ast/merge/debug_logger.rb', line 218

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)


151
152
153
154
155
156
157
158
159
160
# File 'lib/ast/merge/debug_logger.rb', line 151

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



243
244
245
246
247
248
249
250
251
252
# File 'lib/ast/merge/debug_logger.rb', line 243

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



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/ast/merge/debug_logger.rb', line 195

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



186
187
188
# File 'lib/ast/merge/debug_logger.rb', line 186

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