├── .gitignore ├── object_shadow.png ├── Gemfile ├── lib ├── object_shadow │ ├── version.rb │ ├── object_method.rb │ ├── basic_object.rb │ ├── wrap.rb │ ├── instance_variables.rb │ ├── info_inspect.rb │ ├── deep_inspect.rb │ └── method_introspection.rb └── object_shadow.rb ├── CHANGELOG.md ├── object_shadow.gemspec ├── Rakefile ├── MIT-LICENSE.txt ├── .github └── workflows │ └── test.yml ├── spec ├── object_shadow_instance_variables_spec.rb └── object_shadow_method_introspection_spec.rb ├── CODE_OF_CONDUCT.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | /pkg 3 | -------------------------------------------------------------------------------- /object_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janlelis/object_shadow/main/object_shadow.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "minitest" 6 | gem "rake" 7 | gem "irb" 8 | gem "wirb" 9 | gem "paint" -------------------------------------------------------------------------------- /lib/object_shadow/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "basic_object" 4 | 5 | class ObjectShadow 6 | VERSION = "1.1.1" 7 | end 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## CHANGELOG 2 | 3 | ### 1.1.1 4 | 5 | * Relax Ruby requirement to allow Ruby 3.0 6 | 7 | ### 1.1.0 8 | 9 | * Add DeepInspect mode 10 | 11 | ### 1.0.0 12 | 13 | * Initial release 14 | 15 | -------------------------------------------------------------------------------- /lib/object_shadow/object_method.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ObjectShadow 4 | module ObjectMethod 5 | def shadow 6 | ObjectShadow.new(self) 7 | end 8 | end 9 | end 10 | 11 | Object.include(ObjectShadow::ObjectMethod) 12 | -------------------------------------------------------------------------------- /lib/object_shadow/basic_object.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ObjectShadow < BasicObject 4 | def class 5 | ::ObjectShadow 6 | end 7 | 8 | def respond_to?(what) 9 | ::ObjectShadow.instance_methods.include?(what) 10 | end 11 | 12 | def instance_of?(other) 13 | other == ::ObjectShadow 14 | end 15 | 16 | def is_a?(other) 17 | other.ancestors.include? ::ObjectShadow 18 | end 19 | 20 | def singleton_class 21 | class << self; self end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/object_shadow/wrap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ObjectShadow 4 | module Wrap 5 | attr_reader :object 6 | 7 | def initialize(object) 8 | @object = object 9 | end 10 | 11 | # Since shadows are not supposed to be passed around, to_s is left neutral 12 | def to_s 13 | "#" 14 | end 15 | 16 | # The base inspect is boring, too, but it will be improved by InfoInspect 17 | alias inspect to_s 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/object_shadow.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "object_shadow/basic_object" 4 | require_relative "object_shadow/version" 5 | require_relative "object_shadow/object_method" 6 | 7 | require_relative "object_shadow/wrap" 8 | require_relative "object_shadow/instance_variables" 9 | require_relative "object_shadow/method_introspection" 10 | require_relative "object_shadow/info_inspect" 11 | require_relative "object_shadow/deep_inspect" 12 | 13 | class ObjectShadow 14 | include Wrap 15 | include InstanceVariables 16 | include MethodIntrospection 17 | include InfoInspect 18 | end -------------------------------------------------------------------------------- /object_shadow.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | require File.dirname(__FILE__) + "/lib/object_shadow/version" 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = "object_shadow" 7 | gem.version = ObjectShadow::VERSION 8 | gem.summary = "Metaprogramming Level 2" 9 | gem.description = "provides a simple convenient API for accessing an object's state." 10 | gem.authors = ["Jan Lelis"] 11 | gem.email = ["hi@ruby.consulting"] 12 | gem.homepage = "https://github.com/janlelis/object_shadow" 13 | gem.license = "MIT" 14 | 15 | gem.files = Dir["{**/}{.*,*}"].select{ |path| File.file?(path) && path !~ /^pkg/ && path !~ /png\z/ } 16 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ["lib"] 19 | gem.metadata = { "rubygems_mfa_required" => "true" } 20 | 21 | gem.required_ruby_version = ">= 2.0", "< 4.0" 22 | end 23 | -------------------------------------------------------------------------------- /lib/object_shadow/instance_variables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ObjectShadow 4 | module InstanceVariables 5 | # Returns the instance variable given 6 | # Please note: Does not expect @ prefix 7 | def [](ivar_name) 8 | object.instance_variable_get(:"@#{ivar_name}") 9 | end 10 | 11 | # Sets the instance variable given 12 | # Please note: Does not expect @ prefix 13 | def []=(ivar_name, value) 14 | object.instance_variable_set(:"@#{ivar_name}", value) 15 | end 16 | 17 | def remove(ivar_name) 18 | object.remove_instance_variable(:"@#{ivar_name}") 19 | end 20 | 21 | def variable?(ivar_name) 22 | object.instance_variable_defined?(:"@#{ivar_name}") 23 | end 24 | 25 | def variables 26 | object.instance_variables.map{ |ivar| ivar[1..-1].to_sym } 27 | end 28 | 29 | def to_h 30 | variables.map{ |ivar| [ivar, self[ivar]] }.to_h 31 | end 32 | 33 | def to_a 34 | variables.map{ |ivar| self[ivar] } 35 | end 36 | end 37 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # # # 2 | # Get gemspec info 3 | 4 | gemspec_file = Dir["*.gemspec"].first 5 | gemspec = eval File.read(gemspec_file), binding, gemspec_file 6 | info = "#{gemspec.name} | #{gemspec.version} | " \ 7 | "#{gemspec.runtime_dependencies.size} dependencies | " \ 8 | "#{gemspec.files.size} files" 9 | 10 | # # # 11 | # Gem build and install task 12 | 13 | desc info 14 | task :gem do 15 | puts info + "\n\n" 16 | print " "; sh "gem build #{gemspec_file}" 17 | FileUtils.mkdir_p "pkg" 18 | FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", "pkg" 19 | puts; sh %{gem install --no-document pkg/#{gemspec.name}-#{gemspec.version}.gem} 20 | end 21 | 22 | # # # 23 | # Start an IRB session with the gem loaded 24 | 25 | desc "#{gemspec.name} | IRB" 26 | task :irb do 27 | sh "irb -I ./lib -r #{gemspec.name.gsub '-','/'}" 28 | end 29 | 30 | # # # 31 | # Run specs 32 | 33 | desc "#{gemspec.name} | Spec" 34 | task :spec do 35 | ruby "spec/object_shadow_instance_variables_spec.rb" 36 | ruby "spec/object_shadow_method_introspection_spec.rb" 37 | end 38 | task default: :spec 39 | 40 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2021 Jan Lelis, https://janlelis.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Ruby ${{ matrix.ruby }} (${{ matrix.os }}) 8 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 9 | strategy: 10 | matrix: 11 | ruby: 12 | - 3.1 13 | - 3.0 14 | - 2.7 15 | - 2.6 16 | os: 17 | - ubuntu-latest 18 | # - macos-latest 19 | runs-on: ${{matrix.os}} 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{matrix.ruby}} 26 | bundler-cache: true 27 | - name: Run tests 28 | run: bundle exec rake 29 | 30 | test-windows: 31 | name: Ruby ${{ matrix.ruby }} (windows-latest) 32 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 33 | strategy: 34 | matrix: 35 | ruby: 36 | # - 3.1 37 | - 3.0 38 | - 2.7 39 | - 2.6 40 | runs-on: windows-latest 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: Set up Ruby 44 | uses: ruby/setup-ruby@v1 45 | with: 46 | ruby-version: ${{matrix.ruby}} 47 | bundler-cache: true 48 | - run: cinst ansicon 49 | - name: Run tests 50 | run: bundle exec rake 51 | -------------------------------------------------------------------------------- /spec/object_shadow_instance_variables_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../lib/object_shadow" 2 | require "minitest/autorun" 3 | 4 | describe "Object#shadow" do 5 | let :c do 6 | Class.new do 7 | def initialize 8 | @ivar = 42 9 | @another_variable = 43 10 | end 11 | end 12 | end 13 | 14 | let :o do 15 | c.new 16 | end 17 | 18 | describe "#variables" do 19 | it "shows list of instance variable names" do 20 | assert_equal \ 21 | [:ivar, :another_variable], 22 | o.shadow.variables 23 | end 24 | end 25 | 26 | describe "#variable?" do 27 | it "returns true if variable is defined" do 28 | assert \ 29 | o.shadow.variable?(:ivar) 30 | end 31 | 32 | it "returns false if variable is not defined" do 33 | refute \ 34 | o.shadow.variable?(:ovar) 35 | end 36 | end 37 | 38 | describe "#[]" do 39 | it "returns value of given instance variable name" do 40 | assert_equal \ 41 | 42, 42 | o.shadow[:ivar] 43 | end 44 | end 45 | 46 | describe "#[]=" do 47 | it "sets value of given instance variable name" do 48 | o.shadow[:ivar] = 1 49 | assert_equal \ 50 | 1, 51 | o.instance_variable_get(:@ivar) 52 | end 53 | end 54 | 55 | describe "to_h" do 56 | it "returns hash of all instance variables" do 57 | assert_equal( 58 | { ivar: 42, another_variable: 43 }, 59 | o.shadow.to_h 60 | ) 61 | end 62 | end 63 | 64 | describe "to_a" do 65 | it "returns array of all instance variable values" do 66 | assert_equal \ 67 | [42, 43], 68 | o.shadow.to_a 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/object_shadow/info_inspect.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ObjectShadow 4 | module InfoInspect 5 | def inspect 6 | public_methods = methods(scope: :public) 7 | protected_methods = methods(scope: :protected) 8 | private_methods = methods(scope: :private) 9 | 10 | inherit_till = object.instance_of?(Object) ? :all : Object 11 | lookup_chain = method_lookup_chain(inherit: inherit_till).join(" → ") + " → …" 12 | 13 | res = \ 14 | "# ObjectShadow of Object #%{object_id}\n\n" \ 15 | "## Lookup Chain\n\n%{method_lookup_chain}\n\n" 16 | 17 | unless variables.empty? 18 | res += "## %{variables_count} Instance Variable%{variables_plural}\n\n%{variables}\n\n" \ 19 | end 20 | 21 | unless public_methods.empty? 22 | res += "## %{public_methods_count} Public Method%{public_methods_plural} (Non-Class/Object)\n\n%{public_methods}\n\n" \ 23 | end 24 | 25 | unless protected_methods.empty? 26 | res += "## %{protected_methods_count} Protected Method%{protected_methods_plural} (Non-Class/Object)\n\n%{protected_methods}\n\n" \ 27 | end 28 | 29 | unless private_methods.empty? 30 | res += "## %{private_methods_count} Private Method%{private_methods_plural} (Non-Class/Object)\n\n%{private_methods}\n\n" \ 31 | end 32 | 33 | res % { 34 | object_id: object.object_id, 35 | method_lookup_chain: InfoInspect.column100(lookup_chain), 36 | variables_count: variables.size, 37 | variables_plural: variables.size == 1 ? "" : "s", 38 | variables: InfoInspect.column100(variables.inspect), 39 | public_methods_count: public_methods.size, 40 | public_methods_plural: public_methods.size == 1 ? "" : "s", 41 | public_methods: InfoInspect.column100(public_methods.inspect), 42 | protected_methods_count: protected_methods.size, 43 | protected_methods_plural: protected_methods.size == 1 ? "" : "s", 44 | protected_methods: InfoInspect.column100(protected_methods.inspect), 45 | private_methods_count: private_methods.size, 46 | private_methods_plural: private_methods.size == 1 ? "" : "s", 47 | private_methods: InfoInspect.column100(private_methods.inspect), 48 | } 49 | end 50 | 51 | class << self 52 | def column100(input) 53 | words = input.split(" ") 54 | lines = [""] 55 | words.each{ |word| 56 | if lines[-1].size + word.size < 95 # 95 + 1 word space + 4 indent spaces = 95 57 | lines[-1] = lines[-1] + word + " " 58 | else 59 | lines << word + " " 60 | end 61 | } 62 | 63 | lines.map{ |line| 64 | " #{line}" 65 | }.join("\n") 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource@janlelis.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /lib/object_shadow/deep_inspect.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require "paint" 5 | require "wirb" 6 | require "io/console" 7 | rescue LoadError 8 | warn "Not loading ObjectShadow::DeepInspect (required gems missing/not bundled)" 9 | end 10 | 11 | class ObjectShadow 12 | # Improve shadow#inspect 13 | # Optional, because it requires the following gems to be installed: 14 | # - paint 15 | # - wirb 16 | # - io-console 17 | module DeepInspect 18 | def inspect 19 | public_methods = methods(scope: :public) 20 | protected_methods = methods(scope: :protected) 21 | private_methods = methods(scope: :private) 22 | 23 | inherit_till = object.instance_of?(Object) ? :all : Object 24 | lookup_chain = (method_lookup_chain(inherit: inherit_till) + ["…"]).inspect 25 | 26 | res = \ 27 | Paint["# ObjectShadow of Object #%{object_id}\n\n", :underline] + 28 | Paint["## Lookup Chain\n\n", :bold] + 29 | "%{method_lookup_chain}\n\n" 30 | 31 | unless variables.empty? 32 | res += Paint["## %{variables_count} Instance Variable%{variables_plural}\n\n%{variables}\n\n", :bold] 33 | end 34 | 35 | unless public_methods.empty? 36 | res += Paint["## %{public_methods_count} Public Method%{public_methods_plural} (Non-Class/Object)\n\n%{public_methods}\n\n", :bold] 37 | end 38 | 39 | unless protected_methods.empty? 40 | res += Paint["## %{protected_methods_count} Protected Method%{protected_methods_plural} (Non-Class/Object)\n\n%{protected_methods}\n\n", :bold] 41 | end 42 | 43 | unless private_methods.empty? 44 | res += Paint["## %{private_methods_count} Private Method%{private_methods_plural} (Non-Class/Object)\n\n%{private_methods}\n\n", :bold] 45 | end 46 | 47 | res += \ 48 | Paint["## Object Inspect\n\n", :bold] + 49 | "%{object_inspect}\n\n" 50 | 51 | res % { 52 | object_id: object.object_id, 53 | method_lookup_chain: ::Wirb.colorize_result(DeepInspect.column100(lookup_chain)), 54 | variables_count: variables.size, 55 | variables_plural: variables.size == 1 ? "" : "s", 56 | variables: ::Wirb.colorize_result(DeepInspect.column100(variables.inspect)), 57 | public_methods_count: public_methods.size, 58 | public_methods_plural: public_methods.size == 1 ? "" : "s", 59 | public_methods: ::Wirb.colorize_result(DeepInspect.column100(public_methods.inspect)), 60 | protected_methods_count: protected_methods.size, 61 | protected_methods_plural: protected_methods.size == 1 ? "" : "s", 62 | protected_methods: ::Wirb.colorize_result(DeepInspect.column100(protected_methods.inspect)), 63 | private_methods_count: private_methods.size, 64 | private_methods_plural: private_methods.size == 1 ? "" : "s", 65 | private_methods: ::Wirb.colorize_result(DeepInspect.column100(private_methods.inspect)), 66 | object_inspect: ::Wirb.colorize_result(DeepInspect.column100(object.inspect)) 67 | } 68 | end 69 | 70 | class << self 71 | def column100(input) 72 | terminal_size = STDOUT.winsize[1] || 100 73 | words = input.split(" ") 74 | lines = [""] 75 | words.each{ |word| 76 | if lines[-1].size + word.size < terminal_size - 9 # x - 1 word space - 4 indent spaces x2 77 | lines[-1] = lines[-1] + word + " " 78 | else 79 | lines << word + " " 80 | end 81 | } 82 | 83 | lines.map{ |line| 84 | " #{line}" 85 | }.join("\n") 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Object#shadow [![[version]](https://badge.fury.io/rb/object_shadow.svg)](https://badge.fury.io/rb/object_shadow) [![[ci]](https://github.com/janlelis/object_shadow/workflows/Test/badge.svg)](https://github.com/janlelis/object_shadow/actions?query=workflow%3ATest) 2 | 3 | Have you ever been [confused by some of Ruby's meta-programming methods?](https://idiosyncratic-ruby.com/25-meta-methodology.html) 4 | 5 | If your answer is *Yes*, you have come to the right place: 6 | 7 | ![Object and Shadow](/object_shadow.png) 8 | 9 | With **shadow**, every Ruby object has a shadow which provides a clean API to access the object's variables and methods. 10 | 11 | Never again you will have to do the `x.methods - Object.methods` trick to get a meaningful method list. 12 | 13 | ## Setup 14 | 15 | Add to your `Gemfile`: 16 | 17 | ```ruby 18 | gem "object_shadow" 19 | ``` 20 | 21 | ## Usage Example 22 | 23 | ```ruby 24 | class P 25 | def a_public_parent_method 26 | end 27 | end 28 | 29 | class C < P 30 | def initialize 31 | @ivar = 42 32 | @another_variable = 43 33 | end 34 | 35 | attr_reader :another_variable 36 | 37 | def a_public_method 38 | end 39 | 40 | protected 41 | 42 | def a_protected_method 43 | end 44 | 45 | private 46 | 47 | def a_private_method 48 | end 49 | end 50 | 51 | object = C.new 52 | 53 | def object.a_public_singleton_method 54 | end 55 | ``` 56 | 57 | ### # Get an Overview 58 | 59 | ```ruby 60 | require "object_shadow" 61 | object.shadow # ObjectShadow of Object #47023274596520 62 | 63 | ## Lookup Chain 64 | 65 | #> → C → P → Object → … 66 | 67 | ## 2 Instance Variables 68 | 69 | [:ivar, :another_variable] 70 | 71 | ## 4 Public Methods (Non-Class/Object) 72 | 73 | [:a_public_method, :a_public_parent_method, :a_public_singleton_method, :another_variable] 74 | 75 | ## 1 Protected Method (Non-Class/Object) 76 | 77 | [:a_protected_method] 78 | 79 | ## 2 Private Methods (Non-Class/Object) 80 | 81 | [:a_private_method, :initialize] 82 | 83 | ``` 84 | 85 | ### # Read & Manipulate Instance Variables 86 | 87 | ```ruby 88 | object.shadow[:ivar] # => 42 89 | object.shadow[:another_variable] = 23; object.another_variable # => 23 90 | object.shadow.variables # => [:ivar, :another_variable] 91 | object.shadow.to_h # => {:ivar=>42, :another_variable=>23} 92 | object.shadow.remove(:ivar) # => 42 (and removed) 93 | ``` 94 | 95 | ### # List Available Methods 96 | 97 | ```ruby 98 | # shadow features a single method called `methods` which takes some keyword arguments for further listing options 99 | object.shadow.methods # => [:a_public_method, :a_public_parent_method, :a_public_singleton_method, :another_variable] 100 | 101 | # Use scope: option to toggle visibility (default is public) 102 | object.shadow.methods(scope: :public) # => [:a_public_method, :a_public_parent_method, :a_public_singleton_method, :another_variable] 103 | object.shadow.methods(scope: :protected) # => [:a_protected_method] 104 | object.shadow.methods(scope: :private) # => [:a_private_method, :initialize] 105 | object.shadow.methods(scope: :all) # => [:a_private_method, :a_protected_method, :a_public_method, :a_public_parent_method, :a_public_singleton_method, :another_variable, :initialize] 106 | 107 | # Use inherit: option to allow or prevent traversal of the inheritance chain 108 | object.shadow.methods(scope: :public, inherit: :singleton) # => [:a_public_singleton_method] 109 | object.shadow.methods(scope: :public, inherit: :self) # => [:a_public_method, :a_public_singleton_method, :another_variable] 110 | object.shadow.methods(scope: :public, inherit: :exclude_object) # => [:a_public_method, :a_public_parent_method, :a_public_singleton_method, :another_variable] 111 | object.shadow.methods(scope: :public, inherit: :all) # => [:!, :!=, :!~, :<=>, :==, :===, :=~, :__id__, :__send__, :a_public_method, :a_public_parent_method, :a_public_singleton_method, :another_variable, :class, :clone, :define_singleton_method, :display, :dup, :enum_for, :eql?, :equal?, :extend, :freeze, :frozen?, :hash, :inspect, :instance_eval, :instance_exec, :instance_of?, :instance_variable_defined?, :instance_variable_get, :instance_variable_set, :instance_variables, :is_a?, :itself, :kind_of?, :method, :methods, :nil?, :object_id, :private_methods, :protected_methods, :public_method, :public_methods, :public_send, :remove_instance_variable, :respond_to?, :send, :shadow, :singleton_class, :singleton_method, :singleton_methods, :taint, :tainted?, :tap, :then, :to_enum, :to_s, :trust, :untaint, :untrust, :untrusted?, :yield_self] 112 | 113 | # Use target: :instances or :class to jump between child and class method listings 114 | C.shadow.methods == C.new.shadow.methods(target: :class) #=> true 115 | C.shadow.methods(target: :instances) == C.new.shadow.methods #=> true 116 | Enumerable.shadow.methods(target: :instances) # (lists Enumerables' methods) 117 | ``` 118 | 119 | ## Documentation 120 | 121 | ### Instance Variables 122 | 123 | Shadow exposes instance variables in a Hash-like manner: 124 | 125 | Method | Description 126 | -----------|------------ 127 | `[]` | Retrieve instance variables. Takes a symbol without `@` to identify variable. 128 | `[]=` | Sets instance variables. Takes a symbol without `@` to identify variable. 129 | `remove` | Removes an instance variables. Takes a symbol without `@` to identify variable. 130 | `variable?`| Checks if a variable with that name exists. Takes a symbol without `@` to identify variable. 131 | `variables`| Returns the list of instance variables as symbols without `@`. 132 | `to_h` | Returns a hash of instance variable names with `@`-less variables names as the keys. 133 | `to_a` | Returns an array of all instance variable values. 134 | 135 | ### Method Introspection 136 | 137 | All method introspection methods get called on the shadow and take a `target:` keyword argument, which defaults to `:self`. It can take one of the following values: 138 | 139 | Value of `target:` | Meaning 140 | -------------------|-------- 141 | `:self` | Operate on the current object 142 | `:class` | Operate on the current object's class (the class for instances, the singleton class for classes) 143 | `:instances` | Operate on potential instances created by the object, which is a class (or module) 144 | 145 | #### `methods(target: :self, scope: :public, inherit: :exclude_class)` 146 | 147 | Returns a list of methods available to the object. 148 | 149 | Only shows methods matching the given `scope:`, i.e. when you request all **public** methods, **protected** and **private** methods will not be included. You can also pass in `:all` to get methods of *all* scopes. 150 | 151 | The `inherit:` option lets you choose how deep you want to dive into the inheritance chain: 152 | 153 | Value of `inherit:` | Meaning 154 | --------------------|-------- 155 | `:singleton` | Show only methods directly defined in the object's singleton class 156 | `:self` | Show singleton methods and methods directly defined in the object's class, but do not traverse the inheritance chain 157 | `:exclude_class` | Stop inheritance chain just before Class or Module. For non-modules it fallbacks to `:exclude_object` 158 | `:exclude_object` | Stop inheritance chain just before Object 159 | `:all` | Show methods from the whole inheritance chain 160 | 161 | #### `method?(method_name, target: :self)` 162 | 163 | Returns `true` if such a method can be found, `false` otherwise 164 | 165 | #### `method_scope(method_name, target: :self)` 166 | 167 | Returns the visibility scope of the method in question, one of `:public`, `:protected`, `:private`. If the method cannot be located, returns `nil`. 168 | 169 | #### `method(method_name, target: :self, unbind: false, all: false)` 170 | 171 | Returns the `Method` or `UnboundMethod` object of the method requested. Use `unbind: true` to force the return value to be an `UnboundMethod` object. Will always return `UnboundMethod`s if used in conjunction with `target: :instances`. 172 | 173 | If you pass in `all: true`, it will return an array of all (unbound) method objects found in the inheritance chain for the given method name. 174 | 175 | #### `method_lookup_chain(target: :self, inherit: :exclude_class)` 176 | 177 | Shows the lookup chain for the target. See `methods()` for description of the `inherit:` option. 178 | 179 | ## Q & A 180 | 181 | ### Can I Access Hidden Instance Variables? 182 | 183 | Some of Ruby's core classes use `@`-less instance variables, such as [Structs](https://ruby-doc.org/core/Struct.html). They cannot be accessed using shadow. 184 | 185 | ### Does It Support Refinements? 186 | 187 | [Currently not.](https://ruby-doc.org/core/doc/syntax/refinements_rdoc.html#label-Methods+Introspection) 188 | 189 | ### Other Meta Programming? 190 | 191 | Only some aspects of Ruby meta-programming are covered. However, **shadow** aims to cover all kinds of meta-programming. Maybe you have an idea about how to integrate `eval`, `method_missing`, and friends? 192 | 193 | ### Does this Gem Include a Secret Mode which Activates an Improved Shadow Inspect? 194 | 195 | Yes, run the following command. 196 | 197 | ```ruby 198 | ObjectShadow.include(ObjectShadow::DeepInspect) 199 | 42.shadow 200 | ``` 201 | 202 | Requires the following gems: **paint**, **wirb**, **io-console** 203 | 204 | 205 | ## J-_-L 206 | 207 | Copyright (C) 2019-2021 Jan Lelis . Released under the MIT license. 208 | 209 | PS: This gem would not exist if the [instance gem](https://rubyworks.github.io/instance/) did not come up with the idea. 210 | -------------------------------------------------------------------------------- /lib/object_shadow/method_introspection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ObjectShadow 4 | module MethodIntrospection 5 | # #shadow#methods returns a sorted list of methods related to the object 6 | # in question. It lets you specify the kind of methods you want to retrieve 7 | # by the following keyword parameters: 8 | # 9 | # target: (default :self) 10 | # 11 | # - :self - This returns the list of methods available to call 12 | # on the current object, including singleton methods 13 | # If the object is a class/module, this means that it 14 | # will return class methods 15 | # 16 | # - :class - This will refer to the object's class (via the `class`) method 17 | # and return its methods, usually to be called from an instance 18 | # If called for a class or module, it will return `Class`' methods 19 | # 20 | # - :instances - This will list all methods which instances of the class (or the class 21 | # that includes the module, in case of a module) in question will have 22 | # Raises an ArgumentError when called on with a non-`Module` 23 | # 24 | # 25 | # scope: (default :public) 26 | # 27 | # - :public - Restrict to to methods of public visibility 28 | # 29 | # - :protected - Restrict to to methods of protected visibility 30 | # 31 | # - :private - Restrict to to methods of private visibility 32 | # 33 | # - :all - Restrict to to methods of public visibility 34 | # 35 | # 36 | # inherit: (default :exclude_object) 37 | # 38 | # - :singleton - Show only methods directly defined in the object's singleton class 39 | # 40 | # - :self - Show singleton methods and methods directly defined in the object's class, 41 | # but do not traverse the inheritance chain 42 | # 43 | # - :exclude_class - Stop inheritance chain just before Class or Module. For 44 | # non-modules it fallbacks to :exclude_object 45 | # 46 | # - :exclude_object - Stop inheritance chain just before Object 47 | # 48 | # - :all - Show methods from the whole inheritance chain 49 | # 50 | def methods(target: :self, scope: :public, inherit: :exclude_class) 51 | MethodIntrospection.lookup_chain_for(object, target, inherit, true).flat_map { |lookup_class| 52 | full_inheritance_lookup = inherit == :all || inherit == true 53 | 54 | case scope 55 | when :public 56 | lookup_class.public_instance_methods(full_inheritance_lookup) 57 | when :protected 58 | lookup_class.protected_instance_methods(full_inheritance_lookup) 59 | when :private 60 | lookup_class.private_instance_methods(full_inheritance_lookup) 61 | when :all 62 | lookup_class.instance_methods(full_inheritance_lookup) + 63 | lookup_class.private_instance_methods(full_inheritance_lookup) 64 | else 65 | Kernel.raise ArgumentError, \ 66 | "(ObjectShadow) Method scope: must be one of [:public, :protected, :private, :all]" 67 | end 68 | }.uniq.sort 69 | end 70 | 71 | # Returns true if a method of this name is defined 72 | # First parameter is the name of the method in question 73 | # 74 | # - target: must be one of [:self, :class, :instances] 75 | # 76 | def method?(method_name, target: :self) 77 | MethodIntrospection.simple_lookup_chain_for(object, target).any?{ |lookup_class| 78 | if RUBY_VERSION >= "2.6.0" 79 | lookup_class.method_defined?(method_name, true) || 80 | lookup_class.private_method_defined?(method_name, true) 81 | else 82 | lookup_class.method_defined?(method_name) || 83 | lookup_class.private_method_defined?(method_name) 84 | end 85 | } 86 | end 87 | 88 | # Returns the scope of method name given 89 | # 90 | # - target: must be one of [:self, :class, :instances] 91 | # 92 | # Possible return values: [:public, :protected, :private, nil] 93 | # 94 | def method_scope(method_name, target: :self) 95 | MethodIntrospection.simple_lookup_chain_for(object, target).map{ |lookup_class| 96 | if RUBY_VERSION >= "2.6.0" 97 | case 98 | when lookup_class.public_method_defined?(method_name, true) 99 | :public 100 | when lookup_class.protected_method_defined?(method_name, true) 101 | :protected 102 | when lookup_class.private_method_defined?(method_name, true) 103 | :private 104 | else 105 | nil 106 | end 107 | else 108 | case 109 | when lookup_class.public_method_defined?(method_name) 110 | :public 111 | when lookup_class.protected_method_defined?(method_name) 112 | :protected 113 | when lookup_class.private_method_defined?(method_name) 114 | :private 115 | else 116 | nil 117 | end 118 | end 119 | }.compact.first 120 | end 121 | 122 | # Returns the objectified reference to a method 123 | # 124 | # - target: must be one of [:self, :class, :instances] 125 | # 126 | # Pass unbind: true if you always want UnboundMethod objects 127 | # 128 | # Pass all: true to not only get the method that would be called, 129 | # but an Array with every occurrence of this method along the inheritance chain 130 | def method(method_name, target: :self, unbind: false, all: false) 131 | if all 132 | MethodIntrospection.lookup_chain_for(object, target, :all).map{ |lookup_class| 133 | if lookup_class.instance_methods(false).include?(method_name) || 134 | lookup_class.private_instance_methods(false).include?(method_name) 135 | lookup_class.instance_method(method_name) 136 | end 137 | }.compact 138 | else 139 | MethodIntrospection.get_method(object, target, method_name, unbind) 140 | end 141 | end 142 | 143 | # Returns the lookup/ancestor chain for an object 144 | # 145 | # - target: must be one of [:self, :class, :instances] 146 | # 147 | # Takes the same inherit options like #methods 148 | def method_lookup_chain(target: :self, inherit: :exclude_class) 149 | MethodIntrospection.lookup_chain_for(object, target, inherit) 150 | end 151 | 152 | class << self 153 | def lookup_chain_for(object, target, inherit, optimize = false) 154 | validate_arguments! object, target, inherit 155 | singleton, klass, chain = get_singleton_klass_and_chain(object, target) 156 | 157 | case inherit 158 | when :singleton 159 | singleton 160 | when :self, false 161 | singleton + klass 162 | when :exclude_class 163 | singleton + chain[0...(chain.index(Class) || chain.index(Module) || chain.index(Object))] 164 | when :exclude_object 165 | singleton + chain[0...chain.index(Object)] 166 | when Module 167 | singleton + chain[0..chain.index(inherit)] 168 | when :all, true 169 | if optimize # full chain build by Ruby using (all=true) param in list 170 | singleton + klass 171 | else 172 | singleton + chain 173 | end 174 | end 175 | end 176 | 177 | def simple_lookup_chain_for(object, target) 178 | validate_arguments! object, target 179 | singleton, klass, = get_singleton_klass_and_chain(object, target) 180 | 181 | singleton + klass 182 | end 183 | 184 | def get_method(object, target, method_name, unbind) 185 | case target 186 | when :self 187 | if unbind 188 | object.method(method_name).unbind() 189 | else 190 | object.method(method_name) 191 | end 192 | when :class 193 | if unbind 194 | object.class.method(method_name).unbind() 195 | else 196 | object.class.method(method_name) 197 | end 198 | when :instances 199 | object.instance_method(method_name) 200 | else 201 | Kernel.raise ArgumentError, "(ObjectShadow) target: must be one of [:self, :class, :instances]" 202 | end 203 | rescue NameError 204 | nil 205 | end 206 | 207 | private 208 | 209 | # Explanation: 210 | # - singleton: [singleton_class] or [] if not retrievable 211 | # - klass: [first entry of inheritance chain] or [] for modules 212 | # - chain: inheritance chain without singleton 213 | def get_singleton_klass_and_chain(object, target) 214 | target_object = target == :class ? object.class : object 215 | 216 | begin 217 | if target == :instances 218 | singleton = [] 219 | klass = target_object.ancestors[0, 1] 220 | chain = target_object.ancestors 221 | else 222 | singleton = [target_object.singleton_class] 223 | 224 | if target_object.is_a? Module 225 | klass = [] 226 | chain = target_object.singleton_class.ancestors[1..-1] 227 | else 228 | klass = target_object.singleton_class.ancestors[1, 1] 229 | chain = target_object.singleton_class.ancestors[1..-1] 230 | end 231 | end 232 | rescue TypeError # e.g. Integer 233 | singleton = [] 234 | klass = target_object.class.ancestors[0, 1] 235 | chain = target_object.class.ancestors 236 | end 237 | 238 | [singleton, klass, chain] 239 | end 240 | 241 | def validate_arguments!(object, target, inherit = nil) 242 | unless [:self, :class, :instances].include?(target) 243 | Kernel.raise ArgumentError, "(ObjectShadow) target: must be one of [:self, :class, :instances]" 244 | end 245 | 246 | if target == :instances 247 | if !object.is_a?(Module) 248 | Kernel.raise ArgumentError, "(ObjectShadow) target: can only be set to :instances for classes and modules" 249 | end 250 | 251 | if inherit == :singleton 252 | Kernel.raise ArgumentError, "(ObjectShadow) cannot request :singleton inheritance when target is :instances" 253 | end 254 | end 255 | end 256 | end 257 | end 258 | end 259 | -------------------------------------------------------------------------------- /spec/object_shadow_method_introspection_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../lib/object_shadow" 2 | require "minitest/autorun" 3 | 4 | describe "Object#shadow" do 5 | let :astronomical_body do 6 | Class.new do 7 | def fly 8 | end 9 | 10 | protected 11 | 12 | def crash 13 | end 14 | 15 | private 16 | 17 | def explode 18 | end 19 | 20 | 21 | class << self 22 | def make 23 | end 24 | 25 | protected 26 | 27 | def stir 28 | end 29 | 30 | private 31 | 32 | def idea 33 | end 34 | end 35 | end 36 | end 37 | 38 | let :planet do 39 | Class.new(astronomical_body) do 40 | def rotate 41 | end 42 | 43 | protected 44 | 45 | def bump 46 | end 47 | 48 | private 49 | 50 | def implode 51 | end 52 | 53 | class << self 54 | def construct 55 | end 56 | 57 | protected 58 | 59 | def prepare 60 | end 61 | 62 | private 63 | 64 | def think 65 | end 66 | end 67 | end 68 | end 69 | 70 | let :earth do 71 | earth = planet.new 72 | 73 | class << earth 74 | def develop 75 | end 76 | 77 | protected 78 | 79 | def magnetize 80 | end 81 | 82 | private 83 | 84 | def repair 85 | end 86 | end 87 | 88 | earth 89 | end 90 | 91 | 92 | describe "#methods" do 93 | describe "[target: :self]" do 94 | describe "[scope: :public]" do 95 | describe "[inherit: :singleton]" do 96 | it "✓" do 97 | assert_equal \ 98 | [:develop], 99 | earth.shadow.methods(target: :self, scope: :public, inherit: :singleton) 100 | end 101 | end 102 | 103 | describe "[inherit: :self, inherit: false]" do 104 | it "✓" do 105 | assert_equal \ 106 | [:develop, :rotate], 107 | earth.shadow.methods(target: :self, scope: :public, inherit: :self) 108 | end 109 | end 110 | 111 | describe "[inherit: :exclude_object]" do 112 | it "✓" do 113 | assert_equal \ 114 | [:develop, :fly, :rotate], 115 | earth.shadow.methods(target: :self, scope: :public, inherit: :exclude_object) 116 | end 117 | end 118 | 119 | describe "[inherit: Module]" do 120 | it "✓" do 121 | assert_equal \ 122 | [:develop, :fly, :rotate], 123 | earth.shadow.methods(target: :self, scope: :public, inherit: astronomical_body) 124 | end 125 | end 126 | 127 | describe "[inherit: :all, inherit: true]" do 128 | it "✓" do 129 | assert_equal \ 130 | ( 131 | [:develop, :fly, :rotate] + 132 | Object.public_instance_methods 133 | ).sort, 134 | earth.shadow.methods(target: :self, scope: :public, inherit: :all) 135 | end 136 | end 137 | end 138 | 139 | describe "[scope: :protected]" do 140 | describe "[inherit: :singleton]" do 141 | it "✓" do 142 | assert_equal \ 143 | [:magnetize], 144 | earth.shadow.methods(target: :self, scope: :protected, inherit: :singleton) 145 | end 146 | end 147 | 148 | describe "[inherit: :self, inherit: false]" do 149 | it "✓" do 150 | assert_equal \ 151 | [:bump, :magnetize], 152 | earth.shadow.methods(target: :self, scope: :protected, inherit: :self) 153 | end 154 | end 155 | 156 | describe "[inherit: :exclude_class]" do 157 | it "✓" do 158 | assert_equal \ 159 | [:bump, :crash, :magnetize], 160 | earth.shadow.methods(target: :self, scope: :protected, inherit: :exclude_class) 161 | end 162 | end 163 | 164 | describe "[inherit: :exclude_object]" do 165 | it "✓" do 166 | assert_equal \ 167 | [:bump, :crash, :magnetize], 168 | earth.shadow.methods(target: :self, scope: :protected, inherit: :exclude_object) 169 | end 170 | end 171 | 172 | describe "[inherit: Module]" do 173 | it "✓" do 174 | assert_equal \ 175 | [:bump, :crash, :magnetize], 176 | earth.shadow.methods(target: :self, scope: :protected, inherit: astronomical_body) 177 | end 178 | end 179 | 180 | describe "[inherit: :all, inherit: true]" do 181 | it "✓" do 182 | assert_equal \ 183 | ( 184 | [:bump, :crash, :magnetize] + 185 | Object.protected_instance_methods 186 | ).sort, 187 | earth.shadow.methods(target: :self, scope: :protected, inherit: :all) 188 | end 189 | end 190 | end 191 | 192 | describe "[scope: :private]" do 193 | describe "[inherit: :singleton]" do 194 | it "✓" do 195 | assert_equal \ 196 | [:repair], 197 | earth.shadow.methods(target: :self, scope: :private, inherit: :singleton) 198 | end 199 | end 200 | 201 | describe "[inherit: :self, inherit: false]" do 202 | it "✓" do 203 | assert_equal \ 204 | [:implode, :repair], 205 | earth.shadow.methods(target: :self, scope: :private, inherit: :self) 206 | end 207 | end 208 | 209 | describe "[inherit: :exclude_class]" do 210 | it "✓" do 211 | assert_equal \ 212 | [:explode, :implode, :repair], 213 | earth.shadow.methods(target: :self, scope: :private, inherit: :exclude_class) 214 | end 215 | end 216 | 217 | describe "[inherit: :exclude_object]" do 218 | it "✓" do 219 | assert_equal \ 220 | [:explode, :implode, :repair], 221 | earth.shadow.methods(target: :self, scope: :private, inherit: :exclude_object) 222 | end 223 | end 224 | 225 | describe "[inherit: Module]" do 226 | it "✓" do 227 | assert_equal \ 228 | [:explode, :implode, :repair], 229 | earth.shadow.methods(target: :self, scope: :private, inherit: astronomical_body) 230 | end 231 | end 232 | 233 | describe "[inherit: :all, inherit: true]" do 234 | it "✓" do 235 | assert_equal \ 236 | ( 237 | [:explode, :implode, :repair] + 238 | Object.private_instance_methods 239 | ).sort, 240 | earth.shadow.methods(target: :self, scope: :private, inherit: :all) 241 | end 242 | end 243 | end 244 | 245 | describe "[scope: :all]" do 246 | describe "[inherit: :singleton]" do 247 | it "✓" do 248 | assert_equal \ 249 | [:develop, :magnetize, :repair], 250 | earth.shadow.methods(target: :self, scope: :all, inherit: :singleton) 251 | end 252 | end 253 | 254 | describe "[inherit: :self, inherit: false]" do 255 | it "✓" do 256 | assert_equal \ 257 | [:bump, :develop, :implode, :magnetize, :repair, :rotate], 258 | earth.shadow.methods(target: :self, scope: :all, inherit: :self) 259 | end 260 | end 261 | 262 | describe "[inherit: :exclude_class]" do 263 | it "✓" do 264 | assert_equal \ 265 | [:bump, :crash, :develop, :explode, :fly, :implode, :magnetize, :repair, :rotate], 266 | earth.shadow.methods(target: :self, scope: :all, inherit: :exclude_object) 267 | end 268 | end 269 | 270 | describe "[inherit: :exclude_object]" do 271 | it "✓" do 272 | assert_equal \ 273 | [:bump, :crash, :develop, :explode, :fly, :implode, :magnetize, :repair, :rotate], 274 | earth.shadow.methods(target: :self, scope: :all, inherit: :exclude_object) 275 | end 276 | end 277 | 278 | describe "[inherit: Module]" do 279 | it "✓" do 280 | assert_equal \ 281 | [:bump, :crash, :develop, :explode, :fly, :implode, :magnetize, :repair, :rotate], 282 | earth.shadow.methods(target: :self, scope: :all, inherit: astronomical_body) 283 | end 284 | end 285 | 286 | describe "[inherit: :all, inherit: true]" do 287 | it "✓" do 288 | assert_equal \ 289 | ( 290 | [:bump, :crash, :develop, :explode, :fly, :implode, :magnetize, :repair, :rotate] + 291 | Object.instance_methods + 292 | Object.private_instance_methods 293 | ).sort, 294 | earth.shadow.methods(target: :self, scope: :all, inherit: :all) 295 | end 296 | end 297 | end 298 | end 299 | 300 | 301 | describe "[target: :class]" do 302 | describe "[scope: :public]" do 303 | describe "[inherit: :singleton]" do 304 | it "✓" do 305 | assert_equal \ 306 | [:construct], 307 | earth.shadow.methods(target: :class, scope: :public, inherit: :singleton) 308 | end 309 | end 310 | 311 | describe "[inherit: :self, inherit: false]" do 312 | it "✓" do 313 | assert_equal \ 314 | [:construct], 315 | earth.shadow.methods(target: :class, scope: :public, inherit: :self) 316 | end 317 | end 318 | 319 | describe "[inherit: :exclude_class]" do 320 | it "✓" do 321 | assert_equal \ 322 | ( 323 | [:construct, :make] + 324 | Object.singleton_class.public_instance_methods(false) + 325 | BasicObject.singleton_class.public_instance_methods(false) 326 | ).sort, 327 | earth.shadow.methods(target: :class, scope: :public, inherit: :exclude_class) 328 | end 329 | end 330 | 331 | describe "[inherit: :exclude_object]" do 332 | it "✓" do 333 | assert_equal \ 334 | ( 335 | [:construct, :make] + 336 | Object.singleton_class.public_instance_methods(false) + 337 | BasicObject.singleton_class.public_instance_methods(false) + 338 | Class.public_instance_methods(false) + 339 | Module.public_instance_methods(false) 340 | ).sort, 341 | earth.shadow.methods(target: :class, scope: :public, inherit: :exclude_object) 342 | end 343 | end 344 | 345 | describe "[inherit: Module]" do 346 | it "✓" do 347 | assert_equal \ 348 | ( 349 | [:construct, :make] + 350 | Object.singleton_class.public_instance_methods(false) 351 | ).sort, 352 | earth.shadow.methods(target: :class, scope: :public, inherit: Object.singleton_class) 353 | end 354 | end 355 | 356 | describe "[inherit: :all, inherit: true]" do 357 | it "✓" do 358 | assert_equal \ 359 | ( 360 | [:construct, :make] + 361 | Object.singleton_class.public_instance_methods(true) 362 | ).sort, 363 | earth.shadow.methods(target: :class, scope: :public, inherit: :all) 364 | end 365 | end 366 | end 367 | 368 | describe "[scope: :protected]" do 369 | describe "[inherit: :singleton]" do 370 | it "✓" do 371 | assert_equal \ 372 | [:prepare], 373 | earth.shadow.methods(target: :class, scope: :protected, inherit: :singleton) 374 | end 375 | end 376 | 377 | describe "[inherit: :self, inherit: false]" do 378 | it "✓" do 379 | assert_equal \ 380 | [:prepare], 381 | earth.shadow.methods(target: :class, scope: :protected, inherit: :self) 382 | end 383 | end 384 | 385 | describe "[inherit: :exclude_class]" do 386 | it "✓" do 387 | assert_equal \ 388 | ( 389 | [:prepare, :stir] + 390 | Object.singleton_class.protected_instance_methods(false) + 391 | BasicObject.singleton_class.protected_instance_methods(false) 392 | ).sort, 393 | earth.shadow.methods(target: :class, scope: :protected, inherit: :exclude_class) 394 | end 395 | end 396 | 397 | describe "[inherit: :exclude_object]" do 398 | it "✓" do 399 | assert_equal \ 400 | ( 401 | [:prepare, :stir] + 402 | Object.singleton_class.protected_instance_methods(false) + 403 | BasicObject.singleton_class.protected_instance_methods(false) + 404 | Class.protected_instance_methods(false) + 405 | Module.protected_instance_methods(false) 406 | ).sort, 407 | earth.shadow.methods(target: :class, scope: :protected, inherit: :exclude_object) 408 | end 409 | end 410 | 411 | describe "[inherit: Module]" do 412 | it "✓" do 413 | assert_equal \ 414 | ( 415 | [:prepare, :stir] + 416 | Object.singleton_class.protected_instance_methods(false) 417 | ).sort, 418 | earth.shadow.methods(target: :class, scope: :protected, inherit: Object.singleton_class) 419 | end 420 | end 421 | 422 | describe "[inherit: :all, inherit: true]" do 423 | it "✓" do 424 | assert_equal \ 425 | ( 426 | [:prepare, :stir] + 427 | Object.singleton_class.protected_instance_methods(true) 428 | ).sort, 429 | earth.shadow.methods(target: :class, scope: :protected, inherit: :all) 430 | end 431 | end 432 | end 433 | 434 | describe "[scope: :private]" do 435 | describe "[inherit: :singleton]" do 436 | it "✓" do 437 | assert_equal \ 438 | [:think], 439 | earth.shadow.methods(target: :class, scope: :private, inherit: :singleton) 440 | end 441 | end 442 | 443 | describe "[inherit: :self, inherit: false]" do 444 | it "✓" do 445 | assert_equal \ 446 | [:think], 447 | earth.shadow.methods(target: :class, scope: :private, inherit: :self) 448 | end 449 | end 450 | 451 | describe "[inherit: :exclude_class]" do 452 | it "✓" do 453 | assert_equal \ 454 | ( 455 | [:think, :idea] + 456 | Object.singleton_class.private_instance_methods(false) + 457 | BasicObject.singleton_class.private_instance_methods(false) 458 | ).sort, 459 | earth.shadow.methods(target: :class, scope: :private, inherit: :exclude_class) 460 | end 461 | end 462 | 463 | describe "[inherit: :exclude_object]" do 464 | it "✓" do 465 | assert_equal \ 466 | ( 467 | [:think, :idea] + 468 | Object.singleton_class.private_instance_methods(false) + 469 | BasicObject.singleton_class.private_instance_methods(false) + 470 | Class.private_instance_methods(false) + 471 | Module.private_instance_methods(false) 472 | ).uniq.sort, 473 | earth.shadow.methods(target: :class, scope: :private, inherit: :exclude_object) 474 | end 475 | end 476 | 477 | describe "[inherit: Module]" do 478 | it "✓" do 479 | assert_equal \ 480 | ( 481 | [:think, :idea] + 482 | Object.singleton_class.private_instance_methods(false) 483 | ).sort, 484 | earth.shadow.methods(target: :class, scope: :private, inherit: Object.singleton_class) 485 | end 486 | end 487 | 488 | describe "[inherit: :all, inherit: true]" do 489 | it "✓" do 490 | assert_equal \ 491 | ( 492 | [:think, :idea] + 493 | Object.singleton_class.private_instance_methods(true) 494 | ).sort, 495 | earth.shadow.methods(target: :class, scope: :private, inherit: :all) 496 | end 497 | end 498 | end 499 | 500 | describe "[scope: :all]" do 501 | describe "[inherit: :singleton]" do 502 | it "✓" do 503 | assert_equal \ 504 | [:construct, :prepare, :think], 505 | earth.shadow.methods(target: :class, scope: :all, inherit: :singleton) 506 | end 507 | end 508 | 509 | describe "[inherit: :self, inherit: false]" do 510 | it "✓" do 511 | assert_equal \ 512 | [:construct, :prepare, :think], 513 | earth.shadow.methods(target: :class, scope: :all, inherit: :self) 514 | end 515 | end 516 | 517 | describe "[inherit: :exclude_class]" do 518 | it "✓" do 519 | assert_equal \ 520 | ( 521 | [:construct, :idea, :make, :prepare, :stir, :think] + 522 | Object.singleton_class.instance_methods(false) + 523 | Object.singleton_class.private_instance_methods(false) + 524 | BasicObject.singleton_class.instance_methods(false) + 525 | BasicObject.singleton_class.private_instance_methods(false) 526 | ).sort, 527 | earth.shadow.methods(target: :class, scope: :all, inherit: :exclude_class) 528 | end 529 | end 530 | 531 | describe "[inherit: :exclude_object]" do 532 | it "✓" do 533 | assert_equal \ 534 | ( 535 | [:construct, :idea, :make, :prepare, :stir, :think] + 536 | Object.singleton_class.instance_methods(false) + 537 | Object.singleton_class.private_instance_methods(false) + 538 | BasicObject.singleton_class.instance_methods(false) + 539 | BasicObject.singleton_class.private_instance_methods(false) + 540 | Class.instance_methods(false) + 541 | Class.private_instance_methods(false) + 542 | Module.instance_methods(false) + 543 | Module.private_instance_methods(false) 544 | ).uniq.sort, 545 | earth.shadow.methods(target: :class, scope: :all, inherit: :exclude_object) 546 | end 547 | end 548 | 549 | describe "[inherit: Module]" do 550 | it "✓" do 551 | assert_equal \ 552 | ( 553 | [:construct, :idea, :make, :prepare, :stir, :think] + 554 | Object.singleton_class.instance_methods(false) + 555 | Object.singleton_class.private_instance_methods(false) 556 | ).sort, 557 | earth.shadow.methods(target: :class, scope: :all, inherit: Object.singleton_class) 558 | end 559 | end 560 | 561 | describe "[inherit: :all, inherit: true]" do 562 | it "✓" do 563 | assert_equal \ 564 | ( 565 | [:construct, :idea, :make, :prepare, :stir, :think] + 566 | Object.singleton_class.instance_methods(true) + 567 | Object.singleton_class.private_instance_methods(true) 568 | ).sort, 569 | earth.shadow.methods(target: :class, scope: :all, inherit: :all) 570 | end 571 | end 572 | end 573 | end 574 | 575 | 576 | describe "[target: :instances]" do 577 | describe "context: called for a non-module" do 578 | it "✗ will raise an ArgumentError" do 579 | assert_raises(ArgumentError) { 580 | earth.shadow.methods(target: :instances) 581 | } 582 | end 583 | end 584 | 585 | describe "[inherit: :singleton]" do 586 | it "✗ will raise an ArgumentError" do 587 | assert_raises(ArgumentError) { 588 | planet.shadow.methods(target: :instances, inherit: :singleton) 589 | } 590 | end 591 | end 592 | 593 | describe "[scope: :public]" do 594 | describe "[inherit: :self, inherit: false]" do 595 | it "✓" do 596 | assert_equal \ 597 | [:rotate], 598 | planet.shadow.methods(target: :instances, scope: :public, inherit: :self) 599 | end 600 | end 601 | 602 | describe "[inherit: :exclude_object]" do 603 | it "✓" do 604 | assert_equal \ 605 | [:fly, :rotate], 606 | planet.shadow.methods(target: :instances, scope: :public, inherit: :exclude_object) 607 | end 608 | end 609 | 610 | describe "[inherit: Module]" do 611 | it "✓" do 612 | assert_equal \ 613 | [:fly, :rotate], 614 | planet.shadow.methods(target: :instances, scope: :public, inherit: astronomical_body) 615 | end 616 | end 617 | 618 | describe "[inherit: :all, inherit: true]" do 619 | it "✓" do 620 | assert_equal \ 621 | ( 622 | [:fly, :rotate] + 623 | Object.public_instance_methods 624 | ).sort, 625 | planet.shadow.methods(target: :instances, scope: :public, inherit: :all) 626 | end 627 | end 628 | end 629 | 630 | describe "[scope: :protected]" do 631 | describe "[inherit: :self, inherit: false]" do 632 | it "✓" do 633 | assert_equal \ 634 | [:bump], 635 | planet.shadow.methods(target: :instances, scope: :protected, inherit: :self) 636 | end 637 | end 638 | 639 | describe "[inherit: :exclude_class]" do 640 | it "✓" do 641 | assert_equal \ 642 | [:bump, :crash], 643 | planet.shadow.methods(target: :instances, scope: :protected, inherit: :exclude_class) 644 | end 645 | end 646 | 647 | describe "[inherit: :exclude_object]" do 648 | it "✓" do 649 | assert_equal \ 650 | [:bump, :crash], 651 | planet.shadow.methods(target: :instances, scope: :protected, inherit: :exclude_object) 652 | end 653 | end 654 | 655 | describe "[inherit: Module]" do 656 | it "✓" do 657 | assert_equal \ 658 | [:bump, :crash], 659 | planet.shadow.methods(target: :instances, scope: :protected, inherit: astronomical_body) 660 | end 661 | end 662 | 663 | describe "[inherit: :all, inherit: true]" do 664 | it "✓" do 665 | assert_equal \ 666 | ( 667 | [:bump, :crash] + 668 | Object.protected_instance_methods 669 | ).sort, 670 | planet.shadow.methods(target: :instances, scope: :protected, inherit: :all) 671 | end 672 | end 673 | end 674 | 675 | describe "[scope: :private]" do 676 | describe "[inherit: :self, inherit: false]" do 677 | it "✓" do 678 | assert_equal \ 679 | [:implode], 680 | planet.shadow.methods(target: :instances, scope: :private, inherit: :self) 681 | end 682 | end 683 | 684 | describe "[inherit: :exclude_class]" do 685 | it "✓" do 686 | assert_equal \ 687 | [:explode, :implode], 688 | planet.shadow.methods(target: :instances, scope: :private, inherit: :exclude_class) 689 | end 690 | end 691 | 692 | describe "[inherit: :exclude_object]" do 693 | it "✓" do 694 | assert_equal \ 695 | [:explode, :implode], 696 | planet.shadow.methods(target: :instances, scope: :private, inherit: :exclude_object) 697 | end 698 | end 699 | 700 | describe "[inherit: Module]" do 701 | it "✓" do 702 | assert_equal \ 703 | [:explode, :implode], 704 | planet.shadow.methods(target: :instances, scope: :private, inherit: astronomical_body) 705 | end 706 | end 707 | 708 | describe "[inherit: :all, inherit: true]" do 709 | it "✓" do 710 | assert_equal \ 711 | ( 712 | [:explode, :implode] + 713 | Object.private_instance_methods 714 | ).sort, 715 | planet.shadow.methods(target: :instances, scope: :private, inherit: :all) 716 | end 717 | end 718 | end 719 | 720 | describe "[scope: :all]" do 721 | describe "[inherit: :self, inherit: false]" do 722 | it "✓" do 723 | assert_equal \ 724 | [:bump, :implode, :rotate], 725 | planet.shadow.methods(target: :instances, scope: :all, inherit: :self) 726 | end 727 | end 728 | 729 | describe "[inherit: :exclude_class]" do 730 | it "✓" do 731 | assert_equal \ 732 | [:bump, :crash, :explode, :fly, :implode, :rotate], 733 | planet.shadow.methods(target: :instances, scope: :all, inherit: :exclude_object) 734 | end 735 | end 736 | 737 | describe "[inherit: :exclude_object]" do 738 | it "✓" do 739 | assert_equal \ 740 | [:bump, :crash, :explode, :fly, :implode, :rotate], 741 | planet.shadow.methods(target: :instances, scope: :all, inherit: :exclude_object) 742 | end 743 | end 744 | 745 | describe "[inherit: Module]" do 746 | it "✓" do 747 | assert_equal \ 748 | [:bump, :crash, :explode, :fly, :implode, :rotate], 749 | planet.shadow.methods(target: :instances, scope: :all, inherit: astronomical_body) 750 | end 751 | end 752 | 753 | describe "[inherit: :all, inherit: true]" do 754 | it "✓" do 755 | assert_equal \ 756 | ( 757 | [:bump, :crash, :explode, :fly, :implode, :rotate] + 758 | Object.instance_methods + 759 | Object.private_instance_methods 760 | ).sort, 761 | planet.shadow.methods(target: :instances, scope: :all, inherit: :all) 762 | end 763 | end 764 | end 765 | end 766 | end 767 | 768 | describe "#method?" do 769 | it "will return true if method exists" do 770 | assert earth.shadow.method?(:bump) 771 | assert earth.shadow.method?(:magnetize) 772 | assert earth.shadow.method?(:stir, target: :class) 773 | 774 | assert planet.shadow.method?(:idea) 775 | assert planet.shadow.method?(:implode, target: :instances) 776 | end 777 | 778 | it "will return false if method does not exist" do 779 | refute earth.shadow.method?(:idea) 780 | refute earth.shadow.method?(:idiosyncratic) 781 | refute earth.shadow.method?(:magnetize, target: :class) 782 | 783 | refute planet.shadow.method?(:rotate) 784 | refute planet.shadow.method?(:stir, target: :instances) 785 | end 786 | end 787 | 788 | describe "#method_scope" do 789 | it "will return :public for public methods" do 790 | assert_equal :public, earth.shadow.method_scope(:rotate) 791 | assert_equal :public, earth.shadow.method_scope(:develop) 792 | assert_equal :public, earth.shadow.method_scope(:make, target: :class) 793 | 794 | assert_equal :public, planet.shadow.method_scope(:construct) 795 | assert_equal :public, planet.shadow.method_scope(:rotate, target: :instances) 796 | end 797 | 798 | it "will return :protected for protected methods" do 799 | assert_equal :protected, earth.shadow.method_scope(:bump) 800 | assert_equal :protected, earth.shadow.method_scope(:magnetize) 801 | assert_equal :protected, earth.shadow.method_scope(:stir, target: :class) 802 | 803 | assert_equal :protected, planet.shadow.method_scope(:prepare) 804 | assert_equal :protected, planet.shadow.method_scope(:bump, target: :instances) 805 | end 806 | 807 | it "will return :private for private methods" do 808 | assert_equal :private, earth.shadow.method_scope(:implode) 809 | assert_equal :private, earth.shadow.method_scope(:repair) 810 | assert_equal :private, earth.shadow.method_scope(:think, target: :class) 811 | 812 | assert_equal :private, planet.shadow.method_scope(:idea) 813 | assert_equal :private, planet.shadow.method_scope(:implode, target: :instances) 814 | end 815 | 816 | it "will return nil if method does not exist" do 817 | refute earth.shadow.method?(:idea) 818 | refute earth.shadow.method?(:magnetize, target: :class) 819 | 820 | refute planet.shadow.method?(:rotate) 821 | refute planet.shadow.method?(:stir, target: :instances) 822 | end 823 | end 824 | 825 | describe "#method" do 826 | it "will return Method object" do 827 | assert_instance_of Method, earth.shadow.method(:bump) 828 | assert_instance_of Method, earth.shadow.method(:magnetize) 829 | assert_instance_of Method, earth.shadow.method(:stir, target: :class) 830 | 831 | assert_instance_of Method, planet.shadow.method(:idea) 832 | end 833 | 834 | it "will return UnboundMethod object for target: instances "do 835 | assert_instance_of UnboundMethod, planet.shadow.method(:implode, target: :instances) 836 | end 837 | 838 | it "will return UnboundMethod object when unbind: true is passed" do 839 | assert_instance_of UnboundMethod, earth.shadow.method(:bump, unbind: true) 840 | end 841 | 842 | it "will return nil if method does not exist" do 843 | assert_nil earth.shadow.method(:fun) 844 | end 845 | 846 | 847 | it "returns an array of all (unbound) methods in the lookup chain if all: true is passed" do 848 | def earth.bump() end 849 | 850 | res = earth.shadow.method(:bump, all: true) 851 | assert_instance_of Array, res 852 | assert_instance_of UnboundMethod, res[0] 853 | assert_instance_of UnboundMethod, res[1] 854 | end 855 | end 856 | 857 | describe "#method_lookup_chain" do 858 | describe "[inherit: :exclude_class]" do 859 | it "shows the lookup chain (including singleton class) for non-classes, stops lookup chain before Class" do 860 | assert_equal \ 861 | [earth.singleton_class, planet, astronomical_body], 862 | earth.shadow.method_lookup_chain 863 | end 864 | 865 | it "shows the lookup chain for classes, stops lookup chain before Class" do 866 | assert_equal \ 867 | [ 868 | planet, 869 | astronomical_body, 870 | Object, 871 | BasicObject, 872 | ].map(&:singleton_class), 873 | planet.shadow.method_lookup_chain 874 | end 875 | end 876 | 877 | describe "[inherit: :all]" do 878 | it "shows the lookup chain (including singleton class) for non-classes" do 879 | assert_equal \ 880 | [ 881 | earth.singleton_class, 882 | planet, 883 | astronomical_body, 884 | Object, 885 | Minitest::Expectations, 886 | ObjectShadow::ObjectMethod, 887 | Kernel, 888 | BasicObject, 889 | ], 890 | earth.shadow.method_lookup_chain(inherit: :all) 891 | end 892 | 893 | it "shows the lookup chain for classes" do 894 | assert_equal \ 895 | [ 896 | planet, 897 | astronomical_body, 898 | Object, 899 | BasicObject, 900 | ].map(&:singleton_class) + [ 901 | Class, 902 | Module, 903 | Object, 904 | Minitest::Expectations, 905 | ObjectShadow::ObjectMethod, 906 | Kernel, 907 | BasicObject, 908 | ], 909 | planet.shadow.method_lookup_chain(inherit: :all) 910 | end 911 | end 912 | end 913 | end 914 | 915 | --------------------------------------------------------------------------------