├── .gitignore ├── .pelusa.yml ├── .rvmrc ├── .travis.yml ├── Gemfile ├── Rakefile ├── Readme.md ├── bin └── pelusa ├── lib ├── pelusa.rb └── pelusa │ ├── analysis.rb │ ├── analyzer.rb │ ├── class_analyzer.rb │ ├── cli.rb │ ├── configuration.rb │ ├── lint.rb │ ├── lint │ ├── case_statements.rb │ ├── collection_wrappers.rb │ ├── demeter_law.rb │ ├── else_clauses.rb │ ├── eval_usage.rb │ ├── indentation_level.rb │ ├── instance_variables.rb │ ├── line_restriction.rb │ ├── long_identifiers.rb │ ├── many_arguments.rb │ ├── properties.rb │ └── short_identifiers.rb │ ├── report.rb │ ├── reporters │ ├── reporter.rb │ ├── ruby_reporter.rb │ └── stdout_reporter.rb │ ├── runner.rb │ └── version.rb ├── pelusa.gemspec └── test ├── fixtures └── sample_config_one.yml ├── pelusa ├── analysis_test.rb ├── analyzer_test.rb ├── class_analyzer_test.rb ├── cli_test.rb ├── configuration_test.rb ├── lint │ ├── case_statements_test.rb │ ├── collection_wrappers_test.rb │ ├── demeter_law_test.rb │ ├── else_clauses_test.rb │ ├── eval_usage_test.rb │ ├── indentation_level_test.rb │ ├── instance_variables_test.rb │ ├── line_restriction_test.rb │ ├── long_identifiers_test.rb │ ├── many_arguments_test.rb │ ├── properties_test.rb │ └── short_identifiers_test.rb ├── reporters │ └── ruby_reporter_test.rb └── runner_test.rb ├── pelusa_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | .rbx 6 | .ruby-version 7 | -------------------------------------------------------------------------------- /.pelusa.yml: -------------------------------------------------------------------------------- 1 | sources: lib/**/*.rb 2 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use --create rbx@pelusa 2 | RBXOPT="$RBXOPT -X19" 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - rbx-2.2.6 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in pelusa.gemspec 4 | gemspec 5 | gem 'rake' 6 | gem 'rubinius-melbourne' 7 | gem 'minitest' 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 11 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # pelusa - /pe 'lu sa/ [![Build Status](https://secure.travis-ci.org/codegram/pelusa.png)](http://travis-ci.org/codegram/pelusa) [![Dependency Status](https://gemnasium.com/codegram/pelusa.png)](https://gemnasium.com/codegram/pelusa) 2 | ## A Ruby Lint to improve your OO skills 3 | 4 | Pelusa is a static analysis tool and framework to inspect your code style and 5 | notify you about possible red flags or missing best practices. 6 | 7 | Above all pelusa _doesn't run your code_ -- it just analyzes it syntactically 8 | to gain superficial insights about it, and raise red flags when needed. 9 | 10 | Pelusa needs [Rubinius](http://rubini.us) to run, due to how easy it is to work 11 | with a Ruby AST with it, but it doesn't mean that your Ruby code must run on 12 | Rubinius. Since it's a static analysis tool, pelusa doesn't care what your code 13 | runs on, it just looks at it and tells you stuff. 14 | 15 | Here's a sample of pelusa linting on its own code base: 16 | 17 | ![](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) 18 | 19 | ## Why Pelusa? 20 | 21 | Pelusa happens to be Spanish for the word "Lint". Yeah, I couldn't believe it 22 | either. 23 | 24 | ## Installation and usage 25 | 26 | rvm use rbx 27 | gem install pelusa 28 | 29 | To run pelusa, you must run Rubinius in 1.9 mode. To do this, export this 30 | environment variable: 31 | 32 | export RBXOPT=-X19 33 | 34 | Then go to a directory where you have some Ruby code, and type this: 35 | 36 | pelusa path/to/some_file.rb 37 | 38 | Or just run all the Ruby files (`**/*.rb`) without arguments: 39 | 40 | pelusa 41 | 42 | ## Configuration 43 | 44 | You can use per-project configurations as per which lints to use and their 45 | configurations. Just put a `.pelusa.yml` file the root directory of your 46 | project, like this: 47 | 48 | sources: lib/**/*.rb 49 | lints: 50 | InstanceVariables: 51 | limit: 5 52 | LineRestriction: 53 | limit: 80 54 | exclude: 55 | - SomeClass 56 | Properties: 57 | enabled: false 58 | 59 | This allows you to disable lints, configure their variables, and exclude some 60 | classes from being analyzed by certain lints. 61 | 62 | ## About the default set of Lints 63 | 64 | This project was born as an inspiration from [one of our Monday 65 | Talks](http://talks.codegram.com/object-oriented-nirvana) about Object Oriented 66 | Nirvana by [@oriolgual](http://twitter.com/oriolgual). After reading [this blog 67 | post](http://binstock.blogspot.com/2008/04/perfecting-oos-small-classes-and-short.html) 68 | he prepared his talk and I ([@txustice](http://twitter.com/txustice)) found it interesting, so I explored the 69 | possibility of programmatically linting these practices on a Ruby project. This 70 | *doesn't mean* that any of us thinks these are the true and only practices of 71 | Object Orientation, it's just a set of constraints that are fun to follow to 72 | achieve a mindset shift in the long run. 73 | 74 | Anyway, you are always free to implement your own lints, or the ones that suit 75 | your team the best. 76 | 77 | ## Pelusa as a static analysis framework 78 | 79 | With Pelusa, writing your own lints becomes very easy. Check out some of the 80 | default lints under the `lib/pelusa/lint/` directory. 81 | 82 | At some point it will be user-extendable by default, but for now you are better 83 | off forking the project and adding your own lints as you need them in your team 84 | (or removing some default ones you don't like). 85 | 86 | ## Special mentions 87 | 88 | The beautiful UTF-8 flowers before each lint ran are taken from [Testosterone](http://github.com/masylum/testosterone), 89 | a project by [@masylum](http://twitter.com/masylum). They're really beautiful, 90 | thanks!!! 91 | 92 | ## Contributing 93 | 94 | You can easily contribute to Pelusa. Its codebase is simple and 95 | [extensively documented][documentation]. 96 | 97 | * Fork the project. 98 | * Make your feature addition or bug fix. 99 | * Add specs for it. This is important so we don't break it in a future 100 | version unintentionally. 101 | * Commit, do not mess with rakefile, version, or history. 102 | If you want to have your own version, that is fine but bump version 103 | in a commit by itself I can ignore when I pull. 104 | * Send me a pull request. Bonus points for topic branches. 105 | 106 | [documentation]: http://rubydoc.info/github/codegram/pelusa/master/frames 107 | 108 | ## License 109 | 110 | MIT License. Copyright 2011 [Codegram Technologies](http://codegram.com) 111 | 112 | -------------------------------------------------------------------------------- /bin/pelusa: -------------------------------------------------------------------------------- 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 8 | -------------------------------------------------------------------------------- /lib/pelusa.rb: -------------------------------------------------------------------------------- 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) 6 | lints = configuration.enabled_lints 7 | runner = Runner.new(lints, reporter) 8 | runner.run(files) 9 | end 10 | 11 | # Return configuration 12 | def self.configuration 13 | @configuration ||= Configuration.new 14 | end 15 | end 16 | 17 | require 'yaml' 18 | 19 | require 'pelusa/configuration' 20 | require 'pelusa/cli' 21 | require 'pelusa/runner' 22 | require 'pelusa/analyzer' 23 | require 'pelusa/lint' 24 | require 'pelusa/analysis' 25 | require 'pelusa/class_analyzer' 26 | require 'pelusa/report' 27 | 28 | require 'pelusa/reporters/reporter' 29 | require 'pelusa/reporters/stdout_reporter' 30 | require 'pelusa/reporters/ruby_reporter' 31 | -------------------------------------------------------------------------------- /lib/pelusa/analysis.rb: -------------------------------------------------------------------------------- 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 89 | -------------------------------------------------------------------------------- /lib/pelusa/analyzer.rb: -------------------------------------------------------------------------------- 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 | type = class_analyzer.type 23 | analysis = class_analyzer.analyze(@lints) 24 | 25 | Report.new(class_name, type, analysis) 26 | end 27 | @reporter.reports = reports 28 | @reporter 29 | end 30 | 31 | ####### 32 | private 33 | ####### 34 | 35 | # Internal: Extracts the classes out of the AST and returns their nodes. 36 | # 37 | # ast - The abstract syntax tree to extract the classes from. 38 | # 39 | # Returns an Array of Class nodes. 40 | def extract_classes(ast) 41 | classes = [] 42 | if ast.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::Class) || ast.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::Module) 43 | classes << ast 44 | end 45 | 46 | ast.walk do |continue, node| 47 | if node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::Class) || node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::Module) 48 | classes << node 49 | end 50 | true 51 | end 52 | classes 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/pelusa/class_analyzer.rb: -------------------------------------------------------------------------------- 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: Returns the type of container being examined (class or module). 18 | def type 19 | @klass.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::Class) ? "class" : "module" 20 | end 21 | 22 | # Public: Analyzes a class with a series of lints. 23 | # 24 | # lints - The lints to check for. 25 | # 26 | # Returns a collection of Analysis, one for each lint. 27 | def analyze(lints) 28 | lints.map do |lint_class| 29 | lint = lint_class.new 30 | lint.check(@klass) 31 | end 32 | end 33 | 34 | # Public: Walk a node, analyzing it as it goes. 35 | # 36 | # block - supply a block that will be executed as the node gets walked. 37 | def self.walk(start_node) 38 | raise ArgumentError, "Walk requires a block!" unless block_given? 39 | start_node.walk do |continue, node| 40 | yield(node) 41 | true 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/pelusa/cli.rb: -------------------------------------------------------------------------------- 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 | reporters = Pelusa.run(_files) 18 | 19 | reporters.first.class.print_banner unless reporters.empty? 20 | 21 | exit_code = 0 22 | reporters.each do |reporter| 23 | reporter.report 24 | exit_code = 1 unless reporter.successful? 25 | end 26 | exit_code 27 | end 28 | 29 | def files 30 | if glob = @args.detect { |arg| arg =~ /\*/ } 31 | return Dir[glob] 32 | end 33 | _files = @args.select { |arg| arg =~ /\.rb/ } 34 | _files = Dir[Pelusa.configuration.sources] if _files.empty? 35 | _files 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/pelusa/configuration.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | # Public: Class providing configuration for the runner and lints 3 | # 4 | # Examples 5 | # 6 | # configuration = Pelusa::Configuration.new('my_pelusa_config.yml') 7 | # 8 | class Configuration 9 | YAML_PATH = './.pelusa.yml' 10 | DEFAULT_SOURCES = 'lib/**/*.rb' 11 | 12 | # Public: Initializes a configuration instance 13 | # 14 | # yaml_path - optional path to the configuration file 15 | def initialize(yaml_path = YAML_PATH) 16 | @_configuration = if File.exist?(yaml_path) 17 | YAML.load_file(yaml_path) 18 | else 19 | {} 20 | end.freeze 21 | end 22 | 23 | # Public: Returns custom configuration for the given lint 24 | # 25 | # Examples 26 | # 27 | # Pelusa.configuration['LineRestriction'] # => {'limit' => 50} 28 | # 29 | # name - the name of the lint 30 | def [](name) 31 | for_lint(name) 32 | end 33 | 34 | # Public: Returns path to sources that should be analyzed 35 | # 36 | # Examples 37 | # 38 | # Pelusa.configuration.sources # => lib/**/*.rb 39 | # 40 | def sources 41 | @_configuration.fetch('sources') { DEFAULT_SOURCES } 42 | end 43 | 44 | # Public: Returns an Array of enabled lints 45 | # 46 | # Examples 47 | # 48 | # Pelusa.configuration.enabled_lints # => [ Pelusa::Lint::DemeterLaw ] 49 | # 50 | def enabled_lints 51 | (Lint.all - disabled_lints).uniq 52 | end 53 | 54 | ####### 55 | private 56 | ####### 57 | 58 | # Private: Returns a Hash of configuration for lints 59 | def lints 60 | @_configuration.fetch('lints', {}) 61 | end 62 | 63 | # Private: Returns an Array of lints disabled in the configuration 64 | def disabled_lints 65 | lints.select { |lint, conf| conf['enabled'] === false }.map do |lint,| 66 | Lint.const_get(lint) 67 | end 68 | end 69 | 70 | # Private: Public: Returns custom configuration for the given lint 71 | def for_lint(name) 72 | lints.fetch(name, {}) 73 | end 74 | 75 | end # class Configuration 76 | end # module Pelusa 77 | -------------------------------------------------------------------------------- /lib/pelusa/lint.rb: -------------------------------------------------------------------------------- 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 | require 'pelusa/lint/long_identifiers' 11 | require 'pelusa/lint/case_statements' 12 | require 'pelusa/lint/many_arguments' 13 | require 'pelusa/lint/eval_usage' 14 | 15 | module Pelusa 16 | # Public: A Lint is a quality standard, applicable on a given piece of code to 17 | # check its compliance. 18 | # 19 | module Lint 20 | def self.all 21 | [ 22 | LineRestriction, 23 | InstanceVariables, 24 | DemeterLaw, 25 | IndentationLevel, 26 | ElseClauses, 27 | Properties, 28 | CollectionWrappers, 29 | ShortIdentifiers, 30 | LongIdentifiers, 31 | ManyArguments, 32 | EvalUsage 33 | ] 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/pelusa/lint/case_statements.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | module Lint 3 | class CaseStatements 4 | def initialize 5 | @violations = Set.new 6 | end 7 | 8 | def check(klass) 9 | iterate_lines!(klass) 10 | 11 | return SuccessfulAnalysis.new(name) if @violations.empty? 12 | 13 | FailedAnalysis.new(name, @violations) do |violations| 14 | "There are #{violations.length} case statements in lines #{violations.to_a.join(', ')}" 15 | end 16 | end 17 | 18 | private 19 | 20 | def name 21 | "Doesn't use case statements" 22 | end 23 | 24 | def iterate_lines!(klass) 25 | ClassAnalyzer.walk(klass) do |node| 26 | if node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::Case) 27 | @violations << node.line 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/pelusa/lint/collection_wrappers.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | module Lint 3 | class CollectionWrappers 4 | def initialize 5 | @violations = Set.new 6 | end 7 | 8 | def check(klass) 9 | iterate_lines!(klass) 10 | 11 | return SuccessfulAnalysis.new(name) if @violations.empty? 12 | 13 | FailedAnalysis.new(name, @violations) do |violations| 14 | "Using an instance variable apart from the array ivar in lines #{violations.to_a.join(', ')}" 15 | end 16 | end 17 | 18 | private 19 | 20 | def name 21 | "Doesn't mix array instance variables with others" 22 | end 23 | 24 | def iterate_lines!(klass) 25 | # the array of actual assignment nodes -- we only want to assign once 26 | array_assignments = {} 27 | # the name of all the assigned arrays, no others allowed 28 | array_values = {} 29 | 30 | ClassAnalyzer.walk(klass) do |node| 31 | if node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::InstanceVariableAssignment) && 32 | (node.value.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::ArrayLiteral) || 33 | node.value.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::EmptyArray)) 34 | array_assignments[node] = true 35 | array_values[node.name] = true 36 | end 37 | end 38 | 39 | unless array_assignments.empty? 40 | ClassAnalyzer.walk(klass) do |node| 41 | # if this is where we assign the node for the first time, good 42 | unless array_assignments[node] 43 | # otherwise, if it's an instance variable assignment, verboten! 44 | if node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::InstanceVariableAssignment) 45 | @violations << node.line 46 | # or if we access any other ivars 47 | elsif node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::InstanceVariableAccess) && 48 | !array_values[node.name] 49 | @violations << node.line 50 | end 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/pelusa/lint/demeter_law.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | module Lint 3 | class DemeterLaw 4 | def initialize 5 | @violations = Set.new 6 | end 7 | 8 | def check(klass) 9 | iterate_lines!(klass) 10 | 11 | return SuccessfulAnalysis.new(name) if @violations.empty? 12 | 13 | FailedAnalysis.new(name, @violations) do |violations| 14 | "There are #{violations.length} Demeter law violations in lines #{violations.to_a.join(', ')}." 15 | end 16 | end 17 | 18 | private 19 | 20 | def name 21 | "Respects Demeter law" 22 | end 23 | 24 | def iterate_lines!(klass) 25 | ClassAnalyzer.walk(klass) do |node| 26 | if node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::Send) && node.receiver.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::Send) 27 | @violations << node.line unless white_listed?(node.receiver.name) 28 | end 29 | end 30 | end 31 | 32 | # Internal: Default modules whose methods are whitelisted. 33 | DEFAULT_WHITELIST = [Class, Fixnum, Enumerable] 34 | 35 | # Internal: Methods on these common, fundamental classes and modules are 36 | # allowed to be called on other objects. 37 | # 38 | # Note: this doesn't currently work with namespaced objects, but would be 39 | # easy to extend. 40 | # 41 | # Returns an array of classes specified in .pelusa.yml (a comma-separated 42 | # list of class names) or the default values above. 43 | def whitelist_sources 44 | if whitelist = Pelusa.configuration['DemeterLaw']['whitelist'] 45 | whitelist.split(",").map {|k| Kernel.const_get(k.strip)} 46 | else 47 | DEFAULT_WHITELIST 48 | end 49 | end 50 | 51 | # Internal: Allow conversion methods -- matching /^(to_|as_)/ to violate 52 | # the law of Demeter. 53 | def allow_conversions? 54 | Pelusa.configuration['DemeterLaw'].fetch('allow_conversions', false) 55 | end 56 | 57 | def white_listed? method 58 | whitelist_sources.any? do |enclosing_module| 59 | enclosing_module.instance_methods.any? do |instance_method| 60 | instance_method == method or 61 | allow_conversions? && instance_method =~ /^(to_|as_)/ 62 | end 63 | end 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/pelusa/lint/else_clauses.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | module Lint 3 | class ElseClauses 4 | def initialize 5 | @violations = Set.new 6 | end 7 | 8 | def check(klass) 9 | iterate_lines!(klass) 10 | 11 | return SuccessfulAnalysis.new(name) if @violations.empty? 12 | 13 | FailedAnalysis.new(name, @violations) do |violations| 14 | "There are #{violations.length} else clauses in lines #{violations.to_a.join(', ')}" 15 | end 16 | end 17 | 18 | private 19 | 20 | def name 21 | "Doesn't use else clauses" 22 | end 23 | 24 | def iterate_lines!(klass) 25 | ClassAnalyzer.walk(klass) do |node| 26 | if node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::If) 27 | has_body = node.body && !node.body.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::NilLiteral) 28 | has_else = node.else && !node.else.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::NilLiteral) 29 | 30 | if has_body && has_else 31 | @violations << node.else.line 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/pelusa/lint/eval_usage.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Pelusa 4 | module Lint 5 | class EvalUsage 6 | def initialize 7 | @violations = Set.new 8 | end 9 | 10 | def check(klass) 11 | iterate_lines!(klass) 12 | 13 | if @violations.empty? 14 | SuccessfulAnalysis.new(name) 15 | else 16 | FailedAnalysis.new(name, @violations) do |violations| 17 | "There are #{violations.length} eval statement in lines #{violations.to_a.join(', ')}" 18 | end 19 | end 20 | end 21 | 22 | private 23 | 24 | def name 25 | "Doesn't use eval statement" 26 | end 27 | 28 | def iterate_lines!(klass) 29 | ClassAnalyzer.walk(klass) do |node| 30 | @violations << node.line if eval_violation?(node) 31 | end 32 | end 33 | 34 | def eval_violation?(node) 35 | node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::SendWithArguments) && node.name == :eval && node.receiver.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::Self) 36 | end 37 | 38 | end 39 | end 40 | end -------------------------------------------------------------------------------- /lib/pelusa/lint/indentation_level.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | module Lint 3 | class IndentationLevel 4 | def initialize 5 | @violations = Set.new 6 | end 7 | 8 | def check(klass) 9 | iterate_lines!(klass) 10 | 11 | return SuccessfulAnalysis.new(name) if @violations.empty? 12 | 13 | FailedAnalysis.new(name, @violations) do |violations| 14 | "There's too much indentation in lines #{violations.to_a.join(', ')}." 15 | end 16 | end 17 | 18 | private 19 | 20 | def name 21 | "Doesn't use more than one indentation level inside methods" 22 | end 23 | 24 | def iterate_lines!(klass) 25 | # we want to find all nodes inside define blocks that 26 | # contain > 1 indentation levels 27 | # this method totally fails the IndentationLevel level lint :P 28 | ClassAnalyzer.walk(klass) do |node| 29 | if node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::Define) 30 | # we're inside a method body, so see if we indent anywhere 31 | ClassAnalyzer.walk(node) do |inner_node| 32 | if inner_body = get_body_from_node[inner_node] 33 | # if it turns out there's an indented value in there, that 34 | # could be okay -- walk that node to see if there's a 2nd level 35 | if inner_node.line != [inner_body].flatten.first.line 36 | # walk the third level, see if there's another indent 37 | ClassAnalyzer.walk(inner_node) do |innermost_node| 38 | if innermost_body = get_body_from_node[innermost_node] 39 | if innermost_node.line != [innermost_body].flatten.first.line 40 | # there's yet another level of indent -- violation! 41 | # note: this also catches bad outdents, possibly should 42 | # flag those separately 43 | @violations.merge Array(innermost_body).map(&:line) 44 | end 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | end 53 | 54 | def get_body_from_node 55 | lambda do |node| 56 | if node.respond_to?(:body) && !node.body.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::NilLiteral) 57 | node.body 58 | elsif node.respond_to?(:else) 59 | node.else 60 | end 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/pelusa/lint/instance_variables.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | module Lint 3 | class InstanceVariables 4 | def initialize 5 | @ivars = Set.new 6 | end 7 | 8 | def check(klass) 9 | iterate_lines!(klass) 10 | 11 | return SuccessfulAnalysis.new(name) if @ivars.length < limit 12 | 13 | FailedAnalysis.new(name, @ivars) do |ivars| 14 | "This class uses #{ivars.length} instance variables: #{ivars.to_a.join(', ')}." 15 | end 16 | end 17 | 18 | private 19 | 20 | def name 21 | "Uses less than #{limit} ivars" 22 | end 23 | 24 | def limit 25 | Pelusa.configuration['InstanceVariables'].fetch('limit', 3) 26 | end 27 | 28 | def iterate_lines!(klass) 29 | ClassAnalyzer.walk(klass) do |node| 30 | if node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::InstanceVariableAccess) || node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::InstanceVariableAssignment) 31 | @ivars << node.name 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/pelusa/lint/line_restriction.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | module Lint 3 | class LineRestriction 4 | def initialize 5 | @lines = Set.new 6 | end 7 | 8 | def check(klass) 9 | iterate_lines!(klass) 10 | 11 | return SuccessfulAnalysis.new(name) if lines < limit 12 | 13 | FailedAnalysis.new(name, lines) do |lines| 14 | "This class has #{lines} lines." 15 | end 16 | end 17 | 18 | private 19 | 20 | def name 21 | "Is below #{limit} lines" 22 | end 23 | 24 | def limit 25 | Pelusa.configuration['LineRestriction'].fetch('limit', 50) 26 | end 27 | 28 | def lines 29 | @lines.max - @lines.min 30 | end 31 | 32 | def iterate_lines!(klass) 33 | ClassAnalyzer.walk(klass) do |node| 34 | @lines << node.line if node.respond_to?(:line) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/pelusa/lint/long_identifiers.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | module Lint 3 | class LongIdentifiers 4 | def initialize 5 | @violations = Set.new 6 | end 7 | 8 | def check(klass) 9 | iterate_lines!(klass) 10 | 11 | return SuccessfulAnalysis.new(name) if @violations.empty? 12 | 13 | FailedAnalysis.new(name, formatted_violations) do |violations| 14 | "These names are too long: #{violations.join(', ')}" 15 | end 16 | end 17 | 18 | private 19 | 20 | def name 21 | "Uses names of adequate length (less than #{limit})" 22 | end 23 | 24 | def limit 25 | Pelusa.configuration['LongIdentifiers'].fetch('limit', 20) 26 | end 27 | 28 | def iterate_lines!(klass) 29 | ClassAnalyzer.walk(klass) do |node| 30 | if node.respond_to?(:name) 31 | name = node.name.respond_to?(:name) ? node.name.name.to_s : node.name.to_s 32 | if name =~ /[a-z]/ && name.length > limit 33 | @violations << [name, node.line] unless name =~ /^[A-Z]/ # Ignore constants 34 | end 35 | end 36 | end 37 | end 38 | 39 | def formatted_violations 40 | grouped_violations = @violations.inject({}) do |hash, (name, line)| 41 | hash[name] ||= [] 42 | hash[name] << line 43 | hash 44 | end 45 | 46 | violations = [] 47 | 48 | grouped_violations.each_pair do |name, lines| 49 | violations << "#{name} (line #{lines.join(', ')})" 50 | end 51 | violations 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/pelusa/lint/many_arguments.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | module Lint 3 | class ManyArguments 4 | def initialize 5 | @violations = Set.new 6 | end 7 | 8 | def check(klass) 9 | iterate_lines!(klass) 10 | 11 | return SuccessfulAnalysis.new(name) if @violations.empty? 12 | 13 | FailedAnalysis.new(name, formatted_violations) do |violations| 14 | "Methods with more than #{limit} arguments: #{violations.join(', ')}" 15 | end 16 | end 17 | 18 | private 19 | 20 | def name 21 | "Methods have short argument lists" 22 | end 23 | 24 | def limit 25 | Pelusa.configuration['ManyArguments'].fetch('limit', 3) 26 | end 27 | 28 | def iterate_lines!(klass) 29 | ClassAnalyzer.walk(klass) do |node| 30 | if node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::Define) && node.arguments.total_args > limit 31 | @violations << node.name 32 | end 33 | end 34 | end 35 | 36 | def formatted_violations 37 | @violations.to_a 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/pelusa/lint/properties.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | module Lint 3 | class Properties 4 | def initialize 5 | @violations = Set.new 6 | end 7 | 8 | def check(klass) 9 | iterate_lines!(klass) 10 | 11 | return SuccessfulAnalysis.new(name) if @violations.empty? 12 | 13 | FailedAnalysis.new(name, @violations) do |violations| 14 | "There are getters, setters or properties in lines #{violations.to_a.join(', ')}" 15 | end 16 | end 17 | 18 | private 19 | 20 | def name 21 | "Doesn't use getters, setters or properties" 22 | end 23 | 24 | def iterate_lines!(klass) 25 | ClassAnalyzer.walk(klass) do |node| 26 | if node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::Send) 27 | if [:attr_accessor, :attr_writer, :attr_reader].include? node.name 28 | @violations << node.line 29 | end 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/pelusa/lint/short_identifiers.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | module Lint 3 | class ShortIdentifiers 4 | RESERVED_NAMES = ['p', 'pp', 'id'] 5 | 6 | def initialize 7 | @violations = Set.new 8 | end 9 | 10 | def check(klass) 11 | iterate_lines!(klass) 12 | 13 | return SuccessfulAnalysis.new(name) if @violations.empty? 14 | 15 | FailedAnalysis.new(name, formatted_violations) do |violations| 16 | "Names are too short: #{violations.join(', ')}" 17 | end 18 | end 19 | 20 | private 21 | 22 | def name 23 | "Uses descriptive names" 24 | end 25 | 26 | def iterate_lines!(klass) 27 | ClassAnalyzer.walk(klass) do |node| 28 | if node.respond_to?(:name) 29 | name = node.name.respond_to?(:name) ? node.name.name.to_s : node.name.to_s 30 | if name =~ /[a-z]/ && name.length < 3 && !RESERVED_NAMES.include?(name) 31 | @violations << [name, node.line] unless name =~ /^[A-Z]/ # Ignore constants 32 | end 33 | end 34 | end 35 | end 36 | 37 | def formatted_violations 38 | grouped_violations = @violations.inject({}) do |hash, (name, line)| 39 | hash[name] ||= [] 40 | hash[name] << line 41 | hash 42 | end 43 | 44 | violations = [] 45 | 46 | grouped_violations.each_pair do |name, lines| 47 | violations << "#{name} (line #{lines.join(', ')})" 48 | end 49 | violations 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/pelusa/report.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | # Public: A Report is a wrapper that relates a class name with all its 3 | # analyses for different lint checks. 4 | # 5 | class Report 6 | # Public: Initializes a new Report. 7 | # 8 | # class_name - The Symbol name of the class being analyzed. 9 | # type - the String type of the class being analyzed (class or module). 10 | # analyses - An Array of Analysis objects. 11 | def initialize(name, type, analyses) 12 | @class_name = name 13 | @type = type 14 | @analyses = analyses 15 | end 16 | 17 | attr_reader :class_name, :type, :analyses 18 | 19 | def successful? 20 | @analyses.all? { |analysis| analysis.successful? } 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/pelusa/reporters/reporter.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | class Reporter 3 | def self.print_banner 4 | end 5 | 6 | def initialize(filename) 7 | @filename = filename 8 | end 9 | 10 | def reports=(reports) 11 | @reports = reports 12 | end 13 | 14 | def report 15 | raise NotImplementedError 16 | end 17 | 18 | def successful? 19 | @reports.all? { |report| report.successful? } 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/pelusa/reporters/ruby_reporter.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Pelusa 4 | class RubyReporter < Reporter 5 | def report 6 | hash = @reports.inject({}) do |acc, report| 7 | class_name = report.class_name 8 | acc[class_name] = hashify_report(report) 9 | acc 10 | end 11 | hash[:filename] = @filename unless hash.empty? 12 | hash 13 | end 14 | 15 | private 16 | 17 | def hashify_report(report) 18 | analyses = report.analyses 19 | analyses.inject({}) do |acc, analysis| 20 | name = analysis.name 21 | acc[name] = { 22 | status: analysis.status, 23 | message: analysis.message, 24 | } 25 | acc 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/pelusa/reporters/stdout_reporter.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Pelusa 4 | class StdoutReporter < Reporter 5 | def self.print_banner 6 | puts " \e[0;35mϟ\e[0m \e[0;32mPelusa \e[0;35mϟ\e[0m" 7 | puts " \e[0;37m----------\e[0m" 8 | end 9 | 10 | def report 11 | puts " \e[0;36m#{@filename}\e[0m" 12 | puts 13 | 14 | @reports.each do |class_report| 15 | print_report(class_report) 16 | end 17 | end 18 | 19 | private 20 | 21 | def print_report(class_report) 22 | class_name = class_report.class_name 23 | 24 | puts " #{class_report.type} #{class_name}" 25 | 26 | analyses = class_report.analyses 27 | analyses.each do |analysis| 28 | print_analysis(analysis) 29 | end 30 | puts 31 | end 32 | 33 | def print_analysis(analysis) 34 | name = analysis.name 35 | status = analysis.status 36 | message = analysis.message 37 | 38 | print " \e[0;33m✿ %s \e[0m" % name 39 | 40 | if analysis.successful? 41 | print "\e[0;32m✓\e[0m\n" 42 | return 43 | end 44 | 45 | print "\e[0;31m✗\n\t" 46 | puts message 47 | print "\e[0m" 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/pelusa/runner.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | class Runner 3 | # Public: Initializes an Analyzer. 4 | # 5 | # lints - The lints to check the code for. 6 | # reporter - The Reporter to use. Will be used to report back the results in 7 | # methods such as #run. 8 | def initialize(lints, reporter) 9 | @lints = lints 10 | @reporter = reporter 11 | end 12 | 13 | # Public: Runs the analyzer on a set of files. 14 | # 15 | # Returns an Array of Reports of those file runs. 16 | def run(files) 17 | Array(files).map do |file| 18 | run_file(file) 19 | end 20 | end 21 | 22 | # Public: Runs the analyzer on a single file. 23 | # 24 | # Returns a Report of the single run. 25 | def run_file(file) 26 | ast = parser.parse_file(file) 27 | analyzer = Analyzer.new(@lints, @reporter, file) 28 | analyzer.analyze(ast) 29 | end 30 | 31 | ####### 32 | private 33 | ####### 34 | 35 | # Internal: Returns the Melbourne 1.9 parser. 36 | # 37 | # Returns a Rubinius::Melbourne parser. 38 | def parser 39 | ::Rubinius::ToolSets.current::ToolSet::Melbourne 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/pelusa/version.rb: -------------------------------------------------------------------------------- 1 | module Pelusa 2 | VERSION = "0.2.4" 3 | end 4 | -------------------------------------------------------------------------------- /pelusa.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "pelusa/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "pelusa" 7 | s.version = Pelusa::VERSION 8 | s.authors = ["Josep M. Bach"] 9 | s.email = ["josep.m.bach@gmail.com"] 10 | s.homepage = "http://github.com/codegram/pelusa" 11 | s.summary = %q{Static analysis Lint-type tool to improve your OO Ruby code} 12 | s.description = %q{Static analysis Lint-type tool to improve your OO Ruby code} 13 | 14 | s.rubyforge_project = "pelusa" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = Dir["test/**/*.rb"] 18 | s.executables = ["pelusa"] 19 | s.require_paths = ["lib"] 20 | 21 | s.add_development_dependency 'mocha' 22 | end 23 | -------------------------------------------------------------------------------- /test/fixtures/sample_config_one.yml: -------------------------------------------------------------------------------- 1 | sources: lib/**/*.rb 2 | lints: 3 | ElseClauses: 4 | enabled: false 5 | LineRestriction: 6 | enabled: true 7 | limit: 80 8 | -------------------------------------------------------------------------------- /test/pelusa/analysis_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Pelusa 4 | describe SuccessfulAnalysis do 5 | before do 6 | @analysis = SuccessfulAnalysis.new("Is awesome") 7 | end 8 | 9 | it 'is successful' do 10 | @analysis.successful?.must_equal true 11 | end 12 | 13 | describe '#message' do 14 | it 'has an empty message' do 15 | @analysis.message.empty?.must_equal true 16 | end 17 | end 18 | end 19 | 20 | describe FailedAnalysis do 21 | before do 22 | number_of_errors = 42 23 | @analysis = FailedAnalysis.new("Is awesome", number_of_errors) do |errors| 24 | "There have been #{errors} errors." 25 | end 26 | end 27 | 28 | it 'is failed' do 29 | @analysis.failed?.must_equal true 30 | end 31 | 32 | describe '#message' do 33 | it 'describes the failed analysis' do 34 | @analysis.message.must_equal "There have been 42 errors." 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/pelusa/analyzer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Pelusa 4 | describe Analyzer do 5 | describe '#analyze' do 6 | describe 'with a multi-expression AST' do 7 | before do 8 | @ast = Pelusa.to_ast """ 9 | class Foo 10 | def bar 11 | 123 12 | end 13 | end 14 | 15 | class Bar 16 | def baz 17 | 321 18 | end 19 | end 20 | 21 | module Baz 22 | def bar 23 | 2.7 24 | end 25 | end 26 | """ 27 | 28 | lints = stub 29 | @analyzer = Analyzer.new([Lint::LineRestriction], RubyReporter, "foo.rb") 30 | end 31 | 32 | it 'analyzes an ast and returns a report' do 33 | result = @analyzer.analyze(@ast).report 34 | result[:filename].must_equal "foo.rb" 35 | result[:Foo]["Is below 50 lines"][:status].must_equal "successful" 36 | result[:Bar]["Is below 50 lines"][:status].must_equal "successful" 37 | result[:Baz]["Is below 50 lines"][:status].must_equal "successful" 38 | end 39 | end 40 | 41 | describe 'with a single-expression AST' do 42 | before do 43 | @ast = Pelusa.to_ast """ 44 | class Foo 45 | def bar 46 | 123 47 | end 48 | end 49 | """ 50 | 51 | lints = stub 52 | @analyzer = Analyzer.new([Lint::LineRestriction], RubyReporter, "foo.rb") 53 | end 54 | 55 | it 'analyzes an ast and returns a report' do 56 | result = @analyzer.analyze(@ast).report 57 | result[:filename].must_equal "foo.rb" 58 | result[:Foo]["Is below 50 lines"][:status].must_equal "successful" 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/pelusa/class_analyzer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Pelusa 4 | describe ClassAnalyzer do 5 | describe '#analyze' do 6 | before do 7 | @lints = [ 8 | stub(new: stub(check: true)), 9 | stub(new: stub(check: true)) 10 | ] 11 | @klass = stub(name: stub(name: "Foo")) 12 | @analyzer = ClassAnalyzer.new(@klass) 13 | end 14 | 15 | it 'analyzes a Class node for a series of lints' do 16 | @analyzer.analyze(@lints).must_equal [true, true] 17 | end 18 | 19 | describe '#class_name' do 20 | it 'returns the name of the analyzed class' do 21 | @analyzer.class_name.must_equal "Foo" 22 | end 23 | end 24 | 25 | describe "#type" do 26 | it "returns the type module for modules" do 27 | # hacky! 28 | @klass.stubs(:is_a?).with(Rubinius::ToolSets::Runtime::ToolSet::AST::Class).returns(true) 29 | @analyzer.type.must_equal "class" 30 | end 31 | 32 | it "returns the type module for modules" do 33 | # hacky! 34 | @klass.stubs(:is_a?).with(Rubinius::ToolSets::Runtime::ToolSet::AST::Class).returns(false) 35 | @analyzer.type.must_equal "module" 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/pelusa/cli_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Pelusa 4 | describe Cli do 5 | describe "#run" do 6 | before do 7 | @report = stub(empty?: false, report: true, class: Reporter) 8 | Pelusa.stubs(:run).returns [@report] 9 | end 10 | describe 'when the reports are successful' do 11 | it 'returns 0' do 12 | @report.stubs(successful?: true) 13 | Cli.new.run().must_equal 0 14 | end 15 | end 16 | describe 'when the reports have failed' do 17 | it 'returns 1' do 18 | @report.stubs(successful?: false) 19 | Cli.new.run().must_equal 1 20 | end 21 | end 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /test/pelusa/configuration_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Pelusa 4 | describe Configuration do 5 | let(:configuration) do 6 | Pelusa::Configuration.new("#{FIXTURES_PATH}/sample_config_one.yml") 7 | end 8 | 9 | describe '#sources' do 10 | it 'returns path to sources' do 11 | configuration.sources.must_equal 'lib/**/*.rb' 12 | end 13 | 14 | describe 'by default' do 15 | it 'returns lib/**/*.rb' do 16 | empty_configuration = Pelusa::Configuration.new("unexistent_yml") 17 | empty_configuration.sources.must_equal Pelusa::Configuration::DEFAULT_SOURCES 18 | end 19 | end 20 | end 21 | 22 | describe '#enabled_lints' do 23 | let(:enabled_lints) { Lint.all - [ Pelusa::Lint::ElseClauses ] } 24 | 25 | it 'returns all enabled lints' do 26 | configuration.enabled_lints.must_equal(enabled_lints) 27 | end 28 | end 29 | 30 | describe '#[]' do 31 | describe 'when lint configuration exists' do 32 | let(:lint_configuration) { configuration['LineRestriction'] } 33 | 34 | it 'returns a configuration hash for the given lint' do 35 | lint_configuration.must_be_instance_of(Hash) 36 | end 37 | 38 | it 'must return valid configuration' do 39 | lint_configuration['limit'].must_equal(80) 40 | end 41 | end 42 | 43 | describe "when lint configuration doesn't exist" do 44 | let(:lint_configuration) { configuration['DemeterLaw'] } 45 | 46 | it 'returns an empty configuration hash' do 47 | lint_configuration.must_equal({}) 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/pelusa/lint/case_statements_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Pelusa 4 | module Lint 5 | describe CaseStatements do 6 | before do 7 | @lint = CaseStatements.new 8 | end 9 | 10 | describe '#check' do 11 | describe 'when the class does not use switch statements' do 12 | it 'returns a SuccessAnalysis' do 13 | klass = Pelusa.to_ast """ 14 | class Foo 15 | def initialize 16 | return nil 17 | end 18 | end""" 19 | 20 | analysis = @lint.check(klass) 21 | analysis.successful?.must_equal true 22 | end 23 | end 24 | 25 | describe 'when the class uses case statements' do 26 | it 'returns a FailureAnalysis' do 27 | klass = Pelusa.to_ast """ 28 | class Foo 29 | def initialize 30 | case foo 31 | when bar 32 | end 33 | end 34 | end""" 35 | 36 | analysis = @lint.check(klass) 37 | analysis.failed?.must_equal true 38 | end 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/pelusa/lint/collection_wrappers_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Pelusa 4 | module Lint 5 | describe CollectionWrappers do 6 | before do 7 | @lint = CollectionWrappers.new 8 | end 9 | 10 | describe '#check' do 11 | describe 'when the class is not a collection wrapper with more instance variables' do 12 | it 'returns a SuccessAnalysis' do 13 | klass = Pelusa.to_ast """ 14 | class Foo 15 | def initialize 16 | @things = [] 17 | end 18 | end""" 19 | 20 | analysis = @lint.check(klass) 21 | analysis.successful?.must_equal true 22 | end 23 | end 24 | 25 | describe 'when the class has no ivars' do 26 | it 'returns a SuccessAnalysis' do 27 | klass = Pelusa.to_ast """ 28 | class Foo 29 | def initialize 30 | things = [] 31 | end 32 | end""" 33 | 34 | analysis = @lint.check(klass) 35 | analysis.successful?.must_equal true 36 | end 37 | end 38 | 39 | describe 'when the class mixes collection ivars with others' do 40 | it 'returns a FailureAnalysis' do 41 | klass = Pelusa.to_ast """ 42 | class Foo 43 | def initialize 44 | @things = [] 45 | @foo = 'bar' 46 | end 47 | end""" 48 | 49 | analysis = @lint.check(klass) 50 | analysis.failed?.must_equal true 51 | end 52 | end 53 | 54 | describe 'when the class has multiple array assignments' do 55 | it 'returns a FailureAnalysis' do 56 | klass = Pelusa.to_ast """ 57 | class Foo 58 | def initialize 59 | @things = [] 60 | @foos = [] 61 | end 62 | end""" 63 | 64 | analysis = @lint.check(klass) 65 | analysis.failed?.must_equal false 66 | end 67 | end 68 | 69 | describe 'when the class has multiple array and other assignments' do 70 | it 'returns a FailureAnalysis' do 71 | klass = Pelusa.to_ast """ 72 | class Foo 73 | def initialize 74 | @things = [] 75 | @foos = [] 76 | @foo = 'bar' 77 | end 78 | end""" 79 | 80 | analysis = @lint.check(klass) 81 | analysis.failed?.must_equal true 82 | end 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/pelusa/lint/demeter_law_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Pelusa 4 | module Lint 5 | describe DemeterLaw do 6 | before do 7 | @lint = DemeterLaw.new 8 | end 9 | 10 | describe '#check' do 11 | describe 'when the class respects Demeter law' do 12 | it 'returns a SuccessAnalysis' do 13 | klass = Pelusa.to_ast """ 14 | class Foo 15 | def initialize 16 | foo = 'hey'.upcase 17 | foo.downcase 18 | end 19 | end""" 20 | 21 | analysis = @lint.check(klass) 22 | analysis.successful?.must_equal true 23 | end 24 | end 25 | 26 | describe 'when the class does not respect Demeter law' do 27 | it 'returns a FailureAnalysis' do 28 | klass = Pelusa.to_ast """ 29 | class Foo 30 | def initialize 31 | foo = 'hey'.upcase.downcase 32 | end 33 | end""" 34 | 35 | analysis = @lint.check(klass) 36 | analysis.failed?.must_equal true 37 | end 38 | end 39 | 40 | describe 'when instantiating a class' do 41 | it 'returns a SuccessAnalysis' do 42 | klass = Pelusa.to_ast """ 43 | class Foo 44 | def execute 45 | Bar.new.execute 46 | end 47 | end""" 48 | 49 | analysis = @lint.check(klass) 50 | analysis.successful?.must_equal true 51 | end 52 | end 53 | 54 | describe 'when chaining whitelisted operations' do 55 | it 'returns a SuccessAnalysis for chained operations from Enumerable' do 56 | klass = Pelusa.to_ast """ 57 | class Foo 58 | def execute 59 | [1,2,3].map(&:object_id).each {|i| i} 60 | end 61 | end""" 62 | 63 | analysis = @lint.check(klass) 64 | analysis.successful?.must_equal true 65 | end 66 | 67 | it 'returns a SuccessAnalysis when chaining methods from Fixnum' do 68 | klass = Pelusa.to_ast """ 69 | class Foo 70 | def execute 71 | 1 + 2 + 3 + 4 72 | end 73 | end""" 74 | 75 | analysis = @lint.check(klass) 76 | analysis.successful?.must_equal true 77 | end 78 | 79 | it 'returns a SuccessAnalysis for chained operations from Object' do 80 | klass = Pelusa.to_ast """ 81 | class Foo 82 | def execute 83 | Object.new.to_s.inspect 84 | end 85 | end""" 86 | 87 | analysis = @lint.check(klass) 88 | analysis.successful?.must_equal true 89 | end 90 | 91 | it 'returns a SuccessAnalysis for chained operations from optional sources' do 92 | Pelusa.configuration.stubs(:[]).with("DemeterLaw").returns( 93 | {"whitelist" => "Object, Kernel, Hash, Enumerable"} 94 | ) 95 | 96 | klass = Pelusa.to_ast """ 97 | class Foo 98 | def execute 99 | {'a' => 2}.merge.each_pair {|k, v|} 100 | end 101 | end""" 102 | 103 | analysis = @lint.check(klass) 104 | analysis.successful?.must_equal true 105 | end 106 | end 107 | 108 | describe 'conversions' do 109 | it 'returns a SuccessAnalysis for conversion operations if allowed' do 110 | Pelusa.configuration.stubs(:[]).with("DemeterLaw").returns( 111 | {"allow_conversions" => true} 112 | ) 113 | 114 | klass = Pelusa.to_ast """ 115 | class Foo 116 | def execute 117 | {'a' => 2}.merge({}).to_hash.as_json 118 | end 119 | end""" 120 | 121 | analysis = @lint.check(klass) 122 | analysis.successful?.must_equal true 123 | end 124 | 125 | it 'returns a FailureAnalysis for conversions if not allowed' do 126 | klass = Pelusa.to_ast """ 127 | class Foo 128 | def execute 129 | {'a' => 2}.merge({}).to_hash 130 | end 131 | end""" 132 | 133 | analysis = @lint.check(klass) 134 | analysis.successful?.must_equal false 135 | end 136 | end 137 | end 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /test/pelusa/lint/else_clauses_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Pelusa 4 | module Lint 5 | describe ElseClauses do 6 | before do 7 | @lint = ElseClauses.new 8 | end 9 | 10 | describe '#check' do 11 | describe 'when the class does not use else clauses' do 12 | it 'returns a SuccessAnalysis' do 13 | klass = Pelusa.to_ast """ 14 | class Foo 15 | def initialize 16 | if 3 17 | 8 18 | end 19 | unless 9 20 | 3 21 | end 22 | end 23 | end""" 24 | 25 | analysis = @lint.check(klass) 26 | analysis.successful?.must_equal true 27 | end 28 | end 29 | 30 | describe 'when the class uses else clauses' do 31 | it 'returns a FailureAnalysis' do 32 | klass = Pelusa.to_ast """ 33 | class Foo 34 | def initialize 35 | if 3 36 | 8 37 | else 38 | 9 39 | end 40 | end 41 | end""" 42 | 43 | analysis = @lint.check(klass) 44 | analysis.failed?.must_equal true 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/pelusa/lint/eval_usage_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'test_helper' 3 | 4 | module Pelusa 5 | module Lint 6 | describe EvalUsage do 7 | before { @lint = EvalUsage.new } 8 | 9 | describe '#check' do 10 | describe 'when class does not use eval statement' do 11 | it 'return successful analysis' do 12 | klass_str = < { 24 | "Is below 50 lines" => { 25 | status: "failed", 26 | message: "There are 200 lines." 27 | } 28 | }, 29 | 30 | "Bar" => { 31 | "Is below 50 lines" => { 32 | status: "successful", 33 | message: "" 34 | } 35 | }, 36 | 37 | :filename => "foo.rb" 38 | }) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/pelusa/runner_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Pelusa 4 | describe Runner do 5 | describe '#run' do 6 | before do 7 | @report = stub(empty?: false, report: true) 8 | analyzer = stub(:analyze => @report) 9 | Analyzer.stubs(:new).returns analyzer 10 | end 11 | 12 | it 'runs a single file' do 13 | Pelusa.run(__FILE__).must_equal [@report] 14 | end 15 | 16 | it 'runs multiple files' do 17 | Pelusa.run([__FILE__, __FILE__]).must_equal [@report, @report] 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/pelusa_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Pelusa 4 | describe '.run' do 5 | describe 'without arguments' do 6 | before do 7 | @runner = mock 8 | @output = mock 9 | end 10 | 11 | it 'invokes a new Runner on all Ruby files' do 12 | Runner.expects(:new).with(Lint.all, StdoutReporter).returns @runner 13 | @runner.expects(:run).with([]).returns @output 14 | 15 | Pelusa.run.must_equal @output 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'minitest/spec' 3 | require 'mocha/setup' 4 | 5 | require 'pelusa' 6 | 7 | FIXTURES_PATH = File.dirname(__FILE__) + '/fixtures' 8 | 9 | module Pelusa 10 | def self.to_ast(string) 11 | ::Rubinius::ToolSets.current::ToolSet::Melbourne.parse_string(string) 12 | end 13 | end 14 | --------------------------------------------------------------------------------