cane 2.5.2 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Cane History
2
2
 
3
+ ## 2.6.0 - 7 June 2013 (616bb8a5)
4
+
5
+ * Feature: classes with no methods do not require documentation.
6
+ * Feature: modules with methods require documentation.
7
+ * Feature: support all README extensions.
8
+ * Feature: --color option.
9
+ * Bugfix: fix false positive on class matching for doc check.
10
+ * Bugfix: better handling of invalid strings.
11
+ * Compat: fix Ruby 2.0 deprecations.
12
+
3
13
  ## 2.5.2 - 26 January 2013 (a0cf38ba)
4
14
 
5
15
  * Feature: support operators beside `>=` in threshold check.
data/README.md CHANGED
@@ -62,6 +62,7 @@ Customize behaviour with a wealth of options:
62
62
  --max-violations VALUE Max allowed violations (default: 0)
63
63
  --json Output as JSON
64
64
  --parallel Use all processors. Slower on small projects, faster on large.
65
+ --color Colorize output
65
66
 
66
67
  -v, --version Show version
67
68
  -h, --help Show this message
data/lib/cane/cli.rb CHANGED
@@ -4,6 +4,8 @@ require 'cane/version'
4
4
  require 'cane/cli/parser'
5
5
 
6
6
  module Cane
7
+ # Command line interface. This passes off arguments to the parser and starts
8
+ # the Cane runner
7
9
  module CLI
8
10
  def run(args)
9
11
  spec = Parser.parse(args)
@@ -1,6 +1,7 @@
1
1
  require 'cane/default_checks'
2
2
 
3
3
  module Cane
4
+ # Default options for command line interface
4
5
  module CLI
5
6
  def defaults(check)
6
7
  check.options.each_with_object({}) {|(k, v), h|
@@ -118,6 +118,9 @@ BANNER
118
118
  "Use all processors. Slower on small projects, faster on large.",
119
119
  cast: ->(x) { x }
120
120
 
121
+ add_option %w(--color),
122
+ "Colorize output", default: false
123
+
121
124
  parser.separator ""
122
125
  end
123
126
 
@@ -3,6 +3,7 @@ require 'cane/style_check'
3
3
  require 'cane/doc_check'
4
4
  require 'cane/threshold_check'
5
5
 
6
+ # Default checks performed when no checks are provided
6
7
  module Cane
7
8
  def default_checks
8
9
  [
@@ -8,7 +8,15 @@ module Cane
8
8
  class DocCheck < Struct.new(:opts)
9
9
 
10
10
  DESCRIPTION =
11
- "Class definitions require explanatory comments on preceding line"
11
+ "Class and Module definitions require explanatory comments on previous line"
12
+
13
+ ClassDefinition = Struct.new(:values) do
14
+ def line; values.fetch(:line); end
15
+ def label; values.fetch(:label); end
16
+ def missing_doc?; !values.fetch(:has_doc); end
17
+ def requires_doc?; values.fetch(:requires_doc, false); end
18
+ def requires_doc=(value); values[:requires_doc] = value; end
19
+ end
12
20
 
13
21
  def self.key; :doc; end
14
22
  def self.name; "documentation checking"; end
@@ -33,6 +41,10 @@ module Cane
33
41
  MAGIC_COMMENT_REGEX =
34
42
  %r"#(\s+-\*-)?\s+(en)?coding\s*[=:]\s*([[:alnum:]\-_]+)"
35
43
 
44
+ CLASS_REGEX = /^\s*(?:class|module)\s+([^\s;]+)/
45
+
46
+ METHOD_REGEX = /(?:^|\s)def\s+/
47
+
36
48
  def violations
37
49
  return [] if opts[:no_doc]
38
50
 
@@ -42,30 +54,46 @@ module Cane
42
54
  end
43
55
 
44
56
  def find_violations(file_name)
45
- last_line = ""
46
- Cane::File.iterator(file_name).map.with_index do |line, number|
47
- result = if class_definition?(line) && !comment?(last_line)
57
+ class_definitions_in(file_name).map do |class_definition|
58
+ if class_definition.requires_doc? && class_definition.missing_doc?
48
59
  {
49
60
  file: file_name,
50
- line: number + 1,
51
- label: extract_class_name(line),
61
+ line: class_definition.line,
62
+ label: class_definition.label,
52
63
  description: DESCRIPTION
53
64
  }
54
65
  end
55
- last_line = line
56
- result
57
66
  end.compact
58
67
  end
59
68
 
69
+ def class_definitions_in(file_name)
70
+ class_definitions = []
71
+ last_line = ""
72
+
73
+ Cane::File.iterator(file_name).each_with_index do |line, number|
74
+ if class_definition? line
75
+ class_definitions << ClassDefinition.new({
76
+ line: (number + 1),
77
+ label: extract_class_name(line),
78
+ has_doc: comment?(last_line)
79
+ })
80
+ end
81
+
82
+ if method_definition?(line) && !class_definitions.empty?
83
+ class_definitions.last.requires_doc = true
84
+ end
85
+
86
+ last_line = line
87
+ end
88
+
89
+ class_definitions
90
+ end
91
+
60
92
  def missing_file_violations
61
93
  result = []
62
94
  return result if opts[:no_readme]
63
95
 
64
- filenames = ['README', 'readme']
65
- extensions = ['', '.txt', '.md', '.mdown', '.rdoc']
66
- combinations = filenames.product(extensions)
67
-
68
- if combinations.none? {|n, x| Cane::File.exists?(n + x) }
96
+ if Cane::File.case_insensitive_glob("README*").none?
69
97
  result << { description: 'Missing documentation',
70
98
  label: 'No README found' }
71
99
  end
@@ -76,8 +104,12 @@ module Cane
76
104
  Dir[opts.fetch(:doc_glob)].reject { |file| excluded?(file) }
77
105
  end
78
106
 
107
+ def method_definition?(line)
108
+ line =~ METHOD_REGEX
109
+ end
110
+
79
111
  def class_definition?(line)
80
- line =~ /^\s*class\s+/ and $'.index('<<') != 0
112
+ line =~ CLASS_REGEX && $1.index('<<') != 0
81
113
  end
82
114
 
83
115
  def comment?(line)
@@ -85,7 +117,7 @@ module Cane
85
117
  end
86
118
 
87
119
  def extract_class_name(line)
88
- line.match(/class\s+([^\s;]+)/)[1]
120
+ line.match(CLASS_REGEX)[1]
89
121
  end
90
122
 
91
123
  def exclusions
@@ -102,5 +134,4 @@ module Cane
102
134
  Cane.task_runner(opts)
103
135
  end
104
136
  end
105
-
106
137
  end
@@ -9,13 +9,13 @@ module Cane
9
9
  end
10
10
 
11
11
  def each(&block)
12
- return Enumerator.new(self, :each) unless block
12
+ return self.to_enum unless block
13
13
 
14
14
  lines.each do |line|
15
15
  begin
16
16
  line =~ /\s/
17
17
  rescue ArgumentError
18
- line.encode!('UTF-8', 'UTF-8', invalid: :replace)
18
+ line.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace)
19
19
  end
20
20
 
21
21
  block.call(line)
data/lib/cane/file.rb CHANGED
@@ -21,6 +21,10 @@ module Cane
21
21
  def exists?(path)
22
22
  ::File.exists?(path)
23
23
  end
24
+
25
+ def case_insensitive_glob(glob)
26
+ Dir.glob(glob, ::File::FNM_CASEFOLD)
27
+ end
24
28
  end
25
29
  end
26
30
  end
@@ -54,7 +54,11 @@ module Cane
54
54
  @gte = []
55
55
  @options = Cane::CLI.default_options
56
56
 
57
- yield self if block_given?
57
+ if block_given?
58
+ yield self
59
+ else
60
+ self.canefile = './.cane'
61
+ end
58
62
 
59
63
  unless ::Rake.application.last_comment
60
64
  desc %(Check code quality metrics with cane)
data/lib/cane/runner.rb CHANGED
@@ -3,6 +3,7 @@ require 'parallel'
3
3
  require 'cane/violation_formatter'
4
4
  require 'cane/json_formatter'
5
5
 
6
+ # Accepts a parsed configuration and passes those options to a new Runner
6
7
  module Cane
7
8
  def run(*args)
8
9
  Runner.new(*args).run
@@ -19,7 +20,7 @@ module Cane
19
20
  end
20
21
 
21
22
  def run
22
- outputter.print formatter.new(violations)
23
+ outputter.print formatter.new(violations, opts)
23
24
 
24
25
  violations.length <= opts.fetch(:max_violations)
25
26
  end
@@ -1,3 +1,4 @@
1
+ # Provides a SimpleTaskRunner or Parallel task runner based on configuration
1
2
  module Cane
2
3
  def task_runner(opts)
3
4
  if opts[:parallel]
@@ -8,7 +9,7 @@ module Cane
8
9
  end
9
10
  module_function :task_runner
10
11
 
11
- # Mirrors the Parallel gem's interface but does not provide any parralleism.
12
+ # Mirrors the Parallel gem's interface but does not provide any parallelism.
12
13
  # This is faster for smaller tasks since it doesn't incur any overhead for
13
14
  # creating new processes and communicating between them.
14
15
  class SimpleTaskRunner
data/lib/cane/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Cane
2
- VERSION = '2.5.2'
2
+ VERSION = '2.6.0'
3
3
  end
@@ -6,24 +6,32 @@ module Cane
6
6
  # Computes a string to be displayed as output from an array of violations
7
7
  # computed by the checks.
8
8
  class ViolationFormatter
9
- attr_reader :violations
9
+ attr_reader :violations, :options
10
10
 
11
- def initialize(violations)
11
+ def initialize(violations, options = {})
12
12
  @violations = violations.map do |v|
13
13
  v.merge(file_and_line: v[:line] ?
14
14
  "%s:%i" % v.values_at(:file, :line) :
15
15
  v[:file]
16
16
  )
17
17
  end
18
+
19
+ @options = options
18
20
  end
19
21
 
20
22
  def to_s
21
23
  return "" if violations.empty?
22
24
 
23
- violations.group_by {|x| x[:description] }.map do |d, vs|
25
+ string = violations.group_by {|x| x[:description] }.map do |d, vs|
24
26
  format_group_header(d, vs) +
25
27
  format_violations(vs)
26
28
  end.join("\n") + "\n\n" + totals + "\n\n"
29
+
30
+ if violations.count > options.fetch(:max_violations, 0)
31
+ string = colorize(string)
32
+ end
33
+
34
+ string
27
35
  end
28
36
 
29
37
  protected
@@ -54,6 +62,12 @@ module Cane
54
62
  }.join(' ').strip
55
63
  end
56
64
 
65
+ def colorize(string)
66
+ return string unless options[:color]
67
+
68
+ "\e[31m#{string}\e[0m"
69
+ end
70
+
57
71
  def totals
58
72
  "Total Violations: #{violations.length}"
59
73
  end
data/spec/cane_spec.rb CHANGED
@@ -50,8 +50,10 @@ describe 'The cane application' do
50
50
  --unhappy-file #{fn}
51
51
  )
52
52
  output.should include("Lines violated style requirements")
53
- output.should include("Class definitions require explanatory comments")
54
53
  output.should include("Methods exceeded maximum allowed ABC complexity")
54
+ output.should include(
55
+ "Class and Module definitions require explanatory comments"
56
+ )
55
57
  exitstatus.should == 1
56
58
  end
57
59
 
@@ -69,6 +71,12 @@ describe 'The cane application' do
69
71
  Cane.task_runner(parallel: true).should == Parallel
70
72
  end
71
73
 
74
+ it 'colorizes output' do
75
+ output, exitstatus = run("--color --abc-max 0")
76
+
77
+ output.should include("\e[31m")
78
+ end
79
+
72
80
  after do
73
81
  if Object.const_defined?(class_name)
74
82
  Object.send(:remove_const, class_name)
@@ -7,38 +7,60 @@ describe Cane::DocCheck do
7
7
  described_class.new(opts.merge(doc_glob: file_name))
8
8
  end
9
9
 
10
- it 'creates a DocViolation for each undocumented class' do
10
+ it 'creates a DocViolation for each undocumented class with a method' do
11
11
  file_name = make_file <<-RUBY
12
12
  # This class is documented
13
13
  class Doc; end
14
- class NoDoc; end # No doc
15
- class AlsoNoDoc; end
14
+ class Empty; end # No doc is fine
15
+ class NoDoc; def with_method; end; end
16
16
  classIgnore = nil
17
17
  [:class]
18
18
  # class Ignore
19
19
  class Meta
20
20
  class << self; end
21
+ end
22
+ module DontNeedDoc; end
23
+ # This module is documented
24
+ module HasDoc
25
+ def mixin; end
26
+ end
27
+ module AlsoNeedsDoc; def mixin; end; end
28
+ module NoDocIsFine
29
+ module ButThisNeedsDoc
30
+ def self.global
31
+ end
32
+ end
33
+ module AlsoNoDocIsFine; end
34
+ # We've got docs
35
+ module CauseWeNeedThem
36
+ def mixin
37
+ end
38
+ end
21
39
  end
22
40
  RUBY
23
41
 
24
42
  violations = check(file_name).violations
25
- violations.length.should == 2
43
+ violations.length.should == 3
26
44
 
27
45
  violations[0].values_at(:file, :line, :label).should == [
28
- file_name, 3, "NoDoc"
46
+ file_name, 4, "NoDoc"
29
47
  ]
30
48
 
31
49
  violations[1].values_at(:file, :line, :label).should == [
32
- file_name, 4, "AlsoNoDoc"
50
+ file_name, 16, "AlsoNeedsDoc"
51
+ ]
52
+
53
+ violations[2].values_at(:file, :line, :label).should == [
54
+ file_name, 18, "ButThisNeedsDoc"
33
55
  ]
34
56
  end
35
57
 
36
58
  it 'ignores magic encoding comments' do
37
59
  file_name = make_file <<-RUBY
38
60
  # coding = utf-8
39
- class NoDoc; end
61
+ class NoDoc; def do_stuff; end; end
40
62
  # -*- encoding : utf-8 -*-
41
- class AlsoNoDoc; end
63
+ class AlsoNoDoc; def do_more_stuff; end; end
42
64
  # Parse a Transfer-Encoding: Chunked response
43
65
  class Doc; end
44
66
  RUBY
@@ -57,16 +79,7 @@ class Doc; end
57
79
  it 'creates a violation for missing README' do
58
80
  file = fire_replaced_class_double("Cane::File")
59
81
  stub_const("Cane::File", file)
60
- file.should_receive(:exists?).with("README").and_return(false)
61
- file.should_receive(:exists?).with("README.md").and_return(false)
62
- file.should_receive(:exists?).with("README.mdown").and_return(false)
63
- file.should_receive(:exists?).with("README.txt").and_return(false)
64
- file.should_receive(:exists?).with("README.rdoc").and_return(false)
65
- file.should_receive(:exists?).with("readme").and_return(false)
66
- file.should_receive(:exists?).with("readme.md").and_return(false)
67
- file.should_receive(:exists?).with("readme.mdown").and_return(false)
68
- file.should_receive(:exists?).with("readme.txt").and_return(false)
69
- file.should_receive(:exists?).with("readme.rdoc").and_return(false)
82
+ file.should_receive(:case_insensitive_glob).with("README*").and_return([])
70
83
 
71
84
  violations = check("").violations
72
85
  violations.length.should == 1
@@ -76,6 +89,18 @@ class Doc; end
76
89
  ]
77
90
  end
78
91
 
92
+ it 'does not create a violation when readme exists' do
93
+ file = fire_replaced_class_double("Cane::File")
94
+ stub_const("Cane::File", file)
95
+ file
96
+ .should_receive(:case_insensitive_glob)
97
+ .with("README*")
98
+ .and_return(%w(readme.md))
99
+
100
+ violations = check("").violations
101
+ violations.length.should == 0
102
+ end
103
+
79
104
  it 'skips declared exclusions' do
80
105
  file_name = make_file <<-FILE.gsub /^\s{6}/, ''
81
106
  class NeedsDocumentation
@@ -101,4 +126,16 @@ class Doc; end
101
126
 
102
127
  violations.length.should == 0
103
128
  end
129
+
130
+ it 'skips class inside an array' do
131
+ file_name = make_file <<-RUBY
132
+ %w(
133
+ class
134
+ method
135
+ )
136
+ RUBY
137
+
138
+ violations = check(file_name).violations
139
+ violations.length.should == 0
140
+ end
104
141
  end
data/spec/file_spec.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ require 'tmpdir'
3
+
4
+ require 'cane/file'
5
+
6
+ describe Cane::File do
7
+ describe '.case_insensitive_glob' do
8
+ it 'matches all kinds of readmes' do
9
+ expected = %w(
10
+ README
11
+ readme.md
12
+ ReaDME.TEXTILE
13
+ )
14
+
15
+ Dir.mktmpdir do |dir|
16
+ Dir.chdir(dir) do
17
+ expected.each do |x|
18
+ FileUtils.touch(x)
19
+ end
20
+ Cane::File.case_insensitive_glob("README*").should =~ expected
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -47,6 +47,22 @@ describe Cane::RakeTask do
47
47
  out.should include("Quality threshold crossed")
48
48
  end
49
49
 
50
+ it 'defaults to using a canefile without a block' do
51
+ in_tmp_dir do
52
+ conf = "--gte 90,99"
53
+ conf_file = File.open('.cane', 'w') {|f| f.write conf }
54
+
55
+ task = Cane::RakeTask.new(:canefile_quality)
56
+
57
+ task.should_receive(:abort)
58
+ out = capture_stdout do
59
+ Rake::Task['canefile_quality'].invoke
60
+ end
61
+
62
+ out.should include("Quality threshold crossed")
63
+ end
64
+ end
65
+
50
66
  after do
51
67
  Rake::Task.clear
52
68
  end
data/spec/spec_helper.rb CHANGED
@@ -28,6 +28,12 @@ def make_file(content)
28
28
  tempfile.path
29
29
  end
30
30
 
31
+ def in_tmp_dir(&block)
32
+ Dir.mktmpdir do |dir|
33
+ Dir.chdir(dir, &block)
34
+ end
35
+ end
36
+
31
37
  RSpec::Matchers.define :have_violation do |label|
32
38
  match do |check|
33
39
  violations = check.violations
@@ -18,4 +18,22 @@ describe Cane::ViolationFormatter do
18
18
  result = described_class.new(violations).to_s
19
19
  result.should include("Total Violations: 2")
20
20
  end
21
+
22
+ it 'does not colorize output by default' do
23
+ result = described_class.new([violation("FAIL")]).to_s
24
+ result.should_not include("\e[31m")
25
+ end
26
+
27
+ it 'colorizes output when passed color: true' do
28
+ result = described_class.new([violation("FAIL")], color: true).to_s
29
+ result.should include("\e[31m")
30
+ result.should include("\e[0m")
31
+ end
32
+
33
+ it 'does not colorize output if max_violations is not crossed' do
34
+ options = { color: true, max_violations: 1 }
35
+ result = described_class.new([violation("FAIL")], options).to_s
36
+
37
+ result.should_not include("\e[31m")
38
+ end
21
39
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cane
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.2
4
+ version: 2.6.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-26 00:00:00.000000000 Z
12
+ date: 2013-06-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: parallel
@@ -104,6 +104,7 @@ files:
104
104
  - spec/cli_spec.rb
105
105
  - spec/doc_check_spec.rb
106
106
  - spec/encoding_aware_iterator_spec.rb
107
+ - spec/file_spec.rb
107
108
  - spec/json_formatter_spec.rb
108
109
  - spec/parser_spec.rb
109
110
  - spec/rake_task_spec.rb
@@ -165,6 +166,7 @@ test_files:
165
166
  - spec/cli_spec.rb
166
167
  - spec/doc_check_spec.rb
167
168
  - spec/encoding_aware_iterator_spec.rb
169
+ - spec/file_spec.rb
168
170
  - spec/json_formatter_spec.rb
169
171
  - spec/parser_spec.rb
170
172
  - spec/rake_task_spec.rb