skeptic 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +21 -0
- data/Rakefile +52 -0
- data/VERSION +1 -0
- data/bin/skeptic +42 -0
- data/features/skeptic.feature +76 -0
- data/features/step_definitions/skeptic_steps.rb +0 -0
- data/features/support/aruba.rb +1 -0
- data/features/support/env.rb +13 -0
- data/lib/skeptic/critic.rb +36 -0
- data/lib/skeptic/environment.rb +32 -0
- data/lib/skeptic/rules/lines_per_method.rb +59 -0
- data/lib/skeptic/rules/max_nesting_depth.rb +122 -0
- data/lib/skeptic/rules/methods_per_class.rb +69 -0
- data/lib/skeptic/rules/no_semicolons.rb +30 -0
- data/lib/skeptic/scope.rb +60 -0
- data/lib/skeptic/sexp_visitor.rb +65 -0
- data/lib/skeptic.rb +12 -0
- data/spec/skeptic/critic_spec.rb +66 -0
- data/spec/skeptic/environment_spec.rb +48 -0
- data/spec/skeptic/rules/lines_per_method_spec.rb +67 -0
- data/spec/skeptic/rules/max_nesting_depth_spec.rb +134 -0
- data/spec/skeptic/rules/methods_per_class_spec.rb +90 -0
- data/spec/skeptic/rules/no_semicolons_spec.rb +49 -0
- data/spec/skeptic/scope_spec.rb +32 -0
- data/spec/spec_helper.rb +12 -0
- metadata +158 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.9.2
|
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
gem 'trollop', '>= 1.16.2'
|
9
|
+
|
10
|
+
group :development do
|
11
|
+
gem "rspec"
|
12
|
+
gem "cucumber"
|
13
|
+
gem "aruba"
|
14
|
+
gem "bundler"
|
15
|
+
gem "jeweler"
|
16
|
+
gem "rcov"
|
17
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Stefan Kanev
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
= skeptic
|
2
|
+
|
3
|
+
An **very** experimental static Ruby 1.9 analyzer to point out annoying things in your code. You
|
4
|
+
should probably not use it for anything.
|
5
|
+
|
6
|
+
Writing it is fun, though.
|
7
|
+
|
8
|
+
== Contributing to skeptic
|
9
|
+
|
10
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
11
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
12
|
+
* Fork the project
|
13
|
+
* Start a feature/bugfix branch
|
14
|
+
* Commit and push until you are happy with your contribution
|
15
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
16
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
17
|
+
|
18
|
+
== Copyright
|
19
|
+
|
20
|
+
Copyright (c) 2011 Stefan Kanev. See LICENSE.txt for
|
21
|
+
further details.
|
data/Rakefile
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "skeptic"
|
18
|
+
gem.homepage = "http://github.com/skanev/skeptic"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Skeptic points out annoying things in your code}
|
21
|
+
gem.description = %Q{An experimental, half-assed, bug-ridden and highly opinionated code analyzer.}
|
22
|
+
gem.email = "stefan.kanev@gmail.com"
|
23
|
+
gem.authors = ["Stefan Kanev"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rspec/core'
|
29
|
+
require 'rspec/core/rake_task'
|
30
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
31
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
32
|
+
end
|
33
|
+
|
34
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
require 'cucumber/rake/task'
|
40
|
+
Cucumber::Rake::Task.new(:features)
|
41
|
+
|
42
|
+
task :default => :spec
|
43
|
+
|
44
|
+
require 'rake/rdoctask'
|
45
|
+
Rake::RDocTask.new do |rdoc|
|
46
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
47
|
+
|
48
|
+
rdoc.rdoc_dir = 'rdoc'
|
49
|
+
rdoc.title = "skeptic #{version}"
|
50
|
+
rdoc.rdoc_files.include('README*')
|
51
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
52
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
data/bin/skeptic
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
3
|
+
require 'skeptic'
|
4
|
+
require 'trollop'
|
5
|
+
|
6
|
+
opts = Trollop::options do
|
7
|
+
opt :lines_per_method, 'Maximum number of lines per method', type: :int
|
8
|
+
opt :max_nesting_depth, 'Maximum nesting depth', type: :int
|
9
|
+
opt :methods_per_class, 'Maximum number of methods per class', type: :int
|
10
|
+
opt :no_semicolons, 'Complain about semicolons', type: :boolean
|
11
|
+
end
|
12
|
+
code = File.read ARGV[0]
|
13
|
+
|
14
|
+
critic = Skeptic::Critic.new
|
15
|
+
|
16
|
+
critic.no_semicolons = opts[:no_semicolons]
|
17
|
+
critic.max_nesting_depth = opts[:max_nesting_depth]
|
18
|
+
critic.methods_per_class = opts[:methods_per_class]
|
19
|
+
critic.lines_per_method = opts[:lines_per_method]
|
20
|
+
|
21
|
+
critic.criticize code
|
22
|
+
|
23
|
+
if critic.criticism.empty?
|
24
|
+
puts 'OK'
|
25
|
+
exit(0)
|
26
|
+
else
|
27
|
+
messages = Hash.new { |hash, key| hash[key] = [] }
|
28
|
+
|
29
|
+
critic.criticism.each do |message, type|
|
30
|
+
messages[type] << message
|
31
|
+
end
|
32
|
+
|
33
|
+
messages.each do |type, messages|
|
34
|
+
puts type
|
35
|
+
messages.each do |message|
|
36
|
+
puts "* #{message}"
|
37
|
+
end
|
38
|
+
puts ""
|
39
|
+
end
|
40
|
+
|
41
|
+
exit(1)
|
42
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
Feature: Running skeptic
|
2
|
+
Scenario: Nothing to complain about
|
3
|
+
Given a file named "input.rb" with:
|
4
|
+
"""
|
5
|
+
foo
|
6
|
+
"""
|
7
|
+
When I run `skeptic input.rb`
|
8
|
+
Then it should pass with:
|
9
|
+
"""
|
10
|
+
OK
|
11
|
+
"""
|
12
|
+
|
13
|
+
Scenario: Banishing semicolons
|
14
|
+
Given a file named "input.rb" with:
|
15
|
+
"""
|
16
|
+
foo; bar
|
17
|
+
"""
|
18
|
+
When I run `skeptic --no-semicolons input.rb`
|
19
|
+
Then it should fail with:
|
20
|
+
"""
|
21
|
+
No semicolons as expression separators
|
22
|
+
* You have a semicolon at line 1, column 3
|
23
|
+
"""
|
24
|
+
|
25
|
+
Scenario: Limiting method length
|
26
|
+
Given a file named "input.rb" with:
|
27
|
+
"""
|
28
|
+
class Foo
|
29
|
+
def bar
|
30
|
+
one
|
31
|
+
two
|
32
|
+
three
|
33
|
+
end
|
34
|
+
end
|
35
|
+
"""
|
36
|
+
When I run `skeptic --lines-per-method 2 input.rb`
|
37
|
+
Then it should fail with:
|
38
|
+
"""
|
39
|
+
Number of lines per method (2)
|
40
|
+
* Foo#bar is 3 lines long
|
41
|
+
"""
|
42
|
+
|
43
|
+
Scenario: Limiting depth of nesting
|
44
|
+
Given a file named "input.rb" with:
|
45
|
+
"""
|
46
|
+
class Foo
|
47
|
+
def bar
|
48
|
+
while true
|
49
|
+
if false
|
50
|
+
really?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
"""
|
56
|
+
When I run `skeptic --max-nesting-depth 1 input.rb`
|
57
|
+
Then it should fail with:
|
58
|
+
"""
|
59
|
+
Maximum nesting depth (1)
|
60
|
+
* Foo#bar has 2 levels of nesting: while > if
|
61
|
+
"""
|
62
|
+
|
63
|
+
Scenario: Limiting number of methods per class
|
64
|
+
Given a file named "input.rb" with:
|
65
|
+
"""
|
66
|
+
class Foo
|
67
|
+
def bar; end
|
68
|
+
def baz; end
|
69
|
+
end
|
70
|
+
"""
|
71
|
+
When I run `skeptic --methods-per-class 1 input.rb`
|
72
|
+
Then it should fail with:
|
73
|
+
"""
|
74
|
+
Number of methods per class (1)
|
75
|
+
* Foo has 2 methods: #bar, #baz
|
76
|
+
"""
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'aruba/cucumber'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
begin
|
3
|
+
Bundler.setup(:default, :development)
|
4
|
+
rescue Bundler::BundlerError => e
|
5
|
+
$stderr.puts e.message
|
6
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
7
|
+
exit e.status_code
|
8
|
+
end
|
9
|
+
|
10
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
|
11
|
+
require 'skeptic'
|
12
|
+
|
13
|
+
require 'rspec/expectations'
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Skeptic
|
2
|
+
class Critic
|
3
|
+
attr_accessor :lines_per_method
|
4
|
+
attr_accessor :max_nesting_depth
|
5
|
+
attr_accessor :methods_per_class
|
6
|
+
attr_accessor :no_semicolons
|
7
|
+
|
8
|
+
attr_reader :criticism
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@criticism = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def criticize(code)
|
15
|
+
@code = code
|
16
|
+
@tokens = Ripper.lex(code)
|
17
|
+
@sexp = Ripper.sexp(code)
|
18
|
+
|
19
|
+
rules = {
|
20
|
+
Rules::LinesPerMethod => lines_per_method,
|
21
|
+
Rules::MaxNestingDepth => max_nesting_depth,
|
22
|
+
Rules::MethodsPerClass => methods_per_class,
|
23
|
+
Rules::NoSemicolons => no_semicolons,
|
24
|
+
}
|
25
|
+
|
26
|
+
rules.reject { |rule_type, option| option.nil? }.each do |rule_type, option|
|
27
|
+
rule = rule_type.new(option)
|
28
|
+
rule.apply_to @tokens, @sexp
|
29
|
+
|
30
|
+
rule.violations.each do |violation|
|
31
|
+
@criticism << [violation, rule.name]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Skeptic
|
2
|
+
class Environment
|
3
|
+
def initialize
|
4
|
+
@stack = [{}]
|
5
|
+
end
|
6
|
+
|
7
|
+
def []=(name, value)
|
8
|
+
@stack.last[name] = value
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](name)
|
12
|
+
closure = @stack.reverse.detect { |closure| closure.has_key? name }
|
13
|
+
closure[name] if closure
|
14
|
+
end
|
15
|
+
|
16
|
+
def push(closure = {})
|
17
|
+
@stack.push closure
|
18
|
+
end
|
19
|
+
|
20
|
+
def pop
|
21
|
+
raise "You went too far unextending env" if @stack.empty?
|
22
|
+
@stack.pop
|
23
|
+
end
|
24
|
+
|
25
|
+
def scoped(closure = {})
|
26
|
+
push closure
|
27
|
+
yield
|
28
|
+
ensure
|
29
|
+
pop
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Skeptic
|
2
|
+
module Rules
|
3
|
+
class LinesPerMethod
|
4
|
+
include SexpVisitor
|
5
|
+
|
6
|
+
def initialize(limit = nil)
|
7
|
+
env[:line_numbers] = []
|
8
|
+
@line_counts = {}
|
9
|
+
@limit = limit
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply_to(tokens, sexp)
|
13
|
+
visit sexp
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def size_of(qualified_method_name)
|
18
|
+
@line_counts[qualified_method_name]
|
19
|
+
end
|
20
|
+
|
21
|
+
def violations
|
22
|
+
@line_counts.select { |name, lines| lines > @limit }.map do |name, lines|
|
23
|
+
"#{name} is #{lines} lines long"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def name
|
28
|
+
"Number of lines per method (#@limit)"
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
on :class do |name, parent, body|
|
34
|
+
class_name = [env[:class], extract_name(name)].compact.join('::')
|
35
|
+
|
36
|
+
env.scoped :class => class_name do
|
37
|
+
visit body
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
on :def do |name, params, body|
|
42
|
+
method_name = extract_name(name)
|
43
|
+
|
44
|
+
env.scoped method: method_name, line_numbers: [] do
|
45
|
+
visit body
|
46
|
+
|
47
|
+
lines = env[:line_numbers].uniq.compact.length
|
48
|
+
|
49
|
+
full_name = "#{env[:class]}##{env[:method]}"
|
50
|
+
@line_counts[full_name] = lines + @line_counts.fetch(full_name, 0)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
on :@ident, :@const, :@gvar, :@ivar, :@cvar, :@int, :@float, :@tstring_content, :@kw do |text, location|
|
55
|
+
env[:line_numbers] << location.first
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Skeptic
|
2
|
+
module Rules
|
3
|
+
class MaxNestingDepth
|
4
|
+
include SexpVisitor
|
5
|
+
|
6
|
+
def initialize(limit = nil)
|
7
|
+
env[:scope] = Scope.new
|
8
|
+
@scopes = []
|
9
|
+
@limit = limit
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply_to(tokens, sexp)
|
13
|
+
visit sexp
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def nestings
|
18
|
+
@scopes.uniq
|
19
|
+
end
|
20
|
+
|
21
|
+
def violations
|
22
|
+
@scopes.select { |scope| scope.depth > @limit }.map do |scope|
|
23
|
+
"#{scope.location} has #{scope.depth} levels of nesting: #{scope.levels.join(' > ')}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def name
|
28
|
+
"Maximum nesting depth (#@limit)"
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def with(scope)
|
34
|
+
@scopes << scope
|
35
|
+
|
36
|
+
env.scoped scope: scope do
|
37
|
+
yield
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def scope
|
42
|
+
env[:scope]
|
43
|
+
end
|
44
|
+
|
45
|
+
on :class do |name, parent, body|
|
46
|
+
visit name
|
47
|
+
visit parent if parent
|
48
|
+
|
49
|
+
with scope.in_class(extract_name(name)) do
|
50
|
+
visit body
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
on :if, :if_mod, :unless, :unless_mod do |condition, body, alternative|
|
55
|
+
key = sexp_type.to_s.gsub(/_mod$/, '').to_sym
|
56
|
+
|
57
|
+
visit condition
|
58
|
+
|
59
|
+
with scope.push(key) do
|
60
|
+
visit body
|
61
|
+
visit alternative if alternative
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
on :while, :while_mod, :until, :until_mod do |condition, body|
|
66
|
+
key = sexp_type.to_s.gsub(/_mod$/, '').to_sym
|
67
|
+
|
68
|
+
with scope.push(key) do
|
69
|
+
visit condition
|
70
|
+
visit body
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
on :method_add_block do |invocation, block|
|
75
|
+
visit invocation
|
76
|
+
|
77
|
+
with scope.push(:iter) do
|
78
|
+
visit block
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
on :lambda do |params, body|
|
83
|
+
with scope.push(:lambda) do
|
84
|
+
visit params
|
85
|
+
visit body
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
on :for do |params, iterable, body|
|
90
|
+
visit params
|
91
|
+
visit iterable
|
92
|
+
|
93
|
+
with scope.push(:for) do
|
94
|
+
visit body
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
on :case do |testable, alternatives|
|
99
|
+
visit testable
|
100
|
+
|
101
|
+
with scope.push(:case) do
|
102
|
+
visit alternatives
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
on :begin do |body|
|
107
|
+
with scope.push(:begin) do
|
108
|
+
visit body
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
on :def do |name, params, body|
|
113
|
+
visit name
|
114
|
+
visit params
|
115
|
+
|
116
|
+
with scope.in_method(extract_name(name)) do
|
117
|
+
visit body
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Skeptic
|
2
|
+
module Rules
|
3
|
+
class MethodsPerClass
|
4
|
+
include SexpVisitor
|
5
|
+
|
6
|
+
def initialize(limit)
|
7
|
+
@methods = Hash.new { |hash, key| hash[key] = [] }
|
8
|
+
@limit = limit
|
9
|
+
end
|
10
|
+
|
11
|
+
def apply_to(tokens, tree)
|
12
|
+
visit tree
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def methods_in(class_name)
|
17
|
+
@methods[class_name].length
|
18
|
+
end
|
19
|
+
|
20
|
+
def violations
|
21
|
+
violators = @methods.keys.select { |name| @methods[name].length > @limit }
|
22
|
+
|
23
|
+
violators.map do |class_name|
|
24
|
+
method_names = @methods[class_name].map { |name| "##{name}" }
|
25
|
+
count = method_names.length
|
26
|
+
|
27
|
+
"#{class_name} has #{count} methods: #{method_names.join(', ')}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def name
|
32
|
+
"Number of methods per class (#@limit)"
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
on :def do |name, params, body|
|
38
|
+
method_name = extract_name(name)
|
39
|
+
class_name = env[:class]
|
40
|
+
|
41
|
+
@methods[class_name] << method_name
|
42
|
+
|
43
|
+
visit params
|
44
|
+
visit body
|
45
|
+
end
|
46
|
+
|
47
|
+
on :class do |name, parents, body|
|
48
|
+
env.push :class => qualified_class_name(name)
|
49
|
+
|
50
|
+
visit parents if parents
|
51
|
+
visit body
|
52
|
+
|
53
|
+
env.pop
|
54
|
+
end
|
55
|
+
|
56
|
+
on :module do |name, body|
|
57
|
+
env.push :class => qualified_class_name(name)
|
58
|
+
|
59
|
+
visit body
|
60
|
+
|
61
|
+
env.pop
|
62
|
+
end
|
63
|
+
|
64
|
+
def qualified_class_name(name)
|
65
|
+
[env[:class], extract_name(name)].compact.join('::')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Skeptic
|
2
|
+
module Rules
|
3
|
+
class NoSemicolons
|
4
|
+
def initialize(enabled = false)
|
5
|
+
@enabled = enabled
|
6
|
+
end
|
7
|
+
|
8
|
+
def apply_to(tokens, sexp)
|
9
|
+
@locations = tokens.
|
10
|
+
select { |location, type, token| token == ';' and type == :on_semicolon }.
|
11
|
+
map { |location, type, token| location }
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def violations
|
16
|
+
@locations.map do |line, column|
|
17
|
+
"You have a semicolon at line #{line}, column #{column}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def name
|
22
|
+
'No semicolons as expression separators'
|
23
|
+
end
|
24
|
+
|
25
|
+
def semicolon_locations
|
26
|
+
@locations
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Skeptic
|
2
|
+
class Scope
|
3
|
+
attr_accessor :class_name, :method_name, :levels
|
4
|
+
|
5
|
+
def initialize(class_name = nil, method_name = nil, levels = [])
|
6
|
+
@class_name = class_name
|
7
|
+
@method_name = method_name
|
8
|
+
@levels = levels
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
other.kind_of?(Scope) and
|
13
|
+
self.class_name == other.class_name and
|
14
|
+
self.method_name == other.method_name and
|
15
|
+
self.levels == other.levels
|
16
|
+
end
|
17
|
+
|
18
|
+
def depth
|
19
|
+
levels.length
|
20
|
+
end
|
21
|
+
|
22
|
+
def push(level)
|
23
|
+
copy { |n| n.levels.push level }
|
24
|
+
end
|
25
|
+
|
26
|
+
def pop
|
27
|
+
copy { |n| n.levels.pop }
|
28
|
+
end
|
29
|
+
|
30
|
+
def in_class(class_name)
|
31
|
+
copy { |n| n.class_name = class_name }
|
32
|
+
end
|
33
|
+
|
34
|
+
def in_method(method_name)
|
35
|
+
copy { |n| n.method_name = method_name }
|
36
|
+
end
|
37
|
+
|
38
|
+
def location
|
39
|
+
if class_name and method_name then "#{class_name}##{method_name}"
|
40
|
+
elsif class_name then "#{class_name}#[body]"
|
41
|
+
elsif method_name then "Object##{method_name}"
|
42
|
+
else "[top-level]"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
"#{location} #{@levels.join(':')}".strip
|
48
|
+
end
|
49
|
+
|
50
|
+
def inspect
|
51
|
+
"#<Scope: #{to_s}>"
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def copy(&block)
|
57
|
+
Scope.new(class_name, method_name, levels.dup).tap(&block)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|