sugarcane 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/rlqualls/sugarcane.png?branch=master)](https://travis-ci.org/rlqualls/sugarcane)
|
2
|
+
[![Coverage Status](https://coveralls.io/repos/rlqualls/sugarcane/badge.png)](https://coveralls.io/r/rlqualls/sugarcane)
|
3
|
+
[![Code Climate](https://codeclimate.com/github/rlqualls/sugarcane.png)](https://codeclimate.com/github/rlqualls/sugarcane)
|
4
|
+
|
5
|
+
![sugarcane screenshot](http://i.imgur.com/RP7xDLU.png)
|
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
|