pelusa 0.0.1
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/.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 [](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
|
+

|
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
|