yardcheck 0.0.1

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