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