cane 2.5.2 → 2.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.
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