reek 4.5.6 → 4.6.0
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 +4 -4
- data/.rubocop.yml +0 -13
- data/CHANGELOG.md +5 -0
- data/Gemfile +2 -2
- data/README.md +5 -2
- data/features/command_line_interface/options.feature +2 -0
- data/features/command_line_interface/stdin.feature +1 -1
- data/features/configuration_files/exclude_paths_directives.feature +19 -0
- data/features/todo_list.feature +1 -2
- data/lib/reek/ast/sexp_extensions/arguments.rb +0 -5
- data/lib/reek/ast/sexp_extensions/constant.rb +1 -0
- data/lib/reek/cli/application.rb +2 -4
- data/lib/reek/cli/options.rb +19 -4
- data/lib/reek/errors/parse_error.rb +19 -0
- data/lib/reek/source/source_code.rb +2 -1
- data/lib/reek/source/source_locator.rb +15 -3
- data/lib/reek/spec.rb +2 -0
- data/lib/reek/version.rb +1 -1
- data/spec/factories/factories.rb +0 -8
- data/spec/reek/cli/command/todo_list_command_spec.rb +12 -69
- data/spec/reek/cli/options_spec.rb +4 -0
- data/spec/reek/context/code_context_spec.rb +10 -10
- data/spec/reek/context/method_context_spec.rb +1 -1
- data/spec/reek/context/module_context_spec.rb +8 -4
- data/spec/reek/examiner_spec.rb +4 -4
- data/spec/reek/report/code_climate/code_climate_fingerprint_spec.rb +9 -9
- data/spec/reek/smell_warning_spec.rb +6 -6
- data/spec/reek/source/source_code_spec.rb +6 -29
- data/spec/reek/source/source_locator_spec.rb +48 -16
- data/spec/reek/spec/should_reek_only_of_spec.rb +4 -4
- data/spec/spec_helper.rb +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c827e3718e3a4f1524fdaf0d6c28df6e86b671b
|
4
|
+
data.tar.gz: 9755661abe970fb1499ee2b9de2f6d701b8c8ee2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 77122032bb96bbd32994195c9745acc214ecfbfff7963348aff01239402deefb74032883eed3762df8eda34c6054447a5ca076dfe707a9a1460a5c14bc85ddf7
|
7
|
+
data.tar.gz: e86a819d479502f6f091c6d6f8b7d1b6c1aae20779131a3e6e62445eb048db3f1f0e1226b59a0b11ca82e9f3c4471578ce4c71c19d0a1fdff96b51f8f5e7d752
|
data/.rubocop.yml
CHANGED
@@ -49,12 +49,6 @@ RSpec/DescribeClass:
|
|
49
49
|
RSpec/ExampleLength:
|
50
50
|
Enabled: false
|
51
51
|
|
52
|
-
# FIXME: Find a better way to block output during specs, and fix relevant examples.
|
53
|
-
RSpec/ExpectOutput:
|
54
|
-
Exclude:
|
55
|
-
- 'spec/reek/cli/command/todo_list_command_spec.rb'
|
56
|
-
- 'spec/reek/source/source_code_spec.rb'
|
57
|
-
|
58
52
|
# FIXME: Split up files to avoid offenses
|
59
53
|
RSpec/MultipleDescribes:
|
60
54
|
Exclude:
|
@@ -79,13 +73,6 @@ RSpec/NestedGroups:
|
|
79
73
|
Exclude:
|
80
74
|
- 'spec/reek/cli/application_spec.rb'
|
81
75
|
|
82
|
-
# FIXME: Update specs to avoid offenses
|
83
|
-
RSpec/VerifiedDoubles:
|
84
|
-
Exclude:
|
85
|
-
- 'spec/reek/context/code_context_spec.rb'
|
86
|
-
- 'spec/reek/context/method_context_spec.rb'
|
87
|
-
- 'spec/reek/context/module_context_spec.rb'
|
88
|
-
|
89
76
|
# rubocop-rspec expects a CodeClimate namespace to go with the code_climate directory.
|
90
77
|
RSpec/FilePath:
|
91
78
|
Exclude:
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
@@ -12,12 +12,12 @@ group :development do
|
|
12
12
|
gem 'factory_girl', '~> 4.0'
|
13
13
|
gem 'rake', '~> 12.0'
|
14
14
|
gem 'rspec', '~> 3.0'
|
15
|
-
gem 'simplecov', '~> 0.
|
15
|
+
gem 'simplecov', '~> 0.14.0'
|
16
16
|
gem 'yard', '~> 0.9.5'
|
17
17
|
|
18
18
|
if RUBY_VERSION >= '2.3'
|
19
19
|
gem 'rubocop', '~> 0.47.1'
|
20
|
-
gem 'rubocop-rspec', '~> 1.
|
20
|
+
gem 'rubocop-rspec', '~> 1.13.0'
|
21
21
|
end
|
22
22
|
|
23
23
|
platforms :mri do
|
data/README.md
CHANGED
@@ -475,7 +475,7 @@ Or just run the whole test suite:
|
|
475
475
|
bundle exec rake
|
476
476
|
```
|
477
477
|
|
478
|
-
This will run the
|
478
|
+
This will run the RSpec tests, RuboCop and Reek itself.
|
479
479
|
|
480
480
|
You can also run:
|
481
481
|
|
@@ -484,7 +484,7 @@ bundle exec rake ci
|
|
484
484
|
```
|
485
485
|
|
486
486
|
This will run everything the default task runs and also
|
487
|
-
[Ataru](https://github.com/CodePadawans/ataru). This is the task that we run on
|
487
|
+
our Cucumber features and [Ataru](https://github.com/CodePadawans/ataru). This is the task that we run on
|
488
488
|
Travis as well and that determines if your pull request is green or red.
|
489
489
|
|
490
490
|
Another useful Rake task is the `console` task. This will throw you right into an environment where you can play around with Reeks modules and classes:
|
@@ -557,6 +557,9 @@ Just add this to your configuration file:
|
|
557
557
|
enabled: false
|
558
558
|
UtilityFunction:
|
559
559
|
enabled: false
|
560
|
+
"app/mailers":
|
561
|
+
InstanceVariableAssumption:
|
562
|
+
enabled: false
|
560
563
|
```
|
561
564
|
|
562
565
|
Be careful though, Reek does not merge your configuration entries, so if you already have a directory directive for "app/controllers" or "app/helpers" you need to update those directives instead of copying the above YAML sample into your configuration file.
|
@@ -67,6 +67,8 @@ Feature: Reek can be controlled using command-line options
|
|
67
67
|
--sort-by SORTING Sort reported files by the given criterium:
|
68
68
|
smelliness ("smelliest" files first)
|
69
69
|
none (default - output in processing order)
|
70
|
+
--force-exclusion Force excluding files specified in the configuration `exclude_paths`
|
71
|
+
even if they are explicitly passed as arguments
|
70
72
|
|
71
73
|
Exit codes:
|
72
74
|
--success-exit-code CODE The exit code when no smells are found (default: 0)
|
@@ -35,5 +35,5 @@ Feature: Reek reads from $stdin when no files are given
|
|
35
35
|
Scenario: syntax error causes the source to be ignored
|
36
36
|
When I pass "= invalid syntax =" to reek
|
37
37
|
Then it reports a parsing error
|
38
|
-
|
38
|
+
And the exit status indicates an error
|
39
39
|
And it reports nothing
|
@@ -22,3 +22,22 @@ Feature: Exclude paths directives
|
|
22
22
|
When I run `reek -c config.reek .`
|
23
23
|
Then it succeeds
|
24
24
|
And it reports nothing
|
25
|
+
Scenario: Using a file name within an excluded directory
|
26
|
+
Given a file named "bad_files_live_here/smelly.rb" with:
|
27
|
+
"""
|
28
|
+
# A smelly example class
|
29
|
+
class Smelly
|
30
|
+
def alfa(bravo); end
|
31
|
+
end
|
32
|
+
"""
|
33
|
+
And a file named "config.reek" with:
|
34
|
+
"""
|
35
|
+
---
|
36
|
+
exclude_paths:
|
37
|
+
- bad_files_live_here
|
38
|
+
"""
|
39
|
+
When I run `reek -c config.reek bad_files_live_here/smelly.rb`
|
40
|
+
Then the exit status indicates smells
|
41
|
+
When I run `reek -c config.reek --force-exclusion bad_files_live_here/smelly.rb`
|
42
|
+
Then it succeeds
|
43
|
+
And it reports nothing
|
data/features/todo_list.feature
CHANGED
data/lib/reek/cli/application.rb
CHANGED
@@ -18,7 +18,6 @@ module Reek
|
|
18
18
|
|
19
19
|
def initialize(argv)
|
20
20
|
@options = configure_options(argv)
|
21
|
-
@status = options.success_exit_code
|
22
21
|
@configuration = configure_app_configuration(options.config_file)
|
23
22
|
@command = command_class.new(options: options,
|
24
23
|
sources: sources,
|
@@ -31,7 +30,6 @@ module Reek
|
|
31
30
|
|
32
31
|
private
|
33
32
|
|
34
|
-
attr_accessor :status
|
35
33
|
attr_reader :command, :options
|
36
34
|
|
37
35
|
def configure_options(argv)
|
@@ -81,11 +79,11 @@ module Reek
|
|
81
79
|
end
|
82
80
|
|
83
81
|
def working_directory_as_source
|
84
|
-
Source::SourceLocator.new(['.'], configuration: configuration).sources
|
82
|
+
Source::SourceLocator.new(['.'], configuration: configuration, options: options).sources
|
85
83
|
end
|
86
84
|
|
87
85
|
def sources_from_argv
|
88
|
-
Source::SourceLocator.new(argv, configuration: configuration).sources
|
86
|
+
Source::SourceLocator.new(argv, configuration: configuration, options: options).sources
|
89
87
|
end
|
90
88
|
|
91
89
|
def source_from_pipe
|
data/lib/reek/cli/options.rb
CHANGED
@@ -11,8 +11,8 @@ module Reek
|
|
11
11
|
#
|
12
12
|
# See {file:docs/Command-Line-Options.md} for details.
|
13
13
|
#
|
14
|
-
# :reek:TooManyInstanceVariables: { max_instance_variables:
|
15
|
-
# :reek:TooManyMethods: { max_methods:
|
14
|
+
# :reek:TooManyInstanceVariables: { max_instance_variables: 12 }
|
15
|
+
# :reek:TooManyMethods: { max_methods: 18 }
|
16
16
|
# :reek:Attribute: { enabled: false }
|
17
17
|
#
|
18
18
|
class Options
|
@@ -27,7 +27,8 @@ module Reek
|
|
27
27
|
:sorting,
|
28
28
|
:success_exit_code,
|
29
29
|
:failure_exit_code,
|
30
|
-
:generate_todo_list
|
30
|
+
:generate_todo_list,
|
31
|
+
:force_exclusion
|
31
32
|
|
32
33
|
def initialize(argv = [])
|
33
34
|
@argv = argv
|
@@ -41,6 +42,7 @@ module Reek
|
|
41
42
|
@success_exit_code = Status::DEFAULT_SUCCESS_EXIT_CODE
|
42
43
|
@failure_exit_code = Status::DEFAULT_FAILURE_EXIT_CODE
|
43
44
|
@generate_todo_list = false
|
45
|
+
@force_exclusion = false
|
44
46
|
|
45
47
|
set_up_parser
|
46
48
|
end
|
@@ -51,6 +53,10 @@ module Reek
|
|
51
53
|
self
|
52
54
|
end
|
53
55
|
|
56
|
+
def force_exclusion?
|
57
|
+
@force_exclusion
|
58
|
+
end
|
59
|
+
|
54
60
|
private
|
55
61
|
|
56
62
|
# TTY output generally means the output will not undergo further
|
@@ -121,7 +127,7 @@ module Reek
|
|
121
127
|
end
|
122
128
|
end
|
123
129
|
|
124
|
-
# :reek:TooManyStatements: { max_statements:
|
130
|
+
# :reek:TooManyStatements: { max_statements: 7 }
|
125
131
|
def set_report_formatting_options
|
126
132
|
parser.separator "\nText format options:"
|
127
133
|
set_up_color_option
|
@@ -129,6 +135,7 @@ module Reek
|
|
129
135
|
set_up_location_formatting_options
|
130
136
|
set_up_progress_formatting_options
|
131
137
|
set_up_sorting_option
|
138
|
+
set_up_force_exclusion_option
|
132
139
|
end
|
133
140
|
|
134
141
|
def set_up_color_option
|
@@ -175,6 +182,14 @@ module Reek
|
|
175
182
|
end
|
176
183
|
end
|
177
184
|
|
185
|
+
def set_up_force_exclusion_option
|
186
|
+
parser.on('--force-exclusion',
|
187
|
+
'Force excluding files specified in the configuration `exclude_paths`',
|
188
|
+
' even if they are explicitly passed as arguments') do |force_exclusion|
|
189
|
+
self.force_exclusion = force_exclusion
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
178
193
|
# :reek:DuplicateMethodCall: { max_calls: 2 }
|
179
194
|
def set_exit_codes
|
180
195
|
parser.separator "\nExit codes:"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'base_error'
|
3
|
+
|
4
|
+
module Reek
|
5
|
+
module Errors
|
6
|
+
# Gets raised when Reek is unable to process the source
|
7
|
+
class ParseError < BaseError
|
8
|
+
MESSAGE_TEMPLATE = '%s: %s: %s'.freeze
|
9
|
+
|
10
|
+
def initialize(origin:, original_exception:)
|
11
|
+
message = format(MESSAGE_TEMPLATE,
|
12
|
+
origin,
|
13
|
+
original_exception.class.name,
|
14
|
+
original_exception.message)
|
15
|
+
super message
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -5,6 +5,7 @@ Reek::CLI::Silencer.silently do
|
|
5
5
|
end
|
6
6
|
require_relative '../tree_dresser'
|
7
7
|
require_relative '../ast/node'
|
8
|
+
require_relative '../errors/parse_error'
|
8
9
|
|
9
10
|
# Opt in to new way of representing lambdas
|
10
11
|
Parser::Builders::Default.emit_lambda = true
|
@@ -87,7 +88,7 @@ module Reek
|
|
87
88
|
begin
|
88
89
|
ast, comments = parser.parse_with_comments(source, origin)
|
89
90
|
rescue Racc::ParseError, Parser::SyntaxError => error
|
90
|
-
|
91
|
+
raise Errors::ParseError, origin: origin, original_exception: error
|
91
92
|
end
|
92
93
|
|
93
94
|
# See https://whitequark.github.io/parser/Parser/Source/Comment/Associator.html
|
@@ -11,7 +11,8 @@ module Reek
|
|
11
11
|
# Initialize with the paths we want to search.
|
12
12
|
#
|
13
13
|
# paths - a list of paths as Strings
|
14
|
-
def initialize(paths, configuration: Configuration::AppConfiguration.default)
|
14
|
+
def initialize(paths, configuration: Configuration::AppConfiguration.default, options: Reek::CLI::Options.new)
|
15
|
+
@options = options
|
15
16
|
@paths = paths.flat_map do |string|
|
16
17
|
path = Pathname.new(string)
|
17
18
|
current_directory?(path) ? path.entries : path
|
@@ -29,7 +30,7 @@ module Reek
|
|
29
30
|
|
30
31
|
private
|
31
32
|
|
32
|
-
attr_reader :configuration, :paths
|
33
|
+
attr_reader :configuration, :paths, :options
|
33
34
|
|
34
35
|
# :reek:TooManyStatements: { max_statements: 7 }
|
35
36
|
# :reek:NestedIterators: { max_allowed_nesting: 2 }
|
@@ -44,12 +45,23 @@ module Reek
|
|
44
45
|
if path.directory?
|
45
46
|
ignore_path?(path) ? Find.prune : next
|
46
47
|
elsif ruby_file?(path)
|
47
|
-
relevant_paths << path
|
48
|
+
relevant_paths << path unless ignore_file?(path)
|
48
49
|
end
|
49
50
|
end
|
50
51
|
end
|
51
52
|
end
|
52
53
|
|
54
|
+
def ignore_file?(path)
|
55
|
+
if options.force_exclusion?
|
56
|
+
path.ascend do |ascendant|
|
57
|
+
break true if path_excluded?(ascendant)
|
58
|
+
false
|
59
|
+
end
|
60
|
+
else
|
61
|
+
false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
53
65
|
def path_excluded?(path)
|
54
66
|
configuration.path_excluded?(path)
|
55
67
|
end
|
data/lib/reek/spec.rb
CHANGED
@@ -98,6 +98,8 @@ module Reek
|
|
98
98
|
# "reek_only_of" will fail in that case.
|
99
99
|
# 2.) "reek_only_of" doesn't support the additional smell_details hash.
|
100
100
|
#
|
101
|
+
# @param smell_type [Symbol, String, Class] The "smell type" to check for.
|
102
|
+
#
|
101
103
|
# @public
|
102
104
|
#
|
103
105
|
# :reek:UtilityFunction
|
data/lib/reek/version.rb
CHANGED
data/spec/factories/factories.rb
CHANGED
@@ -4,14 +4,6 @@ require_relative '../../lib/reek/smell_warning'
|
|
4
4
|
require_relative '../../lib/reek/cli/options'
|
5
5
|
|
6
6
|
FactoryGirl.define do
|
7
|
-
factory :context, class: Reek::Context::CodeContext do
|
8
|
-
skip_create
|
9
|
-
|
10
|
-
initialize_with do
|
11
|
-
new(nil, nil)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
7
|
factory :method_context, class: Reek::Context::MethodContext do
|
16
8
|
skip_create
|
17
9
|
transient do
|
@@ -4,34 +4,25 @@ require_lib 'reek/cli/options'
|
|
4
4
|
|
5
5
|
RSpec.describe Reek::CLI::Command::TodoListCommand do
|
6
6
|
let(:nil_check) { build :smell_detector, smell_type: :NilCheck }
|
7
|
-
let(:feature_envy) { build :smell_detector, smell_type: :FeatureEnvy }
|
8
7
|
let(:nested_iterators) { build :smell_detector, smell_type: :NestedIterators }
|
9
|
-
let(:too_many_statements) { build :smell_detector, smell_type: :TooManyStatements }
|
10
8
|
|
11
9
|
describe '#execute' do
|
12
10
|
let(:options) { Reek::CLI::Options.new [] }
|
13
11
|
let(:configuration) { instance_double 'Reek::Configuration::AppConfiguration' }
|
12
|
+
let(:sources) { [source_file] }
|
14
13
|
|
15
14
|
let(:command) do
|
16
15
|
described_class.new(options: options,
|
17
|
-
sources:
|
16
|
+
sources: sources,
|
18
17
|
configuration: configuration)
|
19
18
|
end
|
20
19
|
|
21
20
|
before do
|
22
|
-
|
23
|
-
allow(File).to receive(:write)
|
24
|
-
end
|
25
|
-
|
26
|
-
after(:all) do
|
27
|
-
$stdout = STDOUT
|
21
|
+
allow(File).to receive(:write).with(described_class::FILE_NAME, String)
|
28
22
|
end
|
29
23
|
|
30
24
|
context 'smells found' do
|
31
|
-
|
32
|
-
smells = [build(:smell_warning, context: 'Foo#bar')]
|
33
|
-
allow(command).to receive(:scan_for_smells).and_return(smells)
|
34
|
-
end
|
25
|
+
let(:source_file) { SMELLY_FILE }
|
35
26
|
|
36
27
|
it 'shows a proper message' do
|
37
28
|
expected = "\n'.todo.reek' generated! You can now use this as a starting point for your configuration.\n"
|
@@ -39,70 +30,22 @@ RSpec.describe Reek::CLI::Command::TodoListCommand do
|
|
39
30
|
end
|
40
31
|
|
41
32
|
it 'returns a success code' do
|
42
|
-
result = command.execute
|
33
|
+
result = Reek::CLI::Silencer.silently { command.execute }
|
43
34
|
expect(result).to eq(Reek::CLI::Status::DEFAULT_SUCCESS_EXIT_CODE)
|
44
35
|
end
|
45
36
|
|
46
|
-
it 'writes a todo file' do
|
47
|
-
command.execute
|
48
|
-
expected_yaml = { 'FeatureEnvy' => { 'exclude' => ['Foo#bar'] } }.to_yaml
|
49
|
-
expect(File).to have_received(:write).with(described_class::FILE_NAME, expected_yaml)
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
context 'smells with duplicate context found' do
|
54
|
-
before do
|
55
|
-
smells = [
|
56
|
-
build(:smell_warning, context: 'Foo#bar', smell_detector: feature_envy),
|
57
|
-
build(:smell_warning, context: 'Foo#bar', smell_detector: feature_envy)
|
58
|
-
]
|
59
|
-
allow(command).to receive(:scan_for_smells).and_return(smells)
|
60
|
-
end
|
61
|
-
|
62
|
-
it 'writes the context into the todo file once' do
|
63
|
-
command.execute
|
64
|
-
expected_yaml = { 'FeatureEnvy' => { 'exclude' => ['Foo#bar'] } }.to_yaml
|
65
|
-
expect(File).to have_received(:write).with(described_class::FILE_NAME, expected_yaml)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
context 'smells with default exclusions found' do
|
70
|
-
let(:smell) { build :smell_warning, smell_detector: too_many_statements, context: 'Foo#bar' }
|
71
|
-
|
72
|
-
before do
|
73
|
-
allow(command).to receive(:scan_for_smells).and_return [smell]
|
74
|
-
end
|
75
|
-
|
76
|
-
it 'includes the default exclusions in the generated yaml' do
|
77
|
-
command.execute
|
78
|
-
expected_yaml = { 'TooManyStatements' => { 'exclude' => ['initialize', 'Foo#bar'] } }.to_yaml
|
79
|
-
expect(File).to have_received(:write).with(described_class::FILE_NAME, expected_yaml)
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
context 'smells of different types found' do
|
84
|
-
before do
|
85
|
-
smells = [
|
86
|
-
build(:smell_warning, context: 'Foo#bar', smell_detector: nil_check),
|
87
|
-
build(:smell_warning, context: 'Bar#baz', smell_detector: nested_iterators)
|
88
|
-
]
|
89
|
-
allow(command).to receive(:scan_for_smells).and_return(smells)
|
90
|
-
end
|
91
|
-
|
92
|
-
it 'writes the context into the todo file once' do
|
93
|
-
command.execute
|
37
|
+
it 'writes a todo file with exclusions for each smell' do
|
38
|
+
Reek::CLI::Silencer.silently { command.execute }
|
94
39
|
expected_yaml = {
|
95
|
-
'
|
96
|
-
'
|
40
|
+
'UncommunicativeMethodName' => { 'exclude' => ['Smelly#x'] },
|
41
|
+
'UncommunicativeVariableName' => { 'exclude' => ['Smelly#x'] }
|
97
42
|
}.to_yaml
|
98
43
|
expect(File).to have_received(:write).with(described_class::FILE_NAME, expected_yaml)
|
99
44
|
end
|
100
45
|
end
|
101
46
|
|
102
47
|
context 'no smells found' do
|
103
|
-
|
104
|
-
allow(command).to receive(:scan_for_smells).and_return []
|
105
|
-
end
|
48
|
+
let(:source_file) { CLEAN_FILE }
|
106
49
|
|
107
50
|
it 'shows a proper message' do
|
108
51
|
expected = "\n'.todo.reek' not generated because there were no smells found!\n"
|
@@ -110,12 +53,12 @@ RSpec.describe Reek::CLI::Command::TodoListCommand do
|
|
110
53
|
end
|
111
54
|
|
112
55
|
it 'returns a success code' do
|
113
|
-
result = command.execute
|
56
|
+
result = Reek::CLI::Silencer.silently { command.execute }
|
114
57
|
expect(result).to eq Reek::CLI::Status::DEFAULT_SUCCESS_EXIT_CODE
|
115
58
|
end
|
116
59
|
|
117
60
|
it 'does not write a todo file' do
|
118
|
-
command.execute
|
61
|
+
Reek::CLI::Silencer.silently { command.execute }
|
119
62
|
expect(File).not_to have_received(:write)
|
120
63
|
end
|
121
64
|
end
|
@@ -32,6 +32,10 @@ RSpec.describe Reek::CLI::Options do
|
|
32
32
|
allow($stdout).to receive_messages(tty?: false)
|
33
33
|
expect(options.progress_format).to eq :quiet
|
34
34
|
end
|
35
|
+
|
36
|
+
it 'sets force_exclusion to false by default' do
|
37
|
+
expect(options.force_exclusion?).to be false
|
38
|
+
end
|
35
39
|
end
|
36
40
|
|
37
41
|
describe 'parse' do
|
@@ -5,7 +5,7 @@ require_lib 'reek/context/module_context'
|
|
5
5
|
RSpec.describe Reek::Context::CodeContext do
|
6
6
|
context 'name recognition' do
|
7
7
|
let(:ctx) { described_class.new(nil, exp) }
|
8
|
-
let(:exp) {
|
8
|
+
let(:exp) { instance_double('Reek::AST::SexpExtensions::ModuleNode') }
|
9
9
|
let(:exp_name) { 'random_name' }
|
10
10
|
let(:full_name) { "::::::::::::::::::::#{exp_name}" }
|
11
11
|
|
@@ -45,7 +45,7 @@ RSpec.describe Reek::Context::CodeContext do
|
|
45
45
|
context 'when there is an outer' do
|
46
46
|
let(:ctx) { described_class.new(outer, exp) }
|
47
47
|
let(:outer_name) { 'another_random sting' }
|
48
|
-
let(:outer) { described_class.new(nil,
|
48
|
+
let(:outer) { described_class.new(nil, instance_double('Reek::AST::Node')) }
|
49
49
|
|
50
50
|
before do
|
51
51
|
ctx.register_with_parent outer
|
@@ -167,7 +167,7 @@ RSpec.describe Reek::Context::CodeContext do
|
|
167
167
|
let(:expression) { Reek::Source::SourceCode.from(src).syntax_tree }
|
168
168
|
let(:outer) { nil }
|
169
169
|
let(:context) { described_class.new(outer, expression) }
|
170
|
-
let(:sniffer) {
|
170
|
+
let(:sniffer) { class_double('Reek::SmellDetectors::BaseDetector') }
|
171
171
|
|
172
172
|
before do
|
173
173
|
context.register_with_parent(outer)
|
@@ -181,7 +181,7 @@ RSpec.describe Reek::Context::CodeContext do
|
|
181
181
|
end
|
182
182
|
|
183
183
|
context 'when there is an outer context' do
|
184
|
-
let(:outer) { described_class.new(nil,
|
184
|
+
let(:outer) { described_class.new(nil, instance_double('Reek::AST::Node')) }
|
185
185
|
|
186
186
|
before do
|
187
187
|
allow(outer).to receive(:config_for).with(sniffer).and_return(
|
@@ -196,9 +196,9 @@ RSpec.describe Reek::Context::CodeContext do
|
|
196
196
|
end
|
197
197
|
|
198
198
|
describe '#register_with_parent' do
|
199
|
-
let(:context) { described_class.new(nil,
|
200
|
-
let(:first_child) { described_class.new(context,
|
201
|
-
let(:second_child) { described_class.new(context,
|
199
|
+
let(:context) { described_class.new(nil, instance_double('Reek::AST::Node')) }
|
200
|
+
let(:first_child) { described_class.new(context, instance_double('Reek::AST::Node')) }
|
201
|
+
let(:second_child) { described_class.new(context, instance_double('Reek::AST::Node')) }
|
202
202
|
|
203
203
|
it "appends the element to the parent context's list of children" do
|
204
204
|
first_child.register_with_parent context
|
@@ -209,9 +209,9 @@ RSpec.describe Reek::Context::CodeContext do
|
|
209
209
|
end
|
210
210
|
|
211
211
|
describe '#each' do
|
212
|
-
let(:context) { described_class.new(nil,
|
213
|
-
let(:first_child) { described_class.new(context,
|
214
|
-
let(:second_child) { described_class.new(context,
|
212
|
+
let(:context) { described_class.new(nil, instance_double('Reek::AST::Node')) }
|
213
|
+
let(:first_child) { described_class.new(context, instance_double('Reek::AST::Node')) }
|
214
|
+
let(:second_child) { described_class.new(context, instance_double('Reek::AST::Node')) }
|
215
215
|
|
216
216
|
it 'yields each child' do
|
217
217
|
first_child.register_with_parent context
|
@@ -5,7 +5,7 @@ RSpec.describe Reek::Context::MethodContext do
|
|
5
5
|
let(:method_context) { described_class.new(nil, exp) }
|
6
6
|
|
7
7
|
describe '#matches?' do
|
8
|
-
let(:exp) {
|
8
|
+
let(:exp) { instance_double('Reek::AST::SexpExtensions::ModuleNode').as_null_object }
|
9
9
|
|
10
10
|
before do
|
11
11
|
allow(exp).to receive(:full_name).at_least(:once).and_return('mod')
|
@@ -8,7 +8,7 @@ RSpec.describe Reek::Context::ModuleContext do
|
|
8
8
|
module Fred
|
9
9
|
def simple(x) x + 1; end
|
10
10
|
end
|
11
|
-
').to reek_of(:UncommunicativeParameterName, name: 'x')
|
11
|
+
').to reek_of(:UncommunicativeParameterName, name: 'x', context: 'Fred#simple')
|
12
12
|
end
|
13
13
|
|
14
14
|
it 'does not report module with empty class' do
|
@@ -28,9 +28,13 @@ RSpec.describe Reek::Context::ModuleContext do
|
|
28
28
|
end
|
29
29
|
|
30
30
|
describe '#track_visibility' do
|
31
|
-
let(:
|
32
|
-
let(:
|
33
|
-
let(:
|
31
|
+
let(:main_exp) { instance_double('Reek::AST::Node') }
|
32
|
+
let(:first_def) { instance_double('Reek::AST::SexpExtensions::DefNode', name: :foo) }
|
33
|
+
let(:second_def) { instance_double('Reek::AST::SexpExtensions::DefNode') }
|
34
|
+
|
35
|
+
let(:context) { described_class.new(nil, main_exp) }
|
36
|
+
let(:first_child) { Reek::Context::MethodContext.new(context, first_def) }
|
37
|
+
let(:second_child) { Reek::Context::MethodContext.new(context, second_def) }
|
34
38
|
|
35
39
|
it 'sets visibility on subsequent child contexts' do
|
36
40
|
context.append_child_context first_child
|
data/spec/reek/examiner_spec.rb
CHANGED
@@ -28,14 +28,14 @@ RSpec.describe Reek::Examiner do
|
|
28
28
|
context 'with a fragrant String' do
|
29
29
|
let(:examiner) { described_class.new('def good() true; end') }
|
30
30
|
|
31
|
-
|
31
|
+
it_behaves_like 'no smells found'
|
32
32
|
end
|
33
33
|
|
34
34
|
context 'with a smelly String' do
|
35
35
|
let(:examiner) { described_class.new('def fine() y = 4; end') }
|
36
36
|
let(:expected_first_smell) { 'UncommunicativeVariableName' }
|
37
37
|
|
38
|
-
|
38
|
+
it_behaves_like 'one smell found'
|
39
39
|
end
|
40
40
|
|
41
41
|
context 'with a partially masked smelly File' do
|
@@ -48,13 +48,13 @@ RSpec.describe Reek::Examiner do
|
|
48
48
|
let(:path) { CONFIG_PATH.join('partial_mask.reek') }
|
49
49
|
let(:expected_first_smell) { 'UncommunicativeVariableName' }
|
50
50
|
|
51
|
-
|
51
|
+
it_behaves_like 'one smell found'
|
52
52
|
end
|
53
53
|
|
54
54
|
context 'with a fragrant File' do
|
55
55
|
let(:examiner) { described_class.new(CLEAN_FILE) }
|
56
56
|
|
57
|
-
|
57
|
+
it_behaves_like 'no smells found'
|
58
58
|
end
|
59
59
|
|
60
60
|
describe '.new' do
|
@@ -16,19 +16,19 @@ RSpec.describe Reek::Report::CodeClimateFingerprint do
|
|
16
16
|
source: 'a/ruby/source/file.rb')
|
17
17
|
end
|
18
18
|
|
19
|
-
it '
|
19
|
+
it 'computes the fingerprint' do
|
20
20
|
expect(computed).to eq expected_fingerprint
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
24
|
context 'with code at a specific location' do
|
25
25
|
let(:lines) { [1] }
|
26
|
-
|
26
|
+
it_behaves_like 'computes a fingerprint with no parameters'
|
27
27
|
end
|
28
28
|
|
29
29
|
context 'with code at a different location' do
|
30
30
|
let(:lines) { [5] }
|
31
|
-
|
31
|
+
it_behaves_like 'computes a fingerprint with no parameters'
|
32
32
|
end
|
33
33
|
|
34
34
|
context 'when the fingerprint should not be computed' do
|
@@ -41,7 +41,7 @@ RSpec.describe Reek::Report::CodeClimateFingerprint do
|
|
41
41
|
source: 'a/ruby/source/file.rb')
|
42
42
|
end
|
43
43
|
|
44
|
-
it '
|
44
|
+
it 'returns nil' do
|
45
45
|
expect(computed).to be_nil
|
46
46
|
end
|
47
47
|
end
|
@@ -66,14 +66,14 @@ RSpec.describe Reek::Report::CodeClimateFingerprint do
|
|
66
66
|
let(:name) { 'bravo' }
|
67
67
|
let(:expected_fingerprint) { '9c3fd378178118a67e9509f87cae24f9' }
|
68
68
|
|
69
|
-
|
69
|
+
it_behaves_like 'computes a fingerprint with identifying parameters'
|
70
70
|
end
|
71
71
|
|
72
72
|
context 'when the name is another thing it has another fingerprint' do
|
73
73
|
let(:name) { 'echo' }
|
74
74
|
let(:expected_fingerprint) { 'd2a6d2703ce04cca65e7300b7de4b89f' }
|
75
75
|
|
76
|
-
|
76
|
+
it_behaves_like 'computes a fingerprint with identifying parameters'
|
77
77
|
end
|
78
78
|
|
79
79
|
shared_examples_for 'computes a fingerprint with identifying and non-identifying parameters' do
|
@@ -98,7 +98,7 @@ RSpec.describe Reek::Report::CodeClimateFingerprint do
|
|
98
98
|
let(:lines) { [1, 7, 10, 13, 15] }
|
99
99
|
let(:expected_fingerprint) { '238733f4f51ba5473dcbe94a43ec5400' }
|
100
100
|
|
101
|
-
|
101
|
+
it_behaves_like 'computes a fingerprint with identifying and non-identifying parameters'
|
102
102
|
end
|
103
103
|
|
104
104
|
context 'when the non-identifying parameters change, it computes the same fingerprint' do
|
@@ -107,7 +107,7 @@ RSpec.describe Reek::Report::CodeClimateFingerprint do
|
|
107
107
|
let(:lines) { [1, 7, 10, 13, 15, 17, 19, 20, 25] }
|
108
108
|
let(:expected_fingerprint) { '238733f4f51ba5473dcbe94a43ec5400' }
|
109
109
|
|
110
|
-
|
110
|
+
it_behaves_like 'computes a fingerprint with identifying and non-identifying parameters'
|
111
111
|
end
|
112
112
|
|
113
113
|
context 'but when the identifying parameters change, it computes a different fingerprint' do
|
@@ -116,7 +116,7 @@ RSpec.describe Reek::Report::CodeClimateFingerprint do
|
|
116
116
|
let(:lines) { [1, 7, 10, 13, 15] }
|
117
117
|
let(:expected_fingerprint) { 'e0c35e9223cc19bdb9a04fb3e60573e1' }
|
118
118
|
|
119
|
-
|
119
|
+
it_behaves_like 'computes a fingerprint with identifying and non-identifying parameters'
|
120
120
|
end
|
121
121
|
end
|
122
122
|
end
|
@@ -30,14 +30,14 @@ RSpec.describe Reek::SmellWarning do
|
|
30
30
|
let(:first) { build(:smell_warning, smell_detector: duplication_detector) }
|
31
31
|
let(:second) { build(:smell_warning, smell_detector: feature_envy_detector) }
|
32
32
|
|
33
|
-
|
33
|
+
it_behaves_like 'first sorts ahead of second'
|
34
34
|
end
|
35
35
|
|
36
36
|
context 'smells differing only by lines' do
|
37
37
|
let(:first) { build(:smell_warning, smell_detector: feature_envy_detector, lines: [2]) }
|
38
38
|
let(:second) { build(:smell_warning, smell_detector: feature_envy_detector, lines: [3]) }
|
39
39
|
|
40
|
-
|
40
|
+
it_behaves_like 'first sorts ahead of second'
|
41
41
|
end
|
42
42
|
|
43
43
|
context 'smells differing only by context' do
|
@@ -46,7 +46,7 @@ RSpec.describe Reek::SmellWarning do
|
|
46
46
|
build(:smell_warning, smell_detector: duplication_detector, context: 'second')
|
47
47
|
end
|
48
48
|
|
49
|
-
|
49
|
+
it_behaves_like 'first sorts ahead of second'
|
50
50
|
end
|
51
51
|
|
52
52
|
context 'smells differing only by message' do
|
@@ -59,7 +59,7 @@ RSpec.describe Reek::SmellWarning do
|
|
59
59
|
context: 'ctx', message: 'second message')
|
60
60
|
end
|
61
61
|
|
62
|
-
|
62
|
+
it_behaves_like 'first sorts ahead of second'
|
63
63
|
end
|
64
64
|
|
65
65
|
context 'smell name takes precedence over message' do
|
@@ -70,7 +70,7 @@ RSpec.describe Reek::SmellWarning do
|
|
70
70
|
build(:smell_warning, smell_detector: utility_function_detector, message: 'first message')
|
71
71
|
end
|
72
72
|
|
73
|
-
|
73
|
+
it_behaves_like 'first sorts ahead of second'
|
74
74
|
end
|
75
75
|
|
76
76
|
context 'smells differing everywhere' do
|
@@ -86,7 +86,7 @@ RSpec.describe Reek::SmellWarning do
|
|
86
86
|
message: "has the variable name '@s'")
|
87
87
|
end
|
88
88
|
|
89
|
-
|
89
|
+
it_behaves_like 'first sorts ahead of second'
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
@@ -26,36 +26,15 @@ RSpec.describe Reek::Source::SourceCode do
|
|
26
26
|
end
|
27
27
|
|
28
28
|
context 'when the parser fails' do
|
29
|
-
let(:catcher) { StringIO.new }
|
30
29
|
let(:source_name) { 'Test source' }
|
31
30
|
let(:error_message) { 'Error message' }
|
32
31
|
let(:parser) { class_double(Parser::Ruby23) }
|
33
32
|
let(:src) { described_class.new(code: '', origin: source_name, parser: parser) }
|
34
33
|
|
35
|
-
before { $stderr = catcher }
|
36
|
-
|
37
34
|
shared_examples_for 'handling and recording the error' do
|
38
|
-
it '
|
39
|
-
src.syntax_tree
|
40
|
-
|
41
|
-
|
42
|
-
it 'returns an empty syntax tree' do
|
43
|
-
expect(src.syntax_tree).to be_nil
|
44
|
-
end
|
45
|
-
|
46
|
-
it 'records the syntax error' do
|
47
|
-
src.syntax_tree
|
48
|
-
expect(catcher.string).to match(error_class.name)
|
49
|
-
end
|
50
|
-
|
51
|
-
it 'records the source name' do
|
52
|
-
src.syntax_tree
|
53
|
-
expect(catcher.string).to match(source_name)
|
54
|
-
end
|
55
|
-
|
56
|
-
it 'records the error message' do
|
57
|
-
src.syntax_tree
|
58
|
-
expect(catcher.string).to match(error_message)
|
35
|
+
it 'raises an informative error' do
|
36
|
+
expect { src.syntax_tree }.
|
37
|
+
to raise_error(/#{source_name}: #{error_class.name}: #{error_message}/)
|
59
38
|
end
|
60
39
|
end
|
61
40
|
|
@@ -68,7 +47,7 @@ RSpec.describe Reek::Source::SourceCode do
|
|
68
47
|
and_raise error_class.new(diagnostic)
|
69
48
|
end
|
70
49
|
|
71
|
-
|
50
|
+
it_behaves_like 'handling and recording the error'
|
72
51
|
end
|
73
52
|
|
74
53
|
context 'with a Racc::ParseError' do
|
@@ -79,7 +58,7 @@ RSpec.describe Reek::Source::SourceCode do
|
|
79
58
|
and_raise(error_class.new(error_message))
|
80
59
|
end
|
81
60
|
|
82
|
-
|
61
|
+
it_behaves_like 'handling and recording the error'
|
83
62
|
end
|
84
63
|
|
85
64
|
context 'with a generic error' do
|
@@ -91,10 +70,8 @@ RSpec.describe Reek::Source::SourceCode do
|
|
91
70
|
end
|
92
71
|
|
93
72
|
it 'raises the error' do
|
94
|
-
expect { src.syntax_tree }.to raise_error
|
73
|
+
expect { src.syntax_tree }.to raise_error error_class
|
95
74
|
end
|
96
75
|
end
|
97
|
-
|
98
|
-
after { $stderr = STDERR }
|
99
76
|
end
|
100
77
|
end
|
@@ -29,34 +29,66 @@ RSpec.describe Reek::Source::SourceLocator do
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
+
# rubocop:disable RSpec/NestedGroups
|
32
33
|
context 'exclude paths' do
|
33
34
|
let(:configuration) do
|
34
35
|
test_configuration_for(CONFIG_PATH.join('with_excluded_paths.reek'))
|
35
36
|
end
|
36
37
|
|
37
|
-
let(:
|
38
|
+
let(:options) { instance_double('Reek::CLI::Options', force_exclusion?: false) }
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
end
|
40
|
+
context 'when the path is a file name in an excluded directory' do
|
41
|
+
let(:path) { SAMPLES_PATH.join('source_with_exclude_paths', 'ignore_me', 'uncommunicative_method_name.rb') }
|
42
42
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
]
|
48
|
-
end
|
43
|
+
context 'when options.force_exclusion? is true' do
|
44
|
+
before do
|
45
|
+
allow(options).to receive(:force_exclusion?).and_return(true)
|
46
|
+
end
|
49
47
|
|
50
|
-
|
51
|
-
|
52
|
-
|
48
|
+
it 'excludes this file' do
|
49
|
+
sources = described_class.new([path], configuration: configuration, options: options).sources
|
50
|
+
expect(sources).not_to include(path)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'when options.force_exclusion? is false' do
|
55
|
+
before do
|
56
|
+
allow(options).to receive(:force_exclusion?).and_return(false)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'includes this file' do
|
60
|
+
sources = described_class.new([path], configuration: configuration, options: options).sources
|
61
|
+
expect(sources).to include(path)
|
62
|
+
end
|
63
|
+
end
|
53
64
|
end
|
54
65
|
|
55
|
-
|
56
|
-
|
57
|
-
|
66
|
+
context 'when path is a directory' do
|
67
|
+
let(:path) { SAMPLES_PATH.join('source_with_exclude_paths') }
|
68
|
+
|
69
|
+
let(:expected_paths) do
|
70
|
+
[path.join('nested/uncommunicative_parameter_name.rb')]
|
71
|
+
end
|
72
|
+
|
73
|
+
let(:paths_that_are_expected_to_be_ignored) do
|
74
|
+
[
|
75
|
+
path.join('ignore_me/uncommunicative_method_name.rb'),
|
76
|
+
path.join('nested/ignore_me_as_well/irresponsible_module.rb')
|
77
|
+
]
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'does not use excluded paths' do
|
81
|
+
sources = described_class.new([path], configuration: configuration, options: options).sources
|
82
|
+
expect(sources).not_to include(*paths_that_are_expected_to_be_ignored)
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'scans directories that are not excluded' do
|
86
|
+
sources = described_class.new([path], configuration: configuration).sources
|
87
|
+
expect(sources).to eq expected_paths
|
88
|
+
end
|
58
89
|
end
|
59
90
|
end
|
91
|
+
# rubocop:enable RSpec/NestedGroups
|
60
92
|
|
61
93
|
context 'non-Ruby paths' do
|
62
94
|
let(:path) { SAMPLES_PATH.join('source_with_non_ruby_files') }
|
@@ -36,14 +36,14 @@ RSpec.describe Reek::Spec::ShouldReekOnlyOf do
|
|
36
36
|
context 'with no smells' do
|
37
37
|
let(:smells) { [] }
|
38
38
|
|
39
|
-
|
39
|
+
it_behaves_like 'no match'
|
40
40
|
end
|
41
41
|
|
42
42
|
context 'with 1 non-matching smell' do
|
43
43
|
let(:control_couple_detector) { build(:smell_detector, smell_type: 'ControlParameter') }
|
44
44
|
let(:smells) { [build(:smell_warning, smell_detector: control_couple_detector)] }
|
45
45
|
|
46
|
-
|
46
|
+
it_behaves_like 'no match'
|
47
47
|
end
|
48
48
|
|
49
49
|
context 'with 2 non-matching smells' do
|
@@ -56,7 +56,7 @@ RSpec.describe Reek::Spec::ShouldReekOnlyOf do
|
|
56
56
|
]
|
57
57
|
end
|
58
58
|
|
59
|
-
|
59
|
+
it_behaves_like 'no match'
|
60
60
|
end
|
61
61
|
|
62
62
|
context 'with 1 non-matching and 1 matching smell' do
|
@@ -70,7 +70,7 @@ RSpec.describe Reek::Spec::ShouldReekOnlyOf do
|
|
70
70
|
]
|
71
71
|
end
|
72
72
|
|
73
|
-
|
73
|
+
it_behaves_like 'no match'
|
74
74
|
end
|
75
75
|
|
76
76
|
context 'with 1 matching smell' do
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reek
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Rutherford
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2017-
|
14
|
+
date: 2017-04-04 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: codeclimate-engine-rb
|
@@ -229,6 +229,7 @@ files:
|
|
229
229
|
- lib/reek/errors/base_error.rb
|
230
230
|
- lib/reek/errors/garbage_detector_configuration_in_comment_error.rb
|
231
231
|
- lib/reek/errors/incomprehensible_source_error.rb
|
232
|
+
- lib/reek/errors/parse_error.rb
|
232
233
|
- lib/reek/examiner.rb
|
233
234
|
- lib/reek/logging_error_handler.rb
|
234
235
|
- lib/reek/rake/task.rb
|
@@ -438,7 +439,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
438
439
|
version: '0'
|
439
440
|
requirements: []
|
440
441
|
rubyforge_project:
|
441
|
-
rubygems_version: 2.
|
442
|
+
rubygems_version: 2.5.1
|
442
443
|
signing_key:
|
443
444
|
specification_version: 4
|
444
445
|
summary: Code smell detector for Ruby
|