pelusa 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.gitignore +5 -0
  2. data/.rvmrc +1 -0
  3. data/.travis.yml +4 -0
  4. data/Gemfile +4 -0
  5. data/Rakefile +10 -0
  6. data/Readme.md +74 -0
  7. data/bin/pelusa +7 -0
  8. data/lib/pelusa.rb +22 -0
  9. data/lib/pelusa/analysis.rb +88 -0
  10. data/lib/pelusa/analyzer.rb +48 -0
  11. data/lib/pelusa/class_analyzer.rb +28 -0
  12. data/lib/pelusa/cli.rb +27 -0
  13. data/lib/pelusa/iterator.rb +39 -0
  14. data/lib/pelusa/lint.rb +29 -0
  15. data/lib/pelusa/lint/collection_wrappers.rb +50 -0
  16. data/lib/pelusa/lint/demeter_law.rb +35 -0
  17. data/lib/pelusa/lint/else_clauses.rb +40 -0
  18. data/lib/pelusa/lint/indentation_level.rb +94 -0
  19. data/lib/pelusa/lint/instance_variables.rb +39 -0
  20. data/lib/pelusa/lint/line_restriction.rb +41 -0
  21. data/lib/pelusa/lint/properties.rb +37 -0
  22. data/lib/pelusa/lint/short_identifiers.rb +54 -0
  23. data/lib/pelusa/report.rb +27 -0
  24. data/lib/pelusa/reporters/reporter.rb +22 -0
  25. data/lib/pelusa/reporters/ruby_reporter.rb +29 -0
  26. data/lib/pelusa/reporters/stdout_reporter.rb +50 -0
  27. data/lib/pelusa/runner.rb +53 -0
  28. data/lib/pelusa/version.rb +3 -0
  29. data/pelusa.gemspec +22 -0
  30. data/test/pelusa/analysis_test.rb +38 -0
  31. data/test/pelusa/analyzer_test.rb +33 -0
  32. data/test/pelusa/class_analyzer_test.rb +26 -0
  33. data/test/pelusa/iterator_test.rb +28 -0
  34. data/test/pelusa/lint/collection_wrappers_test.rb +42 -0
  35. data/test/pelusa/lint/demeter_law_test.rb +42 -0
  36. data/test/pelusa/lint/else_clauses_test.rb +50 -0
  37. data/test/pelusa/lint/indentation_level_test.rb +47 -0
  38. data/test/pelusa/lint/instance_variables_test.rb +44 -0
  39. data/test/pelusa/lint/line_restriction_test.rb +40 -0
  40. data/test/pelusa/lint/properties_test.rb +44 -0
  41. data/test/pelusa/lint/short_identifiers_test.rb +41 -0
  42. data/test/pelusa/reporters/ruby_reporter_test.rb +42 -0
  43. data/test/pelusa/runner_test.rb +27 -0
  44. data/test/pelusa_test.rb +19 -0
  45. data/test/test_helper.rb +5 -0
  46. metadata +118 -0
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .rbx
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use rbx-head@pelusa
@@ -0,0 +1,4 @@
1
+ env:
2
+ - RBXOPT=-X19
3
+ rvm:
4
+ - rbx-2.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in pelusa.gemspec
4
+ gemspec
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ task :default => :test
@@ -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
+
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
3
+
4
+ require 'pelusa'
5
+
6
+ cli = Pelusa::Cli.new(ARGV)
7
+ exit cli.run
@@ -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
@@ -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
@@ -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