peeky 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +46 -0
- data/.rspec +3 -0
- data/.rubocop.yml +57 -0
- data/.rubocop_todo.yml +54 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +20 -0
- data/Guardfile +30 -0
- data/LICENSE.txt +21 -0
- data/README.md +85 -0
- data/Rakefile +16 -0
- data/bin/console +16 -0
- data/bin/k +36 -0
- data/bin/kgitsync +76 -0
- data/bin/khotfix +244 -0
- data/bin/setup +11 -0
- data/hooks/pre-commit +87 -0
- data/hooks/update-version +21 -0
- data/lib/peeky.rb +21 -0
- data/lib/peeky/attr_info.rb +63 -0
- data/lib/peeky/class_info.rb +92 -0
- data/lib/peeky/method_info.rb +83 -0
- data/lib/peeky/parameter_info.rb +104 -0
- data/lib/peeky/predicates/attr_reader_predicate.rb +39 -0
- data/lib/peeky/predicates/attr_writer_predicate.rb +29 -0
- data/lib/peeky/renderer/class_interface_render.rb +96 -0
- data/lib/peeky/renderer/method_call_minimum_params_render.rb +37 -0
- data/lib/peeky/renderer/method_signature_render.rb +39 -0
- data/lib/peeky/renderer/method_signature_with_debug_render.rb +86 -0
- data/lib/peeky/version.rb +5 -0
- data/peeky.gemspec +40 -0
- metadata +76 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# require 'peeky/attr_info'
|
4
|
+
|
5
|
+
module Peeky
|
6
|
+
# Attr Info is a container that holds read, write or read/write
|
7
|
+
# attributes in the form of MethodInfo objects
|
8
|
+
class AttrInfo
|
9
|
+
attr_reader :reader
|
10
|
+
attr_reader :writer
|
11
|
+
|
12
|
+
def initialize(reader: nil, writer: nil)
|
13
|
+
guard(reader, writer)
|
14
|
+
|
15
|
+
@reader = reader
|
16
|
+
@writer = writer
|
17
|
+
end
|
18
|
+
|
19
|
+
def type
|
20
|
+
@type ||= if @reader.nil?
|
21
|
+
:attr_writer
|
22
|
+
else
|
23
|
+
@writer.nil? ? :attr_reader : :attr_accessor
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def name
|
28
|
+
@name ||= @reader.nil? ? @writer.clean_name : @reader.clean_name
|
29
|
+
end
|
30
|
+
|
31
|
+
# Factory method that will create an AttrInfo from a list of method_infos
|
32
|
+
#
|
33
|
+
# There is an expectation that the list will have 1 or 2 method_infos and
|
34
|
+
# that they will be of type :attr_reader and/or :attr_writer
|
35
|
+
# Currently there are some edge cases that I can see where this may not
|
36
|
+
# be true and in those cases we just ignore those cases
|
37
|
+
def self.create(*method_infos)
|
38
|
+
reader = method_infos.find(&:readable?)
|
39
|
+
writer = method_infos.find(&:writable?)
|
40
|
+
|
41
|
+
new(reader: reader, writer: writer)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def guard(reader, writer)
|
47
|
+
raise 'AttrInfo requires at least one read or write parameter' if reader.nil? && writer.nil?
|
48
|
+
|
49
|
+
guard_reader(reader) unless reader.nil?
|
50
|
+
guard_writer(writer) unless writer.nil?
|
51
|
+
end
|
52
|
+
|
53
|
+
def guard_reader(reader)
|
54
|
+
raise 'reader: parameter must be of type MethodInfo' unless reader.is_a?(Peeky::MethodInfo)
|
55
|
+
raise 'reader: method does not implement the :attr_reader signature' unless reader.implementation_type == :attr_reader
|
56
|
+
end
|
57
|
+
|
58
|
+
def guard_writer(writer)
|
59
|
+
raise 'writer: parameter must be of type MethodInfo' unless writer.is_a?(Peeky::MethodInfo)
|
60
|
+
raise 'writer: method does not implement the :attr_writer signature' unless writer.implementation_type == :attr_writer
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Peeky
|
4
|
+
# Class Info stores information about the
|
5
|
+
# class instance that you pass in.
|
6
|
+
#
|
7
|
+
# The information is collected into MethodInfo objects
|
8
|
+
# that live within the signatures accessor.
|
9
|
+
#
|
10
|
+
# This information is then separated out into
|
11
|
+
# :methods, :attr_accessors, :attr_readers and :attr_writers
|
12
|
+
class ClassInfo
|
13
|
+
attr_reader :instance
|
14
|
+
|
15
|
+
# Peak into class information
|
16
|
+
def initialize(instance)
|
17
|
+
@instance = instance
|
18
|
+
end
|
19
|
+
|
20
|
+
def class_name
|
21
|
+
instance.class.name
|
22
|
+
end
|
23
|
+
|
24
|
+
def accessors
|
25
|
+
@accessors ||= attribute_infos.select { |attribute_info| attribute_info.type == :attr_accessor }
|
26
|
+
end
|
27
|
+
|
28
|
+
def readers
|
29
|
+
@readers ||= attribute_infos.select { |attribute_info| attribute_info.type == :attr_reader }
|
30
|
+
end
|
31
|
+
|
32
|
+
def writers
|
33
|
+
@writers ||= attribute_infos.select { |attribute_info| attribute_info.type == :attr_writer }
|
34
|
+
end
|
35
|
+
|
36
|
+
def methods
|
37
|
+
@methods ||= signatures.select { |signature| signature.implementation_type == :method }
|
38
|
+
end
|
39
|
+
|
40
|
+
# def signatures(types = [:instance])
|
41
|
+
# REFACT: Support different types
|
42
|
+
# such as static, private vs public
|
43
|
+
# deep, deep_to_level, this_instance.
|
44
|
+
def signatures
|
45
|
+
@signatures ||= ruby_instance_methods.map { |im| MethodInfo.new(im, @instance) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def signatures_by_name(name)
|
49
|
+
signatures.select { |im| im.name == name }
|
50
|
+
end
|
51
|
+
|
52
|
+
def signatures_by_clean_name(clean_name)
|
53
|
+
signatures.select { |im| im.clean_name == clean_name }
|
54
|
+
end
|
55
|
+
|
56
|
+
def attribute_infos
|
57
|
+
@attribute_infos ||= begin
|
58
|
+
grouped_method_infos = signatures.select { |signature| signature.readable? || signature.writable? }.group_by(&:clean_name)
|
59
|
+
|
60
|
+
grouped_method_infos.keys.map { |key| AttrInfo.create(*grouped_method_infos[key]) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def debug(format: [:signatures])
|
65
|
+
if format == :method_names
|
66
|
+
puts '-' * 70
|
67
|
+
puts 'Method Names'
|
68
|
+
puts '-' * 70
|
69
|
+
ruby_instance_method_names.each do |method_name|
|
70
|
+
puts method_name
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
return unless format == :signatures
|
75
|
+
|
76
|
+
puts '-' * 70
|
77
|
+
puts 'Methods'
|
78
|
+
puts '-' * 70
|
79
|
+
signatures.each(&:debug)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def ruby_instance_method_names
|
85
|
+
@ruby_instance_method_names ||= instance.class.instance_methods(false).sort
|
86
|
+
end
|
87
|
+
|
88
|
+
def ruby_instance_methods
|
89
|
+
@ruby_instance_methods ||= ruby_instance_method_names.map { |method_name| instance.method(method_name) }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Peeky
|
6
|
+
# Method Info
|
7
|
+
class MethodInfo
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
# List of parameters for this method
|
11
|
+
attr_accessor :parameters
|
12
|
+
|
13
|
+
# MethodInfo delegates to the underlying ruby method object
|
14
|
+
attr_reader :method
|
15
|
+
|
16
|
+
def_delegators :method, :name, :receiver, :arity, :super_method
|
17
|
+
|
18
|
+
# Stage 2 is the method likely to be an attribute reader or writer
|
19
|
+
|
20
|
+
# Implemented As indicates the probable representation of this
|
21
|
+
# method in ruby, was it `def method` or `attr_reader` / `attr_writer`
|
22
|
+
attr_reader :implementation_type
|
23
|
+
|
24
|
+
def initialize(method, target_instance = nil)
|
25
|
+
@method = method
|
26
|
+
@parameters = ParameterInfo.from_method(method)
|
27
|
+
# stage 1
|
28
|
+
# @implementation_type = :method
|
29
|
+
|
30
|
+
# stage 2
|
31
|
+
@implementation_type = infer_implementation_type(target_instance)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Stage 2 (working out) attr_accessor
|
35
|
+
# Name of method minus writer annotations
|
36
|
+
# Example :writable_attribute= becomes :writable_attribute
|
37
|
+
def clean_name
|
38
|
+
@clean_name ||= begin
|
39
|
+
n = name.to_s
|
40
|
+
n.end_with?('=') ? n.delete_suffix('=').to_sym : name
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def infer_implementation_type(target_instance)
|
45
|
+
if target_instance.nil?
|
46
|
+
:method
|
47
|
+
elsif match(target_instance, Peeky::Predicates::AttrReaderPredicate)
|
48
|
+
:attr_reader
|
49
|
+
elsif match(target_instance, Peeky::Predicates::AttrWriterPredicate)
|
50
|
+
:attr_writer
|
51
|
+
else
|
52
|
+
:method
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def match(target_instance, predicate)
|
57
|
+
predicate.new.match(target_instance, self)
|
58
|
+
end
|
59
|
+
|
60
|
+
def method?
|
61
|
+
@implementation_type == :method
|
62
|
+
end
|
63
|
+
|
64
|
+
# https://github.com/rubyide/vscode-ruby/issues/454
|
65
|
+
# if I prefix these methods with attr_ then will get an issue
|
66
|
+
# in the language server.
|
67
|
+
# Cannot read property 'namedChildren' of undefined
|
68
|
+
def readable?
|
69
|
+
@implementation_type == :attr_reader
|
70
|
+
end
|
71
|
+
|
72
|
+
def writable?
|
73
|
+
@implementation_type == :attr_writer
|
74
|
+
end
|
75
|
+
|
76
|
+
def debug
|
77
|
+
puts '-' * 70
|
78
|
+
puts name
|
79
|
+
puts '-' * 70
|
80
|
+
parameters.each(&:debug)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Peeky
|
4
|
+
# Parameter Info takes a ruby paramater pair (*see below) and maps
|
5
|
+
# it to more readily usable properties
|
6
|
+
#
|
7
|
+
# Ruby parameter is an odd array format that with an array of 1 or 2 elements.
|
8
|
+
# Examples:
|
9
|
+
# - [:req]
|
10
|
+
# - [:req, 'aaa']
|
11
|
+
# - [:opt, 'aaa']
|
12
|
+
# - [:rest, 'aaa']
|
13
|
+
# - [:keyreq, 'aaa']
|
14
|
+
# - [:key, 'aaa']
|
15
|
+
# - [:block, 'aaa']
|
16
|
+
class ParameterInfo
|
17
|
+
# name of the parameter
|
18
|
+
attr_accessor :name
|
19
|
+
|
20
|
+
# type of the parameter
|
21
|
+
attr_accessor :type
|
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
|
28
|
+
|
29
|
+
def initialize(param)
|
30
|
+
map(param)
|
31
|
+
end
|
32
|
+
|
33
|
+
private_class_method :new
|
34
|
+
|
35
|
+
# Ruby parameter is an odd array format that with an array of 1 or 2 elements.
|
36
|
+
# Examples:
|
37
|
+
# - [:req]
|
38
|
+
# - [:req, 'aaa']
|
39
|
+
# - [:opt, 'aaa']
|
40
|
+
# - [:rest, 'aaa']
|
41
|
+
# - [:keyreq, 'aaa']
|
42
|
+
# - [:key, 'aaa']
|
43
|
+
# - [:block, 'aaa']
|
44
|
+
def self.from_parameter(ruby_parameter)
|
45
|
+
new(ruby_parameter)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Gets list of parameters fro ma ruby method and maps them to
|
49
|
+
# an array of ParameterInfo
|
50
|
+
def self.from_method(ruby_method)
|
51
|
+
ruby_method.parameters.map { |ruby_paramater| from_parameter(ruby_paramater) }
|
52
|
+
end
|
53
|
+
|
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}"
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Convert the limited information provided by ruby method.parameters
|
64
|
+
# to a richer structure.
|
65
|
+
# rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
|
66
|
+
def map(param)
|
67
|
+
@name = param.length > 1 ? param[1].to_s : ''
|
68
|
+
|
69
|
+
case param[0]
|
70
|
+
when :req
|
71
|
+
@type = :param_required
|
72
|
+
@signature_format = name.to_s
|
73
|
+
@minimal_call_format = "'#{name}'"
|
74
|
+
when :opt
|
75
|
+
@type = :param_optional
|
76
|
+
@signature_format = "#{name} = nil"
|
77
|
+
@minimal_call_format = ''
|
78
|
+
when :rest
|
79
|
+
@type = :splat
|
80
|
+
@signature_format = "*#{name}"
|
81
|
+
@minimal_call_format = ''
|
82
|
+
when :keyreq
|
83
|
+
@type = :key_required
|
84
|
+
@signature_format = "#{name}:"
|
85
|
+
@minimal_call_format = "#{name}: '#{name}'"
|
86
|
+
when :key
|
87
|
+
@type = :key_optional
|
88
|
+
@signature_format = "#{name}: nil"
|
89
|
+
@minimal_call_format = ''
|
90
|
+
when :keyrest
|
91
|
+
@type = :double_splat
|
92
|
+
@signature_format = "**#{name}"
|
93
|
+
@minimal_call_format = ''
|
94
|
+
when :block
|
95
|
+
@type = :block
|
96
|
+
@signature_format = "&#{name}"
|
97
|
+
@minimal_call_format = ''
|
98
|
+
else
|
99
|
+
raise 'unknown type'
|
100
|
+
end
|
101
|
+
end
|
102
|
+
# rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module Peeky
|
6
|
+
module Predicates
|
7
|
+
# Attr Reader Predicate will match true if the method info could be considered
|
8
|
+
# a valid attr_reader
|
9
|
+
class AttrReaderPredicate
|
10
|
+
def match(instance, method_info)
|
11
|
+
return false unless prerequisites(instance, method_info)
|
12
|
+
|
13
|
+
variable_name = "@#{method_info.name}"
|
14
|
+
method_name = method_info.name
|
15
|
+
|
16
|
+
# Refactor: Fragile
|
17
|
+
# Really need to handle exceptions and types better
|
18
|
+
# old_value = instance.send(method_name)
|
19
|
+
new_value = SecureRandom.alphanumeric(20)
|
20
|
+
code = <<-RUBY
|
21
|
+
#{variable_name} = '#{new_value}' # @variable = 'a3bj7a3bj7a3bj7a3bj7'
|
22
|
+
RUBY
|
23
|
+
instance.instance_eval(code)
|
24
|
+
current_value = instance.send(method_name)
|
25
|
+
current_value == new_value
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def prerequisites(instance, method_info)
|
31
|
+
return false if %w[! ? =].include?(method_info.name.to_s[-1..-1])
|
32
|
+
return false unless method_info.parameters.length.zero?
|
33
|
+
return false unless instance.respond_to?(method_info.name)
|
34
|
+
|
35
|
+
true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module Peeky
|
6
|
+
module Predicates
|
7
|
+
# Attr Writer Predicate will match true if the method info could be considered
|
8
|
+
# a valid attr_writer
|
9
|
+
class AttrWriterPredicate
|
10
|
+
def match(instance, method_info)
|
11
|
+
return false unless prerequisites(instance, method_info)
|
12
|
+
|
13
|
+
param = method_info.parameters.first
|
14
|
+
param.type == :param_required && param.name.empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def prerequisites(instance, method_info)
|
20
|
+
return false if %w[! ?].include?(method_info.name.to_s[-1..-1])
|
21
|
+
return false unless method_info.name.to_s.end_with?('=')
|
22
|
+
return false unless instance.respond_to?(method_info.name)
|
23
|
+
return false unless method_info.parameters.length == 1
|
24
|
+
|
25
|
+
true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Peeky
|
4
|
+
module Renderer
|
5
|
+
# Class Interface Render
|
6
|
+
#
|
7
|
+
# Example output:
|
8
|
+
# class SampleClassClassInterfaceRender
|
9
|
+
# attr_accessor :a
|
10
|
+
#
|
11
|
+
# attr_reader :b
|
12
|
+
#
|
13
|
+
# attr_writer :c
|
14
|
+
#
|
15
|
+
# def alpha_sort1; end
|
16
|
+
# def alpha_sort2; end
|
17
|
+
# def d; end
|
18
|
+
# def e(aaa); end
|
19
|
+
# def f(aaa, bbb = nil); end
|
20
|
+
# def g(aaa, bbb = nil, ccc = nil); end
|
21
|
+
# def h(*aaa); end
|
22
|
+
# def i(aaa, bbb, *ccc); end
|
23
|
+
# def j(**aaa); end
|
24
|
+
# def k(aaa, *bbb, **ccc); end
|
25
|
+
# def l(aaa, *bbb, **ccc, &ddd); end
|
26
|
+
# def m(aaa:); end
|
27
|
+
# def n(aaa:, bbb: nil); end
|
28
|
+
# def p?; end
|
29
|
+
# def q!; end
|
30
|
+
# def z(aaa, bbb = nil, *ccc, ddd:, eee: nil, **fff, &ggg); end
|
31
|
+
# end
|
32
|
+
class ClassInterfaceRender
|
33
|
+
attr_reader :class_info
|
34
|
+
|
35
|
+
def initialize(class_info)
|
36
|
+
@class_info = class_info
|
37
|
+
end
|
38
|
+
|
39
|
+
def render
|
40
|
+
@indent = ''
|
41
|
+
output = []
|
42
|
+
output.push render_start
|
43
|
+
@indent = ' '
|
44
|
+
output += render_accessors
|
45
|
+
output += render_readers
|
46
|
+
output += render_writers
|
47
|
+
output += render_methods
|
48
|
+
output.pop if output.last == ''
|
49
|
+
|
50
|
+
@indent = ''
|
51
|
+
output.push render_end
|
52
|
+
|
53
|
+
output.join("\n")
|
54
|
+
end
|
55
|
+
|
56
|
+
def render_start
|
57
|
+
"#{@indent}class #{class_info.class_name}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def render_accessors
|
61
|
+
result = class_info.accessors.map { |attr| "#{@indent}attr_accessor :#{attr.name}" }
|
62
|
+
result.push '' unless result.length.zero?
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
def render_readers
|
67
|
+
result = class_info.readers.map { |attr| "#{@indent}attr_reader :#{attr.name}" }
|
68
|
+
result.push '' unless result.length.zero?
|
69
|
+
result
|
70
|
+
end
|
71
|
+
|
72
|
+
def render_writers
|
73
|
+
result = class_info.writers.map { |attr| "#{@indent}attr_writer :#{attr.name}" }
|
74
|
+
result.push '' unless result.length.zero?
|
75
|
+
result
|
76
|
+
end
|
77
|
+
|
78
|
+
def render_methods
|
79
|
+
result = class_info.methods.map do |method_signature|
|
80
|
+
render_signature = Peeky::Renderer::MethodSignatureRender.new(method_signature)
|
81
|
+
"#{@indent}#{render_signature.render}"
|
82
|
+
end
|
83
|
+
result.push '' unless result.length.zero?
|
84
|
+
result
|
85
|
+
end
|
86
|
+
|
87
|
+
def render_end
|
88
|
+
"#{@indent}end"
|
89
|
+
end
|
90
|
+
|
91
|
+
def debug
|
92
|
+
puts render
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|