rspectre 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/bin/rspectre +32 -0
- data/lib/rspectre.rb +30 -0
- data/lib/rspectre/auto_corrector.rb +24 -0
- data/lib/rspectre/color.rb +22 -0
- data/lib/rspectre/linter.rb +53 -0
- data/lib/rspectre/linter/unused_let.rb +17 -0
- data/lib/rspectre/linter/unused_shared_setup.rb +86 -0
- data/lib/rspectre/linter/unused_subject.rb +18 -0
- data/lib/rspectre/node.rb +33 -0
- data/lib/rspectre/offense.rb +60 -0
- data/lib/rspectre/runner.rb +40 -0
- data/lib/rspectre/source_map.rb +54 -0
- data/lib/rspectre/source_map/parser.rb +39 -0
- data/lib/rspectre/tracker.rb +47 -0
- data/lib/rspectre/version.rb +5 -0
- metadata +171 -0
checksums.yaml
ADDED
|
@@ -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'
|
data/bin/rspectre
ADDED
|
@@ -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
|
data/lib/rspectre.rb
ADDED
|
@@ -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
|
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: []
|