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,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yardcheck
|
4
|
+
class Runner
|
5
|
+
include Concord.new(:observations, :output), Memoizable
|
6
|
+
|
7
|
+
# rubocop:disable MethodLength
|
8
|
+
def self.run(args)
|
9
|
+
options = { rspec: 'spec' }
|
10
|
+
|
11
|
+
parser =
|
12
|
+
OptionParser.new do |opt|
|
13
|
+
opt.on(
|
14
|
+
'--namespace NS',
|
15
|
+
'Namespace to check documentation for and watch methods calls for'
|
16
|
+
) do |arg|
|
17
|
+
options[:namespace] = arg
|
18
|
+
end
|
19
|
+
|
20
|
+
opt.on('--include PATH', 'Path to add to load path') do |arg|
|
21
|
+
options[:include] = arg
|
22
|
+
end
|
23
|
+
|
24
|
+
opt.on('--require LIB', 'Library to require') do |arg|
|
25
|
+
options[:require] = arg
|
26
|
+
end
|
27
|
+
|
28
|
+
opt.on('--rspec ARGS', 'Arguments to give to rspec') do |arg|
|
29
|
+
options[:rspec] = arg
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
parser.parse(args)
|
34
|
+
|
35
|
+
arguments = options.fetch_values(:namespace, :include, :require, :rspec)
|
36
|
+
namespace, include_path, require_target, rspec = arguments
|
37
|
+
|
38
|
+
fail 'All arguments are required' if arguments.any?(&:nil?)
|
39
|
+
|
40
|
+
$LOAD_PATH.unshift(include_path)
|
41
|
+
require require_target
|
42
|
+
|
43
|
+
rspec = rspec.split(' ')
|
44
|
+
|
45
|
+
observations =
|
46
|
+
Yardcheck::SpecObserver
|
47
|
+
.run(rspec, namespace)
|
48
|
+
.associate_with(Yardcheck::Documentation.parse)
|
49
|
+
|
50
|
+
new(observations, $stderr)
|
51
|
+
end
|
52
|
+
|
53
|
+
def check
|
54
|
+
warn_all(warnings)
|
55
|
+
warn_all(offenses)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def warnings
|
61
|
+
observations
|
62
|
+
.flat_map(&:documentation_warnings)
|
63
|
+
.map(&:message)
|
64
|
+
end
|
65
|
+
|
66
|
+
def offenses
|
67
|
+
combined_violations.map(&:offense)
|
68
|
+
end
|
69
|
+
|
70
|
+
def warn_all(output_lines)
|
71
|
+
output_lines.map(&method(:warn))
|
72
|
+
end
|
73
|
+
|
74
|
+
def combined_violations
|
75
|
+
violations.group_by(&:combination_identifier).flat_map do |_, grouped_violations|
|
76
|
+
grouped_violations.reduce(:combine)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
memoize :combined_violations
|
80
|
+
|
81
|
+
def violations
|
82
|
+
observations
|
83
|
+
.flat_map(&:violations)
|
84
|
+
.uniq
|
85
|
+
.compact
|
86
|
+
end
|
87
|
+
memoize :violations
|
88
|
+
|
89
|
+
def warn(message)
|
90
|
+
output.puts(message)
|
91
|
+
end
|
92
|
+
end # Runner
|
93
|
+
end # Yardcheck
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yardcheck
|
4
|
+
class SourceLines
|
5
|
+
include Concord.new(:lines)
|
6
|
+
|
7
|
+
def self.process(contents)
|
8
|
+
new(
|
9
|
+
contents.split("\n").map do |line|
|
10
|
+
line.gsub(/^\s+/, '')
|
11
|
+
end
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def documentation_above(line)
|
16
|
+
first_line = last_line = line - 1
|
17
|
+
|
18
|
+
first_line -= 1 until first_line.equal?(0) || line(first_line) !~ /^\s*#/
|
19
|
+
|
20
|
+
lines[first_line..(last_line - 1)]
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def line(number)
|
26
|
+
lines.fetch(number - 1)
|
27
|
+
end
|
28
|
+
end # SourceLines
|
29
|
+
end # Yardcheck
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yardcheck
|
4
|
+
class SpecObserver
|
5
|
+
include Concord.new(:events), Memoizable
|
6
|
+
|
7
|
+
def self.run(rspec_arguments, namespace)
|
8
|
+
tracer = MethodTracer.new(Object.const_get(namespace))
|
9
|
+
test_runner = TestRunner.new(rspec_arguments)
|
10
|
+
|
11
|
+
test_runner.wrap_test(tracer.method(:trace))
|
12
|
+
test_runner.run
|
13
|
+
|
14
|
+
new(tracer.events)
|
15
|
+
end
|
16
|
+
|
17
|
+
def associate_with(documentation)
|
18
|
+
docs = documentation.method_objects.group_by(&:method_identifier)
|
19
|
+
calls = events.group_by(&:method_identifier)
|
20
|
+
overlap = docs.keys & calls.keys
|
21
|
+
|
22
|
+
overlap.flat_map do |key|
|
23
|
+
method_object = docs.fetch(key).first
|
24
|
+
calls.fetch(key).map { |call| Observation.new(method_object, call) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end # SpecObserver
|
28
|
+
end # Yardcheck
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yardcheck
|
4
|
+
class TestRunner
|
5
|
+
include Concord.new(:arguments)
|
6
|
+
|
7
|
+
def wrap_test(wrapper)
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.around do |test|
|
10
|
+
wrapper.call(&test)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
runner.run_specs(RSpec.world.ordered_example_groups)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def runner
|
22
|
+
RSpec::Core::Runner.new(RSpec::Core::ConfigurationOptions.new(arguments)).tap do |runner|
|
23
|
+
runner.setup($stderr, $stdout)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end # TestRunner
|
27
|
+
end # Yardcheck
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yardcheck
|
4
|
+
class TestValue
|
5
|
+
include Concord.new(:value)
|
6
|
+
|
7
|
+
def self.process(value)
|
8
|
+
case value
|
9
|
+
when RSpec::Mocks::InstanceVerifyingDouble
|
10
|
+
InstanceDouble.process(value)
|
11
|
+
when RSpec::Mocks::Double
|
12
|
+
Double.process(value)
|
13
|
+
else
|
14
|
+
new(Proxy.new(value))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def is?(annotated_value)
|
19
|
+
if annotated_value.is_a?(Module)
|
20
|
+
value.is_a?(annotated_value)
|
21
|
+
else
|
22
|
+
value == annotated_value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def duck_type?(method_name)
|
27
|
+
value.respond_to?(method_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def type
|
31
|
+
value.class
|
32
|
+
end
|
33
|
+
|
34
|
+
def inspect
|
35
|
+
"#{self.class}.new(#{value.inspect})"
|
36
|
+
end
|
37
|
+
|
38
|
+
class InstanceDouble < self
|
39
|
+
include Concord.new(:doubled_module)
|
40
|
+
|
41
|
+
def self.process(value)
|
42
|
+
new(value.instance_variable_get(:@doubled_module).target)
|
43
|
+
end
|
44
|
+
|
45
|
+
def is?(value)
|
46
|
+
doubled_module == value || (value.is_a?(Module) && doubled_module < value)
|
47
|
+
end
|
48
|
+
|
49
|
+
def type
|
50
|
+
doubled_module
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
"#{self.class}.new(#{doubled_module.inspect})"
|
55
|
+
end
|
56
|
+
|
57
|
+
def duck_type?(method_name)
|
58
|
+
doubled_module.instance_methods.include?(method_name)
|
59
|
+
end
|
60
|
+
end # InstanceDouble
|
61
|
+
|
62
|
+
class Double < self
|
63
|
+
include Concord.new(:name)
|
64
|
+
|
65
|
+
def self.process(value)
|
66
|
+
new(value.instance_variable_get(:@name) || '(anonymous)')
|
67
|
+
end
|
68
|
+
|
69
|
+
def is?(_)
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
def type
|
74
|
+
'(double)'
|
75
|
+
end
|
76
|
+
|
77
|
+
def inspect
|
78
|
+
"#{self.class}.new(#{name.inspect})"
|
79
|
+
end
|
80
|
+
end # Double
|
81
|
+
end # TestValue
|
82
|
+
end # Yardcheck
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yardcheck
|
4
|
+
class Typedef
|
5
|
+
include Concord.new(:types)
|
6
|
+
|
7
|
+
def self.parse(types)
|
8
|
+
if types.include?(:undefined)
|
9
|
+
fail 'Cannot combined [undefined] with other types' unless types.one?
|
10
|
+
Undefined.new
|
11
|
+
elsif types.grep(Parser::Invalid).any?
|
12
|
+
Invalid.new(types)
|
13
|
+
else
|
14
|
+
new(types)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def match?(other)
|
19
|
+
types.any? do |type|
|
20
|
+
type.match?(other)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def signature
|
25
|
+
types.to_a.map(&:signature).join(' | ')
|
26
|
+
end
|
27
|
+
|
28
|
+
def +(other)
|
29
|
+
self.class.new((types + other.types).uniq)
|
30
|
+
end
|
31
|
+
|
32
|
+
def invalid_const?
|
33
|
+
types.any?(&:invalid_const?)
|
34
|
+
end
|
35
|
+
|
36
|
+
class Literal < self
|
37
|
+
include Concord.new(:const)
|
38
|
+
|
39
|
+
def match?(value)
|
40
|
+
value.is?(type_class)
|
41
|
+
end
|
42
|
+
|
43
|
+
def signature
|
44
|
+
type_class.inspect
|
45
|
+
end
|
46
|
+
|
47
|
+
def type_class
|
48
|
+
const.constant
|
49
|
+
end
|
50
|
+
|
51
|
+
def invalid_const?
|
52
|
+
!const.valid?
|
53
|
+
end
|
54
|
+
end # Literal
|
55
|
+
|
56
|
+
class Collection < self
|
57
|
+
include Concord.new(:collection_const, :member_typedefs)
|
58
|
+
|
59
|
+
def match?(other)
|
60
|
+
Literal.new(collection_const).match?(other)
|
61
|
+
end
|
62
|
+
|
63
|
+
def signature
|
64
|
+
"#{collection_class}<#{member_typedefs.map(&:signature)}>"
|
65
|
+
end
|
66
|
+
|
67
|
+
def collection_class
|
68
|
+
collection_const.constant
|
69
|
+
end
|
70
|
+
|
71
|
+
def invalid_const?
|
72
|
+
!collection_const.valid? || member_typedefs.any?(&:invalid_const?)
|
73
|
+
end
|
74
|
+
end # Collection
|
75
|
+
|
76
|
+
class Undefined < self
|
77
|
+
include Concord.new
|
78
|
+
|
79
|
+
def match?(_)
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
def signature
|
84
|
+
'Undefined'
|
85
|
+
end
|
86
|
+
|
87
|
+
def invalid_const?
|
88
|
+
false
|
89
|
+
end
|
90
|
+
end # Undefined
|
91
|
+
|
92
|
+
class Ducktype < self
|
93
|
+
include Concord.new(:method_name)
|
94
|
+
|
95
|
+
PATTERN = /\A\#(.+)\z/
|
96
|
+
|
97
|
+
def self.parse(name)
|
98
|
+
new(name[PATTERN, 1].to_sym)
|
99
|
+
end
|
100
|
+
|
101
|
+
def match?(other)
|
102
|
+
other.duck_type?(method_name)
|
103
|
+
end
|
104
|
+
|
105
|
+
def signature
|
106
|
+
"an object responding to ##{method_name}"
|
107
|
+
end
|
108
|
+
|
109
|
+
def invalid_const?
|
110
|
+
false
|
111
|
+
end
|
112
|
+
end # Ducktype
|
113
|
+
|
114
|
+
class Invalid < self
|
115
|
+
include Concord.new(:types)
|
116
|
+
|
117
|
+
def invalid_const?
|
118
|
+
true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end # Typedef
|
122
|
+
end # Yardcheck
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yardcheck
|
4
|
+
class Typedef
|
5
|
+
class Parser
|
6
|
+
include Concord.new(:namespace, :types), Adamantium
|
7
|
+
|
8
|
+
def parse
|
9
|
+
Typedef.parse(types.map(&method(:parse_type)).flatten.compact)
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse_type(type)
|
13
|
+
parsed = parse_yard_type(type)
|
14
|
+
return [parsed] if parsed.is_a?(Invalid)
|
15
|
+
|
16
|
+
parsed.map do |parsed_type|
|
17
|
+
resolve_yard_type(parsed_type)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_yard_type(type)
|
22
|
+
YARD::Tags::TypesExplainer::Parser.parse(type)
|
23
|
+
rescue SyntaxError => error
|
24
|
+
Invalid.new(type, error.message)
|
25
|
+
end
|
26
|
+
|
27
|
+
def resolve_yard_type(yard_type)
|
28
|
+
case yard_type
|
29
|
+
when YARD::Tags::TypesExplainer::CollectionType
|
30
|
+
Collection.new(
|
31
|
+
*resolve_type(yard_type.name),
|
32
|
+
yard_type.types.flat_map(&method(:resolve_yard_type))
|
33
|
+
)
|
34
|
+
when YARD::Tags::TypesExplainer::Type
|
35
|
+
types = resolve_type(yard_type.name)
|
36
|
+
case types
|
37
|
+
when :undefined then Undefined.new
|
38
|
+
when :ducktype then Ducktype.parse(yard_type.name)
|
39
|
+
else
|
40
|
+
types.map { |type| Literal.new(type) }
|
41
|
+
end
|
42
|
+
else
|
43
|
+
fail "wtf! #{yard_type}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def resolve_type(name)
|
48
|
+
case name
|
49
|
+
when 'nil' then [Const.new(NilClass)]
|
50
|
+
when 'true' then [Const.new(TrueClass)]
|
51
|
+
when 'false' then [Const.new(FalseClass)]
|
52
|
+
when 'self' then [namespace_const]
|
53
|
+
when 'undefined', 'void' then :undefined
|
54
|
+
when 'Boolean', 'Bool' then [Const.new(TrueClass), Const.new(FalseClass)]
|
55
|
+
when Ducktype::PATTERN then :ducktype
|
56
|
+
else [tag_const(name)]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def tag_const(name)
|
61
|
+
Const.resolve(name, namespace_constant)
|
62
|
+
end
|
63
|
+
|
64
|
+
def namespace_const
|
65
|
+
Const.resolve(namespace)
|
66
|
+
end
|
67
|
+
|
68
|
+
def namespace_constant
|
69
|
+
namespace_const.constant
|
70
|
+
end
|
71
|
+
memoize :namespace_constant
|
72
|
+
|
73
|
+
class Invalid
|
74
|
+
include Concord.new(:type, :error)
|
75
|
+
|
76
|
+
def signature
|
77
|
+
type
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end # Parser
|
81
|
+
end # Typedef
|
82
|
+
end # Yardcheck
|