rspectre 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ba92a8e8133dfaf7b4b3482717139be699fb8f25
4
+ data.tar.gz: 760cdddf1a3aeb9db71b54fa637f8009218eccc1
5
+ SHA512:
6
+ metadata.gz: f60346768b3217d357455cc3f8c8e8d3048e0f1ee859ff7bf796e3f8b85f5a57457dcc3b2170e146baf211438ef4679c144de46c2fcf4d6c1c164d90d660e411
7
+ data.tar.gz: '06917b75a86f56f949b6edbd681af187b392af58ef4d8ccf567145ed7775914090b5ae0060516ef049cdea8b7358e336fbc30c6bf73b9501fdeff6100fbcdd38'
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+
6
+ require 'shellwords'
7
+ require 'optparse'
8
+
9
+ require 'rspectre'
10
+
11
+ options = { rspec: %w[--fail-fast spec], :'auto-correct' => false }
12
+
13
+ OptionParser.new do |opts|
14
+ opts.banner = 'Usage: rspectre [options]'
15
+
16
+ opts.on('--rspec RSPEC_ARGS', 'Arguments to feed to RSpec.') do |value|
17
+ options[:rspec] = value.shellsplit
18
+ end
19
+
20
+ opts.on(
21
+ '--auto-correct',
22
+ 'Enables auto-correct.',
23
+ 'When auto-correct is enabled, rspectre will modify your source files in place and delete any'\
24
+ ' unused test setup.'
25
+ ) do |value|
26
+ options[:'auto-correct'] = value
27
+ end
28
+ end.parse(ARGV)
29
+
30
+ rspec, auto_correct = options.fetch_values(:rspec, :'auto-correct')
31
+
32
+ RSpectre::Runner.new(rspec, auto_correct).lint
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
4
+
5
+ require 'pathname'
6
+ require 'set'
7
+
8
+ require 'anima'
9
+ require 'concord'
10
+ require 'parser/current'
11
+ require 'rspec'
12
+ require 'unparser'
13
+
14
+ require 'rspectre/auto_corrector'
15
+ require 'rspectre/color'
16
+ require 'rspectre/node'
17
+ require 'rspectre/offense'
18
+ require 'rspectre/runner'
19
+ require 'rspectre/source_map'
20
+ require 'rspectre/source_map/parser'
21
+ require 'rspectre/tracker'
22
+
23
+ require 'rspectre/linter'
24
+ require 'rspectre/linter/unused_let'
25
+ require 'rspectre/linter/unused_subject'
26
+ require 'rspectre/linter/unused_shared_setup'
27
+
28
+ module RSpectre
29
+ TRACKER = Tracker.new
30
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpectre
4
+ class AutoCorrector < Parser::Rewriter
5
+ include Concord.new(:filename, :nodes)
6
+
7
+ def correct
8
+ buffer = Parser::Source::Buffer.new("(#{filename})")
9
+ buffer.source = File.read(filename)
10
+
11
+ File.open(filename, 'w') do |file|
12
+ file.write(rewrite(buffer, Parser::CurrentRuby.new.parse(buffer)))
13
+ end
14
+ end
15
+
16
+ def on_block(node)
17
+ remove(node.location.expression) if nodes.any? do |offense_node|
18
+ node == offense_node && node.location.line == offense_node.location.line
19
+ end
20
+
21
+ super
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpectre
4
+ module Color
5
+ def self.red(str)
6
+ colorize(str, 31)
7
+ end
8
+
9
+ def self.yellow(str)
10
+ colorize(str, 33)
11
+ end
12
+
13
+ def self.light_blue(str)
14
+ colorize(str, 36)
15
+ end
16
+
17
+ def self.colorize(str, color_code)
18
+ "\e[#{color_code}m#{str}\e[0m"
19
+ end
20
+ private_class_method :colorize
21
+ end
22
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpectre
4
+ class Linter
5
+ SOURCE_FILES = {} # rubocop:disable Style/MutableConstant
6
+
7
+ def self.example_group
8
+ RSpec::Core::ExampleGroup
9
+ end
10
+
11
+ def self.register(selector, locations) # rubocop:disable Metrics/MethodLength
12
+ location = locations.first
13
+
14
+ file = File.realpath(location.path)
15
+ line = location.lineno
16
+
17
+ return unless file.to_s.start_with?(File.realpath(Dir.pwd))
18
+
19
+ raw_node = node_map(file).find_method(selector, line)
20
+
21
+ if raw_node
22
+ node = RSpectre::Node.new(file, line, raw_node)
23
+ TRACKER.register(self::TAG, node)
24
+ if block_given?
25
+ yield node
26
+ else
27
+ return node
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.node_map(file)
33
+ SOURCE_FILES[file] ||= SourceMap.parse(file)
34
+ end
35
+
36
+ def self.record(node)
37
+ TRACKER.record(self::TAG, node)
38
+ end
39
+
40
+ def self.prepend_behavior(scope, method_name)
41
+ original_method = scope.instance_method(method_name)
42
+
43
+ scope.__send__(:define_method, method_name) do |*args, &block|
44
+ yield
45
+
46
+ original_method.bind(self).(*args, &block)
47
+ end
48
+ end
49
+
50
+ private_constant(*constants(false))
51
+ private_class_method(*singleton_methods(false) - %i[record register prepend_behavior])
52
+ end
53
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpectre
4
+ class Linter
5
+ class UnusedLet < self
6
+ TAG = 'UnusedLet'
7
+
8
+ def example_group.let(name, &block)
9
+ super(name, &block)
10
+
11
+ UnusedLet.register(:let, caller_locations) do |node|
12
+ UnusedLet.prepend_behavior(self, name) { UnusedLet.record(node) }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpectre
4
+ class Linter
5
+ class UnusedSharedSetup < self
6
+ TAG = 'UnusedSharedSetup'
7
+
8
+ def self.redefine_shared(receiver, method) # rubocop:disable Metrics/MethodLength
9
+ # Capture the original class method
10
+ original_method = receiver.method(method)
11
+
12
+ # Overwrite the class method using define_singleton_method
13
+ receiver.__send__(:define_singleton_method, method) do |name, *args, &block|
14
+ # When we can locate the source of the node, tag it
15
+ if (node = UnusedSharedSetup.register(method, caller_locations))
16
+ # And call the orignal
17
+ original_method.(name, *args) do |*shared_args|
18
+ # But record that it was used in a `before`
19
+ before { UnusedSharedSetup.record(node) }
20
+
21
+ # And then perform the original block in a `class_exec` like the original block was
22
+ # supposed to be
23
+ class_exec(*shared_args, &block)
24
+ end
25
+ else
26
+ # If we couldn't locate the source, just delegate to the original method.
27
+ original_method.(name, *args, &block)
28
+ end
29
+ end
30
+ end
31
+
32
+ if respond_to?(:shared_examples)
33
+ main = TOPLEVEL_BINDING.eval('self')
34
+ redefine_shared(main, :shared_examples)
35
+ redefine_shared(main, :shared_examples_for)
36
+ redefine_shared(main, :shared_context)
37
+ end
38
+
39
+ redefine_shared(RSpec, :shared_examples)
40
+ redefine_shared(RSpec, :shared_examples_for)
41
+ redefine_shared(RSpec, :shared_context)
42
+
43
+ # We cannot dynamically capture the original method for the example group like we can for the
44
+ # binding on main and for the RSpec constant because RSpec checks to see if the receiver of
45
+ # the method is the top level example group which would be the case if we redefined it in
46
+ # terms of the old method. I think we can probably do some kind of module inclusion to reduce
47
+ # this duplication (which is effectively what happens here anyway, i think), but this works
48
+ # for now.
49
+ def example_group.shared_examples(name, *args, &block)
50
+ if (node = UnusedSharedSetup.register(:shared_examples, caller_locations))
51
+ super(name, *args) do |*shared_args|
52
+ before { UnusedSharedSetup.record(node) }
53
+
54
+ class_exec(*shared_args, &block)
55
+ end
56
+ else
57
+ super(name, *args, &block)
58
+ end
59
+ end
60
+
61
+ def example_group.shared_examples_for(name, *args, &block)
62
+ if (node = UnusedSharedSetup.register(:shared_examples_for, caller_locations))
63
+ super(name, *args) do |*shared_args|
64
+ before { UnusedSharedSetup.record(node) }
65
+
66
+ class_exec(*shared_args, &block)
67
+ end
68
+ else
69
+ super(name, *args, &block)
70
+ end
71
+ end
72
+
73
+ def example_group.shared_context(name, *args, &block)
74
+ if (node = UnusedSharedSetup.register(:shared_context, caller_locations))
75
+ super(name, *args) do |*shared_args|
76
+ before { UnusedSharedSetup.record(node) }
77
+
78
+ class_exec(*shared_args, &block)
79
+ end
80
+ else
81
+ super(name, *args, &block)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpectre
4
+ class Linter
5
+ class UnusedSubject < self
6
+ TAG = 'UnusedSubject'
7
+
8
+ def example_group.subject(name = nil, &block)
9
+ super(*name, &block)
10
+
11
+ UnusedSubject.register(:subject, caller_locations) do |node|
12
+ UnusedSubject.prepend_behavior(self, :subject) { UnusedSubject.record(node) }
13
+ alias_method name, :subject if name
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpectre
4
+ class Node
5
+ include Concord::Public.new(:file, :line, :node)
6
+
7
+ def start_column
8
+ location.column + 1
9
+ end
10
+
11
+ def end_column
12
+ if single_line?
13
+ location.last_column + 1
14
+ else
15
+ source_line.length + 1
16
+ end
17
+ end
18
+
19
+ def source_line
20
+ location.expression.source_line.rstrip
21
+ end
22
+
23
+ private
24
+
25
+ def single_line?
26
+ line.equal?(location.last_line)
27
+ end
28
+
29
+ def location
30
+ node.children.first.location
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpectre
4
+ class Offense
5
+ include Anima.new(:file, :line, :source_line, :start_column, :end_column, :type)
6
+
7
+ DESCRIPTIONS = {
8
+ 'UnusedLet' => 'Unused `let` definition.',
9
+ 'UnusedSubject' => 'Unused `subject` definition.',
10
+ 'UnusedSharedSetup' => 'Unused `shared_examples`, `shared_examples_for`, or'\
11
+ ' `shared_context` definition.'
12
+ }.freeze
13
+
14
+ def self.parse(type, node)
15
+ new(
16
+ file: node.file,
17
+ line: node.line,
18
+ source_line: node.source_line,
19
+ start_column: node.start_column,
20
+ end_column: node.end_column,
21
+ type: type
22
+ )
23
+ end
24
+
25
+ def warn
26
+ puts to_s
27
+ end
28
+
29
+ def to_s
30
+ <<~DOC
31
+
32
+ #{source_id}: #{offense_type}: #{description}
33
+ #{source_line}
34
+ #{highlight}
35
+ DOC
36
+ end
37
+
38
+ def description
39
+ DESCRIPTIONS.fetch(type)
40
+ end
41
+
42
+ private
43
+
44
+ def source_id
45
+ Color.light_blue("#{file}:#{line}:#{start_column}")
46
+ end
47
+
48
+ def offense_type
49
+ Color.yellow(type)
50
+ end
51
+
52
+ def highlight
53
+ ' ' * (start_column - 1) + carets(end_column - start_column)
54
+ end
55
+
56
+ def carets(n)
57
+ Color.yellow('^' * n)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpectre
4
+ class Runner
5
+ include Concord.new(:rspec_arguments, :auto_correct)
6
+
7
+ EXIT_SUCCESS = 0
8
+
9
+ def lint
10
+ if runner.run_specs(RSpec.world.ordered_example_groups).equal?(EXIT_SUCCESS)
11
+ handle_offenses
12
+ else
13
+ exit_with_error
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def handle_offenses
20
+ if TRACKER.offenses?
21
+ auto_correct ? TRACKER.correct_offenses : TRACKER.report_offenses
22
+ end
23
+ end
24
+
25
+ def exit_with_error
26
+ abort(
27
+ Color.red(
28
+ 'Running the specs failed. Either your tests do not pass '\
29
+ 'normally or this is a bug in RSpectre.'
30
+ )
31
+ )
32
+ end
33
+
34
+ def runner
35
+ rspec_config = RSpec::Core::ConfigurationOptions.new(rspec_arguments)
36
+
37
+ RSpec::Core::Runner.new(rspec_config).tap { |runner| runner.setup($stderr, StringIO.new) }
38
+ end
39
+ end # Rspec
40
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpectre
4
+ class SourceMap
5
+ include Concord.new(:map)
6
+
7
+ def initialize
8
+ super(Hash.new { [] })
9
+ end
10
+ private_class_method :new
11
+
12
+ def self.parse(file)
13
+ self::Parser.new(file).populate(new)
14
+ end
15
+
16
+ def add(node)
17
+ return unless node.loc.expression
18
+
19
+ map[node.loc.first_line] <<= node
20
+ end
21
+
22
+ def find_method(target_selector, line)
23
+ candidates = find_methods(target_selector, line)
24
+
25
+ if candidates.one?
26
+ candidates.first
27
+ else
28
+ warn Color.yellow("Unable to resolve `#{target_selector}` on line #{line}.")
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def find_methods(target_selector, line)
35
+ block_nodes(line).select do |node|
36
+ send, = *node
37
+ _receiver, selector = *send
38
+
39
+ selector.equal?(target_selector)
40
+ end
41
+ end
42
+
43
+ def block_nodes(line)
44
+ map.fetch(line, []).select { |node| node.type.equal?(:block) }
45
+ end
46
+
47
+ class Null < self
48
+ public_class_method :new
49
+
50
+ def find_let(_)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpectre
4
+ class SourceMap
5
+ class Parser
6
+ include Concord.new(:file)
7
+
8
+ def populate(map)
9
+ walk(parsed_source) { |node| map.add(node) }
10
+
11
+ map.freeze
12
+ rescue ::Parser::SyntaxError => error
13
+ warn Color.yellow("Warning! Skipping #{file} due to parsing error!")
14
+ warn error.diagnostic.render
15
+ Null.new
16
+ end
17
+
18
+ private
19
+
20
+ def walk(node, &block)
21
+ yield node
22
+
23
+ node.children.each do |child|
24
+ next unless child.is_a?(::Parser::AST::Node)
25
+
26
+ walk(child, &block)
27
+ end
28
+ end
29
+
30
+ def parsed_source
31
+ ::Parser::CurrentRuby.parse(raw_source, file)
32
+ end
33
+
34
+ def raw_source
35
+ Pathname.new(file).read
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpectre
4
+ class Tracker
5
+ include Concord.new(:registry, :tracker)
6
+
7
+ def initialize
8
+ super(Hash.new { Set.new }, Hash.new { Set.new })
9
+ end
10
+
11
+ def register(type, node)
12
+ registry[type] <<= node
13
+ end
14
+
15
+ def record(type, node)
16
+ tracker[type] <<= node
17
+ end
18
+
19
+ def report_offenses
20
+ offenses.each(&:warn)
21
+ abort
22
+ end
23
+
24
+ def offenses?
25
+ offenses.any?
26
+ end
27
+
28
+ def correct_offenses
29
+ registry.flat_map { |type, locations| (locations - tracker[type]).to_a }
30
+ .group_by(&:file)
31
+ .transform_values { |nodes| nodes.map(&:node) }
32
+ .each do |file, nodes|
33
+ AutoCorrector.new(file, nodes).correct
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def offenses
40
+ registry.flat_map do |type, locations|
41
+ missing = locations - tracker[type]
42
+
43
+ missing.map { |node| Offense.parse(type, node) }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpectre
4
+ VERSION = '0.0.1'
5
+ end
metadata ADDED
@@ -0,0 +1,171 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspectre
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Gollahon
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-03-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: anima
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: concord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: parser
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: unparser
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.10'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.10'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.47'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.47'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.15'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.15'
125
+ description:
126
+ email:
127
+ executables:
128
+ - rspectre
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - bin/rspectre
133
+ - lib/rspectre.rb
134
+ - lib/rspectre/auto_corrector.rb
135
+ - lib/rspectre/color.rb
136
+ - lib/rspectre/linter.rb
137
+ - lib/rspectre/linter/unused_let.rb
138
+ - lib/rspectre/linter/unused_shared_setup.rb
139
+ - lib/rspectre/linter/unused_subject.rb
140
+ - lib/rspectre/node.rb
141
+ - lib/rspectre/offense.rb
142
+ - lib/rspectre/runner.rb
143
+ - lib/rspectre/source_map.rb
144
+ - lib/rspectre/source_map/parser.rb
145
+ - lib/rspectre/tracker.rb
146
+ - lib/rspectre/version.rb
147
+ homepage: http://github.com/dgollahon/rspectre
148
+ licenses:
149
+ - MIT
150
+ metadata: {}
151
+ post_install_message:
152
+ rdoc_options: []
153
+ require_paths:
154
+ - lib
155
+ required_ruby_version: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '2.3'
160
+ required_rubygems_version: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ requirements: []
166
+ rubyforge_project:
167
+ rubygems_version: 2.6.11
168
+ signing_key:
169
+ specification_version: 4
170
+ summary: A tool for linting RSpec test suites.
171
+ test_files: []