sugarcane 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/HISTORY.md +126 -0
- data/LICENSE +14 -0
- data/README.md +235 -0
- data/bin/sugarcane +7 -0
- data/lib/cane.rb +4 -0
- data/lib/sugarcane/abc_check.rb +216 -0
- data/lib/sugarcane/cli/options.rb +30 -0
- data/lib/sugarcane/cli/parser.rb +191 -0
- data/lib/sugarcane/cli.rb +21 -0
- data/lib/sugarcane/default_checks.rb +17 -0
- data/lib/sugarcane/doc_check.rb +156 -0
- data/lib/sugarcane/encoding_aware_iterator.rb +30 -0
- data/lib/sugarcane/ext/array.rb +24 -0
- data/lib/sugarcane/file.rb +30 -0
- data/lib/sugarcane/json_formatter.rb +17 -0
- data/lib/sugarcane/menu.rb +228 -0
- data/lib/sugarcane/rake_task.rb +78 -0
- data/lib/sugarcane/runner.rb +59 -0
- data/lib/sugarcane/style_check.rb +91 -0
- data/lib/sugarcane/task_runner.rb +20 -0
- data/lib/sugarcane/threshold_check.rb +83 -0
- data/lib/sugarcane/version.rb +3 -0
- data/lib/sugarcane/violation_formatter.rb +75 -0
- data/spec/abc_check_spec.rb +164 -0
- data/spec/cane_spec.rb +136 -0
- data/spec/cli_spec.rb +23 -0
- data/spec/doc_check_spec.rb +163 -0
- data/spec/encoding_aware_iterator_spec.rb +32 -0
- data/spec/file_spec.rb +25 -0
- data/spec/json_formatter_spec.rb +10 -0
- data/spec/parser_spec.rb +161 -0
- data/spec/rake_task_spec.rb +68 -0
- data/spec/runner_spec.rb +23 -0
- data/spec/spec_helper.rb +71 -0
- data/spec/style_check_spec.rb +57 -0
- data/spec/threshold_check_spec.rb +101 -0
- data/spec/violation_formatter_spec.rb +38 -0
- data/sugarcane.gemspec +41 -0
- metadata +196 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 584226c2dcce2cb64f8634a831906986f21f92f2
|
4
|
+
data.tar.gz: 6a906ebfd9c1088440dfc76fd2db8ca621bac061
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 40e65acb33a378c9e7f779750dee561d982c6aedeb19a24141313657e53b6a8407ec876b23aa6cafbdc9911087758152f5a04d8ba7aeb129a800ec5e801cc5c3
|
7
|
+
data.tar.gz: 78dfc2e99724f66855473ae69c06923989dbc5266d1daf4819e3ceec99039feb28be21ea170a3d769c57ab6141e0ab71399b018a11f81de7188a9805fec19a0c
|
data/HISTORY.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# SugarCane History
|
2
|
+
|
3
|
+
## 0.0.1 - 6 February 2014
|
4
|
+
|
5
|
+
* Feature: Menu that takes you from a violation to the line in a text editor
|
6
|
+
* Change: expect .sugarcane or .cane configuration files
|
7
|
+
* Change: old cane functionality with --report
|
8
|
+
|
9
|
+
# Cane History
|
10
|
+
|
11
|
+
## 2.6.1 - 30 October 2013 (2ea008)
|
12
|
+
|
13
|
+
* Feature: Don't require doc for one-line class w/out method.
|
14
|
+
* Bugfix: JsonFormatter initializer needs to take an options hash.
|
15
|
+
* Doc: Add license definition to gemspec.
|
16
|
+
|
17
|
+
## 2.6.0 - 7 June 2013 (616bb8a5)
|
18
|
+
|
19
|
+
* Feature: classes with no methods do not require documentation.
|
20
|
+
* Feature: modules with methods require documentation.
|
21
|
+
* Feature: support all README extensions.
|
22
|
+
* Feature: --color option.
|
23
|
+
* Bugfix: fix false positive on class matching for doc check.
|
24
|
+
* Bugfix: better handling of invalid strings.
|
25
|
+
* Compat: fix Ruby 2.0 deprecations.
|
26
|
+
|
27
|
+
## 2.5.2 - 26 January 2013 (a0cf38ba)
|
28
|
+
|
29
|
+
* Feature: support operators beside `>=` in threshold check.
|
30
|
+
|
31
|
+
## 2.5.1 - 26 January 2013 (93819f19)
|
32
|
+
|
33
|
+
* Feature: documentation check supports `.mdown` and `.rdoc` extensions.
|
34
|
+
* Feature: expanded threshold regex to support `coverage/.last_run.json` from
|
35
|
+
`SimpleCov`.
|
36
|
+
* Compat: Ruby 2.0 compatibility.
|
37
|
+
|
38
|
+
## 2.5.0 - 17 November 2012 (628cc1e9)
|
39
|
+
|
40
|
+
* Feature: `--doc-exclude` option to exclude globs from documentation checks.
|
41
|
+
* Feature: `--style-exclude` supports globbing.
|
42
|
+
|
43
|
+
## 2.4.0 - 21 October 2012 (46949e77)
|
44
|
+
|
45
|
+
* Feature: Rake task can load configuration from a `.cane` file.
|
46
|
+
* Feature: Coverage threshold can be specifed in a file.
|
47
|
+
* Feature: Provide `--all` option for working with single files.
|
48
|
+
* Bugfix: Allow README file to be lowercase.
|
49
|
+
|
50
|
+
## 2.3.0 - 16 September 2012 (229252ff)
|
51
|
+
|
52
|
+
* Feature: `--json` option for machine-readable output.
|
53
|
+
* Feature: absence of a README will cause a failure.
|
54
|
+
* Bugfix: `--no-style` option actually works now.
|
55
|
+
|
56
|
+
## 2.2.3 - 3 September 2012 (e4fe90ee)
|
57
|
+
|
58
|
+
* Bugfix: Allow multiple spaces before class name. (#34)
|
59
|
+
* Bugfix: Remove wacky broken conditional in AbcCheck. (#33)
|
60
|
+
* Doc: Better guidance on class level comments. (#35)
|
61
|
+
|
62
|
+
## 2.2.2 - 29 August 2012 (3a9be454)
|
63
|
+
|
64
|
+
* Bugfix: Stricter magic comment regex to avoid false positives (#31)
|
65
|
+
|
66
|
+
## 2.2.1 - 26 August 2012 (b5e5a362)
|
67
|
+
|
68
|
+
* Bugfix: parallel option can be set in rake tasks
|
69
|
+
|
70
|
+
## 2.2.0 - 26 August 2012 (f4198619)
|
71
|
+
|
72
|
+
* Gracefully handle ambiguous options like `-abc-max` (#27)
|
73
|
+
* Provide the `--parallel` option to use all processors. This will be faster on
|
74
|
+
larger projects, but slower on smaller ones (#28)
|
75
|
+
|
76
|
+
## 2.1.0 - 26 August 2012 (2962d8fb)
|
77
|
+
|
78
|
+
* Support for user-defined checks (#30)
|
79
|
+
|
80
|
+
## 2.0.0 - 19 August 2012 (35cae086)
|
81
|
+
|
82
|
+
* ABC check labels `MyClass = Struct.new {}` and `Class.new` correctly (#20)
|
83
|
+
* Magic comments (`# encoding: utf-8`) are not recognized as appropriate class documentation (#21)
|
84
|
+
* Invalid UTF-8 is handled correctly (#22)
|
85
|
+
* Gracefully handle unknown options
|
86
|
+
* ABC check output uses a standard format (`Foo::Bar#method` rather than `Foo > Bar > method`)
|
87
|
+
* **BREAKING** Add `--abc-exclude`, `--style-exclude` CLI flags, remove YAML support
|
88
|
+
* **BREAKING-INTERNAL** Use hashes rather than explicit violation classes
|
89
|
+
* **BREAKING-INTERNAL** Remove translator class, pass CLI args direct to checks
|
90
|
+
* **INTERNAL** Wiring in a new check only requires changing one file (#15)
|
91
|
+
|
92
|
+
This snippet will convert your YAML exclusions file to the new CLI syntax:
|
93
|
+
|
94
|
+
y = YAML.load(File.read('exclusions.yml'))
|
95
|
+
puts (
|
96
|
+
y.fetch('abc', []).map {|x| %|--abc-exclude "#{x}"| } +
|
97
|
+
y.fetch('style', []).map {|x| %|--style-exclude "#{x}"| }
|
98
|
+
).join("\n")
|
99
|
+
|
100
|
+
## 1.4.0 - 2 July 2012 (1afc999d)
|
101
|
+
|
102
|
+
* Allow files and methods to be whitelisted (#16)
|
103
|
+
* Show total number of violations in output (#14)
|
104
|
+
|
105
|
+
## 1.3.0 - 20 April 2012 (c166dfa0)
|
106
|
+
|
107
|
+
* Remove dependency on tailor. Fewer styles checks are performed, but the three
|
108
|
+
remaining are the only ones I've found useful.
|
109
|
+
|
110
|
+
## 1.2.0 - 31 March 2012 (adce51b9)
|
111
|
+
|
112
|
+
* Gracefully handle files with invalid syntax (#1)
|
113
|
+
* Included class methods in ABC check (#8)
|
114
|
+
* Can disable style and doc checks from rake task (#9)
|
115
|
+
|
116
|
+
## 1.1.0 - 24 March 2012 (ba8a74fc)
|
117
|
+
|
118
|
+
* `app` added to default globs
|
119
|
+
* Added `cane/rake_task`
|
120
|
+
* `class << obj` syntax ignore by documentation check
|
121
|
+
* Line length checks no longer include trailing new lines
|
122
|
+
* Add support for a `.cane` file for setting per-project default options.
|
123
|
+
|
124
|
+
## 1.0.0 - 14 January 2012 (4e400534)
|
125
|
+
|
126
|
+
* Initial release.
|
data/LICENSE
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
Copyright 2012 Square Inc.
|
3
|
+
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
you may not use this file except in compliance with the License.
|
6
|
+
You may obtain a copy of the License at
|
7
|
+
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
See the License for the specific language governing permissions and
|
14
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
[](https://travis-ci.org/rlqualls/sugarcane)
|
2
|
+
[](https://coveralls.io/r/rlqualls/sugarcane)
|
3
|
+
[](https://codeclimate.com/github/rlqualls/sugarcane)
|
4
|
+
|
5
|
+

|
6
|
+
|
7
|
+
> It's best to get beat with something sweet...
|
8
|
+
|
9
|
+
**Sugarcane** is an enhancement of the **cane** code quality tool. It makes various
|
10
|
+
minor changes, but most importantly, it closes the gap between the text editor
|
11
|
+
and cane.
|
12
|
+
|
13
|
+
You can find the original cane project at [square/cane](https://github.com/square/cane)
|
14
|
+
|
15
|
+
## Features
|
16
|
+
|
17
|
+
- Go straight from violations to their lines in a text editor
|
18
|
+
- Otherwise does what cane does
|
19
|
+
- Editors supported: sublimetext (untested), vim, gedit, nano
|
20
|
+
|
21
|
+
## Controls
|
22
|
+
|
23
|
+
- K,W, UP - up
|
24
|
+
- J,S, DOWN - down
|
25
|
+
- Q,X - quit
|
26
|
+
- O, Enter, Space - open file in text editor at the violation
|
27
|
+
|
28
|
+
## Installation
|
29
|
+
|
30
|
+
$ gem install sugarcane
|
31
|
+
|
32
|
+
## Installation (development)
|
33
|
+
|
34
|
+
$ git clone https://github.com/rlqualls/sugarcane
|
35
|
+
$ cd sugarcane
|
36
|
+
$ bundle
|
37
|
+
$ rake install
|
38
|
+
|
39
|
+
## Usage Examples
|
40
|
+
|
41
|
+
To run the default checks on all files in your project, navigate to the
|
42
|
+
project root and run sugarcane
|
43
|
+
|
44
|
+
$ sugarcane
|
45
|
+
|
46
|
+
If you just want to run all checks on a specific file:
|
47
|
+
|
48
|
+
$ sugarcane -f README.md
|
49
|
+
|
50
|
+
If you want to run checks on files matching a pattern:
|
51
|
+
|
52
|
+
$ sugarcane --abc-glob '{lib,spec}/**/*.rb' --abc-max 15
|
53
|
+
|
54
|
+
Sugarcane tries to find an editor in your PATH, choosing sublime then vim
|
55
|
+
first if one is available. You can specify a different editor with `--editor`.
|
56
|
+
|
57
|
+
$ sugarcane --editor nano
|
58
|
+
$ sugarcane --editor=gedit
|
59
|
+
|
60
|
+
**NOTE**: Right now, navigating to a line number relies on the convention
|
61
|
+
of `+<num_lines>` as an argument (sublimetext is included as an exception).
|
62
|
+
|
63
|
+
## Not far from the Tree
|
64
|
+
|
65
|
+
For original `cane` functionality, add the --report option
|
66
|
+
|
67
|
+
$ sugarcane --report
|
68
|
+
|
69
|
+
Methods exceeded maximum allowed ABC complexity (2):
|
70
|
+
|
71
|
+
lib/sugarcane.rb Cane#sample 23
|
72
|
+
lib/sugarcane.rb Cane#sample_2 17
|
73
|
+
|
74
|
+
Lines violated style requirements (2):
|
75
|
+
|
76
|
+
lib/sugarcane.rb:20 Line length >80
|
77
|
+
lib/sugarcane.rb:42 Trailing whitespace
|
78
|
+
|
79
|
+
Class definitions require explanatory comments on preceding line (1):
|
80
|
+
lib/sugarcane:3 SomeClass
|
81
|
+
|
82
|
+
## Options
|
83
|
+
|
84
|
+
$ sugarcane --help
|
85
|
+
Usage: sugarcane [options]
|
86
|
+
|
87
|
+
Default options are loaded from a .sugarcane file in the current directory.
|
88
|
+
|
89
|
+
-r, --require FILE Load a Ruby file containing user-defined checks
|
90
|
+
-c, --check CLASS Use the given user-defined check
|
91
|
+
|
92
|
+
--abc-glob GLOB Glob to run ABC metrics over (default: {app,lib}/**/*.rb)
|
93
|
+
--abc-max VALUE Ignore methods under this complexity (default: 15)
|
94
|
+
--abc-exclude METHOD Exclude method from analysis (eg. Foo::Bar#method)
|
95
|
+
--no-abc Disable ABC checking
|
96
|
+
|
97
|
+
--style-glob GLOB Glob to run style checks over (default: {app,lib,spec}/**/*.rb)
|
98
|
+
--style-measure VALUE Max line length (default: 80)
|
99
|
+
--style-exclude GLOB Exclude file or glob from style checking
|
100
|
+
--no-style Disable style checking
|
101
|
+
|
102
|
+
--doc-glob GLOB Glob to run doc checks over (default: {app,lib}/**/*.rb)
|
103
|
+
--doc-exclude GLOB Exclude file or glob from documentation checking
|
104
|
+
--no-readme Disable readme checking
|
105
|
+
--no-doc Disable documentation checking
|
106
|
+
|
107
|
+
--lt FILE,THRESHOLD Check the number in FILE is < to THRESHOLD (a number or another file name)
|
108
|
+
--lte FILE,THRESHOLD Check the number in FILE is <= to THRESHOLD (a number or another file name)
|
109
|
+
--eq FILE,THRESHOLD Check the number in FILE is == to THRESHOLD (a number or another file name)
|
110
|
+
--gte FILE,THRESHOLD Check the number in FILE is >= to THRESHOLD (a number or another file name)
|
111
|
+
--gt FILE,THRESHOLD Check the number in FILE is > to THRESHOLD (a number or another file name)
|
112
|
+
|
113
|
+
-f, --all FILE Apply all checks to given file
|
114
|
+
--max-violations VALUE Max allowed violations (default: 0)
|
115
|
+
--editor PROGRAM Text editor to use
|
116
|
+
--json Output as JSON
|
117
|
+
--report Original cane output
|
118
|
+
--parallel Use all processors. Slower on small projects, faster on large.
|
119
|
+
--color Colorize output
|
120
|
+
|
121
|
+
-v, --version Show version
|
122
|
+
-h, --help Show this message
|
123
|
+
|
124
|
+
## Configuration Files
|
125
|
+
|
126
|
+
Set default options using a `.cane` or '.sugarcane'. This is easier than telling
|
127
|
+
sugarcane what editor you want to use every time.
|
128
|
+
|
129
|
+
$ cat .sugarcane
|
130
|
+
--no-doc
|
131
|
+
--abc-glob **/*.rb
|
132
|
+
--editor gedit
|
133
|
+
|
134
|
+
It works exactly the same as specifying the options on the command-line.
|
135
|
+
Command-line arguments will override arguments specified in the configuration
|
136
|
+
file. Sugarcane will ignore a `.cane` file if it sees a `.sugarcane` file.
|
137
|
+
|
138
|
+
## Integrating with Rake
|
139
|
+
|
140
|
+
Sugarcane retains the ability to create rake tasks. The tasks will look for
|
141
|
+
configuration files the same way running sugarcane normally does. The build
|
142
|
+
will fail if the task finds more than `max_violations` violations which is 0
|
143
|
+
by default. Keep this in mind if using sugarcane tasks in continuous
|
144
|
+
integration.
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
require 'sugarcane/rake_task'
|
148
|
+
|
149
|
+
desc "Run sugarcane to check quality metrics"
|
150
|
+
SugarCane::RakeTask.new(:quality)
|
151
|
+
|
152
|
+
task :default => :quality
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
Instead of using a configuration file, you can specify options in a block.
|
157
|
+
Rescuing `LoadError` is a good idea, since `rake -T` failing is totally
|
158
|
+
frustrating.
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
begin
|
162
|
+
require 'sugarcane/rake_task'
|
163
|
+
|
164
|
+
desc "Run sugarcane to check quality metrics"
|
165
|
+
SugarCane::RakeTask.new(:quality) do |cane|
|
166
|
+
cane.abc_max = 10
|
167
|
+
cane.no_style = true
|
168
|
+
cane.max_violations = 3
|
169
|
+
cane.abc_exclude = %w(Foo::Bar#some_method)
|
170
|
+
end
|
171
|
+
|
172
|
+
task :default => :quality
|
173
|
+
rescue LoadError
|
174
|
+
warn "sugarcane not available, quality task not provided."
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
If you have multiple configuration files, you can specify which one to use
|
179
|
+
with `SugarCane::RakeTask#canefile=`. For that, the name does not matter.
|
180
|
+
|
181
|
+
## Implementing your own checks
|
182
|
+
|
183
|
+
Checks must implement:
|
184
|
+
|
185
|
+
* A class level `options` method that returns a hash of available options. This
|
186
|
+
will be included in help output if the check is added before `--help`. If
|
187
|
+
your check does not require any configuration, return an empty hash.
|
188
|
+
* A one argument constructor, into which will be passed the options specified
|
189
|
+
for your check.
|
190
|
+
* A `violations` method that returns an array of violations.
|
191
|
+
|
192
|
+
See existing checks for guidance. Create your check in a new file:
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
# unhappy.rb
|
196
|
+
class UnhappyCheck < Struct.new(:opts)
|
197
|
+
def self.options
|
198
|
+
{
|
199
|
+
unhappy_file: ["File to check", default: [nil]]
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
def violations
|
204
|
+
[
|
205
|
+
description: "Files are unhappy",
|
206
|
+
file: opts.fetch(:unhappy_file),
|
207
|
+
label: ":("
|
208
|
+
]
|
209
|
+
end
|
210
|
+
end
|
211
|
+
```
|
212
|
+
|
213
|
+
Include your check either using command-line options:
|
214
|
+
|
215
|
+
sugarcane -r unhappy.rb --check UnhappyCheck --unhappy-file myfile
|
216
|
+
|
217
|
+
Or in your rake task:
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
require 'unhappy'
|
221
|
+
|
222
|
+
SugarCane::RakeTask.new(:quality) do |c|
|
223
|
+
c.use UnhappyCheck, unhappy_file: 'myfile'
|
224
|
+
end
|
225
|
+
```
|
226
|
+
## Compatibility
|
227
|
+
|
228
|
+
Requires MRI 1.9, since it depends on the `ripper` library to calculate
|
229
|
+
complexity metrics. This only applies to the Ruby used to run SugarCane, not the
|
230
|
+
project it is being run against. In other words, you can run Cane against your
|
231
|
+
1.8 or JRuby project.
|
232
|
+
|
233
|
+
## Support
|
234
|
+
|
235
|
+
Make a [new github issue](https://github.com/rlqualls/sugarcane/issues/new).
|
data/bin/sugarcane
ADDED
data/lib/cane.rb
ADDED
@@ -0,0 +1,216 @@
|
|
1
|
+
require 'ripper'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
require 'sugarcane/file'
|
5
|
+
require 'sugarcane/ext/array'
|
6
|
+
require 'sugarcane/task_runner'
|
7
|
+
|
8
|
+
module SugarCane
|
9
|
+
|
10
|
+
# Creates violations for methods that are too complicated using a simple
|
11
|
+
# algorithm run against the parse tree of a file to count assignments,
|
12
|
+
# branches, and conditionals. Borrows heavily from metric_abc.
|
13
|
+
class AbcCheck < Struct.new(:opts)
|
14
|
+
|
15
|
+
def self.key; :abc; end
|
16
|
+
def self.name; "ABC check"; end
|
17
|
+
def self.options
|
18
|
+
{
|
19
|
+
abc_glob: ['Glob to run ABC metrics over',
|
20
|
+
default: '{app,lib}/**/*.rb',
|
21
|
+
variable: 'GLOB',
|
22
|
+
clobber: :no_abc],
|
23
|
+
abc_max: ['Ignore methods under this complexity',
|
24
|
+
default: 15,
|
25
|
+
cast: :to_i,
|
26
|
+
clobber: :no_abc],
|
27
|
+
abc_exclude: ['Exclude method from analysis (eg. Foo::Bar#method)',
|
28
|
+
variable: 'METHOD',
|
29
|
+
type: Array,
|
30
|
+
default: [],
|
31
|
+
clobber: :no_abc],
|
32
|
+
no_abc: ['Disable ABC checking',
|
33
|
+
cast: ->(x) { !x }]
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def violations
|
38
|
+
return [] if opts[:no_abc]
|
39
|
+
|
40
|
+
order worker.map(file_names) {|file_name|
|
41
|
+
find_violations(file_name)
|
42
|
+
}.flatten
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def find_violations(file_name)
|
48
|
+
ast = Ripper::SexpBuilder.new(SugarCane::File.contents(file_name)).parse
|
49
|
+
case ast
|
50
|
+
when nil
|
51
|
+
ast_type = InvalidAst.new(file_name)
|
52
|
+
else
|
53
|
+
ast_type = RubyAst.new(file_name, max_allowed_complexity,
|
54
|
+
ast, exclusions)
|
55
|
+
end
|
56
|
+
ast_type.violations
|
57
|
+
end
|
58
|
+
|
59
|
+
# Null object for when the file cannot be parsed.
|
60
|
+
class InvalidAst < Struct.new(:file_name)
|
61
|
+
def violations
|
62
|
+
[{file: file_name, description: "File contained invalid syntax"}]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Wrapper object around sexps returned from ripper.
|
67
|
+
class RubyAst < Struct.new(:file_name, :max_allowed_complexity,
|
68
|
+
:sexps, :exclusions)
|
69
|
+
|
70
|
+
def initialize(*args)
|
71
|
+
super
|
72
|
+
self.anon_method_add = true
|
73
|
+
end
|
74
|
+
|
75
|
+
def violations
|
76
|
+
process_ast(sexps).
|
77
|
+
select do
|
78
|
+
|nesting, complexity| complexity[:value] > max_allowed_complexity
|
79
|
+
end.
|
80
|
+
map do |violation|
|
81
|
+
{
|
82
|
+
# Here, a violation is an array, like:
|
83
|
+
# ["Class#method", {:value => xx, :line => xx}]
|
84
|
+
file: file_name,
|
85
|
+
line: violation.last[:line],
|
86
|
+
label: violation.first,
|
87
|
+
value: violation.last[:value],
|
88
|
+
description: "Methods exceeded maximum allowed ABC complexity",
|
89
|
+
menu_description: "#{violation.first} exceeded maximum "\
|
90
|
+
"ABC complexity"
|
91
|
+
}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
|
97
|
+
# Stateful flag used to determine whether we are currently parsing an
|
98
|
+
# anonymous class. See #container_label.
|
99
|
+
attr_accessor :anon_method_add
|
100
|
+
|
101
|
+
# Recursive function to process an AST. The `complexity` variable mutates,
|
102
|
+
# which is a bit confusing. `nesting` does not.
|
103
|
+
def process_ast(node, complexity = {}, nesting = [])
|
104
|
+
if method_nodes.include?(node[0])
|
105
|
+
nesting = nesting + [label_for(node)]
|
106
|
+
desc = method_description(node, *nesting)
|
107
|
+
unless excluded?(desc)
|
108
|
+
complexity[desc] = {
|
109
|
+
:value => calculate_abc(node),
|
110
|
+
:line => node.line_number
|
111
|
+
}
|
112
|
+
end
|
113
|
+
elsif parent = container_label(node)
|
114
|
+
nesting = nesting + [parent]
|
115
|
+
end
|
116
|
+
|
117
|
+
if node.is_a? Array
|
118
|
+
node[1..-1].each {|n| process_ast(n, complexity, nesting) if n }
|
119
|
+
end
|
120
|
+
complexity
|
121
|
+
end
|
122
|
+
|
123
|
+
def calculate_abc(method_node)
|
124
|
+
a = count_nodes(method_node, assignment_nodes)
|
125
|
+
b = count_nodes(method_node, branch_nodes) + 1
|
126
|
+
c = count_nodes(method_node, condition_nodes)
|
127
|
+
abc = Math.sqrt(a**2 + b**2 + c**2).round
|
128
|
+
abc
|
129
|
+
end
|
130
|
+
|
131
|
+
def container_label(node)
|
132
|
+
if container_nodes.include?(node[0])
|
133
|
+
# def foo, def self.foo
|
134
|
+
node[1][-1][1]
|
135
|
+
elsif node[0] == :method_add_block
|
136
|
+
if anon_method_add
|
137
|
+
# Class.new do ...
|
138
|
+
"(anon)"
|
139
|
+
else
|
140
|
+
# MyClass = Class.new do ...
|
141
|
+
# parent already added when processing a parent node
|
142
|
+
anon_method_add = true
|
143
|
+
nil
|
144
|
+
end
|
145
|
+
elsif node[0] == :assign && node[2][0] == :method_add_block
|
146
|
+
# MyClass = Class.new do ...
|
147
|
+
self.anon_method_add = false
|
148
|
+
node[1][-1][1]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def label_for(node)
|
153
|
+
# A default case is deliberately omitted since I know of no way this
|
154
|
+
# could fail and want it to fail fast.
|
155
|
+
node.detect {|x|
|
156
|
+
[:@ident, :@op, :@kw, :@const, :@backtick].include?(x[0])
|
157
|
+
}[1]
|
158
|
+
end
|
159
|
+
|
160
|
+
def count_nodes(node, types)
|
161
|
+
node.flatten.select {|n| types.include?(n) }.length
|
162
|
+
end
|
163
|
+
|
164
|
+
def assignment_nodes
|
165
|
+
[:assign, :opassign]
|
166
|
+
end
|
167
|
+
|
168
|
+
def method_nodes
|
169
|
+
[:def, :defs]
|
170
|
+
end
|
171
|
+
|
172
|
+
def container_nodes
|
173
|
+
[:class, :module]
|
174
|
+
end
|
175
|
+
|
176
|
+
def branch_nodes
|
177
|
+
[:call, :fcall, :brace_block, :do_block]
|
178
|
+
end
|
179
|
+
|
180
|
+
def condition_nodes
|
181
|
+
[:==, :===, :"<>", :"<=", :">=", :"=~", :>, :<, :else, :"<=>"]
|
182
|
+
end
|
183
|
+
|
184
|
+
METH_CHARS = { def: '#', defs: '.' }
|
185
|
+
|
186
|
+
def excluded?(method_description)
|
187
|
+
exclusions.include?(method_description)
|
188
|
+
end
|
189
|
+
|
190
|
+
def method_description(node, *modules, meth_name)
|
191
|
+
separator = METH_CHARS.fetch(node.first)
|
192
|
+
description = [modules.join('::'), meth_name].join(separator)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def file_names
|
197
|
+
Dir[opts.fetch(:abc_glob)]
|
198
|
+
end
|
199
|
+
|
200
|
+
def order(result)
|
201
|
+
result.sort_by {|x| x[:value].to_i }.reverse
|
202
|
+
end
|
203
|
+
|
204
|
+
def max_allowed_complexity
|
205
|
+
opts.fetch(:abc_max)
|
206
|
+
end
|
207
|
+
|
208
|
+
def exclusions
|
209
|
+
opts.fetch(:abc_exclude, []).flatten.to_set
|
210
|
+
end
|
211
|
+
|
212
|
+
def worker
|
213
|
+
SugarCane.task_runner(opts)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'sugarcane/default_checks'
|
2
|
+
|
3
|
+
module SugarCane
|
4
|
+
# Default options for command line interface
|
5
|
+
module CLI
|
6
|
+
def defaults(check)
|
7
|
+
check.options.each_with_object({}) {|(k, v), h|
|
8
|
+
option_opts = v[1] || {}
|
9
|
+
if option_opts[:type] == Array
|
10
|
+
h[k] = []
|
11
|
+
else
|
12
|
+
h[k] = option_opts[:default]
|
13
|
+
end
|
14
|
+
}
|
15
|
+
end
|
16
|
+
module_function :defaults
|
17
|
+
|
18
|
+
def default_options
|
19
|
+
{
|
20
|
+
max_violations: 0,
|
21
|
+
parallel: false,
|
22
|
+
exclusions_file: nil,
|
23
|
+
checks: SugarCane.default_checks
|
24
|
+
}.merge(SugarCane.default_checks.inject({}) {|a, check|
|
25
|
+
a.merge(defaults(check))
|
26
|
+
})
|
27
|
+
end
|
28
|
+
module_function :default_options
|
29
|
+
end
|
30
|
+
end
|