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