peeky 0.0.14

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