skeptic 0.0.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.
@@ -0,0 +1,65 @@
1
+ module Skeptic
2
+ module SexpVisitor
3
+ def self.included(receiver)
4
+ receiver.send :include, InstanceMethods
5
+ receiver.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def handlers
10
+ @handlers ||= {}
11
+ end
12
+
13
+ def on(*types, &block)
14
+ types.each do |type|
15
+ handlers[type] = block
16
+ end
17
+ end
18
+ end
19
+
20
+ module InstanceMethods
21
+ private
22
+
23
+ def visit(sexp)
24
+ if Symbol === sexp[0] and self.class.handlers.has_key? sexp[0]
25
+ type, *args = *sexp
26
+ handler = self.class.handlers[type]
27
+
28
+ with_sexp_type(type) { instance_exec *args, &handler }
29
+ else
30
+ range = sexp[0].kind_of?(Symbol) ? 1..-1 : 0..-1
31
+
32
+ sexp[range].each do |subtree|
33
+ visit subtree if subtree.kind_of?(Array) and not subtree[0].kind_of?(Fixnum)
34
+ end
35
+ end
36
+ end
37
+
38
+ def env
39
+ @env ||= Environment.new
40
+ end
41
+
42
+ def with_sexp_type(type)
43
+ @current_sexp_type, old_sexp_type = type, @current_sexp_type
44
+ yield
45
+ @current_sexp_type = old_sexp_type
46
+ end
47
+
48
+ def sexp_type
49
+ @current_sexp_type
50
+ end
51
+
52
+ def extract_name(tree)
53
+ type, first, second = *tree
54
+ case type
55
+ when :const_path_ref then "#{extract_name(first)}::#{extract_name(second)}"
56
+ when :const_ref then extract_name(first)
57
+ when :var_ref then extract_name(first)
58
+ when :@const then first
59
+ when :@ident then first
60
+ else '<unknown>'
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
data/lib/skeptic.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'ripper'
2
+
3
+ require 'skeptic/environment'
4
+ require 'skeptic/scope'
5
+ require 'skeptic/sexp_visitor'
6
+
7
+ require 'skeptic/rules/max_nesting_depth'
8
+ require 'skeptic/rules/methods_per_class'
9
+ require 'skeptic/rules/lines_per_method'
10
+ require 'skeptic/rules/no_semicolons'
11
+
12
+ require 'skeptic/critic'
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+
3
+ module Skeptic
4
+ describe Critic do
5
+ let(:critic) { Critic.new }
6
+
7
+ it "can locate semicolons in the code" do
8
+ criticize 'foo; bar', no_semicolons: true
9
+
10
+ expect_criticism 'You have a semicolon at line 1, column 3', 'No semicolons as expression separators'
11
+ end
12
+
13
+ it "can locate deep levels of nesting" do
14
+ criticize <<-RUBY, max_nesting_depth: 1
15
+ class Foo
16
+ def bar
17
+ while true
18
+ if false
19
+ really?
20
+ end
21
+ end
22
+ end
23
+ end
24
+ RUBY
25
+
26
+ expect_criticism 'Foo#bar has 2 levels of nesting: while > if', 'Maximum nesting depth (1)'
27
+ end
28
+
29
+ it "can locate classes with too many methods" do
30
+ criticize <<-RUBY, methods_per_class: 1
31
+ class Foo
32
+ def bar; end
33
+ def baz; end
34
+ end
35
+ RUBY
36
+
37
+ expect_criticism 'Foo has 2 methods: #bar, #baz', 'Number of methods per class (1)'
38
+ end
39
+
40
+ it "can locate methods that are too long" do
41
+ criticize <<-RUBY, lines_per_method: 1
42
+ class Foo
43
+ def bar
44
+ one
45
+ two
46
+ three
47
+ end
48
+ end
49
+ RUBY
50
+
51
+ expect_criticism 'Foo#bar is 3 lines long', 'Number of lines per method (1)'
52
+ end
53
+
54
+ def criticize(code, options)
55
+ options.each do |key, value|
56
+ critic.send "#{key}=", value
57
+ end
58
+
59
+ critic.criticize code
60
+ end
61
+
62
+ def expect_criticism(message, type)
63
+ critic.criticism.should include [message, type]
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ module Skeptic
4
+ describe Environment do
5
+ let(:env) { Environment.new }
6
+
7
+ it "maps names to objects" do
8
+ env[:foo] = 42
9
+ env[:foo].should eq 42
10
+ end
11
+
12
+ it "returns nil for names that are not set" do
13
+ env[:foo].should be_nil
14
+ end
15
+
16
+ it "allows environments to be extender" do
17
+ env.push foo: 2
18
+ env[:foo].should eq 2
19
+ end
20
+
21
+ it "allows environments to be unextended" do
22
+ env[:foo] = 1
23
+ env.push foo: 2
24
+ env.pop
25
+ env[:foo].should eq 1
26
+ end
27
+
28
+ it "looks up undefined names in the closure" do
29
+ env[:foo] = 1
30
+ env.push
31
+ env[:foo].should eq 1
32
+ end
33
+
34
+ it "can be extended for a block" do
35
+ executed_block = false
36
+
37
+ env[:foo] = 1
38
+ env.scoped do
39
+ env[:foo] = 2
40
+ env[:foo].should eq 2
41
+ executed_block = true
42
+ end
43
+
44
+ executed_block.should be_true
45
+ env[:foo].should eq 1
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+
3
+ module Skeptic
4
+ module Rules
5
+ describe LinesPerMethod do
6
+ describe "calculating method size" do
7
+ it "can count the size of a method" do
8
+ code = <<-RUBY
9
+ class Foo
10
+ def bar
11
+ first
12
+ second
13
+ end
14
+ end
15
+ RUBY
16
+
17
+ analyze(code).size_of('Foo#bar').should eq 2
18
+ end
19
+
20
+ it "does not count empty lines" do
21
+ expect_line_count 2, <<-RUBY
22
+ foo
23
+
24
+ bar
25
+ RUBY
26
+ end
27
+
28
+ it "does not count lines containing one end" do
29
+ expect_line_count 2, <<-RUBY
30
+ if foo
31
+ bar
32
+ end
33
+ RUBY
34
+ end
35
+ end
36
+
37
+ describe "reporting" do
38
+ it "can tell which methods are too long" do
39
+ analyzer = analyze 1, <<-RUBY
40
+ class Foo
41
+ def bar
42
+ one
43
+ two
44
+ three
45
+ end
46
+ end
47
+ RUBY
48
+
49
+ analyzer.violations.should include 'Foo#bar is 3 lines long'
50
+ end
51
+
52
+ it "reports under 'Number of lines per method'" do
53
+ LinesPerMethod.new(2).name.should eq 'Number of lines per method (2)'
54
+ end
55
+ end
56
+
57
+ def expect_line_count(count, code)
58
+ code = "class Foo\ndef bar\n#{code}\nend\nend"
59
+ analyze(code).size_of('Foo#bar').should eq count
60
+ end
61
+
62
+ def analyze(limit = nil, code)
63
+ LinesPerMethod.new(limit).apply_to nil, Ripper.sexp(code)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+
3
+ module Skeptic
4
+ module Rules
5
+ describe MaxNestingDepth do
6
+ describe "structure analysis" do
7
+ it "counts all conditional forms as a level of nesting" do
8
+ expect_deepest_nesting :if, 'if condition?; action; end'
9
+ expect_deepest_nesting :if, 'if condition? then action end'
10
+ expect_deepest_nesting :if, 'action if condition?'
11
+ expect_deepest_nesting :unless, 'action unless condition?'
12
+ expect_deepest_nesting :unless, 'unless condition?; action; end'
13
+ expect_deepest_nesting :unless, 'unless condition? then action end'
14
+ expect_deepest_nesting :if, :if, 'a if b? if c?'
15
+ end
16
+
17
+ it "counts else blocks as a level of nesting" do
18
+ expect_deepest_nesting :if, :unless, 'if a?; else; foo unless b? end'
19
+ expect_deepest_nesting :if, :unless, 'if a?; elsif? b?; else; foo unless c? end'
20
+ expect_deepest_nesting :unless, :if, 'unless a?; else; foo if b? end'
21
+ end
22
+
23
+ it "counts elsif blocks as a level of nesting" do
24
+ expect_deepest_nesting :if, :unless, 'if a?; elsif b?; foo unless c?; end'
25
+ expect_deepest_nesting :if, :unless, 'if a?; elsif b?; foo unless c?; else; end'
26
+ end
27
+
28
+ it "counts unbound loops as a level of nesting" do
29
+ expect_deepest_nesting :while, :if, 'while a?; b if c? end'
30
+ expect_deepest_nesting :while, :if, '(a if b?) while c?'
31
+ expect_deepest_nesting :until, :if, 'until a?; b if c? end'
32
+ expect_deepest_nesting :until, :if, '(a if b?) until c?'
33
+ end
34
+
35
+ it "counts blocks as a level of nesting" do
36
+ expect_deepest_nesting :iter, :if, 'a { b if c? }'
37
+ expect_deepest_nesting :iter, :if, 'a(1) { b if c? }'
38
+ expect_deepest_nesting :iter, :if, 'a do; b if c?; end'
39
+ expect_deepest_nesting :iter, :if, 'a(1) do; b if c?; end'
40
+
41
+ expect_deepest_nesting :iter, :if, 'loop { a if b }'
42
+ end
43
+
44
+ it "counts lambdas as a level of nesting" do
45
+ expect_deepest_nesting :iter, :if, 'lambda { a if b }'
46
+ expect_deepest_nesting :iter, :if, 'Proc.new { a if b }'
47
+ expect_deepest_nesting :lambda, :if, '-> { a if b }'
48
+ end
49
+
50
+ it "counts for loops as a level of nesting" do
51
+ expect_deepest_nesting :for, :if, 'for a in b; c if d; end'
52
+ end
53
+
54
+ it "counts case statements as a level of nesting" do
55
+ expect_deepest_nesting :case, :if, 'case a; when b; c if d?; end'
56
+ expect_deepest_nesting :case, :if, 'case a; when b; when c; d if e?; end'
57
+ expect_deepest_nesting :case, :if, 'case a; when b; else; d if e?; end'
58
+ end
59
+
60
+ it "counts begin blocks as a level of nesting" do
61
+ expect_deepest_nesting :begin, :if, 'begin; a if b; end'
62
+ end
63
+
64
+ it "does not count the method invocation as a block" do
65
+ expect_a_nesting :if, 'a((b if c)) { d }'
66
+ expect_a_nesting :iter, :if, 'a.b { c if d? }.e { g }'
67
+ expect_a_nesting :iter, :unless, 'a.b { c unless d? }.c { }'
68
+ end
69
+
70
+ it "does not count the if condition as a level of nesting" do
71
+ expect_a_nesting :iter, 'a if b { c }'
72
+ end
73
+ end
74
+
75
+ describe "nesting location" do
76
+ it "recognizes top-level code" do
77
+ expect_scope nil, nil, :if, 'a if b'
78
+ end
79
+
80
+ it "recognizes class definitions" do
81
+ expect_scope 'A', nil, :if, 'class A; a if b; end'
82
+ expect_scope 'A::B', nil, :if, 'class A::B; a if b; end'
83
+ end
84
+
85
+ it "recognizes method definitions" do
86
+ expect_scope nil, 'a', :if, 'def a; b if c; end'
87
+ expect_scope 'A', 'b', :if, 'class A; def b; c if d; end; end'
88
+ end
89
+ end
90
+
91
+ describe "reporting" do
92
+ it "can report methods that contain a deep level of nesting" do
93
+ analyzer = analyze 1, <<-RUBY
94
+ class Foo
95
+ def bar
96
+ while true
97
+ if false
98
+ really?
99
+ end
100
+ end
101
+ end
102
+ end
103
+ RUBY
104
+
105
+ analyzer.violations.should include 'Foo#bar has 2 levels of nesting: while > if'
106
+ end
107
+
108
+ it "reports under 'Maximum nesting depth'" do
109
+ MaxNestingDepth.new(2).name.should eq 'Maximum nesting depth (2)'
110
+ end
111
+ end
112
+
113
+ def nestings(code)
114
+ analyze(code).nestings
115
+ end
116
+
117
+ def expect_scope(class_name, method_name, *levels, code)
118
+ analyze(code).nestings.should include Scope.new(class_name, method_name, levels)
119
+ end
120
+
121
+ def expect_a_nesting(*levels, code)
122
+ analyze(code).nestings.should include Scope.new(nil, nil, levels)
123
+ end
124
+
125
+ def expect_deepest_nesting(*levels, code)
126
+ analyze(code).nestings.max_by(&:depth).should eq Scope.new(nil, nil, levels)
127
+ end
128
+
129
+ def analyze(limit = nil, code)
130
+ MaxNestingDepth.new(limit).apply_to nil, Ripper.sexp(code)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+
3
+ module Skeptic
4
+ module Rules
5
+ describe MethodsPerClass do
6
+ describe "counting methods" do
7
+ it "counts methods defined in classes" do
8
+ expect_method_count 'Foo', 2, <<-RUBY
9
+ class Foo
10
+ def bar; end
11
+ def baz; end
12
+ end
13
+ RUBY
14
+ end
15
+
16
+ it "counts methods defined in modules" do
17
+ expect_method_count 'Foo', 2, <<-RUBY
18
+ module Foo
19
+ def bar; end
20
+ def baz; end
21
+ end
22
+ RUBY
23
+ end
24
+
25
+ it "counts method defined when the class is reopened" do
26
+ expect_method_count 'Foo', 2, <<-RUBY
27
+ class Foo; def bar; end; end
28
+ class Foo; def baz; end; end
29
+ RUBY
30
+ end
31
+
32
+ it "counts redefining the method as a new method" do
33
+ expect_method_count 'Foo', 2, <<-RUBY
34
+ class Foo
35
+ def bar; end
36
+ def bar; end
37
+ end
38
+ RUBY
39
+ end
40
+
41
+ it "works with multiple classes" do
42
+ counter = analyze <<-RUBY
43
+ class Foo; def name; end; end
44
+ class Bar; def name; end; end
45
+ RUBY
46
+
47
+ counter.methods_in('Foo').should eq 1
48
+ counter.methods_in('Bar').should eq 1
49
+ end
50
+
51
+ it "recognizes qualified module names" do
52
+ expect_method_count 'Foo::Bar', 1, <<-RUBY
53
+ class Foo::Bar; def baz; end; end
54
+ RUBY
55
+ end
56
+
57
+ it "recognizes modules nested under other modules" do
58
+ expect_method_count 'Foo::Bar', 1, <<-RUBY
59
+ class Foo; module Bar; def baz; end; end; end
60
+ RUBY
61
+ end
62
+ end
63
+
64
+ describe "reporting" do
65
+ it "reports classes, violating the rule" do
66
+ analyzer = analyze 1, <<-RUBY
67
+ class Foo
68
+ def bar; end
69
+ def baz; end
70
+ end
71
+ RUBY
72
+
73
+ analyzer.violations.should include 'Foo has 2 methods: #bar, #baz'
74
+ end
75
+
76
+ it "reports under 'Number of methods per class'" do
77
+ MethodsPerClass.new(42).name.should eq 'Number of methods per class (42)'
78
+ end
79
+ end
80
+
81
+ def analyze(limit = nil, code)
82
+ MethodsPerClass.new(limit).apply_to nil, Ripper.sexp(code)
83
+ end
84
+
85
+ def expect_method_count(class_name, count, code)
86
+ analyze(code).methods_in(class_name).should eq count
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ module Skeptic
4
+ module Rules
5
+ describe NoSemicolons do
6
+ describe "detecting semicolons" do
7
+ it "complains if it finds a semicolon in the code" do
8
+ expect_complaint 'foo; bar'
9
+ expect_complaint 'this; that; other'
10
+ expect_complaint '"#{foo;bar}"'
11
+ end
12
+
13
+ it "does not complain for semicolons in literals" do
14
+ expect_fine_and_dandy '"foo;"'
15
+ expect_fine_and_dandy '";"'
16
+ expect_fine_and_dandy '/;/'
17
+ end
18
+
19
+ it "can tell the locations of the semicolons" do
20
+ analyze("foo;\n;bar").semicolon_locations.should =~ [[1, 3], [2, 0]]
21
+ end
22
+ end
23
+
24
+ describe "reporting" do
25
+ it "points out file locations with semicolons" do
26
+ analyzer = analyze 'foo; bar'
27
+
28
+ analyzer.violations.should include 'You have a semicolon at line 1, column 3'
29
+ end
30
+
31
+ it "reports under 'No semicolons'" do
32
+ NoSemicolons.new(true).name.should eq 'No semicolons as expression separators'
33
+ end
34
+ end
35
+
36
+ def expect_fine_and_dandy(code)
37
+ analyze(code).semicolon_locations.should be_empty
38
+ end
39
+
40
+ def expect_complaint(code)
41
+ analyze(code).semicolon_locations.should_not be_empty
42
+ end
43
+
44
+ def analyze(code)
45
+ NoSemicolons.new(true).apply_to Ripper.lex(code), nil
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ module Skeptic
4
+ describe Scope do
5
+ it "contains levels of nesting" do
6
+ Scope.new(nil, nil, [:for, :if]).levels.should eq [:for, :if]
7
+ Scope.new(nil, nil, []).levels.should eq []
8
+ Scope.new.levels.should eq []
9
+ end
10
+
11
+ it "can be compared to another scope" do
12
+ Scope.new(nil, nil, [:for, :if]).should eq Scope.new(nil, nil, [:for, :if])
13
+ Scope.new(nil, nil, []).should_not eq Scope.new(nil, nil, [:if])
14
+ Scope.new('Bar', nil).should_not eq Scope.new('Foo', nil)
15
+ Scope.new(nil, 'bar').should_not eq Scope.new(nil, 'foo')
16
+ end
17
+
18
+ it "can be extended and unextended" do
19
+ Scope.new.push(:if).should eq Scope.new(nil, nil, [:if])
20
+ Scope.new(nil, nil, [:for, :if]).pop.should eq Scope.new(nil, nil, [:for])
21
+
22
+ Scope.new.in_class('Foo').should eq Scope.new('Foo')
23
+ Scope.new.in_method('bar').should eq Scope.new(nil, 'bar')
24
+ end
25
+
26
+ it "can tell its depth" do
27
+ Scope.new(nil, nil, [:for]).depth.should eq 1
28
+ Scope.new(nil, nil, [:for, :if]).depth.should eq 2
29
+ Scope.new(nil, nil, [:for, :if, :if]).depth.should eq 3
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'skeptic'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end