├── .ruby-version ├── .rspec ├── lib ├── deathnote │ └── version.rb └── deathnote.rb ├── changelog ├── .travis.yml ├── Rakefile ├── exe └── deathnote ├── bin ├── setup └── console ├── .gitignore ├── Gemfile ├── spec ├── deathnote_spec.rb └── spec_helper.rb ├── Gemfile.lock ├── LICENSE.txt ├── deathnote.gemspec └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.4 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/deathnote/version.rb: -------------------------------------------------------------------------------- 1 | module Deathnote 2 | VERSION = '0.1.5' 3 | end 4 | -------------------------------------------------------------------------------- /changelog: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.1.5 4 | 5 | - support to pass whitelist file to debride 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.4.4 5 | before_install: gem install bundler -v 1.16.1 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /exe/deathnote: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "deathnote" 4 | 5 | if Deathnote.run(ARGV).empty? 6 | exit 0 7 | else 8 | exit 1 9 | end 10 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in deathnote.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /spec/deathnote_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Deathnote do 2 | it "has a version number" do 3 | expect(Deathnote::VERSION).not_to be nil 4 | end 5 | 6 | it "does something useful" do 7 | expect(false).to eq(true) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "deathnote" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "deathnote" 3 | 4 | RSpec.configure do |config| 5 | # Enable flags like --only-failures and --next-failure 6 | config.example_status_persistence_file_path = ".rspec_status" 7 | 8 | # Disable RSpec exposing methods globally on `Module` and `main` 9 | config.disable_monkey_patching! 10 | 11 | config.expect_with :rspec do |c| 12 | c.syntax = :expect 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | deathnote (0.1.4) 5 | debride (~> 1.8) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | debride (1.8.1) 11 | path_expander (~> 1.0) 12 | ruby_parser (~> 3.6) 13 | sexp_processor (~> 4.5) 14 | diff-lcs (1.3) 15 | path_expander (1.0.3) 16 | rake (10.5.0) 17 | rspec (3.7.0) 18 | rspec-core (~> 3.7.0) 19 | rspec-expectations (~> 3.7.0) 20 | rspec-mocks (~> 3.7.0) 21 | rspec-core (3.7.1) 22 | rspec-support (~> 3.7.0) 23 | rspec-expectations (3.7.0) 24 | diff-lcs (>= 1.2.0, < 2.0) 25 | rspec-support (~> 3.7.0) 26 | rspec-mocks (3.7.0) 27 | diff-lcs (>= 1.2.0, < 2.0) 28 | rspec-support (~> 3.7.0) 29 | rspec-support (3.7.1) 30 | ruby_parser (3.11.0) 31 | sexp_processor (~> 4.9) 32 | sexp_processor (4.11.0) 33 | 34 | PLATFORMS 35 | ruby 36 | 37 | DEPENDENCIES 38 | bundler (~> 1.16) 39 | deathnote! 40 | rake (~> 10.0) 41 | rspec (~> 3.0) 42 | 43 | BUNDLED WITH 44 | 1.16.1 45 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Shia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /deathnote.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "deathnote/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "deathnote" 8 | spec.version = Deathnote::VERSION 9 | spec.authors = ["Shia"] 10 | spec.email = ["rise.shia@gmail.com"] 11 | 12 | spec.summary = %q{Find out dead code between base branch and compare branch.} 13 | spec.description = %q{Find out dead code between base branch and compare branch. This works on git.} 14 | spec.homepage = "https://github.com/riseshia/deathnote" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|features)/}) 19 | end 20 | spec.bindir = "exe" 21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 22 | spec.require_paths = ["lib"] 23 | 24 | spec.add_development_dependency "bundler", "~> 1.16" 25 | spec.add_development_dependency "rake", "~> 10.0" 26 | spec.add_development_dependency "rspec", "~> 3.0" 27 | spec.add_dependency "debride", "~> 1.8" 28 | end 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deathnote 2 | 3 | deathnote gem receive 2 commit hash and compare those dead code from each version of project code. 4 | This will be helpful if you have a lot false-positive result. 5 | 6 | You may get better understanding with [this slide](https://speakerdeck.com/riseshia/find-out-potential-dead-codes-from-diff). 7 | 8 | ## Requirement 9 | 10 | deathnote assumes you are using git. 11 | It will use `git checkout` to switch to each revision. 12 | 13 | ## Installation 14 | 15 | Add this line to your application's Gemfile: 16 | 17 | ```ruby 18 | gem 'deathnote', require: false 19 | ``` 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install deathnote 24 | 25 | ## Usage 26 | 27 | ``` 28 | Usage: deathnote [options] files_or_dirs 29 | -h, --help Display this help. 30 | -p, --past-commit=[commit hash] Specify past commit hash. 31 | -n, --newer-commit=[commit hash] Specify newer commit hash. 32 | -r, --rails Filter some rails call conversions. 33 | -w, --whitelist=FILE Whitelist with separated by \n 34 | -e, --exclude=FILE1,FILE2,... Exclude files or directories in comma-separated list. 35 | ``` 36 | 37 | ## Development 38 | 39 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 40 | 41 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 42 | 43 | ## Contributing 44 | 45 | Bug reports and pull requests are welcome on GitHub at https://github.com/riseshia/deathnote. 46 | 47 | ## License 48 | 49 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 50 | -------------------------------------------------------------------------------- /lib/deathnote.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | require 'optparse' 3 | 4 | require 'deathnote/version' 5 | require 'debride' 6 | require 'path_expander' 7 | 8 | module Deathnote 9 | class << self 10 | def run(argv) 11 | options = parse_options(argv) 12 | backup_commit = cmd('git rev-parse --abbrev-ref HEAD') 13 | 14 | cmd("git checkout #{options[:past_commit]}") 15 | past_missing = DeadCodes.new(options.deep_clone).run 16 | 17 | cmd("git checkout #{options[:newer_commit]}") 18 | newer_missing = DeadCodes.new(options.deep_clone).run 19 | 20 | newer_missing. 21 | reject { |unused, _location| past_missing.has_key?(unused) }. 22 | each { |unused, location| puts "#{unused} #{location}" } 23 | ensure 24 | cmd("git checkout #{backup_commit}") 25 | end 26 | 27 | private 28 | 29 | def cmd(command) 30 | Open3.popen3(command) { |_i, o, _e, _t| o.read.chomp } 31 | end 32 | 33 | def parse_options(argv) 34 | options = { 35 | whitelist: [] 36 | } 37 | OptionParser.new do |opts| 38 | opts.banner = 'Usage: deathnote [options] files_or_dirs' 39 | 40 | opts.on('-h', '--help', 'Display this help.') do 41 | puts opts 42 | exit 43 | end 44 | opts.on('-p[commit hash]', '--past-commit=[commit hash]', 'Specify past commit hash.') do |past_commit| 45 | options[:past_commit] = past_commit 46 | end 47 | opts.on('-n[commit hash]', '--newer-commit=[commit hash]', 'Specify newer commit hash.') do |newer_commit| 48 | options[:newer_commit] = newer_commit 49 | end 50 | opts.on('-r', '--rails', 'Filter some rails call conversions.') do 51 | options[:rails] = true 52 | end 53 | opts.on('-w', '--whitelist=FILE', 'Whitelist with separated by \n') do 54 | options[:whitelist] = File.read(s).split(/\n+/) rescue [] 55 | end 56 | opts.on('-e', '--exclude=FILE1,FILE2,...', Array, 'Exclude files or directories in comma-separated list.') do |list| 57 | options[:exclude] = list 58 | end 59 | end.parse! 60 | options[:target_paths] = argv 61 | 62 | options 63 | rescue OptionParser::InvalidOption => e 64 | warn "Fail to parse options." 65 | warn e.message 66 | exit 1 67 | end 68 | end 69 | 70 | class DeadCodes 71 | def initialize(options) 72 | @options = options 73 | end 74 | 75 | def run 76 | to_list(run_debride) 77 | end 78 | 79 | private 80 | 81 | def run_debride 82 | debride = Debride.new(@options) 83 | 84 | extensions = Debride.file_extensions 85 | glob = "**/*.{#{extensions.join(",")}}" 86 | expander = PathExpander.new(@options[:target_paths], glob) 87 | files = expander.process 88 | excl = debride.option[:exclude] 89 | files = expander.filter_files files, StringIO.new(excl.join "\n") if excl 90 | 91 | debride.run(files) 92 | debride 93 | end 94 | 95 | def to_list(debride) 96 | unuseds = {} 97 | method_locations = debride.method_locations 98 | 99 | debride.missing.each do |klass, meths| 100 | meths.each do |meth| 101 | type = method_locations["#{klass}##{meth}"].nil? ? '::' : '#' 102 | location = method_locations["#{klass}#{type}#{meth}"] 103 | path = location[/(.+):\d+$/, 1] 104 | 105 | unuseds["#{klass}#{type}#{meth}"] = location 106 | end 107 | end 108 | 109 | unuseds 110 | end 111 | end 112 | end 113 | --------------------------------------------------------------------------------