yardcheck 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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +214 -0
  5. data/.rubocop_todo.yml +69 -0
  6. data/.ruby-version +1 -0
  7. data/Gemfile +22 -0
  8. data/Guardfile +5 -0
  9. data/README.md +83 -0
  10. data/bin/yardcheck +6 -0
  11. data/lib/yardcheck.rb +26 -0
  12. data/lib/yardcheck/color.rb +27 -0
  13. data/lib/yardcheck/const.rb +39 -0
  14. data/lib/yardcheck/documentation.rb +29 -0
  15. data/lib/yardcheck/documentation/method_object.rb +111 -0
  16. data/lib/yardcheck/method_call.rb +38 -0
  17. data/lib/yardcheck/method_tracer.rb +86 -0
  18. data/lib/yardcheck/observation.rb +72 -0
  19. data/lib/yardcheck/proxy.rb +33 -0
  20. data/lib/yardcheck/runner.rb +93 -0
  21. data/lib/yardcheck/source_lines.rb +29 -0
  22. data/lib/yardcheck/spec_observer.rb +28 -0
  23. data/lib/yardcheck/test_runner.rb +27 -0
  24. data/lib/yardcheck/test_value.rb +82 -0
  25. data/lib/yardcheck/typedef.rb +122 -0
  26. data/lib/yardcheck/typedef/parser.rb +82 -0
  27. data/lib/yardcheck/version.rb +5 -0
  28. data/lib/yardcheck/violation.rb +156 -0
  29. data/lib/yardcheck/warning.rb +14 -0
  30. data/spec/integration/yardcheck_spec.rb +57 -0
  31. data/spec/spec_helper.rb +51 -0
  32. data/spec/unit/yardcheck/const_spec.rb +48 -0
  33. data/spec/unit/yardcheck/documentation_spec.rb +148 -0
  34. data/spec/unit/yardcheck/method_tracer_spec.rb +59 -0
  35. data/spec/unit/yardcheck/runner_spec.rb +183 -0
  36. data/spec/unit/yardcheck/test_value_spec.rb +25 -0
  37. data/spec/unit/yardcheck/typedef/parser_spec.rb +37 -0
  38. data/spec/unit/yardcheck/typedef_spec.rb +32 -0
  39. data/test_app/.rspec +1 -0
  40. data/test_app/Gemfile +7 -0
  41. data/test_app/Gemfile.lock +66 -0
  42. data/test_app/lib/test_app.rb +119 -0
  43. data/test_app/spec/test_app_spec.rb +49 -0
  44. data/yardcheck.gemspec +24 -0
  45. metadata +158 -0
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yardcheck
4
+ module Color
5
+ private
6
+
7
+ def color(code, text)
8
+ "\e[#{code}m#{text}\e[0m"
9
+ end
10
+
11
+ def blue(text)
12
+ color(34, text)
13
+ end
14
+
15
+ def red(text)
16
+ color(31, text)
17
+ end
18
+
19
+ def yellow(text)
20
+ color(33, text)
21
+ end
22
+
23
+ def grey(text)
24
+ color(30, text)
25
+ end
26
+ end # Color
27
+ end # Yardcheck
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yardcheck
4
+ class Const
5
+ include Concord::Public.new(:constant)
6
+
7
+ def self.resolve(constant_name, scope = Object)
8
+ return new(scope.const_get(constant_name)) if scope.const_defined?(constant_name)
9
+
10
+ parent = parent_namespace(scope)
11
+ from_parent = resolve(constant_name, parent.constant) if parent.valid?
12
+ from_parent && from_parent.valid? ? from_parent : Invalid.new(scope, constant_name)
13
+ end
14
+
15
+ def self.parent_namespace(scope)
16
+ parent_name = scope.name.split('::').slice(0...-1).join('::')
17
+
18
+ if parent_name.empty?
19
+ Invalid.new(Object, parent_name)
20
+ else
21
+ resolve(parent_name)
22
+ end
23
+ end
24
+
25
+ def valid?
26
+ true
27
+ end
28
+
29
+ class Invalid < self
30
+ include Concord.new(:scope, :constant)
31
+
32
+ public :constant
33
+
34
+ def valid?
35
+ false
36
+ end
37
+ end # Invalid
38
+ end # Const
39
+ end # Yardcheck
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yardcheck
4
+ class Documentation
5
+ include Concord.new(:yardocs), Memoizable
6
+
7
+ # mutest:disable
8
+ def self.parse
9
+ load_yard
10
+ new(YARD::Registry.all(:method))
11
+ end
12
+
13
+ # This is just YARD implementation details so I don't want to mutation cover this
14
+ # mutest:disable
15
+ def self.load_yard
16
+ # YARD doesn't write to .yardoc/ without this lock_for_writing and save
17
+ YARD::Registry.lock_for_writing do
18
+ YARD.parse(['lib/**/*.rb'], [], YARD::Logger::ERROR)
19
+ YARD::Registry.save(true)
20
+ end
21
+
22
+ YARD::Registry.load!
23
+ end
24
+
25
+ def method_objects
26
+ yardocs.map { |yardoc| MethodObject.new(yardoc) }
27
+ end
28
+ end # Documentation
29
+ end # Yardcheck
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yardcheck
4
+ class Documentation
5
+ class MethodObject
6
+ include Concord.new(:yardoc), Adamantium::Flat
7
+
8
+ def selector
9
+ yardoc.name
10
+ end
11
+
12
+ def namespace
13
+ singleton? ? unscoped_namespace.singleton_class : unscoped_namespace
14
+ end
15
+ memoize :namespace
16
+
17
+ def params
18
+ param_typedefs.select { |key, value| key && !value.invalid_const? }
19
+ end
20
+ memoize :params
21
+
22
+ def return_type
23
+ return_typedef unless return_typedef&.invalid_const?
24
+ end
25
+
26
+ def singleton?
27
+ scope.equal?(:class)
28
+ end
29
+
30
+ def scope
31
+ yardoc.scope
32
+ end
33
+
34
+ def location
35
+ [yardoc.file, yardoc.line]
36
+ end
37
+
38
+ def method_identifier
39
+ [namespace, selector, scope]
40
+ end
41
+
42
+ def shorthand
43
+ "#{namespace}##{selector}"
44
+ end
45
+
46
+ def source
47
+ [documentation_source, yardoc.source].join("\n")
48
+ end
49
+
50
+ def location_pointer
51
+ location.join(':')
52
+ end
53
+
54
+ def warnings
55
+ param_warnings = param_typedefs.select { |_, typedef| typedef.invalid_const? }.values
56
+ return_warning = return_typedef if return_typedef&.invalid_const?
57
+
58
+ [*param_warnings, *return_warning].map { |warning| Warning.new(self, warning) }
59
+ end
60
+
61
+ private
62
+
63
+ def return_typedef
64
+ return_tag.map(&method(:typedefs)).reduce(:+)
65
+ end
66
+ memoize :return_typedef
67
+
68
+ def return_tag
69
+ tags(:return)
70
+ end
71
+
72
+ def param_typedefs
73
+ tags(:param).map do |param_tag|
74
+ param_name = param_tag.name.to_sym if param_tag.name
75
+ [param_name, typedefs(param_tag)]
76
+ end.to_h
77
+ end
78
+ memoize :param_typedefs
79
+
80
+ def documentation_source
81
+ file_source.documentation_above(source_starting_line)
82
+ end
83
+
84
+ def source_starting_line
85
+ location.last
86
+ end
87
+
88
+ def file_source
89
+ SourceLines.process(File.read(location.first))
90
+ end
91
+ memoize :file_source
92
+
93
+ def typedefs(tags)
94
+ Typedef::Parser.new(qualified_namespace, tags.types.to_a).parse
95
+ end
96
+
97
+ def unscoped_namespace
98
+ Const.resolve(qualified_namespace).constant
99
+ end
100
+ memoize :unscoped_namespace
101
+
102
+ def qualified_namespace
103
+ yardoc.namespace.to_s
104
+ end
105
+
106
+ def tags(type)
107
+ yardoc.tags(type)
108
+ end
109
+ end # MethodObject
110
+ end # Documentation
111
+ end # Yardcheck
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yardcheck
4
+ class MethodCall
5
+ include Anima.new(
6
+ :scope,
7
+ :selector,
8
+ :namespace,
9
+ :params,
10
+ :example_location,
11
+ :return_value,
12
+ :error_raised
13
+ )
14
+
15
+ def self.process(params:, return_value:, **attributes)
16
+ params =
17
+ params.map do |key, value|
18
+ [key, TestValue.process(value)]
19
+ end.to_h
20
+
21
+ return_value = TestValue.process(return_value)
22
+
23
+ new(params: params, return_value: return_value, **attributes)
24
+ end
25
+
26
+ def method_identifier
27
+ [namespace, selector, scope]
28
+ end
29
+
30
+ def initialize?
31
+ selector == :initialize && scope == :instance
32
+ end
33
+
34
+ def raised?
35
+ error_raised
36
+ end
37
+ end # MethodCall
38
+ end # Yardcheck
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yardcheck
4
+ class MethodTracer
5
+ include Concord.new(:namespace, :seen, :call_stack), Memoizable
6
+
7
+ def initialize(namespace)
8
+ super(namespace, [], [])
9
+ end
10
+
11
+ def trace(&block)
12
+ tracer.enable(&block)
13
+ end
14
+
15
+ def events
16
+ seen.freeze
17
+ end
18
+
19
+ private
20
+
21
+ def tracer
22
+ TracePoint.new(:call, :return, :raise) do |event|
23
+ tracer.disable do
24
+ process(event) if target?(event.defined_class)
25
+ end
26
+ end
27
+ end
28
+ memoize :tracer
29
+
30
+ def process(trace_event)
31
+ case trace_event.event
32
+ when :call then process_call(trace_event)
33
+ when :return then process_return(trace_event)
34
+ when :raise then process_raise
35
+ end
36
+ end
37
+
38
+ def process_call(trace_event)
39
+ parameter_names =
40
+ trace_event
41
+ .defined_class
42
+ .instance_method(trace_event.method_id)
43
+ .parameters.map { |_, name| name }
44
+
45
+ scope = trace_event.binding
46
+ params =
47
+ scope
48
+ .local_variables
49
+ .select { |lvar| parameter_names.include?(lvar) }
50
+ .map { |lvar| [lvar, scope.local_variable_get(lvar)] }.to_h
51
+
52
+ event = event_details(trace_event).update(params: params)
53
+ call_stack.push(event)
54
+ end
55
+
56
+ def process_return(trace_event)
57
+ seen << MethodCall.process(
58
+ call_stack.pop.merge(return_value: trace_event.return_value)
59
+ )
60
+ end
61
+
62
+ def process_raise
63
+ call_stack.last[:error_raised] = true
64
+ end
65
+
66
+ def event_details(event)
67
+ {
68
+ scope: event.defined_class.__send__(:singleton_class?) ? :class : :instance,
69
+ selector: event.method_id,
70
+ namespace: event.defined_class,
71
+ example_location: RSpec.current_example.location,
72
+ error_raised: false
73
+ }
74
+ end
75
+
76
+ def target?(klass)
77
+ return false unless klass
78
+
79
+ if klass.__send__(:singleton_class?)
80
+ klass.to_s =~ /\A#{Regexp.quote("#<Class:#{namespace}")}/
81
+ else
82
+ klass.name && klass.name.start_with?(namespace.name)
83
+ end
84
+ end
85
+ end # MethodTracer
86
+ end # Yardcheck
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yardcheck
4
+ class Observation
5
+ include Concord.new(:documentation, :event)
6
+
7
+ def violations
8
+ param_violations + return_violations
9
+ end
10
+
11
+ def source_code
12
+ documentation.source
13
+ end
14
+
15
+ def source_location
16
+ documentation.location_pointer
17
+ end
18
+
19
+ def test_location
20
+ event.example_location
21
+ end
22
+
23
+ def method_shorthand
24
+ documentation.shorthand
25
+ end
26
+
27
+ def documented_param(name)
28
+ documentation.params.fetch(name)
29
+ end
30
+
31
+ def observed_param(name)
32
+ event.params.fetch(name)
33
+ end
34
+
35
+ def documented_return_type
36
+ documentation.return_type
37
+ end
38
+
39
+ def actual_return_type
40
+ event.return_value.type
41
+ end
42
+
43
+ def documentation_warnings
44
+ documentation.warnings
45
+ end
46
+
47
+ private
48
+
49
+ def param_violations
50
+ overlapping_keys = documentation.params.keys & event.params.keys
51
+
52
+ overlapping_keys.map do |key|
53
+ type_definition = documentation.params.fetch(key)
54
+ test_value = event.params.fetch(key)
55
+
56
+ next if type_definition.match?(test_value)
57
+ Violation::Param.new(key, self)
58
+ end
59
+ end
60
+
61
+ def return_violations
62
+ invalid_return_type? ? [Violation::Return.new(self)] : []
63
+ end
64
+
65
+ def invalid_return_type?
66
+ documentation.return_type &&
67
+ !documentation.return_type.match?(event.return_value) &&
68
+ !event.raised? &&
69
+ !event.initialize?
70
+ end
71
+ end # Observation
72
+ end # Yardcheck
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yardcheck
4
+ class Proxy < BasicObject
5
+ def initialize(target)
6
+ @target = target
7
+ end
8
+
9
+ undef_method :==
10
+ undef_method :!=
11
+ undef_method :!
12
+
13
+ def method_missing(method_name, *args, &block)
14
+ if target_respond_to?(method_name)
15
+ @target.__send__(method_name, *args, &block)
16
+ else
17
+ ::Object
18
+ .instance_method(method_name)
19
+ .bind(@target)
20
+ .call(*args, &block)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def target_respond_to?(method_name)
27
+ ::Object
28
+ .instance_method(:respond_to?)
29
+ .bind(@target)
30
+ .call(method_name, true)
31
+ end
32
+ end # Proxy
33
+ end # Yardcheck