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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +214 -0
- data/.rubocop_todo.yml +69 -0
- data/.ruby-version +1 -0
- data/Gemfile +22 -0
- data/Guardfile +5 -0
- data/README.md +83 -0
- data/bin/yardcheck +6 -0
- data/lib/yardcheck.rb +26 -0
- data/lib/yardcheck/color.rb +27 -0
- data/lib/yardcheck/const.rb +39 -0
- data/lib/yardcheck/documentation.rb +29 -0
- data/lib/yardcheck/documentation/method_object.rb +111 -0
- data/lib/yardcheck/method_call.rb +38 -0
- data/lib/yardcheck/method_tracer.rb +86 -0
- data/lib/yardcheck/observation.rb +72 -0
- data/lib/yardcheck/proxy.rb +33 -0
- data/lib/yardcheck/runner.rb +93 -0
- data/lib/yardcheck/source_lines.rb +29 -0
- data/lib/yardcheck/spec_observer.rb +28 -0
- data/lib/yardcheck/test_runner.rb +27 -0
- data/lib/yardcheck/test_value.rb +82 -0
- data/lib/yardcheck/typedef.rb +122 -0
- data/lib/yardcheck/typedef/parser.rb +82 -0
- data/lib/yardcheck/version.rb +5 -0
- data/lib/yardcheck/violation.rb +156 -0
- data/lib/yardcheck/warning.rb +14 -0
- data/spec/integration/yardcheck_spec.rb +57 -0
- data/spec/spec_helper.rb +51 -0
- data/spec/unit/yardcheck/const_spec.rb +48 -0
- data/spec/unit/yardcheck/documentation_spec.rb +148 -0
- data/spec/unit/yardcheck/method_tracer_spec.rb +59 -0
- data/spec/unit/yardcheck/runner_spec.rb +183 -0
- data/spec/unit/yardcheck/test_value_spec.rb +25 -0
- data/spec/unit/yardcheck/typedef/parser_spec.rb +37 -0
- data/spec/unit/yardcheck/typedef_spec.rb +32 -0
- data/test_app/.rspec +1 -0
- data/test_app/Gemfile +7 -0
- data/test_app/Gemfile.lock +66 -0
- data/test_app/lib/test_app.rb +119 -0
- data/test_app/spec/test_app_spec.rb +49 -0
- data/yardcheck.gemspec +24 -0
- 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
|