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,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
|