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