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