peeky 0.0.14
Sign up to get free protection for your applications and to get access to all the features.
- 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
|