{"id":13483466,"url":"https://github.com/tcopeland/pippi","last_synced_at":"2025-04-04T11:13:50.601Z","repository":{"id":10830148,"uuid":"13107923","full_name":"tcopeland/pippi","owner":"tcopeland","description":"pippi","archived":false,"fork":false,"pushed_at":"2019-01-05T01:47:37.000Z","size":563,"stargazers_count":286,"open_issues_count":4,"forks_count":11,"subscribers_count":18,"default_branch":"master","last_synced_at":"2025-03-28T10:08:07.479Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tcopeland.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2013-09-25T23:16:07.000Z","updated_at":"2025-01-23T23:57:02.000Z","dependencies_parsed_at":"2022-08-29T11:20:55.736Z","dependency_job_id":null,"html_url":"https://github.com/tcopeland/pippi","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tcopeland%2Fpippi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tcopeland%2Fpippi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tcopeland%2Fpippi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tcopeland%2Fpippi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tcopeland","download_url":"https://codeload.github.com/tcopeland/pippi/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247166168,"owners_count":20894654,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T17:01:11.539Z","updated_at":"2025-04-04T11:13:50.575Z","avatar_url":"https://github.com/tcopeland.png","language":"Ruby","readme":"# Pippi\n\n[![Build Status](http://img.shields.io/travis/tcopeland/pippi.svg)](http://travis-ci.org/tcopeland/pippi)\n\nPippi is a utility for finding suboptimal Ruby class API usage.\n\nConsider this little array:\n\n```ruby\n[1, 2, 3]\n```\n\nNow suppose we want to find the first element in that array that's greater than one. We can use Array#select, which returns another Array, and then use Array#first:\n\n```ruby\n[1, 2, 3].select { |x| x \u003e 1 }.first\n```\n\nOf course that's terribly inefficient. Since we only need one element we don't need to select all elements that match the predicate. We should use Array#detect instead:\n\n```ruby\n[1, 2, 3].detect { |x| x \u003e 1 }\n```\n\nA change like this is a small optimization, but they can add up.  More importantly, they communicate the intent of the programmer; the use of Array#detect makes it clear that we're just looking for the first item to match the predicate.\n\nThis sort of thing can be be found during a code review, or maybe when you're just poking around the code. But why not have a tool find it instead? Thus, pippi. Pippi observes code while it's running - by hooking into your test suite execution - and reports misuse of class-level APIs.\n\nThere are many nifty Ruby static analysis tools - flay, reek, flog, etc. This is not like those. It doesn't parse source code; it doesn't examine an abstract syntax tree or even sequences of MRI instructions. So it cannot find the types of issues that those tools can find. Instead, it's focused on runtime analysis; that is, method calls and method call sequences.\n\nHere's an important caveat: pippi is not, and more importantly cannot, be free of false positives. That's because of the halting problem. Pippi finds suboptimal API usage based on data flows as driven by a project's test suite. There may be alternate data flows where this API usage is correct. For example, in the code below, if rand \u003c 0.5 is true, then the Array will be mutated and the program cannot correctly be simplified by replacing \"select followed by first\" with \"detect\":\n\n```ruby\nx = [1, 2, 3].select { |y| y \u003e 1 }\nx.reject! { |y| y \u003e 2 } if rand \u003c 0.5\nx.first\n```\n\nThere are various techniques that eliminate many of these false positives. For example, after flagging an issue, pippi watches subsequent method invocations and if those indicate the initial problem report was in error it'll remove the problem from the report.\n\nPippi is entirely dependent on the test suite to execute code in order to find problems. If a project's test code coverage is small, pippi probably won't find much.\n\nHere's how pippi stacks up using the [Aaron Quint](https://twitter.com/aq) [Ruby Performance Character Profiles](https://www.youtube.com/watch?v=cOaVIeX6qGg\u0026t=8m50s) system:\n\n* Specificity - very specific, finds actual detailed usages of bad code\n* Impact - very impactful, slows things down lots\n* Difficulty of Operator Use - easy to install, just a new gemfile entry\n* Readability - results are easy to read\n* Realtimedness - finds stuff right away\n* Special Abilities - ?\n\nFinally, why \"pippi\"? Because Pippi Longstocking was a \u003ca href=\"http://www.laredoisd.org/cdbooks/NOVELS/Pippi%20Longstocking/CH02.txt\"\u003eThing-Finder\u003c/a\u003e, and pippi finds things.\n\n## Usage\n\n### Rails with test-unit\n\n* Add `gem 'pippi'` to the `test` group in your project's `Gemfile`\n* Add this to `test_helper.rb` just before the `require 'rails/test_help'` line\n\n```ruby\nif ENV['USE_PIPPI'].present?\n  Pippi::AutoRunner.new(:checkset =\u003e ENV['PIPPI_CHECKSET'] || \"basic\")\n  # you can also pass in an IO:\n  # Pippi::AutoRunner.new(:checkset =\u003e \"basic\", :io =\u003e $stdout)\nend\n```\n* Run it:\n\n```text\nUSE_PIPPI=true bundle exec rake test:units \u0026\u0026 cat log/pippi.log\n```\n\n* You can also select a different checkset:\n\n```text\nUSE_PIPPI=true PIPPI_CHECKSET=rails bundle exec rake test:units \u0026\u0026 cat log/pippi.log\n```\n\n* And you can run multiple checksets:\n\n```text\nUSE_PIPPI=true PIPPI_CHECKSET=basic,rails bundle exec rake test:units \u0026\u0026 cat log/pippi.log\n```\n\nHere's a [demo Rails application](https://github.com/tcopeland/pippi_demo#pippi-demo).\n\n### Rails with rspec\n\n* Add `gem 'pippi'` to the `test` group in your project's `Gemfile`\n* Add this to `spec/spec_helper.rb` or `spec/rails_helper.rb`, just below the `require 'rspec/rails'` line (if there is one):\n\n```ruby\nif ENV['USE_PIPPI'].present?\n  require 'pippi'\n  Pippi::AutoRunner.new(:checkset =\u003e ENV['PIPPI_CHECKSET'] || \"basic\")\nend\n```\n\n* Run it:\n\n```text\nUSE_PIPPI=true bundle exec rake spec \u0026\u0026 cat log/pippi.log\n```\n\n### As part of a continuous integration job\n\n[Dan Kohn](https://github.com/dankohn) suggests you could use something like:\n\n```bash\nif grep -v gem \u003c log/pippi.log; then echo \"$(wc -l \u003c log/pippi.log) Pippi flaws found\" \u0026\u0026 false; else echo 'No pippi flaws found'; fi\n```\n\n### From the command line:\n\nAssuming you're using bundler:\n\n```bash\n# Add this to your project's Gemfile:\ngem 'pippi'\n# Run 'bundle', see some output\n# To run a particular check:\nbundle exec pippi tmp/tmpfile.rb MapFollowedByFlatten Foo.new.bar out.txt\n# Or to run all the basic Pippi checks on your code and exercise it with MyClass.new.exercise_some_code:\nbundle exec ruby -rpippi/auto_runner -e \"MyClass.new.exercise_some_code\"\n```\n\n\n## Checksets\n\nPippi has the concept of \"checksets\" which are, well, sets of checks.  The current checksets are listed below.\n\n### basic\n\n#### ReverseFollowedByEach\n\nDon't use reverse followed by each; use reverse_each instead\n\nFor example, rather than doing this:\n\n```ruby\n[1,2,3].reverse.each {|x| x+1 }\n```\n\nInstead, consider doing this:\n\n```ruby\n[1,2,3].reverse_each {|x| x+1 }\n```\n\n#### SelectFollowedByAny\n\nDon't use select followed by any?; use any? with a block instead\n\nFor example, rather than doing this:\n\n```ruby\n[1,2,3].select {|x| x \u003e 1 }.any?\n```\n\nInstead, consider doing this:\n\n```ruby\n[1,2,3].any? {|x| x \u003e 1 }\n```\n\n#### SelectFollowedByEmpty\n\nDon't use select followed by empty?; use none? instead\n\nFor example, rather than doing this:\n\n```ruby\n[1,2,3].select {|x| x \u003e 1 }.empty?\n```\n\nInstead, consider doing this:\n\n```ruby\n[1,2,3].none? {|x| x \u003e 1 }\n```\n\n#### SelectFollowedByFirst\n\nDon't use select followed by first; use detect instead\n\nFor example, rather than doing this:\n\n```ruby\n[1,2,3].select {|x| x \u003e 1 }.first\n```\n\nInstead, consider doing this:\n\n```ruby\n[1,2,3].detect {|x| x \u003e 1 }\n```\n\n#### SelectFollowedByNone\n\nDon't use select followed by none?; use none? with a block instead\n\nFor example, rather than doing this:\n\n```ruby\n[1,2,3].select {|x| x \u003e 1 }.none?\n```\n\nInstead, consider doing this:\n\n```ruby\n[1,2,3].none? {|x| x \u003e 1 }\n```\n\n#### SelectFollowedBySelect\n\nDon't use consecutive select blocks; use a single select instead\n\nFor example, rather than doing this:\n\n```ruby\n[1,2,3].select {|x| x \u003e 1 }.select {|x| x \u003e 2 }\n```\n\nInstead, consider doing this:\n\n```ruby\n[1,2,3].select {|x| x \u003e 2 }\n```\n\n#### SelectFollowedBySize\n\nDon't use select followed by size; use count instead\n\nFor example, rather than doing this:\n\n```ruby\n[1,2,3].select {|x| x \u003e 1 }.size\n```\n\nInstead, consider doing this:\n\n```ruby\n[1,2,3].count {|x| x \u003e 1 }\n```\n### buggy\n\n#### AssertWithNil\n\nDon't use assert_equal with nil as a first argument; use assert_nil instead\n\nFor example, rather than doing this:\n\n```ruby\nx = nil ; assert_equal(nil, x)\n```\n\nInstead, consider doing this:\n\n```ruby\nx = nil ; assert_nil(x)\n```\n\n#### MapFollowedByFlatten\n\nDon't use map followed by flatten(1); use flat_map instead\n\nFor example, rather than doing this:\n\n```ruby\n[1,2,3].map {|x| [x,x+1] }.flatten(1)\n```\n\nInstead, consider doing this:\n\n```ruby\n[1,2,3].flat_map {|x| [x, x+1]}\n```\n### rails\n\n#### StripFollowedByEmpty\n\nDon't use String#strip followed by empty?; use String#blank? instead\n\nFor example, rather than doing this:\n\n```ruby\n'   '.strip.empty?\n```\n\nInstead, consider doing this:\n\n```ruby\n'   '.blank?\n```\n\n## Ideas for other problems to detect:\n\n```ruby\n# unnecessary assignment since String#strip! mutates receiver\n# wrong\nx = x.strip!\n# right\nx.strip!\n\n# Use Pathname\n# wrong\nFile.read(File.join(Rails.root, \"config\", \"database.yml\")\n# right\nRails.root.join(\"config\", \"database.yml\").read\n\n# Use Kernel#tap\n# wrong\nx = [1,2]\nx \u003c\u003c 3\nreturn x\n# right\n[1,2].tap {|y| y \u003c\u003c 3 }\n\n\n# Rails checks\n\n# No need to call to_i on ActiveRecord::Base methods passed to route generators\n# wrong\nproduct_path(@product.to_i)\n# right\nproduct_path(@product)\n\n# something with replacing x.map.compact with x.select.map\n````\n\n## Here are some things that Pippi is not well suited for\n\n### \"Use `self.new` vs `MyClass.new`\"\n\nThis is not a good fit for Pippi because it involves a receiver usage that can be detected with static analysis.\n\n**wrong**:\n\n```\nclass Foo\n  def self.bar\n    Foo.new\n  end\nend\n```\n\n**right**:\n\n```\nclass Foo\n  def self.bar\n    self.new\n  end\nend\n```\n\n### Proxying certain String instance methods\n\nYou might wonder why Pippi \"rails\" checkset doesn't have the rule \"replace `\"foobar\".gsub(/foo/, '')` with `\"foobar\".remove(/foo/)`\".  That's because of the behavior of global variables such as `$\u0026`.  This behavior is [nicely explained by Frederick Cheung on this StackOverflow comment](https://stackoverflow.com/questions/30512945/programmatically-alias-method-that-uses-global-variable/30534264#30534264).  It's also broken down by David Black [here](https://www.ruby-forum.com/topic/198458) and by Aaron Patterson and others [here](https://github.com/rails/rails/issues/1555).  Due to the issue explained there, Pippi's technique of prepending a proxy method breaks code that's further downstream when used with the block form of `gsub`.\n\n## TODO\n\n* Clean up this initial hacked out metaprogramming\n* Finish refactoring duplicated code into MethodSequenceChecker\n\n## Developing\n\nTo see teacher output for a file `tmp/baz.rb`:\n\n```bash\nrm -f pippi_debug.log ; PIPPI_DEBUG=1 bundle exec pippi tmp/baz.rb DebugCheck Foo.new.bar tmp/out.txt ; cat pippi_debug.log\n```\n\nWhen trying to find issues in a project:\n\n```bash\n# in project directory (e.g., aasm)\nrm -rf pippi_debug.log pippi.log .bundle/gems/pippi-0.0.1/ .bundle/cache/pippi-0.0.1.gem .bundle/specifications/pippi-0.0.1.gemspec \u0026\u0026 bundle update pippi --local \u0026\u0026 PIPPI_DEBUG=1 bundle exec ruby -rpippi/auto_runner -e \"puts 'hi'\" \u0026\u0026 grep -C 5 BOOM pippi_debug.log\n# or to run some specs with pippi watching:\nrm -rf pippi_debug.log pippi.log .bundle/gems/pippi-0.0.1/ .bundle/cache/pippi-0.0.1.gem .bundle/specifications/pippi-0.0.1.gemspec \u0026\u0026 bundle update pippi --local \u0026\u0026 PIPPI_DEBUG=1 bundle exec ruby -rpippi/auto_runner -Ispec spec/unit/*.rb\n```\n\n## How to do a release\n\n* Bump version number\n* Move anything from 'training' to 'buggy' or elsewhere\n* Tie off Changelog notes\n* Regenerate docs with `pippi:generate_docs`, copy and paste that into README\n* Commit, push\n* Tag the release (e.g., `git tag -a v0.0.8 -m 'v0.0.8' \u0026\u0026 git push origin v0.0.8`)\n* `bundle exec gem build pippi.gemspec`\n* `gem push pippi-x.gem`\n* Update pippi_demo\n\n## Credits\n\n* [Andrew Kozin](https://twitter.com/nepalez): fixes to :io option\n* Christopher Schramm([@cschramm](https://github.com/cschramm)) bugfixes in fault proc clearing\n* Enrique Delgado: Documentation fixes\n* [Evan Phoenix](https://twitter.com/evanphx)([@evanphx](https://github.com/evanphx)) for the idea of watching method invocations at runtime using metaprogramming rather than using `Tracepoint`.\n* Hubert Dąbrowski: Ruby 2.0.0 fixes\n* [Igor Kapkov](https://twitter.com/igasgeek)([@igas](https://github.com/igas)) documentation fixes\n* [Josh Bodah](https://github.com/jbodah): Better logging support\n* [LivingSocial](https://www.livingsocial.com/) for letting me develop and open source this utility.\n* Martin Spickermann: Better output format\n* [Michael Bernstein](https://twitter.com/mrb_bk)([@mrb](https://github.com/mrb)) (of [CodeClimate](https://codeclimate.com/) fame) for an inspirational discussion of code anaysis in general.\n* [Olle Jonsson](https://twitter.com/olleolleolle)([@olleolleolle](https://github.com/olleolleolle)) rubocop fixes\n","funding_links":[],"categories":["Ruby","Code Analysis and Metrics"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftcopeland%2Fpippi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftcopeland%2Fpippi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftcopeland%2Fpippi/lists"}