yardcheck 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +214 -0
  5. data/.rubocop_todo.yml +69 -0
  6. data/.ruby-version +1 -0
  7. data/Gemfile +22 -0
  8. data/Guardfile +5 -0
  9. data/README.md +83 -0
  10. data/bin/yardcheck +6 -0
  11. data/lib/yardcheck.rb +26 -0
  12. data/lib/yardcheck/color.rb +27 -0
  13. data/lib/yardcheck/const.rb +39 -0
  14. data/lib/yardcheck/documentation.rb +29 -0
  15. data/lib/yardcheck/documentation/method_object.rb +111 -0
  16. data/lib/yardcheck/method_call.rb +38 -0
  17. data/lib/yardcheck/method_tracer.rb +86 -0
  18. data/lib/yardcheck/observation.rb +72 -0
  19. data/lib/yardcheck/proxy.rb +33 -0
  20. data/lib/yardcheck/runner.rb +93 -0
  21. data/lib/yardcheck/source_lines.rb +29 -0
  22. data/lib/yardcheck/spec_observer.rb +28 -0
  23. data/lib/yardcheck/test_runner.rb +27 -0
  24. data/lib/yardcheck/test_value.rb +82 -0
  25. data/lib/yardcheck/typedef.rb +122 -0
  26. data/lib/yardcheck/typedef/parser.rb +82 -0
  27. data/lib/yardcheck/version.rb +5 -0
  28. data/lib/yardcheck/violation.rb +156 -0
  29. data/lib/yardcheck/warning.rb +14 -0
  30. data/spec/integration/yardcheck_spec.rb +57 -0
  31. data/spec/spec_helper.rb +51 -0
  32. data/spec/unit/yardcheck/const_spec.rb +48 -0
  33. data/spec/unit/yardcheck/documentation_spec.rb +148 -0
  34. data/spec/unit/yardcheck/method_tracer_spec.rb +59 -0
  35. data/spec/unit/yardcheck/runner_spec.rb +183 -0
  36. data/spec/unit/yardcheck/test_value_spec.rb +25 -0
  37. data/spec/unit/yardcheck/typedef/parser_spec.rb +37 -0
  38. data/spec/unit/yardcheck/typedef_spec.rb +32 -0
  39. data/test_app/.rspec +1 -0
  40. data/test_app/Gemfile +7 -0
  41. data/test_app/Gemfile.lock +66 -0
  42. data/test_app/lib/test_app.rb +119 -0
  43. data/test_app/spec/test_app_spec.rb +49 -0
  44. data/yardcheck.gemspec +24 -0
  45. metadata +158 -0
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yardcheck
4
+ VERSION = '0.0.1'
5
+ end # Yardcheck
@@ -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
@@ -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