skeptic 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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