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.
@@ -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