peeky 0.0.25 → 0.0.39

Sign up to get free protection for your applications and to get access to all the features.
data/USAGE2.md ADDED
@@ -0,0 +1,5 @@
1
+ # Peeky
2
+
3
+ This document will show you how to use Peeky.
4
+
5
+ Peeky is a Ruby GEM for peaking into ruby classes and extracting meta
data/lib/peeky.rb CHANGED
@@ -12,6 +12,7 @@ require 'peeky/parameter_info'
12
12
  require 'peeky/predicates/attr_reader_predicate'
13
13
  require 'peeky/predicates/attr_writer_predicate'
14
14
 
15
+ require 'peeky/renderer/class_debug_render'
15
16
  require 'peeky/renderer/class_interface_render'
16
17
  require 'peeky/renderer/class_interface_yard_render'
17
18
  require 'peeky/renderer/method_call_minimum_params_render'
data/lib/peeky/api.rb CHANGED
@@ -16,11 +16,24 @@ module Peeky
16
16
  # ClassInfo stores information about the instance of a
17
17
  # class that is passed in including methods, attr_accessors
18
18
  # attr_readers and attr_writers.
19
- def build_class_info(instance)
20
- Peeky::ClassInfo.new(instance)
19
+ #
20
+ # @param instance [Object] instance of class to gather information about (required)
21
+ # @param lazy [TrueClass] lazy load method and parameter information, laze: is optional, defaults to true
22
+ def build_class_info(instance, lazy: true)
23
+ ci = Peeky::ClassInfo.new(instance)
24
+ ci.load unless lazy
25
+ ci
21
26
  end
22
27
 
23
28
  # Render a class using a predefined class renderer
29
+ #
30
+ # ^1: One of class_info and instance must supplied, they are mutually
31
+ # exclusive to each other.
32
+ #
33
+ # @param render_key [Symbol] class render key (required)
34
+ # @param class_info [Object] class_info: is optional^1, defaults to nil
35
+ # @param instance [Object] instance: is optional^1, defaults to nil
36
+ # @param _opts [<key: value>...] **_opts - list of key/values that can help configure render
24
37
  def render_class(render_key, class_info: nil, instance: nil, **_opts)
25
38
  raise 'Call render_class with class_info OR instance.' if class_info.nil? && instance.nil?
26
39
  raise 'Call render_class with class_info OR instance, these parameters are mutually exclusive' if !class_info.nil? && !instance.nil?
@@ -35,6 +48,7 @@ module Peeky
35
48
  # Get a method renderer by :key
36
49
  #
37
50
  # TODO: Refactor to a configurable system
51
+ # @param key [String] key is a shortcut to a specific Peeky::Render that handles method_info (required)
38
52
  def method_renderer(key)
39
53
  case key
40
54
  when :signature
@@ -51,8 +65,11 @@ module Peeky
51
65
  # Get a class renderer by :key
52
66
  #
53
67
  # TODO: Refactor to a configurable system
68
+ # @param key [String] key is a shortcut to a specific Peeky::Renderer that handles class_info (required)
54
69
  def class_renderer(key)
55
70
  case key
71
+ when :class_debug
72
+ Peeky::Renderer::ClassDebugRender
56
73
  when :class_interface
57
74
  Peeky::Renderer::ClassInterfaceRender
58
75
  when :class_interface_yard
@@ -6,7 +6,9 @@ module Peeky
6
6
  # Attr Info is a container that holds read, write or read/write
7
7
  # attributes in the form of MethodInfo objects
8
8
  class AttrInfo
9
+ # reader stores a MethodInfo for a matching reader, nil if readable style method_info not found
9
10
  attr_reader :reader
11
+ # writer stores a MethodInfo for a matching writer, nil if writable style method_info not found
10
12
  attr_reader :writer
11
13
 
12
14
  def initialize(reader: nil, writer: nil)
@@ -16,6 +18,7 @@ module Peeky
16
18
  @writer = writer
17
19
  end
18
20
 
21
+ # Type of the attribute [:attr_writer, :attr_reader or :attr_accessor]
19
22
  def type
20
23
  @type ||= if @reader.nil?
21
24
  :attr_writer
@@ -24,6 +27,7 @@ module Peeky
24
27
  end
25
28
  end
26
29
 
30
+ # Name of the attribute
27
31
  def name
28
32
  @name ||= @reader.nil? ? @writer.clean_name : @reader.clean_name
29
33
  end
@@ -17,6 +17,43 @@ module Peeky
17
17
  @instance = instance
18
18
  end
19
19
 
20
+ # rubocop:disable Metrics/AbcSize
21
+ def to_s
22
+ result = []
23
+ result.push kv('class', class_full_name)
24
+ if defined?(@_ruby_instance_method_names)
25
+ result.push kv('# of instance methods', @_ruby_instance_method_names.join(', '))
26
+ else
27
+ result.push kv('# of instance methods', '')
28
+ end
29
+ if defined?(@_signatures)
30
+ result.push kv('# of accessors', accessors.length)
31
+ result.push kv('# of readers', readers.length)
32
+ result.push kv('# of writers', writers.length)
33
+ result.push kv('# of methods', methods.length)
34
+ else
35
+ result.push kv('# of accessors', '')
36
+ result.push kv('# of readers', '')
37
+ result.push kv('# of writers', '')
38
+ result.push kv('# of methods', '')
39
+ end
40
+ result.join("\n")
41
+ end
42
+ # rubocop:enable Metrics/AbcSize
43
+
44
+ # Load class_info
45
+ #
46
+ # Accessing information about methods and parameters is currently
47
+ # lazy-loaded via memoization.
48
+ #
49
+ # At times during debug or other edge cases, it may be useful to
50
+ # pre-load this information early.
51
+ def load
52
+ ruby_instance_methods
53
+ ruby_instance_method_names
54
+ signatures
55
+ end
56
+
20
57
  # Class full name includes the module namespace
21
58
  def class_full_name
22
59
  instance.class.name
@@ -144,53 +181,20 @@ module Peeky
144
181
  signatures.select { |im| im.name == name && im.implementation_type == filter_type }
145
182
  end
146
183
 
147
- # Build (not sure where I am going with this yet)
148
- # TODO: Refact: Currently the idea is to pre-load data
149
- # this is slower when you are not accessing things, but
150
- # it is easier to debug, so think about what I really want
151
- # here
152
- def build()
153
- ruby_instance_methods
154
- ruby_instance_method_names
155
- signatures
156
- end
157
-
158
- # Debug
159
- #
160
- # Refact: PATTERN: Come up it an debug inclusion system so that
161
- # so that debug helpers can be included for development and excluded
162
- # for production
163
- # @param format [String] format: <value for format> (optional)
164
- def debug(format: [:signatures])
165
- if format.include?(:method_names)
166
- puts '-' * 70
167
- puts 'Method Names'
168
- puts '-' * 70
169
- ruby_instance_method_names.each do |method_name|
170
- puts method_name
171
- end
172
- end
173
-
174
- return unless format.include?(:signatures)
184
+ private
175
185
 
176
- puts '-' * 70
177
- puts 'Methods'
178
- puts '-' * 70
179
- signatures.each(&:debug)
186
+ def kv(key, value)
187
+ "#{key.to_s.ljust(25)}: #{value}"
180
188
  end
181
189
 
182
- private
183
-
184
190
  def ruby_instance_method_names
185
191
  @_ruby_instance_method_names ||= instance.class.instance_methods(false).sort
186
192
  end
187
193
 
188
194
  def ruby_instance_methods
189
- begin
190
- @_ruby_instance_methods ||= ruby_instance_method_names.map { |method_name| instance.method(method_name) }
191
- rescue => exception
192
- puts exception
193
- end
195
+ @_ruby_instance_methods ||= ruby_instance_method_names.map { |method_name| instance.method(method_name) }
196
+ rescue StandardError => e
197
+ puts e
194
198
  end
195
199
  end
196
200
  end
@@ -47,16 +47,16 @@ module Peeky
47
47
  # F method with required param and optional param
48
48
  #
49
49
  # @param first_name [String] first name (required)
50
- # @param last_name [String] last name (optional)
51
- def f_method_with_required_param_and_optional_param(first_name, last_name = nil)
50
+ # @param last_name [String] last_name is optional, defaults to ''
51
+ def f_method_with_required_param_and_optional_param(first_name, last_name = '')
52
52
  end
53
53
 
54
54
  # G method with required param and two optional params
55
55
  #
56
56
  # @param first_name [String] first name (required)
57
- # @param last_name [String] last name (optional)
58
- # @param age [String] age (optional)
59
- def g_method_with_required_param_and_two_optional_params(first_name, last_name = nil, age = nil)
57
+ # @param last_name [String] last_name is optional, defaults to ''
58
+ # @param age [Integer] age is optional, defaults to 21
59
+ def g_method_with_required_param_and_two_optional_params(first_name, last_name = '', age = 21)
60
60
  end
61
61
 
62
62
  # H list of optional parameters
@@ -94,11 +94,12 @@ module Peeky
94
94
  # N method with key value param required and optional key value
95
95
  #
96
96
  # @param last_name [String] last_name: <value for last name> (required)
97
- # @param salutation [String] salutation: <value for salutation> (optional)
98
- def n_method_with_key_value_param_required_and_optional_key_value(last_name:, salutation: nil)
97
+ # @param salutation [String] salutation: is optional, defaults to 'Mr'
98
+ def n_method_with_key_value_param_required_and_optional_key_value(last_name:, salutation: 'Mr')
99
99
  end
100
100
 
101
101
  # P available?
102
+
102
103
  # @return [Boolean] true when p available?
103
104
  def p_available?
104
105
  end
@@ -110,13 +111,29 @@ module Peeky
110
111
  # Z complex
111
112
  #
112
113
  # @param aaa [String] aaa (required)
113
- # @param bbb [String] bbb (optional)
114
+ # @param bbb [Integer] bbb is optional, defaults to 1
114
115
  # @param ccc [Array<Object>] *ccc - list of ccc
115
116
  # @param ddd [String] ddd: <value for ddd> (required)
116
- # @param eee [String] eee: <value for eee> (optional)
117
+ # @param eee [Integer] eee: is optional, defaults to 1
117
118
  # @param fff [<key: value>...] **fff - list of key/values
118
119
  # @param ggg [Block] &ggg
119
- def z_complex(aaa, bbb = nil, *ccc, ddd:, eee: nil, **fff, &ggg)
120
+ def z_complex(aaa, bbb = 1, *ccc, ddd:, eee: 1, **fff, &ggg)
121
+ end
122
+
123
+ # Z optional styles
124
+ #
125
+ # @param aaa [String] aaa (required)
126
+ # @param bbb [Integer] bbb is optional, defaults to 123
127
+ # @param ccc [String] ccc is optional, defaults to 'abc'
128
+ # @param ddd [TrueClass] ddd is optional, defaults to true
129
+ # @param eee [FalseClass] eee is optional, defaults to false
130
+ # @param fff [Object] fff is optional, defaults to nil
131
+ # @param ggg [Integer] ggg: is optional, defaults to 123
132
+ # @param hhh [String] hhh: is optional, defaults to 'xyz'
133
+ # @param iii [TrueClass] iii: is optional, defaults to true
134
+ # @param jjj [FalseClass] jjj: is optional, defaults to false
135
+ # @param kkk [Object] kkk: is optional, defaults to nil
136
+ def z_optional_styles(aaa, bbb = 123, ccc = 'abc', ddd = true, eee = false, fff = nil, ggg: 123, hhh: 'xyz', iii: true, jjj: false, kkk: )
120
137
  end
121
138
  end
122
139
  end
@@ -16,26 +16,35 @@ module Peeky
16
16
 
17
17
  def_delegators :focal_method, :name, :receiver, :arity, :super_method
18
18
 
19
- # Stage 2 is the method likely to be an attribute reader or writer
19
+ ## Stage 2 is the method likely to be an attribute reader or writer
20
+ #
20
21
 
21
22
  # Implementation type indicates the probable representation of this
22
23
  # method in ruby, was it `def method` or `attr_reader` / `attr_writer`
23
24
  attr_reader :implementation_type
24
25
 
25
- # TODO: target_instance is required...
26
- def initialize(method, target_instance = nil)
26
+ def initialize(method, target_instance)
27
27
  @focal_method = method
28
+ @target_instance = target_instance
28
29
  @parameters = ParameterInfo.from_method(method)
29
30
  # stage 1
30
31
  # @implementation_type = :method
31
32
 
32
33
  # stage 2
33
- @implementation_type = infer_implementation_type(target_instance)
34
+ infer_implementation_type
35
+
36
+ # stage 3
37
+ infer_default_paramaters
34
38
  end
35
39
 
36
40
  # Stage 2 (working out) attr_accessor
41
+ #
42
+
37
43
  # Name of method minus writer annotations
38
- # Example :writable_attribute= becomes :writable_attribute
44
+ # Example
45
+ # :writable_attribute=
46
+ # # becomes
47
+ # :writable_attribute
39
48
  def clean_name
40
49
  @clean_name ||= begin
41
50
  n = name.to_s
@@ -43,43 +52,113 @@ module Peeky
43
52
  end
44
53
  end
45
54
 
46
- def infer_implementation_type(target_instance)
47
- if target_instance.nil?
48
- :method
49
- elsif match(target_instance, Peeky::Predicates::AttrReaderPredicate)
50
- :attr_reader
51
- elsif match(target_instance, Peeky::Predicates::AttrWriterPredicate)
52
- :attr_writer
53
- else
54
- :method
55
+ # Infer implementation type [:method, :attr_reader or :attr_writer]
56
+ # rubocop:disable Lint/DuplicateBranch
57
+ def infer_implementation_type
58
+ @implementation_type = if @target_instance.nil?
59
+ :method
60
+ elsif match(Peeky::Predicates::AttrReaderPredicate)
61
+ :attr_reader
62
+ elsif match(Peeky::Predicates::AttrWriterPredicate)
63
+ :attr_writer
64
+ else
65
+ :method
66
+ end
67
+ end
68
+ # rubocop:enable Lint/DuplicateBranch
69
+
70
+ # Get parameter by name
71
+ #
72
+ # @param name [String] name (required)
73
+ def get_parameter(name)
74
+ name = name.to_s
75
+ parameters.find { |p| p.name == name }
76
+ end
77
+
78
+ # Has any optional paramaters?
79
+
80
+ # @return [Boolean] true when any parameter is optional?
81
+ def optional?
82
+ parameters.any?(&:optional?)
83
+ end
84
+
85
+ # Infer default paramater values
86
+ #
87
+ # WARNING: Unit test coverage went from .1 seconds to 30-40 seconds
88
+ # when I first introduced this method.
89
+ #
90
+ # I now only call TracePoint if I have optional parameters to be inferred.
91
+ #
92
+ # The tests are now down to 5 seconds, but it highlights the cost of use
93
+ # TracePoint.
94
+ def infer_default_paramaters
95
+ minimalist_method = Peeky::Renderer::MethodCallMinimumParamsRender.new(self).render
96
+
97
+ return if minimalist_method.end_with?('=')
98
+ return unless optional?
99
+
100
+ tracer.enable do
101
+ @target_instance.instance_eval(minimalist_method)
102
+ rescue StandardError => e
103
+ # just print the error for now, we are only attempting to capture the
104
+ # first call, any errors inside the call cannot be dealt with and should
105
+ # not be re-raised
106
+ puts e.message
107
+ end
108
+ end
109
+
110
+ def tracer
111
+ TracePoint.trace(:call, :c_call) do |tp|
112
+ next unless tp.self.is_a?(@target_instance.class)
113
+ next unless tp.method_id == name
114
+
115
+ tp.parameters.each do |_type, param_name|
116
+ method_paramater = get_parameter(param_name)
117
+
118
+ if method_paramater.optional?
119
+ value = tp.binding.local_variable_get(param_name)
120
+ method_paramater.default_value = value
121
+ end
122
+ end
55
123
  end
56
124
  end
57
125
 
58
- def match(target_instance, predicate)
59
- predicate.new.match(target_instance, self)
126
+ # Match
127
+ #
128
+ # @param predicate [String] use a predicate object with the signature match(instance, method_info)
129
+ def match(predicate)
130
+ predicate.new.match(@target_instance, self)
60
131
  end
61
132
 
133
+ # Method?
134
+
135
+ # @return [Boolean] true when implementation type is method?
62
136
  def method?
63
137
  @implementation_type == :method
64
138
  end
65
139
 
66
- # https://github.com/rubyide/vscode-ruby/issues/454
67
- # if I prefix these methods with attr_ then will get an issue
68
- # in the language server.
69
- # Cannot read property 'namedChildren' of undefined
140
+ # Readable?
141
+ #
142
+ # @return [Boolean] true when readable?
70
143
  def readable?
144
+ # Method naming issue: VSCode Ruby Language Server
145
+ #
146
+ # If this method is renamed to attr_readable, same for attr_writable.
147
+ #
148
+ # https://github.com/rubyide/vscode-ruby/issues/454
149
+ # if I prefix these methods with attr_ then will get an issue
150
+ # in the language server.
151
+ #
152
+ # Cannot read property 'namedChildren' of undefined
153
+
71
154
  @implementation_type == :attr_reader
72
155
  end
73
156
 
157
+ # Writable?
158
+
159
+ # @return [Boolean] true when implementation_type writable?
74
160
  def writable?
75
161
  @implementation_type == :attr_writer
76
162
  end
77
-
78
- def debug
79
- puts '-' * 70
80
- puts name
81
- puts '-' * 70
82
- parameters.each(&:debug)
83
- end
84
163
  end
85
164
  end
@@ -20,11 +20,8 @@ module Peeky
20
20
  # type of the parameter
21
21
  attr_accessor :type
22
22
 
23
- # ruby code format when used in a signature
24
- attr_accessor :signature_format
25
-
26
- # minimal required usage in a call to the method with this paramater
27
- attr_accessor :minimal_call_format
23
+ # default value for positional or keyed parameters
24
+ attr_accessor :default_value
28
25
 
29
26
  def initialize(param)
30
27
  map(param)
@@ -51,54 +48,133 @@ module Peeky
51
48
  ruby_method.parameters.map { |ruby_paramater| from_parameter(ruby_paramater) }
52
49
  end
53
50
 
54
- def debug
55
- puts "name : #{name}"
56
- puts "type : #{type}"
57
- puts "signature_format : #{signature_format}"
58
- puts "minimal_call_format : #{minimal_call_format}"
51
+ # ruby code formatted for use in a method signature
52
+ def signature_format
53
+ @_signature_format ||= begin
54
+ method_name = "signature_format_#{@type}".to_sym
55
+
56
+ m = method(method_name)
57
+ m.call
58
+ end
59
+ end
60
+
61
+ # minimal required usage in a call to the method with this paramater
62
+ def minimal_call_format
63
+ @_minimal_call_format ||= begin
64
+ method_name = "minimal_call_format_#{@type}".to_sym
65
+
66
+ if respond_to?(method_name, true)
67
+ m = method(method_name)
68
+ m.call
69
+ else
70
+ minimal_call_format_ignore
71
+ end
72
+ end
73
+ end
74
+
75
+ # Optional?
76
+
77
+ # @return [Boolean] true when parameter is one of the optional types?
78
+ def optional?
79
+ @_optional |= (@type == :param_optional || @type == :key_optional)
80
+ end
81
+
82
+ # Default value type will look at the default value and try to
83
+ # infer the class behind it. Will default to 'Object' fi nil
84
+ def default_value_type
85
+ if @default_value.nil?
86
+ 'Object'
87
+ else
88
+ @default_value.class
89
+ end
90
+ end
91
+
92
+ # Wrap default value in quotes if string, or no wrapping otherwise
93
+ #
94
+ # @param value_for_nil [String] value for nil, generally '' or 'nil' (required)
95
+ def wrap_default_value(value_for_nil)
96
+ if @default_value.is_a?(String)
97
+ "'#{@default_value}'"
98
+ else
99
+ @default_value.nil? ? value_for_nil : @default_value
100
+ end
59
101
  end
60
102
 
61
103
  private
62
104
 
63
105
  # Convert the limited information provided by ruby method.parameters
64
106
  # to a richer structure.
65
- # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
107
+ # rubocop:disable Metrics/CyclomaticComplexity
66
108
  def map(param)
67
109
  @name = param.length > 1 ? param[1].to_s : ''
68
110
 
111
+ @default_value = nil
112
+
69
113
  case param[0]
70
114
  when :req
71
115
  @type = :param_required
72
- @signature_format = name.to_s
73
- @minimal_call_format = "'#{name}'"
74
116
  when :opt
75
117
  @type = :param_optional
76
- @signature_format = "#{name} = nil"
77
- @minimal_call_format = ''
78
118
  when :rest
79
119
  @type = :splat
80
- @signature_format = "*#{name}"
81
- @minimal_call_format = ''
82
120
  when :keyreq
83
121
  @type = :key_required
84
- @signature_format = "#{name}:"
85
- @minimal_call_format = "#{name}: '#{name}'"
86
122
  when :key
87
123
  @type = :key_optional
88
- @signature_format = "#{name}: nil"
89
- @minimal_call_format = ''
90
124
  when :keyrest
91
125
  @type = :double_splat
92
- @signature_format = "**#{name}"
93
- @minimal_call_format = ''
94
126
  when :block
95
127
  @type = :block
96
- @signature_format = "&#{name}"
97
- @minimal_call_format = ''
98
128
  else
99
129
  raise 'unknown type'
100
130
  end
101
131
  end
102
- # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
132
+ # rubocop:enable Metrics/CyclomaticComplexity
133
+
134
+ # Signature format *: Is used to format a parameter when it is used
135
+ # inside of a method signature, eg. def my_method(p1, p2 = 'xyz', p3: :name_value)
136
+
137
+ def signature_format_param_required
138
+ name.to_s
139
+ end
140
+
141
+ def signature_format_param_optional
142
+ "#{name} = #{wrap_default_value('nil')}" # signature format needs to be moved to a method
143
+ end
144
+
145
+ def signature_format_splat
146
+ "*#{name}"
147
+ end
148
+
149
+ def signature_format_key_required
150
+ "#{name}:"
151
+ end
152
+
153
+ def signature_format_key_optional
154
+ "#{name}: #{wrap_default_value('')}"
155
+ end
156
+
157
+ def signature_format_double_splat
158
+ "**#{name}"
159
+ end
160
+
161
+ def signature_format_block
162
+ "&#{name}"
163
+ end
164
+
165
+ # Minimal call format *: Is used to format a call to a method with the least
166
+ # number of parameters needed to make it work.
167
+
168
+ def minimal_call_format_ignore
169
+ ''
170
+ end
171
+
172
+ def minimal_call_format_param_required
173
+ "'#{@name}'"
174
+ end
175
+
176
+ def minimal_call_format_key_required
177
+ "#{@name}: '#{@name}'"
178
+ end
103
179
  end
104
180
  end