loading
Generated 2025-10-08T23:59:42+00:00

All Files ( 100.0% covered at 21.31 hits/line )

16 files in total.
272 relevant lines, 272 lines covered and 0 lines missed. ( 100.0% )
88 total branches, 88 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
lib/marameters.rb 100.00 % 29 15 15 0 1.20 100.00 % 0 0 0
lib/marameters/categorizer.rb 100.00 % 47 28 28 0 15.93 100.00 % 21 21 0
lib/marameters/models/forward.rb 100.00 % 10 4 4 0 1.00 100.00 % 0 0 0
lib/marameters/probe.rb 100.00 % 79 39 39 0 70.33 100.00 % 2 2 0
lib/marameters/signature.rb 100.00 % 26 13 13 0 22.54 100.00 % 2 2 0
lib/marameters/signatures/builder.rb 100.00 % 31 19 19 0 14.26 100.00 % 9 9 0
lib/marameters/signatures/defaulter.rb 100.00 % 20 11 11 0 7.64 100.00 % 9 9 0
lib/marameters/signatures/forwarder.rb 100.00 % 18 11 11 0 7.36 100.00 % 7 7 0
lib/marameters/signatures/inheritor.rb 100.00 % 39 22 22 0 34.18 100.00 % 10 10 0
lib/marameters/signatures/super.rb 100.00 % 55 31 31 0 15.94 100.00 % 16 16 0
lib/marameters/sourcers/function.rb 100.00 % 28 11 11 0 12.00 100.00 % 0 0 0
lib/marameters/sourcers/readers/any.rb 100.00 % 29 15 15 0 14.07 100.00 % 4 4 0
lib/marameters/sourcers/readers/disk.rb 100.00 % 18 10 10 0 6.40 100.00 % 0 0 0
lib/marameters/sourcers/readers/memory.rb 100.00 % 10 4 4 0 1.75 100.00 % 0 0 0
lib/marameters/sources/extractor.rb 100.00 % 36 15 15 0 2.20 100.00 % 2 2 0
lib/marameters/sources/reader.rb 100.00 % 48 24 24 0 6.75 100.00 % 6 6 0

lib/marameters.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "zeitwerk"
  3. 1 Zeitwerk::Loader.new.then do |loader|
  4. 1 loader.tag = File.basename __FILE__, ".rb"
  5. 1 loader.push_dir __dir__
  6. 1 loader.setup
  7. end
  8. # Main namespace.
  9. 1 module Marameters
  10. 1 KINDS = %i[req opt rest nokey keyreq key keyrest block].freeze
  11. 1 def self.loader registry = Zeitwerk::Registry
  12. 4 @loader ||= registry.loaders.each.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
  13. end
  14. 1 def self.categorize parameters, arguments
  15. 1 @categorize ||= Categorizer.new
  16. 1 @categorize.call parameters, arguments
  17. end
  18. 1 def self.of(...) = Probe.of(...)
  19. 1 def self.for(...) = Probe.new(...)
  20. 1 def self.signature(...) = Signature.new(...)
  21. end

lib/marameters/categorizer.rb

100.0% lines covered

100.0% branches covered

28 relevant lines. 28 lines covered and 0 lines missed.
21 total branches, 21 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Marameters
  3. # Builds the primary argument categories based on method parameters and arguments.
  4. 1 class Categorizer
  5. 1 def initialize model: Models::Forward
  6. 29 @model = model
  7. end
  8. 1 def call parameters, arguments
  9. 29 @record = model.new
  10. 29 then: 28 else: 1 map parameters, arguments.is_a?(Array) ? arguments : [arguments]
  11. end
  12. 1 private
  13. 1 attr_reader :model, :record
  14. 1 def map parameters, arguments
  15. 29 size = arguments.size
  16. 130 then: 73 else: 28 parameters.each.with_index { |pair, index| filter pair, arguments[index] if index < size }
  17. 27 record
  18. end
  19. 1 def filter pair, value
  20. 73 in: 10 case pair
  21. 10 in: 6 in [:rest] | [:rest, :*] then to_array value
  22. 6 in: 16 in [:keyrest] | [:keyrest, :**] then record.keywords = Hash value
  23. 16 in: 10 in [:req, *] | [:opt, *] then record.positionals.append value
  24. 10 in: 2 in [:rest, *] then record.positionals.append(*value)
  25. 2 in: 11 in [:nokey] then nil
  26. 11 in: 6 then: 9 else: 2 in [:keyreq, *] | [:key, *] then record.keywords.merge! value if value
  27. 6 in: 11 then: 4 else: 2 in [:keyrest, *] then record.keywords.merge!(**value) if value
  28. 11 else: 1 in [:block, *] then record.block = value
  29. 1 else fail ArgumentError, "Invalid parameter kind: #{pair.first.inspect}."
  30. end
  31. rescue TypeError
  32. 1 raise TypeError, "#{value.inspect} is an invalid #{pair.first.inspect} value."
  33. end
  34. 1 def to_array value
  35. 10 else: 7 then: 3 return unless value
  36. 7 then: 5 else: 2 record.positionals = value.is_a?(Array) ? value : [value]
  37. end
  38. end
  39. end

lib/marameters/models/forward.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Marameters
  3. 1 module Models
  4. # Models arguments, by category, for forwarding.
  5. 1 Forward = Struct.new :positionals, :keywords, :block do
  6. 1 def initialize(positionals: [], keywords: {}, block: nil) = super
  7. end
  8. end
  9. end

lib/marameters/probe.rb

100.0% lines covered

100.0% branches covered

39 relevant lines. 39 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "forwardable"
  3. 1 module Marameters
  4. # Provides information on a method's parameters.
  5. 1 class Probe
  6. 1 extend Forwardable
  7. 1 CATEGORIES = {positionals: %i[req opt], keywords: %i[keyreq key]}.freeze
  8. 1 def self.of klass, name, collection: []
  9. 6 method = klass.instance_method name
  10. 5 collection << new(method.parameters)
  11. 5 super_method = method.super_method
  12. 5 of super_method.owner, super_method.name, collection:
  13. rescue NameError
  14. 5 collection
  15. end
  16. 1 delegate %i[any? deconstruct empty? hash include? inspect to_a] => :parameters
  17. 1 attr_reader :keywords, :positionals
  18. 1 def initialize parameters, categories: CATEGORIES
  19. 170 @parameters = parameters
  20. 510 categories.each { |category, kinds| define_variable category, kinds }
  21. 170 freeze
  22. end
  23. 1 def ==(other) = hash == other.hash
  24. 1 alias eql? ==
  25. 1 def <=>(other) = to_a <=> other.to_a
  26. 1 def keywords? = keywords.any?
  27. 1 def keywords_for(*keys, **attributes)
  28. 9 attributes.select { |key| !keys.include?(key) || keywords.include?(key) }
  29. end
  30. 36 def kind?(value) = parameters.any? { |kind, _name| kind == value }
  31. 8 def kinds = parameters.map { |kind, _name| kind }
  32. 66 def name?(value) = parameters.any? { |_kind, name| name == value }
  33. 15 def names = parameters.map { |_kind, name| name }
  34. 1 def only_bare_splats?
  35. 10 parameters in [[:rest]] \
  36. | [[:rest, :*]] \
  37. | [[:keyrest]] \
  38. | [[:keyrest, :**]] \
  39. | [[:rest], [:keyrest]] \
  40. | [[:rest, :*], [:keyrest, :**]]
  41. end
  42. 8 def only_double_splats? = (parameters in [[:keyrest]] | [[:keyrest, *]])
  43. 9 def only_single_splats? = (parameters in [[:rest]] | [[:rest, *]])
  44. 1 def positionals? = positionals.any?
  45. 1 def positionals_and_maybe_keywords?
  46. 42 (positionals? && !keywords?) || (positionals? && keywords?)
  47. end
  48. 1 private
  49. 1 attr_reader :parameters
  50. 1 def define_variable category, kinds
  51. 1304 then: 276 else: 688 parameters.filter_map { |kind, name| next name if kinds.include? kind }
  52. 340 .then { |collection| instance_variable_set :"@#{category}", collection }
  53. end
  54. end
  55. end

lib/marameters/signature.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Marameters
  3. # Builds a method's parameter signature.
  4. 1 class Signature
  5. 1 def initialize parameters, builder: Signatures::Builder.new
  6. 46 @parameters = parameters
  7. 46 @builder = builder
  8. 46 freeze
  9. end
  10. 46 then: 2 else: 43 def to_s = parameters == :all ? "..." : build.join(", ")
  11. 1 alias to_str to_s
  12. 1 private
  13. 1 attr_reader :parameters, :builder
  14. 1 def build
  15. 43 parameters.reduce [] do |signature, (kind, name, default)|
  16. 59 signature << builder.call(kind, name, default:)
  17. end
  18. end
  19. end
  20. end

lib/marameters/signatures/builder.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
9 total branches, 9 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Marameters
  3. 1 module Signatures
  4. # Builds a single parameter for a method's signature.
  5. 1 class Builder
  6. 1 def initialize defaulter: Defaulter
  7. 60 @defaulter = defaulter
  8. 60 freeze
  9. end
  10. 1 def call kind, name = nil, default: nil
  11. 72 when: 6 case kind
  12. 6 when: 15 when :req then name
  13. 15 when: 9 when :opt then "#{name} = #{defaulter.call default}"
  14. 9 when: 3 when :rest then "*#{name}"
  15. 3 when: 6 when :nokey then "**nil"
  16. 6 when: 13 when :keyreq then "#{name}:"
  17. 13 when: 9 when :key then "#{name}: #{defaulter.call default}"
  18. 9 when: 9 when :keyrest then "**#{name}"
  19. 9 else: 2 when :block then "&#{name}"
  20. 2 else fail ArgumentError, "Wrong kind (#{kind}), name (#{name}), or default (#{default})."
  21. end
  22. end
  23. 1 private
  24. 1 attr_reader :defaulter
  25. end
  26. end
  27. end

lib/marameters/signatures/defaulter.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
9 total branches, 9 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Marameters
  3. 1 module Signatures
  4. # Computes a method parameter's default value.
  5. 1 Defaulter = lambda do |value, extractor: Sourcers::Function.new|
  6. 35 case value
  7. when: 7 when Proc
  8. 7 then: 1 else: 6 fail TypeError, "Use procs instead of lambdas for defaults." if value.lambda?
  9. 6 then: 1 else: 5 fail ArgumentError, "Avoid using parameters for proc defaults." if value.arity.nonzero?
  10. 5 when: 7 extractor.call value
  11. 7 when: 5 when String then value.dump
  12. 5 when: 3 when Symbol then value.inspect
  13. 3 else: 13 when nil then "nil"
  14. 13 else value
  15. end
  16. end
  17. end
  18. end

lib/marameters/signatures/forwarder.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
7 total branches, 7 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Marameters
  3. 1 module Signatures
  4. # Builds single argument for super method's signature when argument forwarding.
  5. 1 Forwarder = lambda do |kind, name = nil|
  6. 39 when: 9 case kind
  7. 9 when: 7 when :req, :opt then name
  8. 7 when: 1 when :rest then "*#{name}"
  9. 1 when: 7 when :nokey then ""
  10. 7 when: 8 when :keyreq, :key then "#{name}:"
  11. 8 when: 6 when :keyrest then "**#{name}"
  12. 6 else: 1 when :block then "&#{name}"
  13. 1 else fail ArgumentError, "Unable to forward unknown kind: #{kind.inspect}."
  14. end
  15. end
  16. end
  17. end

lib/marameters/signatures/inheritor.rb

100.0% lines covered

100.0% branches covered

22 relevant lines. 22 lines covered and 0 lines missed.
10 total branches, 10 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Marameters
  3. 1 module Signatures
  4. # Blends ancestor and descendant method parameters together while allowing default overrides.
  5. 1 class Inheritor
  6. 1 def initialize key_length: 1, kinds: KINDS
  7. 22 @key_length = key_length
  8. 22 @kinds = kinds
  9. 22 freeze
  10. end
  11. 1 def call ancestor, descendant
  12. 89 merge(ancestor, descendant).values.sort_by! { |(kind, *)| kinds.index kind }
  13. end
  14. 1 private
  15. 1 attr_reader :key_length, :kinds
  16. 1 def merge ancestor, descendant
  17. 21 ancestor.to_a.union(descendant.to_a).each.with_object({}) do |parameter, all|
  18. 122 key = parameter[..key_length]
  19. 122 kind = key.first
  20. 122 when: 34 case kind
  21. 34 when: 1 then: 16 else: 18 when :req, :opt then all[key] = parameter if descendant.positionals_and_maybe_keywords?
  22. 1 when :nokey then all
  23. when: 40 when :keyreq, :key
  24. 40 different = ancestor.keywords? && ancestor.keywords.sort != descendant.keywords.sort
  25. 40 then: 32 else: 8 all[:keyrest] = [:keyrest] if different
  26. 40 else: 47 then: 13 else: 27 all[key] = parameter if descendant.include? parameter
  27. 47 else all[kind] = parameter
  28. end
  29. end
  30. end
  31. end
  32. end
  33. end

lib/marameters/signatures/super.rb

100.0% lines covered

100.0% branches covered

31 relevant lines. 31 lines covered and 0 lines missed.
16 total branches, 16 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Marameters
  3. 1 module Signatures
  4. # Blends ancestor and descendant method arguments for forwarding to the super keyword.
  5. 1 class Super
  6. 1 def initialize key_length: 1, kinds: KINDS, forwarder: Signatures::Forwarder
  7. 22 @key_length = key_length
  8. 22 @kinds = kinds
  9. 22 @forwarder = forwarder
  10. 22 freeze
  11. end
  12. 1 def call ancestor, descendant
  13. 21 then: 1 else: 20 return "" if ancestor.empty?
  14. 20 merge(ancestor, descendant).values
  15. 27 .sort_by! { |(kind, *)| kinds.index kind }
  16. 20 .then { |parameters| build parameters }
  17. end
  18. 1 private
  19. 1 attr_reader :key_length, :kinds, :forwarder
  20. 1 def merge ancestor, descendant
  21. 20 ancestor.to_a.union(descendant.to_a).each.with_object({}) do |parameter, all|
  22. 45 key = parameter[..key_length]
  23. 45 kind, name = key
  24. 45 case kind
  25. when: 14 when :req, :opt
  26. 14 then: 2 else: 12 if ancestor.positionals? && !descendant.positionals? then all[:rest] = [:rest]
  27. 12 then: 8 else: 4 elsif ancestor.name? name then all[key] = parameter
  28. 4 else all
  29. when: 2 end
  30. 3 when :nokey then all.delete_if { |nokey, _| %i[keyreq key keyrest].include? nokey }
  31. when: 15 when :keyreq, :key
  32. 15 included = ancestor.name?(name) && descendant.name?(name)
  33. 15 different = ancestor.keywords? && ancestor.keywords.sort != descendant.keywords.sort
  34. 15 then: 6 else: 9 all[key] = parameter if included
  35. 15 else: 14 then: 7 else: 8 all[:keyrest] = [:keyrest] if different
  36. 14 then: 13 else: 1 else all[kind] = parameter if ancestor.kind? kind
  37. end
  38. end
  39. end
  40. 1 def build parameters
  41. 47 parameters.filter_map { |kind, name| forwarder.call kind, name }
  42. .join ", "
  43. end
  44. end
  45. end
  46. end

lib/marameters/sourcers/function.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Marameters
  3. 1 module Sourcers
  4. # Obtains the literal source of a function's body.
  5. 1 class Function
  6. 1 PATTERN = /
  7. (?:(?<function>proc|->))? # Statement.
  8. \s* # Optional space.
  9. \{ # Block open.
  10. (?<body>.*?) # Source code body.
  11. \} # Block close.
  12. /x
  13. 1 def initialize pattern: PATTERN, reader: Readers::Any.new
  14. 39 @pattern = pattern
  15. 39 @reader = reader
  16. 39 freeze
  17. end
  18. 8 def call(function) = reader.call(function).then { |line| line.match(pattern)[:body].strip }
  19. 1 private
  20. 1 attr_reader :pattern, :reader
  21. end
  22. end
  23. end

lib/marameters/sourcers/readers/any.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Marameters
  3. 1 module Sourcers
  4. 1 module Readers
  5. # Reads source code of callable from disk or memory.
  6. 1 class Any
  7. 1 def initialize parser: RubyVM::InstructionSequence, disk: Disk, memory: Memory
  8. 43 @parser = parser
  9. 43 @disk = disk
  10. 43 @memory = memory
  11. 43 freeze
  12. end
  13. 1 def call callable
  14. 11 instructions = parser.of callable
  15. 11 else: 9 then: 2 fail StandardError, "Unable to load source for: #{callable.inspect}." unless instructions
  16. 9 then: 8 else: 1 instructions.absolute_path ? disk.call(instructions) : memory.call(instructions)
  17. end
  18. 1 private
  19. 1 attr_reader :parser, :disk, :memory
  20. end
  21. end
  22. end
  23. end

lib/marameters/sourcers/readers/disk.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Marameters
  3. 1 module Sourcers
  4. 1 module Readers
  5. # Reads source code from on-disk instruction sequence.
  6. 1 Disk = lambda do |instructions|
  7. 10 path = instructions.absolute_path
  8. 10 line_start, column_start, line_end, column_end = instructions.to_a.dig 4, :code_location
  9. 10 lines = File.read(path).lines[(line_start - 1)..(line_end - 1)]
  10. 10 lines[-1] = lines.last.byteslice(...column_end)
  11. 10 lines[0] = lines.first.byteslice(column_start..)
  12. 10 lines.join
  13. end
  14. end
  15. end
  16. end

lib/marameters/sourcers/readers/memory.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Marameters
  3. 1 module Sourcers
  4. 1 module Readers
  5. # Reads source code from in-memory instruction sequence.
  6. 4 Memory = -> instructions { instructions.script_lines.join.chomp }
  7. end
  8. end
  9. end

lib/marameters/sources/extractor.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Marameters
  3. 1 module Sources
  4. # Extracts the literal source of a Proc's body.
  5. 1 class Extractor
  6. 1 PATTERN = /
  7. proc # Proc statement.
  8. \s* # Optional space.
  9. \{ # Block open.
  10. (?<body>.*?) # Source code body.
  11. \} # Block close.
  12. /x
  13. 1 def initialize pattern: PATTERN, reader: Reader.new
  14. 4 warn "`#{self.class}` is deprecated, use `Sourcers::Function` instead.",
  15. category: :deprecated
  16. 4 @pattern = pattern
  17. 4 @reader = reader
  18. 4 @fallback = "nil"
  19. 4 freeze
  20. end
  21. 1 def call function
  22. 3 reader.call(function).then do |line|
  23. 2 then: 1 else: 1 line.match?(pattern) ? line.match(pattern)[:body].strip : fallback
  24. end
  25. end
  26. 1 private
  27. 1 attr_reader :pattern, :reader, :fallback
  28. end
  29. end
  30. end

lib/marameters/sources/reader.rb

100.0% lines covered

100.0% branches covered

24 relevant lines. 24 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Marameters
  3. 1 module Sources
  4. # Reads object source code from memory or file (assumes implementation is a one-liner).
  5. 1 class Reader
  6. 1 def initialize offset: 1, parser: RubyVM::InstructionSequence, io: File
  7. 9 warn "`#{self.class}` is deprecated, use `Sourcers::Readers::Any` instead.",
  8. category: :deprecated
  9. 9 @offset = offset
  10. 9 @parser = parser
  11. 9 @io = io
  12. 9 freeze
  13. end
  14. 1 def call object
  15. 7 instructions = parser.of object
  16. 7 else: 5 then: 2 fail StandardError, "Unable to load source for: #{object.inspect}." unless instructions
  17. 5 process object, instructions
  18. end
  19. 1 private
  20. 1 attr_reader :offset, :parser, :io
  21. 1 def process object, instructions
  22. 5 lines = instructions.script_lines
  23. 5 then: 1 else: 4 return lines.first if lines
  24. 4 then: 3 else: 1 return extract(*object.source_location) if io.readable? instructions.absolute_path
  25. 1 fail StandardError, "Unable to load source for: #{object.inspect}."
  26. end
  27. 4 def extract(path, line_number) = io.open(path) { |body| pluck body, line_number }
  28. 1 def pluck body, line_number
  29. 3 body.each_line
  30. .with_index
  31. 67 .find { |_line, index| index + offset == line_number }
  32. .first
  33. end
  34. end
  35. end
  36. end