duck_testing 0.0.1

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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +35 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +15 -0
  5. data/.travis.yml +7 -0
  6. data/.yardopts +4 -0
  7. data/CHANGELOG.md +3 -0
  8. data/Gemfile +7 -0
  9. data/Guardfile +19 -0
  10. data/LICENSE +22 -0
  11. data/README.md +75 -0
  12. data/Rakefile +8 -0
  13. data/duck_testing.gemspec +28 -0
  14. data/lib/duck_testing.rb +19 -0
  15. data/lib/duck_testing/errors.rb +4 -0
  16. data/lib/duck_testing/method_call_data.rb +19 -0
  17. data/lib/duck_testing/reporter/base.rb +17 -0
  18. data/lib/duck_testing/reporter/raise_error.rb +30 -0
  19. data/lib/duck_testing/tester.rb +50 -0
  20. data/lib/duck_testing/type/base.rb +13 -0
  21. data/lib/duck_testing/type/class_instance.rb +24 -0
  22. data/lib/duck_testing/type/constant.rb +26 -0
  23. data/lib/duck_testing/type/duck_type.rb +24 -0
  24. data/lib/duck_testing/type/hash.rb +60 -0
  25. data/lib/duck_testing/type/order_dependent_array.rb +27 -0
  26. data/lib/duck_testing/type/order_independent_array.rb +27 -0
  27. data/lib/duck_testing/version.rb +3 -0
  28. data/lib/duck_testing/violation.rb +41 -0
  29. data/lib/duck_testing/yard.rb +51 -0
  30. data/lib/duck_testing/yard/builder.rb +70 -0
  31. data/lib/duck_testing/yard/class_object.rb +20 -0
  32. data/lib/duck_testing/yard/code_object.rb +22 -0
  33. data/lib/duck_testing/yard/method_object.rb +87 -0
  34. data/lib/duck_testing/yard/method_parameter.rb +49 -0
  35. data/lib/duck_testing/yard/parser.rb +30 -0
  36. data/sample/.gitignore +35 -0
  37. data/sample/.rspec +3 -0
  38. data/sample/Gemfile +5 -0
  39. data/sample/lib/concern.rb +9 -0
  40. data/sample/lib/sample.rb +14 -0
  41. data/sample/spec/sample_spec.rb +33 -0
  42. data/sample/spec/spec_helper.rb +4 -0
  43. data/spec/.rubocop.yml +8 -0
  44. data/spec/class_type_integration_spec.rb +98 -0
  45. data/spec/constant_type_integration_spec.rb +90 -0
  46. data/spec/duck_testing/method_call_data_spec.rb +24 -0
  47. data/spec/duck_testing/reporter/raise_error_spec.rb +35 -0
  48. data/spec/duck_testing/tester_spec.rb +73 -0
  49. data/spec/duck_testing/violation_spec.rb +58 -0
  50. data/spec/duck_testing/yard/builder_spec.rb +179 -0
  51. data/spec/duck_testing/yard/parser_spec.rb +38 -0
  52. data/spec/duck_type_integration_spec.rb +89 -0
  53. data/spec/hash_type_integration_spec.rb +112 -0
  54. data/spec/order_dependent_array_type_integration_spec.rb +121 -0
  55. data/spec/order_independent_array_type_integration_spec.rb +104 -0
  56. data/spec/spec_helper.rb +78 -0
  57. metadata +212 -0
@@ -0,0 +1,24 @@
1
+ require "duck_testing/type/base"
2
+
3
+ module DuckTesting
4
+ module Type
5
+ class DuckType < Base
6
+ attr_reader :name
7
+
8
+ def initialize(name)
9
+ @name = name
10
+ end
11
+
12
+ # @param object [Object]
13
+ # @return [Boolean]
14
+ def match?(object)
15
+ object.respond_to?(name)
16
+ end
17
+
18
+ # @return [String]
19
+ def to_s
20
+ "##{name}"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,60 @@
1
+ require "duck_testing/type/base"
2
+
3
+ module DuckTesting
4
+ module Type
5
+ class Hash < Base
6
+ attr_reader :key_types, :value_types
7
+
8
+ # @param key_types [Array<DuckTesting::Type::Base>]
9
+ # @param value_types [Array<DuckTesting::Type::Base>]
10
+ def initialize(key_types, value_types)
11
+ @key_types = key_types
12
+ @value_types = value_types
13
+ end
14
+
15
+ # @param object [Object]
16
+ # @return [Boolean]
17
+ def match?(object)
18
+ return false unless object.is_a?(::Hash)
19
+ match_keys?(object) && match_values?(object)
20
+ end
21
+
22
+ # @return [String]
23
+ def to_s
24
+ "Hash{#{key_types_to_s} => #{value_types_to_s}}"
25
+ end
26
+
27
+ private
28
+
29
+ # @param hash [Hash]
30
+ # @return [Boolean]
31
+ def match_keys?(hash)
32
+ hash.keys.all? do |key|
33
+ key_types.all? do |type|
34
+ type.match?(key)
35
+ end
36
+ end
37
+ end
38
+
39
+ # @param hash [Hash]
40
+ # @return [Boolean]
41
+ def match_values?(hash)
42
+ hash.values.all? do |key|
43
+ value_types.all? do |type|
44
+ type.match?(key)
45
+ end
46
+ end
47
+ end
48
+
49
+ # @return [String]
50
+ def key_types_to_s
51
+ key_types.map(&:to_s).join(", ")
52
+ end
53
+
54
+ # @return [String]
55
+ def value_types_to_s
56
+ value_types.map(&:to_s).join(", ")
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,27 @@
1
+ require "duck_testing/type/base"
2
+
3
+ module DuckTesting
4
+ module Type
5
+ class OrderDependentArray < Base
6
+ attr_reader :types
7
+
8
+ def initialize(*types)
9
+ @types = types
10
+ end
11
+
12
+ # @param [Object] array
13
+ # @return [Boolean]
14
+ def match?(array)
15
+ return false unless array.is_a?(Array) && array.size == types.size
16
+ array.zip(types).all? do |array_element, type|
17
+ type.match?(array_element)
18
+ end
19
+ end
20
+
21
+ # @return [String]
22
+ def to_s
23
+ "Array<(#{types.map(&:to_s).join(', ')})>"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ require "duck_testing/type/base"
2
+
3
+ module DuckTesting
4
+ module Type
5
+ class OrderIndependentArray < Base
6
+ attr_reader :types
7
+
8
+ def initialize(*types)
9
+ @types = types
10
+ end
11
+
12
+ # @param [Object] array
13
+ # @return [Boolean]
14
+ def match?(array)
15
+ return false unless array.is_a?(Array)
16
+ array.all? do |array_element|
17
+ types.any? { |type| type.match?(array_element) }
18
+ end
19
+ end
20
+
21
+ # @return [String]
22
+ def to_s
23
+ "Array<#{types.map(&:to_s).join(', ')}>"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module DuckTesting
2
+ VERSION = "0.0.1".freeze
3
+ end
@@ -0,0 +1,41 @@
1
+ require "forwardable"
2
+
3
+ module DuckTesting
4
+ class Violation
5
+ extend Forwardable
6
+
7
+ def_delegators :@call_data, :method_expr
8
+
9
+ attr_reader :param
10
+
11
+ # @param call_data [DuckTesting::MethodCallData]
12
+ # @param param [Object] the given object.
13
+ # @param expected_types [Array<DuckTesting::Type::Base>] the expected object types.
14
+ # @param param_or_return [Symbol] the report type, `:param` or `:return`
15
+ def initialize(call_data: nil, param: nil, expected_types: nil, param_or_return: nil)
16
+ @call_data = call_data
17
+ @param = param
18
+ @expected_types = expected_types
19
+ @param_or_return = param_or_return
20
+ end
21
+
22
+ # @return [Boolean]
23
+ def param?
24
+ param_or_return == :param
25
+ end
26
+
27
+ # @return [Boolean]
28
+ def return?
29
+ param_or_return == :return
30
+ end
31
+
32
+ # @return [String]
33
+ def expected
34
+ expected_types.map(&:to_s).join(", ")
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :call_data, :expected_types, :param_or_return
40
+ end
41
+ end
@@ -0,0 +1,51 @@
1
+ require "duck_testing/yard/builder"
2
+ require "duck_testing/yard/parser"
3
+
4
+ module DuckTesting
5
+ module YARD
6
+ # The default glob of files to be parsed.
7
+ DEFAULT_PATH_GLOB = ["{lib,app}/**/*.rb"]
8
+
9
+ # Parses a path or set of paths then prepend them into corresponding classes..
10
+ #
11
+ # @param paths [String, Array<String>] a path, glob or list of paths to parse
12
+ # @param excluded [Array<String, Regexp>] a list of excluded path matchers
13
+ # @return [void]
14
+ def apply(paths: DEFAULT_PATH_GLOB, excluded: [])
15
+ Parser.parse(paths, excluded).each do |class_object|
16
+ builder = Builder.new(class_object)
17
+ begin
18
+ klass = Object.const_get(class_object.path)
19
+ rescue NameError
20
+ next
21
+ end
22
+ klass.send(:prepend, builder.build(:instance))
23
+ klass.singleton_class.send(:prepend, builder.build(:class))
24
+ end
25
+ end
26
+
27
+ # @param types [Array<String>]
28
+ # @return [Array<DuckTesting::Type::Base>]
29
+ def expected_types(types)
30
+ types.map do |type|
31
+ if type == "Boolean"
32
+ [
33
+ DuckTesting::Type::Constant.new(true),
34
+ DuckTesting::Type::Constant.new(false),
35
+ ]
36
+ elsif DuckTesting::Type::Constant::CONSTANTS.include?(type)
37
+ DuckTesting::Type::Constant.new(type)
38
+ elsif type == "void"
39
+ nil
40
+ elsif type.start_with?("Array")
41
+ # TODO: Support specifing types of array elements.
42
+ DuckTesting::Type::ClassInstance.new(Array)
43
+ else
44
+ DuckTesting::Type::ClassInstance.new(Object.const_get(type))
45
+ end
46
+ end.flatten.compact
47
+ end
48
+
49
+ module_function :apply, :expected_types
50
+ end
51
+ end
@@ -0,0 +1,70 @@
1
+ module DuckTesting
2
+ module YARD
3
+ class Builder
4
+ # @param class_object [DuckTesting::YARD::ClassObject]
5
+ def initialize(class_object)
6
+ @class_object = class_object
7
+ end
8
+
9
+ # Build duck testing for the `@class_object`.
10
+ #
11
+ # @param scope [Symbol] `:instance` or `:class`
12
+ # @return [Module] duck testing module of `class_object` in the `scope`.
13
+ def build(scope = :instance)
14
+ @building_module = prepare_module
15
+ @class_object.method_objects.each do |method_object|
16
+ next unless method_object.scope == scope
17
+ handle_method_object(method_object)
18
+ end
19
+ @building_module
20
+ ensure
21
+ remove_instance_variable :@building_module
22
+ end
23
+
24
+ # rubocop:disable Lint/NestedMethodDefinition, Metrics/AbcSize
25
+
26
+ private
27
+
28
+ def prepare_module
29
+ Module.new do
30
+ private
31
+
32
+ def __dk_test_normal_parameters(tester, method_object, parameters)
33
+ parameters.each_with_index do |parameter, index|
34
+ tester.test_param(
35
+ parameter,
36
+ method_object.method_parameters[index].expected_types
37
+ )
38
+ end
39
+ end
40
+
41
+ def __dk_test_keyword_parameters(tester, method_object, hash_parameter)
42
+ method_object.keyword_parameters.each do |method_parameter|
43
+ next unless hash_parameter.key?(method_parameter.key_name)
44
+ tester.test_param(
45
+ hash_parameter[method_parameter.key_name],
46
+ method_parameter.expected_types
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ # @param method_object [DuckTesting::YARD::MethodObject]
54
+ def handle_method_object(method_object)
55
+ @building_module.module_eval do
56
+ define_method method_object.name do |*parameters|
57
+ tester = DuckTesting::Tester.new(self, method_object.name)
58
+ if method_object.keyword_parameters.any? && parameters.last.is_a?(Hash)
59
+ __dk_test_normal_parameters(tester, method_object, parameters[0...-1])
60
+ __dk_test_keyword_parameters(tester, method_object, parameters.last)
61
+ else
62
+ __dk_test_normal_parameters(tester, method_object, parameters)
63
+ end
64
+ tester.test_return(super(*parameters), method_object.expected_return_types)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,20 @@
1
+ require "duck_testing/yard/code_object"
2
+
3
+ module DuckTesting
4
+ module YARD
5
+ # Encapsulate `YARD::CodeObjects::ClassObject`.
6
+ class ClassObject < CodeObject
7
+ # @return [Array<DuckTesting::YARD::MethodObject>]
8
+ def method_objects
9
+ @method_objects ||= yard_object.meths.map do |method_object|
10
+ MethodObject.new(method_object)
11
+ end
12
+ end
13
+
14
+ # @return [String]
15
+ def verbose_name
16
+ @verbose_name ||= yard_object.path.gsub(/::/, "")
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ require "forwardable"
2
+
3
+ module DuckTesting
4
+ module YARD
5
+ class CodeObject
6
+ extend Forwardable
7
+
8
+ attr_reader :yard_object
9
+
10
+ # @!attribute [r] path
11
+ # Represents the unique path of the object.
12
+ #
13
+ # @return [String]
14
+ def_delegators :@yard_object, "path"
15
+
16
+ # @param yard_object [YARD::CodeObjects::Base]
17
+ def initialize(yard_object)
18
+ @yard_object = yard_object
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,87 @@
1
+ require "duck_testing/yard/code_object"
2
+
3
+ module DuckTesting
4
+ module YARD
5
+ # Encapsulate `YARD::CodeObjects::MethodObject`.
6
+ class MethodObject < CodeObject
7
+ # @!attribute [r] name
8
+ # Returns the name of the object.
9
+ #
10
+ # @return [String]
11
+ # @!attribute [r] scope
12
+ # Returns the scope of the object.
13
+ #
14
+ # @return [Symbol]
15
+ def_delegators :@yard_object, "name", "scope"
16
+
17
+ # @return [String]
18
+ def signature
19
+ "#{name}(#{parameters_signature})"
20
+ end
21
+
22
+ # @return [Array<DuckTesting::YARD::MethodParameter>]
23
+ def method_parameters
24
+ @method_parameters ||= begin
25
+ if yard_object.parameters.any?
26
+ yard_object.parameters.map do |name, default|
27
+ MethodParameter.new(self, name, default, get_parameter_tag(name))
28
+ end
29
+ elsif parameter_tags.any?
30
+ # Maybe the method is defined using a DSL and its signature is
31
+ # declared with "@!method" directive.
32
+ parameter_tags.map do |tag|
33
+ MethodParameter.new(self, tag.name, nil, tag)
34
+ end
35
+ else
36
+ # The method does not take any arguments.
37
+ []
38
+ end
39
+ end
40
+ end
41
+
42
+ # @return [Array<DuckTesting::YARD::MethodParameter>]
43
+ def keyword_parameters
44
+ method_parameters.select(&:keyword?)
45
+ end
46
+
47
+ # @return [YARD::Tags::Tag]
48
+ def return_tag
49
+ @return_tag ||= yard_object.tags.find { |tag| tag.tag_name == "return" }
50
+ end
51
+
52
+ # @return [Array<DuckTesting::Type::Base>]
53
+ def expected_return_types
54
+ return_tag ? DuckTesting::YARD.expected_types(return_tag.types) : []
55
+ end
56
+
57
+ # @return [Boolean]
58
+ def public_instance_method?
59
+ yard_object.scope == :instance
60
+ end
61
+
62
+ # @return [Boolean]
63
+ def public_class_method?
64
+ yard_object.scope == :class
65
+ end
66
+
67
+ private
68
+
69
+ # @return [String]
70
+ def parameters_signature
71
+ method_parameters.map(&:to_s).join(", ")
72
+ end
73
+
74
+ # @return [YARD::Tags::Tag]
75
+ def get_parameter_tag(name)
76
+ parameter_tags.find do |tag|
77
+ name == (name.end_with?(":") ? "#{tag.name}:" : tag.name)
78
+ end
79
+ end
80
+
81
+ # @return [Array<YARD::Tags::Tag>]
82
+ def parameter_tags
83
+ @parameter_tags ||= yard_object.tags.select { |tag| tag.tag_name == "param" }
84
+ end
85
+ end
86
+ end
87
+ end