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,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yardcheck
|
4
|
+
class Violation
|
5
|
+
extend Color
|
6
|
+
include Color
|
7
|
+
|
8
|
+
def initialize(observation, test_locations = [observation.test_location])
|
9
|
+
@observation = observation
|
10
|
+
@test_locations = test_locations.sort.uniq
|
11
|
+
end
|
12
|
+
|
13
|
+
def offense
|
14
|
+
indented_source = indent(observation.source_code)
|
15
|
+
source = "\n#{CodeRay.encode(indented_source, :ruby, :terminal)}\n"
|
16
|
+
|
17
|
+
location_hint = indent(grey("source: #{observation.source_location}"))
|
18
|
+
test_lines = test_locations.map { |l| " - #{l}" }.join("\n")
|
19
|
+
tests_block = "tests:\n#{test_lines}"
|
20
|
+
test_hint = indent(grey(tests_block))
|
21
|
+
|
22
|
+
"#{explanation}\n\n#{location_hint}\n#{test_hint}\n#{source}\n"
|
23
|
+
end
|
24
|
+
|
25
|
+
def combine(other)
|
26
|
+
fail 'Cannot combine' unless combine_with?(other)
|
27
|
+
|
28
|
+
with_tests(other.test_locations)
|
29
|
+
end
|
30
|
+
|
31
|
+
def combination_identifier
|
32
|
+
combine_requirements.map(&method(:__send__))
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader :test_locations
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_reader :observation
|
40
|
+
|
41
|
+
def combine_with?(other)
|
42
|
+
combination_identifier == other.combination_identifier
|
43
|
+
end
|
44
|
+
|
45
|
+
def with_tests(other_test_locations)
|
46
|
+
self.class.new(observation, test_locations + other_test_locations)
|
47
|
+
end
|
48
|
+
|
49
|
+
def indent(string)
|
50
|
+
string.gsub(/^/, ' ')
|
51
|
+
end
|
52
|
+
|
53
|
+
def shorthand
|
54
|
+
observation.method_shorthand
|
55
|
+
end
|
56
|
+
|
57
|
+
def signature
|
58
|
+
expected_type.signature
|
59
|
+
end
|
60
|
+
|
61
|
+
def observed_type
|
62
|
+
observed_value.type
|
63
|
+
end
|
64
|
+
|
65
|
+
class Return < self
|
66
|
+
include Equalizer.new(:observation)
|
67
|
+
|
68
|
+
FORMAT =
|
69
|
+
"Expected #{blue('%<shorthand>s')} to return " \
|
70
|
+
"#{yellow('%<signature>s')} but observed " \
|
71
|
+
"#{red('%<observed_type>s')}"
|
72
|
+
|
73
|
+
def initialize(observation, test_locations = [observation.test_location])
|
74
|
+
super
|
75
|
+
end
|
76
|
+
|
77
|
+
def explanation
|
78
|
+
format(
|
79
|
+
FORMAT,
|
80
|
+
shorthand: shorthand,
|
81
|
+
signature: signature,
|
82
|
+
observed_type: observed_type
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
def observed_type
|
89
|
+
observation.actual_return_type
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def combine_requirements
|
95
|
+
%i[shorthand signature observed_type]
|
96
|
+
end
|
97
|
+
|
98
|
+
def expected_type
|
99
|
+
observation.documented_return_type
|
100
|
+
end
|
101
|
+
end # Return
|
102
|
+
|
103
|
+
class Param < self
|
104
|
+
include Equalizer.new(:name, :observation)
|
105
|
+
|
106
|
+
def initialize(name, observation, test_locations = [observation.test_location])
|
107
|
+
@name = name
|
108
|
+
|
109
|
+
super(observation, test_locations)
|
110
|
+
end
|
111
|
+
|
112
|
+
FORMAT =
|
113
|
+
"Expected #{blue('%<shorthand>s')} to " \
|
114
|
+
"receive #{yellow('%<signature>s')} for #{blue('%<name>s')} " \
|
115
|
+
"but observed #{red('%<test_value>s')}"
|
116
|
+
|
117
|
+
def explanation
|
118
|
+
format(
|
119
|
+
FORMAT,
|
120
|
+
shorthand: shorthand,
|
121
|
+
signature: signature,
|
122
|
+
name: name,
|
123
|
+
test_value: observed_type
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
attr_reader :name
|
130
|
+
|
131
|
+
def observed_type
|
132
|
+
test_value.type
|
133
|
+
end
|
134
|
+
|
135
|
+
def combine_requirements
|
136
|
+
%i[name shorthand signature observed_type]
|
137
|
+
end
|
138
|
+
|
139
|
+
def with_tests(other_test_locations)
|
140
|
+
self.class.new(
|
141
|
+
name,
|
142
|
+
observation,
|
143
|
+
test_locations + other_test_locations
|
144
|
+
)
|
145
|
+
end
|
146
|
+
|
147
|
+
def test_value
|
148
|
+
observation.observed_param(name)
|
149
|
+
end
|
150
|
+
|
151
|
+
def expected_type
|
152
|
+
observation.documented_param(name)
|
153
|
+
end
|
154
|
+
end # Param
|
155
|
+
end # Violation
|
156
|
+
end # Yardcheck
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yardcheck
|
4
|
+
class Warning
|
5
|
+
extend Color
|
6
|
+
include Concord.new(:method_object, :typedef)
|
7
|
+
|
8
|
+
MSG = "#{red('WARNING:')} Unabled to resolve #{yellow('%<typedef>s')} for %<location>s"
|
9
|
+
|
10
|
+
def message
|
11
|
+
format(MSG, typedef: typedef.signature, location: method_object.location_pointer)
|
12
|
+
end
|
13
|
+
end # Warning
|
14
|
+
end # Yardcheck
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
RSpec.describe 'test app integration' do
|
6
|
+
let(:report) { remove_color(run_yardcheck) }
|
7
|
+
|
8
|
+
def run_yardcheck
|
9
|
+
Bundler.with_clean_env do
|
10
|
+
Dir.chdir('test_app') do
|
11
|
+
system("bundle install --gemfile=#{File.join(Dir.pwd, 'Gemfile')}")
|
12
|
+
system('bundle exec yardcheck --namespace TestApp --include lib --require test_app')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def system(command)
|
18
|
+
output = nil
|
19
|
+
|
20
|
+
Open3.popen3(command) do |_stdin, _stdout, stderr|
|
21
|
+
output = stderr.read
|
22
|
+
end
|
23
|
+
|
24
|
+
output
|
25
|
+
end
|
26
|
+
|
27
|
+
def expect_report(report_substring)
|
28
|
+
expect(report).to match(a_string_including(report_substring))
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove_color(string)
|
32
|
+
string.gsub(/\e\[(?:1\;)?\d+m/, '')
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'generates a warning for invalid constant' do
|
36
|
+
expect_report('WARNING: Unabled to resolve "What" for lib/test_app.rb:37')
|
37
|
+
expect_report('WARNING: Unabled to resolve "Wow" for lib/test_app.rb:37')
|
38
|
+
expect_report('WARNING: Unabled to resolve :foo for lib/test_app.rb:109')
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'reports expectations' do
|
42
|
+
aggregate_failures do
|
43
|
+
expect_report('Expected TestApp::Namespace#add to return String but observed Fixnum')
|
44
|
+
expect_report('Expected #<Class:TestApp::Namespace>#add to return String but observed Fixnum')
|
45
|
+
expect_report(
|
46
|
+
'Expected TestApp::Namespace#documents_relative ' \
|
47
|
+
'to return TestApp::Namespace::Child but observed String'
|
48
|
+
)
|
49
|
+
expect_report(
|
50
|
+
'Expected TestApp::Namespace#improperly_tested_with_instance_double ' \
|
51
|
+
'to receive String for value but observed Integer'
|
52
|
+
)
|
53
|
+
matches = report.scan(/^Expected .+ to return .+ but observed .+$/)
|
54
|
+
expect(matches.size).to be(3)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'bundler'
|
5
|
+
require 'timeout'
|
6
|
+
|
7
|
+
Bundler.with_clean_env { system('cd test_app && yard --no-cache --no-output > /dev/null') }
|
8
|
+
|
9
|
+
begin
|
10
|
+
require 'mutest'
|
11
|
+
|
12
|
+
module Mutest
|
13
|
+
class Selector
|
14
|
+
class Expression < self
|
15
|
+
def call(_subject)
|
16
|
+
integration.all_tests
|
17
|
+
end
|
18
|
+
end # Expression
|
19
|
+
end # Selector
|
20
|
+
end # Mutest
|
21
|
+
rescue LoadError
|
22
|
+
end
|
23
|
+
|
24
|
+
module YardcheckSpec
|
25
|
+
ROOT = Pathname.new(__dir__).parent
|
26
|
+
TEST_APP = ROOT.join('test_app')
|
27
|
+
|
28
|
+
Yardcheck::Documentation.load_yard
|
29
|
+
test_app_yardoc = TEST_APP.join('.yardoc')
|
30
|
+
YARD::Registry.load!(test_app_yardoc.to_s)
|
31
|
+
YARDOCS = YARD::Registry.all(:method)
|
32
|
+
end # YardcheckSpec
|
33
|
+
|
34
|
+
RSpec.configure do |config|
|
35
|
+
# Define metadata for all tests which live under spec/integration
|
36
|
+
config.define_derived_metadata(file_path: %r{\bspec/integration/}) do |metadata|
|
37
|
+
# Set the type of these tests as 'integration'
|
38
|
+
metadata[:type] = :integration
|
39
|
+
|
40
|
+
# Define metadata for mutant so it knows to never run these tests
|
41
|
+
metadata[:mutest] = false
|
42
|
+
end
|
43
|
+
|
44
|
+
config.around(file_path: %r{\bspec/unit/}) do |example|
|
45
|
+
Timeout.timeout(0.1, &example)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
$LOAD_PATH.unshift(YardcheckSpec::TEST_APP.join('lib').to_s)
|
50
|
+
|
51
|
+
require 'test_app'
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Yardcheck::Const do
|
4
|
+
before do
|
5
|
+
stub_const('Foo', Module.new)
|
6
|
+
|
7
|
+
module Foo
|
8
|
+
module Bar
|
9
|
+
module Baz
|
10
|
+
end # Baz
|
11
|
+
end # Bar
|
12
|
+
end # Foo
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'resolves top level constant' do
|
16
|
+
expect(described_class.resolve('Foo')).to eql(described_class.new(Foo))
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'resolves namespaced constant with qualified top level parent' do
|
20
|
+
expect(described_class.resolve('Foo::Bar')).to eql(described_class.new(Foo::Bar))
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'resolves nested constant from nested scope' do
|
24
|
+
expect(described_class.resolve('Bar', Foo)).to eql(described_class.new(Foo::Bar))
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'resolves top parent constant from scope of nested constant' do
|
28
|
+
expect(described_class.resolve('Foo', Foo::Bar)).to eql(described_class.new(Foo))
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'resolves nested constant form deeply nested scope' do
|
32
|
+
expect(described_class.resolve('Foo::Bar', Foo::Bar::Baz)).to eql(described_class.new(Foo::Bar))
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'resolves top level stdlib constant from scope of nested constant' do
|
36
|
+
expect(described_class.resolve('String', Foo::Bar::Baz)).to eql(described_class.new(String))
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'resolves the scope when given the scope name' do
|
40
|
+
expect(described_class.resolve('Bar', Foo::Bar)).to eql(described_class.new(Foo::Bar))
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'returns an invalid const object when given an unresolveable reference' do
|
44
|
+
expect(described_class.resolve('What', Foo::Bar)).to eql(
|
45
|
+
described_class::Invalid.new(Foo::Bar, 'What')
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
require 'yard'
|
6
|
+
|
7
|
+
RSpec.describe Yardcheck::Documentation do
|
8
|
+
def yardocs_for(title)
|
9
|
+
selection = YardcheckSpec::YARDOCS.select { |doc| doc.title == title }
|
10
|
+
fail "Unable to find doc with title #{title}" if selection.empty?
|
11
|
+
selection
|
12
|
+
end
|
13
|
+
|
14
|
+
def doc_for(title)
|
15
|
+
described_class.new(yardocs_for(title))
|
16
|
+
end
|
17
|
+
|
18
|
+
def method_object(title)
|
19
|
+
doc_for(title).method_objects.first
|
20
|
+
end
|
21
|
+
|
22
|
+
def literal(const)
|
23
|
+
Yardcheck::Typedef::Literal.new(const)
|
24
|
+
end
|
25
|
+
|
26
|
+
def typedef(*members)
|
27
|
+
Yardcheck::Typedef.new(members)
|
28
|
+
end
|
29
|
+
|
30
|
+
def const(constant)
|
31
|
+
Yardcheck::Const.new(constant)
|
32
|
+
end
|
33
|
+
|
34
|
+
let(:namespace_add) { method_object('TestApp::Namespace#add') }
|
35
|
+
let(:undocumented) { method_object('TestApp::Namespace#undocumented') }
|
36
|
+
let(:invalid) { method_object('TestApp::Namespace#ignoring_invalid_types') }
|
37
|
+
|
38
|
+
it 'resolves constant' do
|
39
|
+
expect(namespace_add.namespace).to eql(TestApp::Namespace)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'resolves parameters' do
|
43
|
+
expect(namespace_add.params).to eql(
|
44
|
+
left: typedef(literal(const(Integer))),
|
45
|
+
right: typedef(literal(const(Integer)))
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'resolves return value' do
|
50
|
+
expect(namespace_add.return_type).to eql(typedef(literal(const(String))))
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'labels instance scope' do
|
54
|
+
expect(namespace_add.scope).to be(:instance)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'exposes the selector' do
|
58
|
+
expect(namespace_add.selector).to be(:add)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'handles documented returns without types' do
|
62
|
+
expect(method_object('TestApp::Namespace#return_tag_without_type').return_type)
|
63
|
+
.to eql(typedef)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'handles returns with a literal nil' do
|
67
|
+
expect(method_object('TestApp::Namespace#return_nil').return_type)
|
68
|
+
.to eql(typedef(literal(const(NilClass))))
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'handles methods that return instance of the class' do
|
72
|
+
expect(method_object('TestApp::Namespace#return_self').return_type)
|
73
|
+
.to eql(typedef(literal(const(TestApp::Namespace))))
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'supports [undefined]' do
|
77
|
+
expect(method_object('TestApp::Namespace#undefined_return').return_type)
|
78
|
+
.to eql(typedef(Yardcheck::Typedef::Undefined.new))
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'supports [Boolean]' do
|
82
|
+
expect(method_object('TestApp::Namespace#bool_return').return_type)
|
83
|
+
.to eql(typedef(literal(const(TrueClass)), literal(const(FalseClass))))
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'supports [Array<String>]' do
|
87
|
+
expect(method_object('TestApp::Namespace#array_return').return_type).to eql(
|
88
|
+
typedef(Yardcheck::Typedef::Collection.new(const(Array), [literal(const(String))]))
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'supports multiple @return' do
|
93
|
+
expect(method_object('TestApp::Namespace#multiple_returns').return_type)
|
94
|
+
.to eql(typedef(literal(const(String)), literal(const(NilClass))))
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'ignores documented params without names' do
|
98
|
+
expect(method_object('TestApp::Namespace#param_without_name').params).to eql({})
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'ignores invalid constant resolve' do
|
102
|
+
expect(method_object('TestApp::Namespace#ignoring_invalid_types').params).to be_empty
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'produces warnings for unresolvable params and returns' do
|
106
|
+
expect(invalid.warnings).to eql([
|
107
|
+
Yardcheck::Warning.new(
|
108
|
+
invalid,
|
109
|
+
Yardcheck::Typedef::Parser.new('TestApp::Namespace', %w[What]).parse
|
110
|
+
),
|
111
|
+
Yardcheck::Warning.new(
|
112
|
+
invalid,
|
113
|
+
Yardcheck::Typedef::Parser.new('TestApp::Namespace', %w[Wow]).parse
|
114
|
+
)
|
115
|
+
])
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'does not provide a return type when documentation is invalid' do
|
119
|
+
expect(invalid.return_type).to be(nil)
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'does not produce warnings for normal methods' do
|
123
|
+
aggregate_failures do
|
124
|
+
expect(method_object('TestApp::Namespace#add').warnings).to eql([])
|
125
|
+
expect(undocumented.warnings).to eql([])
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'exposes source' do
|
130
|
+
yardoc = yardocs_for('TestApp::Namespace#return_self').first
|
131
|
+
|
132
|
+
allow(yardoc).to receive(:file).and_wrap_original do |method|
|
133
|
+
File.join('./test_app', method.call).to_s
|
134
|
+
end
|
135
|
+
|
136
|
+
method_object = described_class::MethodObject.new(yardoc)
|
137
|
+
expect(method_object.source).to eql(<<~RUBY.chomp)
|
138
|
+
# @return [Namespace]
|
139
|
+
def return_self
|
140
|
+
self
|
141
|
+
end
|
142
|
+
RUBY
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'return type of undocumented' do
|
146
|
+
expect(undocumented.return_type).to be(nil)
|
147
|
+
end
|
148
|
+
end
|