pelusa 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rvmrc +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/Rakefile +10 -0
- data/Readme.md +74 -0
- data/bin/pelusa +7 -0
- data/lib/pelusa.rb +22 -0
- data/lib/pelusa/analysis.rb +88 -0
- data/lib/pelusa/analyzer.rb +48 -0
- data/lib/pelusa/class_analyzer.rb +28 -0
- data/lib/pelusa/cli.rb +27 -0
- data/lib/pelusa/iterator.rb +39 -0
- data/lib/pelusa/lint.rb +29 -0
- data/lib/pelusa/lint/collection_wrappers.rb +50 -0
- data/lib/pelusa/lint/demeter_law.rb +35 -0
- data/lib/pelusa/lint/else_clauses.rb +40 -0
- data/lib/pelusa/lint/indentation_level.rb +94 -0
- data/lib/pelusa/lint/instance_variables.rb +39 -0
- data/lib/pelusa/lint/line_restriction.rb +41 -0
- data/lib/pelusa/lint/properties.rb +37 -0
- data/lib/pelusa/lint/short_identifiers.rb +54 -0
- data/lib/pelusa/report.rb +27 -0
- data/lib/pelusa/reporters/reporter.rb +22 -0
- data/lib/pelusa/reporters/ruby_reporter.rb +29 -0
- data/lib/pelusa/reporters/stdout_reporter.rb +50 -0
- data/lib/pelusa/runner.rb +53 -0
- data/lib/pelusa/version.rb +3 -0
- data/pelusa.gemspec +22 -0
- data/test/pelusa/analysis_test.rb +38 -0
- data/test/pelusa/analyzer_test.rb +33 -0
- data/test/pelusa/class_analyzer_test.rb +26 -0
- data/test/pelusa/iterator_test.rb +28 -0
- data/test/pelusa/lint/collection_wrappers_test.rb +42 -0
- data/test/pelusa/lint/demeter_law_test.rb +42 -0
- data/test/pelusa/lint/else_clauses_test.rb +50 -0
- data/test/pelusa/lint/indentation_level_test.rb +47 -0
- data/test/pelusa/lint/instance_variables_test.rb +44 -0
- data/test/pelusa/lint/line_restriction_test.rb +40 -0
- data/test/pelusa/lint/properties_test.rb +44 -0
- data/test/pelusa/lint/short_identifiers_test.rb +41 -0
- data/test/pelusa/reporters/ruby_reporter_test.rb +42 -0
- data/test/pelusa/runner_test.rb +27 -0
- data/test/pelusa_test.rb +19 -0
- data/test/test_helper.rb +5 -0
- metadata +118 -0
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use rbx-head@pelusa
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
data/Readme.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# pelusa - a Ruby Lint to improve your OO skills [![Build Status](https://secure.travis-ci.org/codegram/pelusa.png)](http://travis-ci.org/codegram/pelusa)
|
2
|
+
|
3
|
+
Pelusa is a static analysis tool and framework to inspect your code style and
|
4
|
+
notify you about possible red flags or missing best practices.
|
5
|
+
|
6
|
+
Above all pelusa _doesn't run your code_ -- it just analyzes it syntactically
|
7
|
+
to gain superficial insights about it, and raise red flags when needed.
|
8
|
+
|
9
|
+
Pelusa needs [Rubinius](http://rubini.us) to run, due to how easy it is to work
|
10
|
+
with a Ruby AST with it, but it doesn't mean that your Ruby code must run on
|
11
|
+
Rubinius. Since it's a static analysis tool, pelusa doesn't care what your code
|
12
|
+
runs on, it just looks at it and tells you stuff.
|
13
|
+
|
14
|
+
Here's a sample of pelusa linting on its own code base:
|
15
|
+
|
16
|
+
![](http://f.cl.ly/items/3Z341M0q2u1K242m0144/%D0%A1%D0%BD%D0%B8%D0%BC%D0%BE%D0%BA%20%D1%8D%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202012-02-14%20%D0%B2%203.29.38%20PM.png)
|
17
|
+
|
18
|
+
## Install and usage
|
19
|
+
|
20
|
+
rvm use rbx-head
|
21
|
+
gem install pelusa
|
22
|
+
|
23
|
+
Then go to a directory where you have some Ruby code, and type this:
|
24
|
+
|
25
|
+
pelusa path/to/some_file.rb
|
26
|
+
|
27
|
+
Or just run all the Ruby files (`**/*.rb`) without arguments:
|
28
|
+
|
29
|
+
pelusa
|
30
|
+
|
31
|
+
## About the default set of Lints
|
32
|
+
|
33
|
+
This project was born as an inspiration from [one of our Monday
|
34
|
+
Talks](http://talks.codegram.com/object-oriented-nirvana) about Object Oriented
|
35
|
+
Nirvana by @oriolgual. After reading [this blog
|
36
|
+
post](http://binstock.blogspot.com/2008/04/perfecting-oos-small-classes-and-short.html)
|
37
|
+
he prepared his talk and I (Txus) found it interesting, so I explored the
|
38
|
+
possibility of programmatically linting these practices on a Ruby project. This
|
39
|
+
*doesn't mean* that any of us thinks these are the true and only practices of
|
40
|
+
Object Orientation, it's just a set of constraints that are fun to follow to
|
41
|
+
achieve a mindset shift in the long run.
|
42
|
+
|
43
|
+
Anyway, you are always free to implement your own lints, or the ones that suit
|
44
|
+
your team the best.
|
45
|
+
|
46
|
+
## Pelusa as a static analysis framework
|
47
|
+
|
48
|
+
With Pelusa, writing your own lints becomes very easy. Check out some of the
|
49
|
+
default lints under the `lib/pelusa/lint/` directory.
|
50
|
+
|
51
|
+
At some point it will be user-extendable by default, but for now you are better
|
52
|
+
off forking the project and adding your own lints as you need them in your team
|
53
|
+
(or removing some default ones you don't like).
|
54
|
+
|
55
|
+
## Contributing
|
56
|
+
|
57
|
+
You can easily contribute to Pelusa. Its codebase is simple and
|
58
|
+
[extensively documented][documentation].
|
59
|
+
|
60
|
+
* Fork the project.
|
61
|
+
* Make your feature addition or bug fix.
|
62
|
+
* Add specs for it. This is important so we don't break it in a future
|
63
|
+
version unintentionally.
|
64
|
+
* Commit, do not mess with rakefile, version, or history.
|
65
|
+
If you want to have your own version, that is fine but bump version
|
66
|
+
in a commit by itself I can ignore when I pull.
|
67
|
+
* Send me a pull request. Bonus points for topic branches.
|
68
|
+
|
69
|
+
[documentation]: http://rubydoc.info/github/codegram/pelusa/master/frames
|
70
|
+
|
71
|
+
## License
|
72
|
+
|
73
|
+
MIT License. Copyright 2011 [Codegram Technologies](http://codegram.com)
|
74
|
+
|
data/bin/pelusa
ADDED
data/lib/pelusa.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Pelusa
|
2
|
+
# Public: Runs the runner on a set of files.
|
3
|
+
#
|
4
|
+
# Returns an Array of results of a given Reporter
|
5
|
+
def self.run(files=[], reporter=StdoutReporter, lints=Lint.all)
|
6
|
+
runner = Runner.new(lints, reporter)
|
7
|
+
runner.run(files)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'pelusa/cli'
|
12
|
+
require 'pelusa/runner'
|
13
|
+
require 'pelusa/analyzer'
|
14
|
+
require 'pelusa/lint'
|
15
|
+
require 'pelusa/analysis'
|
16
|
+
require 'pelusa/class_analyzer'
|
17
|
+
require 'pelusa/report'
|
18
|
+
require 'pelusa/iterator'
|
19
|
+
|
20
|
+
require 'pelusa/reporters/reporter'
|
21
|
+
require 'pelusa/reporters/stdout_reporter'
|
22
|
+
require 'pelusa/reporters/ruby_reporter'
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Pelusa
|
2
|
+
# Public: An Analysis the result of applying a Lint check to a class.
|
3
|
+
#
|
4
|
+
# Examples
|
5
|
+
#
|
6
|
+
# analysis = SuccessfulAnalysis.new("Is below 50 lines")
|
7
|
+
# analysis.successful?
|
8
|
+
# # => true
|
9
|
+
#
|
10
|
+
# failure = FailedAnalysis.new("Is below 50 lines", 123) do |lines|
|
11
|
+
# "There are too many lines (#{lines})"
|
12
|
+
# end
|
13
|
+
# failure.successful?
|
14
|
+
# # => false
|
15
|
+
# failure.message
|
16
|
+
# # => "There are too many lines (123)"
|
17
|
+
#
|
18
|
+
class Analysis
|
19
|
+
def initialize(name)
|
20
|
+
@name = name
|
21
|
+
end
|
22
|
+
|
23
|
+
def name
|
24
|
+
@name
|
25
|
+
end
|
26
|
+
|
27
|
+
def successful?
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
|
31
|
+
def failed?
|
32
|
+
not successful?
|
33
|
+
end
|
34
|
+
|
35
|
+
def message
|
36
|
+
""
|
37
|
+
end
|
38
|
+
|
39
|
+
def status
|
40
|
+
successful? ? "successful" : "failed"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Public: A SuccessfulAnalysis is an analysis that has passed a particular
|
45
|
+
# lint check.
|
46
|
+
#
|
47
|
+
class SuccessfulAnalysis < Analysis
|
48
|
+
# Public: A successful analysis is always successful, obviously.
|
49
|
+
#
|
50
|
+
# Returns true.
|
51
|
+
def successful?
|
52
|
+
true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public: A FailedAnalysis is an analysis that has failed a particular
|
57
|
+
# lint check.
|
58
|
+
#
|
59
|
+
class FailedAnalysis < Analysis
|
60
|
+
# Public: Initializes a new FailedAnalysis.
|
61
|
+
#
|
62
|
+
# name - The name of the lint check.
|
63
|
+
# payload - An object to use in the message to aid the user in fixing the
|
64
|
+
# problem.
|
65
|
+
# block - A block to generate the message. It must yield the payload
|
66
|
+
# object.
|
67
|
+
def initialize(name, payload, &block)
|
68
|
+
super(name)
|
69
|
+
@payload = payload
|
70
|
+
@block = block
|
71
|
+
end
|
72
|
+
|
73
|
+
# Public: A failed analysis is never successful , obviously.
|
74
|
+
#
|
75
|
+
# Returns false.
|
76
|
+
def successful?
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
# Public: Generates an explicative message yielding the payload object to
|
81
|
+
# the block, so that the user gets some hint to fix the problem.
|
82
|
+
#
|
83
|
+
# Returns the String message.
|
84
|
+
def message
|
85
|
+
@block.call(@payload)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Pelusa
|
2
|
+
class Analyzer
|
3
|
+
# Public: Initializes an Analyzer.
|
4
|
+
#
|
5
|
+
# ast - The abstract syntax tree to analyze.
|
6
|
+
# reporter - The class that will be used to create the report.
|
7
|
+
# filename - The name of the file that we're analyzing.
|
8
|
+
def initialize(lints, reporter, filename)
|
9
|
+
@lints = lints
|
10
|
+
@reporter = reporter.new(filename)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Public: Makes a report out of several classes contained in the AST.
|
14
|
+
#
|
15
|
+
# ast - The abstract syntax tree to analyze.
|
16
|
+
#
|
17
|
+
# Returns a Report of all the classes.
|
18
|
+
def analyze(ast)
|
19
|
+
reports = extract_classes(ast).map do |klass|
|
20
|
+
class_analyzer = ClassAnalyzer.new(klass)
|
21
|
+
class_name = class_analyzer.class_name
|
22
|
+
analysis = class_analyzer.analyze(@lints)
|
23
|
+
|
24
|
+
Report.new(class_name, analysis)
|
25
|
+
end
|
26
|
+
@reporter.reports = reports
|
27
|
+
@reporter
|
28
|
+
end
|
29
|
+
|
30
|
+
#######
|
31
|
+
private
|
32
|
+
#######
|
33
|
+
|
34
|
+
# Internal: Extracts the classes out of the AST and returns their nodes.
|
35
|
+
#
|
36
|
+
# ast - The abstract syntax tree to extract the classes from.
|
37
|
+
#
|
38
|
+
# Returns an Array of Class nodes.
|
39
|
+
def extract_classes(ast)
|
40
|
+
classes = []
|
41
|
+
class_iterator = Iterator.new do |node|
|
42
|
+
classes << node if node.is_a?(Rubinius::AST::Class)
|
43
|
+
end
|
44
|
+
Array(ast).each(&class_iterator)
|
45
|
+
classes
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Pelusa::ClassAnalyzer
|
2
|
+
# Public: Initializes a ClassAnalyzer.
|
3
|
+
#
|
4
|
+
# klass - The class AST node.
|
5
|
+
def initialize(klass)
|
6
|
+
@klass = klass
|
7
|
+
end
|
8
|
+
|
9
|
+
# Public: Returns the name of the Class being analyzed.
|
10
|
+
#
|
11
|
+
# Returns the String name.
|
12
|
+
def class_name
|
13
|
+
name = @klass.name
|
14
|
+
name.name
|
15
|
+
end
|
16
|
+
|
17
|
+
# Public: Analyzes a class with a series of lints.
|
18
|
+
#
|
19
|
+
# lints - The lints to check for.
|
20
|
+
#
|
21
|
+
# Returns a collection of Analysis, one for each lint.
|
22
|
+
def analyze(lints)
|
23
|
+
lints.map do |lint_class|
|
24
|
+
lint = lint_class.new
|
25
|
+
lint.check(@klass)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/pelusa/cli.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Pelusa
|
2
|
+
# The cli is a class responsible of handling all the command line interface
|
3
|
+
# logic.
|
4
|
+
#
|
5
|
+
class Cli
|
6
|
+
def initialize(args=ARGV)
|
7
|
+
@args = args
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
_files = files
|
12
|
+
if _files.empty?
|
13
|
+
warn "\n No files specified -- PROCESS ALL THE FILES!\n"
|
14
|
+
_files = Dir["**/*.rb"]
|
15
|
+
end
|
16
|
+
|
17
|
+
Pelusa.run(_files)
|
18
|
+
end
|
19
|
+
|
20
|
+
def files
|
21
|
+
if glob = @args.detect { |arg| arg =~ /\*/ }
|
22
|
+
return Dir[glob]
|
23
|
+
end
|
24
|
+
@args.select { |arg| arg =~ /\.rb/ }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Pelusa
|
2
|
+
class Iterator
|
3
|
+
NodeIterator = lambda do |node, check|
|
4
|
+
check.call(node)
|
5
|
+
|
6
|
+
if node.respond_to?(:each)
|
7
|
+
return node.each { |node| NodeIterator.call(node, check) }
|
8
|
+
end
|
9
|
+
|
10
|
+
ivars = node.instance_variables
|
11
|
+
children = ivars.map { |ivar| node.instance_variable_get(ivar) }
|
12
|
+
|
13
|
+
return children.each { |node| NodeIterator.call(node, check) }
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: Initializes a new Iterator with a particular lint check.
|
17
|
+
#
|
18
|
+
# lint - The lint block that yields a node to assert for particular
|
19
|
+
# conditions in that node.
|
20
|
+
def initialize(&lint)
|
21
|
+
@iterator = lambda { |node| NodeIterator.call(node, lint) }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Public: Calls the iterator with the given arguments.
|
25
|
+
#
|
26
|
+
# node - The root node from which to iterate.
|
27
|
+
def call(node)
|
28
|
+
@iterator.call(node)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Public: Returns the iterator. Useful when using symbol to proc
|
32
|
+
# conversions, such as &iterator.
|
33
|
+
#
|
34
|
+
# Returns the Proc iterator.
|
35
|
+
def to_proc
|
36
|
+
@iterator
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/pelusa/lint.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'pelusa/lint/line_restriction'
|
3
|
+
require 'pelusa/lint/instance_variables'
|
4
|
+
require 'pelusa/lint/demeter_law'
|
5
|
+
require 'pelusa/lint/indentation_level'
|
6
|
+
require 'pelusa/lint/else_clauses'
|
7
|
+
require 'pelusa/lint/properties'
|
8
|
+
require 'pelusa/lint/collection_wrappers'
|
9
|
+
require 'pelusa/lint/short_identifiers'
|
10
|
+
|
11
|
+
module Pelusa
|
12
|
+
# Public: A Lint is a quality standard, applicable on a given piece of code to
|
13
|
+
# check its compliance.
|
14
|
+
#
|
15
|
+
module Lint
|
16
|
+
def self.all
|
17
|
+
[
|
18
|
+
LineRestriction,
|
19
|
+
InstanceVariables,
|
20
|
+
DemeterLaw,
|
21
|
+
IndentationLevel,
|
22
|
+
ElseClauses,
|
23
|
+
Properties,
|
24
|
+
CollectionWrappers,
|
25
|
+
ShortIdentifiers,
|
26
|
+
]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Pelusa
|
2
|
+
module Lint
|
3
|
+
class CollectionWrappers
|
4
|
+
def initialize
|
5
|
+
@violations = Set.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def check(klass)
|
9
|
+
initialize
|
10
|
+
iterate_lines!(klass)
|
11
|
+
|
12
|
+
return SuccessfulAnalysis.new(name) if @violations.empty?
|
13
|
+
|
14
|
+
FailedAnalysis.new(name, @violations) do |violations|
|
15
|
+
"Using an instance variable apart from the array ivar in lines #{violations.to_a.join(', ')}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def name
|
22
|
+
"Doesn't mix array instance variables with others"
|
23
|
+
end
|
24
|
+
|
25
|
+
def iterate_lines!(klass)
|
26
|
+
array_assignment = nil
|
27
|
+
|
28
|
+
get_array_assignment = Iterator.new do |node|
|
29
|
+
if node.is_a?(Rubinius::AST::InstanceVariableAssignment) &&
|
30
|
+
(node.value.is_a?(Rubinius::AST::ArrayLiteral) ||
|
31
|
+
node.value.is_a?(Rubinius::AST::EmptyArray))
|
32
|
+
array_assignment = node
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
iterator = Iterator.new do |node|
|
37
|
+
next if node == array_assignment || !array_assignment
|
38
|
+
|
39
|
+
if node.is_a?(Rubinius::AST::InstanceVariableAssignment)
|
40
|
+
@violations << node.line
|
41
|
+
elsif node.is_a?(Rubinius::AST::InstanceVariableAccess) && node.name != array_assignment.name
|
42
|
+
@violations << node.line
|
43
|
+
end
|
44
|
+
end
|
45
|
+
Array(klass).each(&get_array_assignment)
|
46
|
+
Array(klass).each(&iterator)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|