[
  {
    "path": ".gitignore",
    "content": "/.bundle/\n/.yardoc\n/_yardoc/\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\n*.lock\n"
  },
  {
    "path": ".travis.yml",
    "content": "---\nsudo: false\nlanguage: ruby\ncache: bundler\nrvm:\n  - 2.3.8\nbefore_install: gem install bundler -v 1.17.3\n"
  },
  {
    "path": "Gemfile",
    "content": "source \"https://rubygems.org\"\n\ngit_source(:github) {|repo_name| \"https://github.com/#{repo_name}\" }\n\n# Specify your gem's dependencies in suggest.gemspec\ngemspec\n"
  },
  {
    "path": "README.md",
    "content": "# Suggest\n\ntells you which method does the thing you want to do\n\n## Disclaimer\n\nI don't recommend you ship this in your Gemfile. Keep it in your system's gems (e.g. `gem install`) and load it as needed (e.g. `irb -rsuggest`, `RUBY_OPT=-rsuggest irb`, etc)\n\n## Installation\n\n```\ngem install suggest_rb\n```\n\n## Usage\n\n```rb\nrequire 'suggest'\n\n# Object#what_returns? tells you which method returns the value\n[1,2,3].what_returns? 1\n=> [:first, :min]\n\n# You can also specify the args you want that method to take\n[1,2,3].what_returns? [1], args: [1]\n=> [:first, :take, :grep, :min]\n\n# By default, it only returns methods that don't mutate the object\n[1,2,3].what_returns? [1], args: [1], allow_mutation: true\n=> [:first, :take, :shift, :grep, :min]\n\n# It works on several core modules including String\n\"HELLO\".what_returns? \"hello\"\n=> [:downcase, :swapcase]\n\n# You can also specify a block that you want the method to accept\n[1,2,3,4].what_returns?({true => [2,4], false => [1,3]}) { |n| n % 2 == 0 }\n=> [:group_by]\n\n# Object#what_mutates? tells you which method changes the object to the desired state\n[1,2,3].what_mutates? [2, 3]\n=> [:shift]\n\n# You can also match on the return value\n[1,2,3].what_mutates? [2, 3], returns: 1\n=> [:shift]\n\n[1,2,3].what_mutates? [2, 3], returns: 2\n=> []\n\n# You can specify which args to pass to the method\n[1,2,3].what_mutates? [3], args: [2]\n=> [:shift]\n\n# It also works on a bunch of core modules\n\"HELLO\".what_mutates? \"hello\"\n=> [:swapcase!, :downcase!]\n\n# And you can give it a block as well\n[1,2,3,4].what_mutates? [2,4] { |n| n % 2 == 0 }\n=> [:select!, :keep_if]\n\n# You can use a lambda as an expected\n[1,2,3,4].what_returns? -> (something_that) { something_that.to_i == 4 }\n=> [:count, :length, :size, :last, :max]\n\n# It respects the ruby version\n# ruby 2.4.3\n{a: 1, b: 2}.what_returns?({})\n=> []\n# ruby 2.5.0\n{a: 1, b: 2}.what_returns?({})\n=> [:slice]\n```\n\n## Note to Self\n\nSnippet to use in `bin/console` for finding methods for blacklisting:\n\n```\nSuggest::SUGGEST_MODS.flat_map { |k| [k].product(k.instance_methods) }.select { |k, v| v == :rand }.map { |k, v| k.instance_method(v).owner }.uniq\n```\n"
  },
  {
    "path": "Rakefile",
    "content": "require \"bundler/gem_tasks\"\nrequire \"rake/testtask\"\n\nRake::TestTask.new(:test) do |t|\n  t.libs << \"test\"\n  t.libs << \"lib\"\n  t.test_files = FileList[\"test/**/*_test.rb\"]\nend\n\ntask :default => :test\n"
  },
  {
    "path": "bin/console",
    "content": "#!/usr/bin/env ruby\n\nrequire \"bundler/setup\"\nrequire \"suggest\"\n\n# You can add fixtures and/or initialization code here to make experimenting\n# with your gem easier. You can also use a different console, if you like.\n\n# (If you use this, don't forget to add pry to your Gemfile!)\n# require \"pry\"\n# Pry.start\n\nrequire \"irb\"\nIRB.start(__FILE__)\n"
  },
  {
    "path": "bin/setup",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\nIFS=$'\\n\\t'\nset -vx\n\nbundle install\n\n# Do any other automated setup that you need to do here\n"
  },
  {
    "path": "lib/suggest/version.rb",
    "content": "module Suggest\n  VERSION = \"0.5.2\"\nend\n"
  },
  {
    "path": "lib/suggest.rb",
    "content": "require \"suggest/version\"\nrequire \"set\"\n\nmodule Suggest\n  SUGGEST_MODS = Set.new([\n    Array,\n    BasicObject,\n    Comparable,\n    Complex,\n    Enumerable,\n    FalseClass,\n    Float,\n    Hash,\n    Integer,\n    Math,\n    NilClass,\n    Numeric,\n    Range,\n    Regexp,\n    Regexp,\n    Set,\n    String,\n    Struct,\n    Symbol,\n    Time,\n    TrueClass,\n  ])\n\n  UNSAFE_WITH_BLOCK = Set.new([\n    [Array, :cycle],\n    [Enumerable, :cycle]\n  ])\n\n  INCONSISTENT = Set.new([\n    [Array, :sample],\n    [Array, :shuffle],\n    [Array, :shuffle!]\n  ])\n\n  TOO_COMPLICATED = Set.new([\n    [String, :freeze],\n    [Set, :freeze],\n    [Set, :taint],\n    [Set, :untaint],\n    [Numeric, :singleton_method_added],\n    [Numeric, :clone],\n    [Numeric, :dup],\n    [BasicObject, :instance_eval],\n    [BasicObject, :instance_exec],\n    [BasicObject, :__send__],\n    [BasicObject, :singleton_method_added],\n    [BasicObject, :singleton_method_removed],\n    [BasicObject, :singleton_method_undefined]\n  ])\n\n  SELECTOR = ->(m) do\n    SUGGEST_MODS.include?(m.owner) &&\n      !INCONSISTENT.include?([m.owner, m.name]) &&\n      !TOO_COMPLICATED.include?([m.owner, m.name])\n  end\n\n  module Mixin\n    def what_returns?(expected, args: [], allow_mutation: false, allow_not_public: false, &block)\n      methods.map(&method(:method)).select(&SELECTOR).select do |m|\n        arity = m.arity\n        next unless arity < 0 || arity == args.count\n\n        post = clone\n\n        next if block && UNSAFE_WITH_BLOCK.include?([m.owner, m.name])\n        result = post.__send__(allow_not_public ? :send : :public_send, m.name, *args, &block) rescue next\n\n        next unless allow_mutation || self == post\n\n        if expected.is_a?(Proc) && expected.lambda?\n          expected.call(result) rescue false\n        else\n          Suggest.eq?(result, expected)\n        end\n      end.map(&:name)\n    end\n\n    def what_mutates?(expected, args: [], allow_not_public: false, **opts, &block)\n      methods.map(&method(:method)).select(&SELECTOR).select do |m|\n        arity = m.arity\n        next unless arity < 0 || arity == args.count\n\n        post = clone\n\n        next if block && UNSAFE_WITH_BLOCK.include?([m.owner, m.name])\n        result = post.__send__(allow_not_public ? :send : :public_send, m.name, *args, &block) rescue next\n\n        next if opts.key?(:returns) && !Suggest.eq?(result, opts[:returns])\n\n        Suggest.eq?(post, expected)\n      end.map(&:name)\n    end\n  end\n\n  def self.eq?(result, expected)\n    result.is_a?(expected.class) && result == expected\n  end\n\n  def self.suggestable!(mod, **corrections) # unsafe_with_block: [], inconsistent: [], too_complicated: []\n    raise ArgumentError.new(\"Must support smart comparison (implement «#{mod}#==»)\") if mod.instance_method(:==).owner == BasicObject\n\n    SUGGEST_MODS << mod\n    %w[unsafe_with_block inconsistent too_complicated].each do |correction|\n      c = Suggest.const_get(correction.upcase)\n      [mod].product(corrections.fetch(correction, [])).each(&c.method(:<<))\n    end\n    mod.include(Suggest::Mixin) unless mod.ancestors.include?(Suggest::Mixin)\n  end\n\n  def self.suggestable_methods\n    SUGGEST_MODS.each_with_object([]) do |mod, candidates|\n      owned_methods = mod.instance_methods.select { |m| mod.instance_method(m).owner == mod }\n      next if owned_methods.none?\n      candidates += [mod].product(owned_methods)\n    end.reject do |m|\n      INCONSISTENT.include?(m) || TOO_COMPLICATED.include?(m)\n    end\n  end\nend\n\nSuggest::SUGGEST_MODS.each do |mod|\n  mod.include(Suggest::Mixin) unless mod.ancestors.include?(Suggest::Mixin)\nend\n"
  },
  {
    "path": "suggest.gemspec",
    "content": "\nlib = File.expand_path(\"../lib\", __FILE__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)\nrequire \"suggest/version\"\n\nGem::Specification.new do |spec|\n  spec.name          = \"suggest_rb\"\n  spec.version       = Suggest::VERSION\n  spec.authors       = [\"Josh Bodah\"]\n  spec.email         = [\"jbodah@cargurus.com\"]\n\n  spec.summary       = %q{tells you which method does the thing you want to do}\n  spec.homepage      = \"https://github.com/jbodah/suggest_rb\"\n\n  # Specify which files should be added to the gem when it is released.\n  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.\n  spec.files         = Dir.chdir(File.expand_path('..', __FILE__)) do\n    `git ls-files -z`.split(\"\\x0\").reject { |f| f.match(%r{^(test|spec|features)/}) }\n  end\n  spec.bindir        = \"exe\"\n  spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }\n  spec.require_paths = [\"lib\"]\n\n  spec.add_development_dependency \"bundler\"\n  spec.add_development_dependency \"rake\", \"~> 10.0\"\n  spec.add_development_dependency \"minitest\", \"~> 5.0\"\n  spec.add_development_dependency \"minitest-tagz\"\n  spec.add_development_dependency \"pry-byebug\"\nend\n"
  },
  {
    "path": "test/suggest_test.rb",
    "content": "require \"test_helper\"\n\nclass SuggestTest < Minitest::Spec\n  describe \"#what_returns?\" do\n    it \"works for Arrays\" do\n      assert_includes [1,2,3].what_returns?(1), :first\n      assert_includes [1,2,3].what_returns?(1), :min\n    end\n\n    it \"doesn't return methods that mutate\" do\n      refute_includes [1,2,3].what_returns?([1], args: [1]), :shift\n      assert_includes [1,2,3].what_returns?([1], args: [1]), :take\n    end\n\n    it \"can be told to allow mutation\" do\n      assert_includes [1,2,3].what_returns?([1], args: [1], allow_mutation: true), :shift\n      assert_includes [1,2,3].what_returns?([1], args: [1], allow_mutation: true), :take\n    end\n\n    it \"works on Strings\" do\n      assert_includes \"HELLO\".what_returns?(\"hello\"), :downcase\n      refute_includes \"HELLO\".what_returns?(\"hello\"), :downcase!\n\n      assert_includes \"HELLO\".what_returns?(\"hello\", allow_mutation: true), :downcase\n      assert_includes \"HELLO\".what_returns?(\"hello\", allow_mutation: true), :downcase!\n    end\n\n    it \"works on block expressions\" do\n      rv = [1,2,3,4].what_returns?({true => [2,4], false => [1,3]}) { |n| n % 2 == 0 }\n      assert_includes rv, :group_by\n    end\n\n    it \"doesn't return inconsistent methods\" do\n      rv = [1].what_returns?(1)\n      refute_includes rv, :sample\n\n      rv = [1].what_returns?([1])\n      refute_includes rv, :shuffle\n    end\n\n    it \"returns a private method of arity -2\" do\n      rv = Set.new([1]).what_returns? Set.new([1]), args: [[1]]\n      refute_includes rv, :flatten_merge\n\n      rv = Set.new([1]).what_returns? Set.new([1]), args: [[1]], allow_not_public: true\n      assert_includes rv, :flatten_merge\n    end\n\n    it \"allows dynamic convertion of anything to suggestable\" do\n      rv = NotYetSuggestable.new.what_returns?(42)\n      refute_includes rv, :foo\n\n      Suggest.suggestable!(NotYetSuggestable)\n      rv = NotYetSuggestable.new.what_returns?(42)\n      assert_includes rv, :foo\n\n      assert_raises ArgumentError do\n        Suggest.suggestable!(NotSuggestable)\n      end\n    end\n\n    it \"given a lambda, yields to the lambda to see if result is equal\" do\n      rv = [1,2,3].what_returns? -> (thing) { thing.to_s == \"1\" }\n      assert_includes rv, :first\n    end\n\n    it \"given a lambda, doesn't blow up\" do\n      [1,2,3].what_returns? -> (thing) { thing.first == 1 }\n    end\n  end\n\n  describe \"#what_mutates?\" do\n    it \"returns methods that mutate\" do\n      assert_includes [1,2,3].what_mutates?([2, 3]), :shift\n    end\n\n    it \"can check return values\" do\n      assert_includes [1,2,3].what_mutates?([2, 3], returns: 1), :shift\n    end\n\n    it \"can be passed args\" do\n      assert_includes [1,2,3].what_mutates?([3], args: [2]), :shift\n    end\n\n    it \"works on Strings\" do\n      assert_includes \"HELLO\".what_mutates?(\"hello\"), :downcase!\n    end\n\n    it \"works on block expressions\" do\n      rv = [1,2,3,4].what_mutates?([2,4]) { |n| n % 2 == 0 }\n      assert_includes rv, :select!\n    end\n\n    it \"doesn't return inconsistent methods\" do\n      rv = [1].what_mutates?([1])\n      refute_includes rv, :shuffle!\n    end\n  end\n\n  describe \"suggestable_methods\" do\n    it \"skips scary methods\" do\n      scary = [\n        :taint,\n        :untaint,\n        :freeze,\n        :trust,\n        :untrust,\n        /method_added/,\n        /variable/,\n        /method/,\n        :clone,\n        :dup,\n      ]\n\n      scary.each do |s|\n        found = Suggest.suggestable_methods.find { |_klass, name| s === name }\n        assert_nil found, \"didn't expect #{found.inspect}\"\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/test_helper.rb",
    "content": "require \"bundler/setup\"\n\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire \"suggest\"\n\nrequire \"minitest/autorun\"\nrequire \"minitest/spec\"\nrequire \"minitest/pride\"\n\nclass NotYetSuggestable\n  def foo\n    42\n  end\n\n  def ==(other)\n    other.is_a?(NotYetSuggestable) && other.foo == foo\n  end\nend\n\nclass NotSuggestable; end\n"
  }
]