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