New Upstream Snapshot - ruby-docile

Ready changes

Summary

Merged new upstream version: 1.4.0+git20220423.1.08cf67d (was: 1.1.5).

Resulting package

Built on 2022-10-21T09:34 (took 3m17s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-snapshots ruby-docile

Diff

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..2fd1771
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,15 @@
+version: 2
+
+updates:
+
+  # Maintain dependencies for GitHub Actions
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "daily"
+
+  # Maintain dependencies for Ruby's Bundler
+  - package-ecosystem: "bundler"
+    directory: "/"
+    schedule:
+      interval: "daily"
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..982b44e
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,42 @@
+name: Main
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+    branches:
+      - main
+jobs:
+  test:
+    name: 'CI Tests'
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest]
+        # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0'
+        ruby: [jruby, truffleruby, 2.5, 2.6, 2.7, '3.0', 3.1, head]
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v3.0.2
+
+      # Conditionally configure bundler via environment variables as advised
+      #   * https://github.com/ruby/setup-ruby#bundle-config
+      - name: Set bundler environment variables
+        run: |
+          echo "BUNDLE_WITHOUT=checks" >> $GITHUB_ENV
+        if: matrix.ruby != 3.1
+
+      - uses: ruby/setup-ruby@v1
+        with:
+          ruby-version: ${{ matrix.ruby }}
+          bundler-cache: true
+
+      - run: bundle exec rspec
+
+      - uses: codecov/codecov-action@v3.1.0
+        with:
+          name: ${{ matrix.ruby }}
+          file: ./coverage/coverage.xml
+
+      - run: bundle exec rubocop
+        if: matrix.ruby == 3.1
diff --git a/.gitignore b/.gitignore
index 3604ac0..b1261ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,6 @@ pkg
 doc
 .yardoc
 coverage
-vendor
\ No newline at end of file
+vendor
+.ruby-gemset
+.ruby-version
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..c3fc127
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,2 @@
+inherit_gem:
+  panolint: panolint-rubocop.yml
diff --git a/.ruby-gemset b/.ruby-gemset
deleted file mode 100644
index 960d9cf..0000000
--- a/.ruby-gemset
+++ /dev/null
@@ -1 +0,0 @@
-docile
diff --git a/.ruby-version b/.ruby-version
deleted file mode 100644
index 9304515..0000000
--- a/.ruby-version
+++ /dev/null
@@ -1 +0,0 @@
-ruby-2.1.0
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 85909c2..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-language: ruby
-cache: bundler
-rvm:
-  - ruby-head
-  - 2.1.2
-  - 2.1.1
-  - 2.1.0
-  - 2.0.0
-  - 1.9.3
-  - 1.9.2
-  - 1.8.7
-  - ree
-  - jruby-head
-  - jruby-19mode
-  - jruby-18mode
-  - rbx-2
-matrix:
-  allow_failures:
-    - rvm: ruby-head
-    - rvm: jruby-head
-  fast_finish: true
diff --git a/Gemfile b/Gemfile
index aabeeca..43b9e22 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,4 +1,26 @@
-source 'https://rubygems.org'
+# frozen_string_literal: true
 
-# Specify gem's dependencies in docile.gemspec
+source "https://rubygems.org"
+
+# Specify gem's runtime dependencies in docile.gemspec
 gemspec
+
+group :test do
+  gem "rspec", "~> 3.10"
+  gem "simplecov", require: false
+
+  # CI-only test dependencies go here
+  if ENV.fetch("CI", nil) == "true"
+    gem "simplecov-cobertura", require: false, group: "test"
+  end
+end
+
+# Excluded from CI except on latest MRI Ruby, to reduce compatibility burden
+group :checks do
+  gem "panolint", github: "panorama-ed/panolint", branch: "main"
+end
+
+# Optional, only used locally to release to rubygems.org
+group :release, optional: true do
+  gem "rake"
+end
diff --git a/HISTORY.md b/HISTORY.md
index 22ac340..3484013 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,5 +1,71 @@
 # HISTORY
 
+## [Unreleased changes](http://github.com/ms-ati/docile/compare/v1.4.0...main)
+
+## [v1.4.0 (May 12, 2021)](http://github.com/ms-ati/docile/compare/v1.3.5...v1.4.0)
+
+  - Special thanks to Matt Schreiber (@tomeon):
+    - Short-circuit to calling #instance_exec directly on the DSL object (prior to
+      constructing a proxy object) when the DSL object and block context object are
+      identical (*Sorry it took over a year to review and merge this!*)
+  - Renamed default branch from master to main, see: https://github.com/github/renaming
+  - Temporarily removed YARD doc configuration, to replace after
+    migration to Github Actions
+  - Removed support for all EOL Rubies < 2.5
+  - Migrated CI from Travis to Github Actions
+  - Special thanks (again!) to Taichi Ishitani (@taichi-ishitani):
+    - Use more reliable codecov github action (via simplecov-cobertura)
+      rather than less reliable codecov gem
+    - Enable bundle caching in github action setup-ruby 
+  - Added Rubocop, and configured it to run in CI
+  - Added Dependabot, and configured it to run daily
+  - Added SECURITY.md for vulnerability reporting policy
+
+## [v1.3.5 (Jan 13, 2021)](http://github.com/ms-ati/docile/compare/v1.3.4...v1.3.5)
+
+  - Special thanks to Jochen Seeber (@jochenseeber):
+    - Fix remaining delegation on Ruby 2.7 (PR #62)
+  - Remove support for Ruby 1.8.7 and REE, because they
+    [are no longer runnable on Travis CI](https://travis-ci.community/t/ruby-1-8-7-and-ree-builds-broken-by-ssl-certificate-failure/10866)
+  - Announce that continued support for any EOL Ruby versions (that is, versions
+    prior to Ruby 2.5 as of Jan 13 2021) will be decided on **Feb 1, 2021**
+    based on comments to [issue #58](https://github.com/ms-ati/docile/issues/58)
+
+## [v1.3.4 (Dec 22, 2020)](http://github.com/ms-ati/docile/compare/v1.3.3...v1.3.4)
+
+  - Special thanks to Benoit Daloze (@eregon):
+    - Fix delegation on Ruby 2.7 (issues #45 and #44, PR #52)
+
+## [v1.3.3 (Dec 18, 2020)](http://github.com/ms-ati/docile/compare/v1.3.2...v1.3.3)
+
+  - Special thanks (again!) to Taichi Ishitani (@taichi-ishitani):
+    - Fix keyword arg warnings on Ruby 2.7 (issue #44, PR #45)
+    - Filter Docile's source files from backtrace (issue #35, PR #36)
+
+## [v1.3.2 (Jun 12, 2019)](http://github.com/ms-ati/docile/compare/v1.3.1...v1.3.2)
+
+  - Special thanks (again!) to Taichi Ishitani (@taichi-ishitani):
+    - Fix for DSL object is replaced when #dsl_eval is nested (#33, PR #34)
+
+## [v1.3.1 (May 24, 2018)](http://github.com/ms-ati/docile/compare/v1.3.0...v1.3.1)
+
+  - Special thanks to Taichi Ishitani (@taichi-ishitani):
+    - Fix for when DSL object is also the block's context (#30)
+
+## [v1.3.0 (Feb 7, 2018)](http://github.com/ms-ati/docile/compare/v1.2.0...v1.3.0)
+
+  - Allow helper methods in block's context to call DSL methods
+  - Add SemVer release policy explicitly
+  - Standardize on double-quoted string literals
+  - Workaround some more Travis CI shenanigans
+
+## [v1.2.0 (Jan 11, 2018)](http://github.com/ms-ati/docile/compare/v1.1.5...v1.2.0)
+
+  - Special thanks to Christina Koller (@cmkoller)
+    - add DSL evaluation returning *return value of the block* (see `.dsl_eval_with_block_return`)
+  - add an example to README
+  - keep travis builds passing on old ruby versions
+
 ## [v1.1.5 (Jun 15, 2014)](http://github.com/ms-ati/docile/compare/v1.1.4...v1.1.5)
 
   - as much as possible, loosen version restrictions on development dependencies
diff --git a/LICENSE b/LICENSE
index 26593ee..e62ec46 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2012-2014 Marc Siegel
+Copyright (c) 2012-2022 Marc Siegel
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 098e0a4..48439c7 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,14 @@
 # Docile
-[![Gem Version](https://badge.fury.io/rb/docile.png)](http://badge.fury.io/rb/docile)
-[![Build Status](https://travis-ci.org/ms-ati/docile.png)](https://travis-ci.org/ms-ati/docile)
-[![Dependency Status](https://gemnasium.com/ms-ati/docile.png)](https://gemnasium.com/ms-ati/docile)
-[![Code Climate](https://codeclimate.com/github/ms-ati/docile.png)](https://codeclimate.com/github/ms-ati/docile)
-[![Coverage Status](https://coveralls.io/repos/ms-ati/docile/badge.png)](https://coveralls.io/r/ms-ati/docile)
-[![Inline docs](http://inch-ci.org/github/ms-ati/docile.png)](http://inch-ci.org/github/ms-ati/docile)
-[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/ms-ati/docile/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
+
+[![Gem Version](https://img.shields.io/gem/v/docile.svg)](https://rubygems.org/gems/docile)
+[![Gem Downloads](https://img.shields.io/gem/dt/docile.svg)](https://rubygems.org/gems/docile)
+
+[![Join the chat at https://gitter.im/ms-ati/docile](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ms-ati/docile?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/ms-ati/docile)
+
+[![Build Status](https://github.com/ms-ati/docile/actions/workflows/main.yml/badge.svg)](https://github.com/ms-ati/docile/actions/workflows/main.yml)
+[![Code Coverage](https://img.shields.io/codecov/c/github/ms-ati/docile.svg)](https://codecov.io/github/ms-ati/docile)
+[![Maintainability](https://api.codeclimate.com/v1/badges/79ca631bc123f7b83b34/maintainability)](https://codeclimate.com/github/ms-ati/docile/maintainability)
 
 Ruby makes it possible to create very expressive **Domain Specific
 Languages**, or **DSL**'s for short. However, it requires some deep knowledge and
@@ -20,7 +23,7 @@ coding a bit more docile...
 
 ## Usage
 
-### Basic
+### Basic: Ruby [Array](http://ruby-doc.org/core-3.0.0/Array.html) as DSL
 
 Let's say that we want to make a DSL for modifying Array objects.
 Wouldn't it be great if we could just treat the methods of Array as a DSL?
@@ -45,7 +48,80 @@ end
 
 Easy!
 
-### Advanced
+### Next step: Allow helper methods to call DSL methods
+
+What if, in our use of the methods of Array as a DSL, we want to extract
+helper methods which in turn call DSL methods?
+
+```ruby
+def pop_sum_and_push(n)
+  sum = 0
+  n.times { sum += pop }
+  push sum
+end
+
+Docile.dsl_eval([]) do
+  push 5
+  push 6
+  pop_sum_and_push(2)
+end
+#=> [11]
+```
+
+Without Docile, you may find this sort of code extraction to be more
+challenging.
+
+### Wait! Can't I do that with just `instance_eval` or `instance_exec`?
+
+Good question!
+
+In short: **No**. 
+
+Not if you want the code in the block to be able to refer to anything
+the block would normally have access to from the surrounding context.
+
+Let's be very specific. Docile internally uses `instance_exec` (see [execution.rb#26](lib/docile/execution.rb#L26)), adding a small layer to support referencing *local variables*, *instance variables*, and *methods* from the _block's context_ **or** the target _object's context_, interchangeably. This is "**the hard part**", where most folks making a DSL in Ruby throw up their hands.
+
+For example:
+
+```ruby
+class ContextOfBlock
+  def example_of_contexts
+    @block_instance_var = 1
+    block_local_var = 2
+
+    with_array do
+      push @block_instance_var
+      push block_local_var
+      pop
+      push block_sees_this_method 
+    end
+  end
+  
+  def block_sees_this_method
+    3
+  end  
+
+  def with_array(&block)
+    {
+      docile: Docile.dsl_eval([], &block),
+      instance_eval: ([].instance_eval(&block) rescue $!),
+      instance_exec: ([].instance_exec(&block) rescue $!)
+    }  
+  end
+end
+
+ContextOfBlock.new.example_of_contexts
+#=> {
+      :docile=>[1, 3],
+      :instance_eval=>#<NameError: undefined local variable or method `block_sees_this_method' for [nil]:Array>,
+      :instance_exec=>#<NameError: undefined local variable or method `block_sees_this_method' for [nil]:Array>
+    }
+```
+
+As you can see, it won't be possible to call methods or access instance variables defined in the block's context using just the raw `instance_eval` or `instance_exec` methods. And in fact, Docile goes further, making it easy to maintain this support even in multi-layered DSLs.
+
+### Build a Pizza
 
 Mutating (changing) an Array instance is fine, but what usually makes a good DSL is a [Builder Pattern][2].
 
@@ -83,7 +159,7 @@ PizzaBuilder.new.cheese.pepperoni.sauce(:extra).build
 
 Then implement your DSL like this:
 
-``` ruby
+```ruby
 def pizza(&block)
   Docile.dsl_eval(PizzaBuilder.new, &block).build
 end
@@ -93,6 +169,38 @@ It's just that easy!
 
 [2]: http://stackoverflow.com/questions/328496/when-would-you-use-the-builder-pattern  "Builder Pattern"
 
+### Multi-level and Recursive DSLs
+
+Docile is a very easy way to write a multi-level DSL in Ruby, even for
+a [recursive data structure such as a tree][4]:
+
+```ruby
+Person = Struct.new(:name, :mother, :father)
+
+person {
+  name 'John Smith'
+  mother {
+    name 'Mary Smith'
+  }
+  father {
+    name 'Tom Smith'
+    mother {
+      name 'Jane Smith'
+    }
+  }
+}
+
+#=> #<struct Person name="John Smith",
+#                   mother=#<struct Person name="Mary Smith", mother=nil, father=nil>,
+#                   father=#<struct Person name="Tom Smith",
+#                                          mother=#<struct Person name="Jane Smith", mother=nil, father=nil>,
+#                                          father=nil>>
+```
+
+See the full [person tree example][4] for details.
+
+[4]: https://gist.github.com/ms-ati/2bb17bdf10a430faba98
+
 ### Block parameters
 
 Parameters can be passed to the DSL block.
@@ -153,18 +261,18 @@ end
 
 [3]: http://www.sinatrarb.com "Sinatra"
 
-### Functional-Style DSL Objects
+### Functional-Style Immutable DSL Objects
 
 Sometimes, you want to use an object as a DSL, but it doesn't quite fit the
 [imperative](http://en.wikipedia.org/wiki/Imperative_programming) pattern shown
 above.
 
 Instead of methods like
-[Array#push](http://www.ruby-doc.org/core-2.0/Array.html#method-i-push), which
+[Array#push](http://www.ruby-doc.org/core-3.0.0/Array.html#method-i-push), which
 modifies the object at hand, it has methods like
-[String#reverse](http://www.ruby-doc.org/core-2.0/String.html#method-i-reverse),
+[String#reverse](http://www.ruby-doc.org/core-3.0.0/String.html#method-i-reverse),
 which returns a new object without touching the original. Perhaps it's even
-[frozen](http://www.ruby-doc.org/core-2.0/Object.html#method-i-freeze) in
+[frozen](http://www.ruby-doc.org/core-3.0.0/Object.html#method-i-freeze) in
 order to enforce [immutability](http://en.wikipedia.org/wiki/Immutable_object).
 
 Wouldn't it be great if we could just treat these methods as a DSL as well?
@@ -192,6 +300,33 @@ end
 
 All set!
 
+### Accessing the block's return value
+
+Sometimes you might want to access the return value of your provided block,
+as opposed to the DSL object itself. In these cases, use
+`dsl_eval_with_block_return`. It behaves exactly like `dsl_eval`, but returns
+the output from executing the block, rather than the DSL object.
+
+```ruby
+arr = []
+with_array(arr) do
+  push "a"
+  push "b"
+  push "c"
+  length
+end
+#=> 3
+
+arr
+#=> ["a", "b", "c"]
+```
+
+```ruby
+def with_array(arr=[], &block)
+  Docile.dsl_eval_with_block_return(arr, &block)
+end
+```
+
 ## Features
 
   1.  Method lookup falls back from the DSL object to the block's context
@@ -215,9 +350,19 @@ $ gem install docile
 
 ## Status
 
-Works on [all ruby versions since 1.8.7](https://github.com/ms-ati/docile/blob/master/.travis.yml), or so Travis CI [tells us](https://travis-ci.org/ms-ati/docile).
+Works on [all currently supported ruby versions](https://github.com/ms-ati/docile/blob/master/.github/workflows/main.yml),
+or so [Github Actions](https://github.com/ms-ati/docile/actions)
+tells us.
 
-Used by some pretty cool gems to implement their DSLs, notably including [SimpleCov](https://github.com/colszowka/simplecov). Keep an eye out for new gems using Docile at the [Ruby Toolbox](https://www.ruby-toolbox.com/projects/docile).
+Used by some pretty cool gems to implement their DSLs, notably including
+[SimpleCov](https://github.com/colszowka/simplecov). Keep an eye out for new
+gems using Docile at the
+[Ruby Toolbox](https://www.ruby-toolbox.com/projects/docile).
+
+## Release Policy
+
+Docile releases follow
+[Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
 
 ## Note on Patches/Pull Requests
 
@@ -232,10 +377,33 @@ Used by some pretty cool gems to implement their DSLs, notably including [Simple
       commit by itself I can ignore when I pull)
   * Send me a pull request. Bonus points for topic branches.
 
-## Copyright & License
+## Releasing
 
-Copyright (c) 2012-2014 Marc Siegel.
+To make a new release of `Docile` to
+[RubyGems](https://rubygems.org/gems/docile), first install the release
+dependencies (e.g. `rake`) as follows:
 
-Licensed under the [MIT License](http://choosealicense.com/licenses/mit/), see [LICENSE](LICENSE) for details.
+```shell
+bundle config set --local with 'release'
+bundle install
+```
+
+Then carry out these steps:
+
+1. Update `HISTORY.md`:
+    - Add an entry for the upcoming version _x.y.z_
+    - Move content from _Unreleased_ to the upcoming version _x.y.z_
+    - Commit with title `Update HISTORY.md for x.y.z`
+
+2. Update `lib/docile/version.rb`
+    - Replace with upcoming version _x.y.z_
+    - Commit with title `Bump version to x.y.z`
+
+3. `bundle exec rake release`
+
+## Copyright & License
 
+Copyright (c) 2012-2022 Marc Siegel.
 
+Licensed under the [MIT License](http://choosealicense.com/licenses/mit/),
+see [LICENSE](LICENSE) for details.
diff --git a/Rakefile b/Rakefile
index 5528825..83e2b67 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,28 +1,14 @@
-require 'rake/clean'
-require 'bundler/gem_tasks'
-require 'rspec/core/rake_task'
-require File.expand_path('on_what', File.dirname(__FILE__))
+# frozen_string_literal: true
+
+require "rake/clean"
+require "bundler/gem_tasks"
+require "rspec/core/rake_task"
 
 # Default task for `rake` is to run rspec
-task :default => [:spec]
+task default: [:spec]
 
 # Use default rspec rake task
 RSpec::Core::RakeTask.new
 
 # Configure `rake clobber` to delete all generated files
-CLOBBER.include('pkg', 'doc', 'coverage')
-
-# To limit needed compatibility with versions of dependencies, only configure
-#   yard doc generation when *not* on Travis, JRuby, or 1.8
-if !on_travis? && !on_jruby? && !on_1_8?
-  require 'github/markup'
-  require 'redcarpet'
-  require 'yard'
-  require 'yard/rake/yardoc_task'
-
-  YARD::Rake::YardocTask.new do |t|
-    OTHER_PATHS = %w()
-    t.files   = ['lib/**/*.rb', OTHER_PATHS]
-    t.options = %w(--markup-provider=redcarpet --markup=markdown --main=README.md)
-  end
-end
+CLOBBER.include("pkg", "doc", "coverage")
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..947561c
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,19 @@
+# Security Policy
+
+## Supported Versions
+
+Use this section to tell people about which versions of your project are
+currently being supported with security updates.
+
+| Version | Supported          |
+| ------- | ------------------ |
+| 1.4.x   | :white_check_mark: |
+| 1.3.x   | :white_check_mark: |
+| < 1.3   | :x:                |
+
+## Reporting a Vulnerability
+
+At this time, security issues and vulnerabilities in Docile should
+be reported like any other issue. Please create an issue in the
+[public issue tracker](https://github.com/ms-ati/docile/issues) on
+Github.
diff --git a/checksums.yaml.gz b/checksums.yaml.gz
deleted file mode 100644
index bf864f2..0000000
Binary files a/checksums.yaml.gz and /dev/null differ
diff --git a/debian/changelog b/debian/changelog
index 5f42443..ae644a6 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,4 +1,4 @@
-ruby-docile (1.1.5-3) UNRELEASED; urgency=medium
+ruby-docile (1.4.0+git20220423.1.08cf67d-1) UNRELEASED; urgency=medium
 
   [ Utkarsh Gupta ]
   * Add salsa-ci.yml
@@ -12,8 +12,9 @@ ruby-docile (1.1.5-3) UNRELEASED; urgency=medium
   * Update Vcs-* headers from URL redirect.
   * Use canonical URL in Vcs-Git.
   * Bump debhelper from old 12 to 13.
+  * New upstream snapshot.
 
- -- Utkarsh Gupta <guptautkarsh2102@gmail.com>  Tue, 13 Aug 2019 04:09:40 +0530
+ -- Utkarsh Gupta <guptautkarsh2102@gmail.com>  Fri, 21 Oct 2022 09:32:50 -0000
 
 ruby-docile (1.1.5-2) unstable; urgency=medium
 
diff --git a/docile.gemspec b/docile.gemspec
index 4ebc077..01444e6 100644
--- a/docile.gemspec
+++ b/docile.gemspec
@@ -1,43 +1,36 @@
-require File.expand_path('on_what', File.dirname(__FILE__))
-$:.push File.expand_path('../lib', __FILE__)
-require 'docile/version'
+# frozen_string_literal: true
+
+require_relative "lib/docile/version"
 
 Gem::Specification.new do |s|
-  s.name        = 'docile'
+  s.name        = "docile"
   s.version     = Docile::VERSION
-  s.author      = 'Marc Siegel'
-  s.email       = 'marc@usainnov.com'
-  s.homepage    = 'https://ms-ati.github.io/docile/'
-  s.summary     = 'Docile keeps your Ruby DSLs tame and well-behaved'
-  s.description = 'Docile turns any Ruby object into a DSL. Especially useful with the Builder pattern.'
-  s.license     = 'MIT'
-
-  # Files included in the gem
-  s.files         = `git ls-files`.split("\n")
-  s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
-  s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
-  s.require_paths = %w(lib)
+  s.author      = "Marc Siegel"
+  s.email       = "marc@usainnov.com"
+  s.homepage    = "https://ms-ati.github.io/docile/"
+  s.summary     = "Docile keeps your Ruby DSLs tame and well-behaved."
+  s.description = "Docile treats the methods of a given ruby object as a DSL " \
+                  "(domain specific language) within a given block. \n\n"      \
+                  "Killer feature: you can also reference methods, instance "  \
+                  "variables, and local variables from the original (non-DSL) "\
+                  "context within the block. \n\n"                             \
+                  "Docile releases follow Semantic Versioning as defined at "  \
+                  "semver.org."
+  s.license     = "MIT"
 
-  # Specify oldest supported Ruby version
-  s.required_ruby_version = '>= 1.8.7'
+  # Specify oldest supported Ruby version (2.5 to support JRuby 9.2.17.0)
+  s.required_ruby_version = ">= 2.5.0"
 
-  # Run rspec tests from rake
-  s.add_development_dependency 'rake'
-  s.add_development_dependency 'rspec', '~> 3.0.0'
-
-  # NOTE: needed for Travis builds on 1.8, but can't yet reproduce failure locally
-  s.add_development_dependency 'mime-types', '~> 1.25.1' if on_1_8?
-
-  # To limit needed compatibility with versions of dependencies, only configure
-  #   yard doc generation when *not* on Travis, JRuby, or 1.8
-  if !on_travis? && !on_jruby? && !on_1_8?
-    # Github flavored markdown in YARD documentation
-    # http://blog.nikosd.com/2011/11/github-flavored-markdown-in-yard.html
-    s.add_development_dependency 'yard'
-    s.add_development_dependency 'redcarpet'
-    s.add_development_dependency 'github-markup'
+  # Files included in the gem
+  s.files = `git ls-files -z`.split("\x0").reject do |f|
+    f.match(%r{^(test|spec|features)/})
   end
+  s.require_paths = ["lib"]
 
-  # Coveralls test coverage tool, basically hosted SimpleCov
-  s.add_development_dependency 'coveralls'
+  s.metadata = {
+    "homepage_uri" => "https://ms-ati.github.io/docile/",
+    "changelog_uri" => "https://github.com/ms-ati/docile/blob/main/HISTORY.md",
+    "source_code_uri" => "https://github.com/ms-ati/docile",
+    "rubygems_mfa_required" => "true",
+  }
 end
diff --git a/lib/docile.rb b/lib/docile.rb
index dc51096..90e6e84 100644
--- a/lib/docile.rb
+++ b/lib/docile.rb
@@ -1,7 +1,10 @@
-require 'docile/version'
-require 'docile/execution'
-require 'docile/fallback_context_proxy'
-require 'docile/chaining_fallback_context_proxy'
+# frozen_string_literal: true
+
+require "docile/version"
+require "docile/execution"
+require "docile/fallback_context_proxy"
+require "docile/chaining_fallback_context_proxy"
+require "docile/backtrace_filter"
 
 # Docile keeps your Ruby DSLs tame and well-behaved.
 module Docile
@@ -43,8 +46,53 @@ module Docile
     exec_in_proxy_context(dsl, FallbackContextProxy, *args, &block)
     dsl
   end
+
+  ruby2_keywords :dsl_eval if respond_to?(:ruby2_keywords, true)
   module_function :dsl_eval
 
+  # Execute a block in the context of an object whose methods represent the
+  # commands in a DSL, and return *the block's return value*.
+  #
+  # @note Use with an *imperative* DSL (commands modify the context object)
+  #
+  # Use this method to execute an *imperative* DSL, which means that:
+  #
+  #   1. Each command mutates the state of the DSL context object
+  #   2. The return value of each command is ignored
+  #   3. The final return value is the original context object
+  #
+  # @example Use a String as a DSL
+  #   Docile.dsl_eval_with_block_return("Hello, world!") do
+  #     reverse!
+  #     upcase!
+  #     first
+  #   end
+  #   #=> "!"
+  #
+  # @example Use an Array as a DSL
+  #   Docile.dsl_eval_with_block_return([]) do
+  #     push "a"
+  #     push "b"
+  #     pop
+  #     push "c"
+  #     length
+  #   end
+  #   #=> 2
+  #
+  # @param dsl   [Object] context object whose methods make up the DSL
+  # @param args  [Array]  arguments to be passed to the block
+  # @param block [Proc]   the block of DSL commands to be executed against the
+  #                         `dsl` context object
+  # @return      [Object] the return value from executing the block
+  def dsl_eval_with_block_return(dsl, *args, &block)
+    exec_in_proxy_context(dsl, FallbackContextProxy, *args, &block)
+  end
+
+  if respond_to?(:ruby2_keywords, true)
+    ruby2_keywords :dsl_eval_with_block_return
+  end
+  module_function :dsl_eval_with_block_return
+
   # Execute a block in the context of an immutable object whose methods,
   # and the methods of their return values, represent the commands in a DSL.
   #
@@ -80,5 +128,7 @@ module Docile
   def dsl_eval_immutable(dsl, *args, &block)
     exec_in_proxy_context(dsl, ChainingFallbackContextProxy, *args, &block)
   end
+
+  ruby2_keywords :dsl_eval_immutable if respond_to?(:ruby2_keywords, true)
   module_function :dsl_eval_immutable
 end
diff --git a/lib/docile/backtrace_filter.rb b/lib/docile/backtrace_filter.rb
new file mode 100644
index 0000000..06a6605
--- /dev/null
+++ b/lib/docile/backtrace_filter.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Docile
+  # @api private
+  #
+  # This is used to remove entries pointing to Docile's source files
+  # from {Exception#backtrace} and {Exception#backtrace_locations}.
+  #
+  # If {NoMethodError} is caught then the exception object will be extended
+  # by this module to add filter functionalities.
+  module BacktraceFilter
+    FILTER_PATTERN = %r{/lib/docile/}.freeze
+
+    def backtrace
+      super.grep_v(FILTER_PATTERN)
+    end
+
+    if ::Exception.public_method_defined?(:backtrace_locations)
+      def backtrace_locations
+        super.reject { |location| location.absolute_path =~ FILTER_PATTERN }
+      end
+    end
+  end
+end
diff --git a/lib/docile/chaining_fallback_context_proxy.rb b/lib/docile/chaining_fallback_context_proxy.rb
index 20d0b63..4fea047 100644
--- a/lib/docile/chaining_fallback_context_proxy.rb
+++ b/lib/docile/chaining_fallback_context_proxy.rb
@@ -1,4 +1,6 @@
-require 'docile/fallback_context_proxy'
+# frozen_string_literal: true
+
+require "docile/fallback_context_proxy"
 
 module Docile
   # @api private
@@ -10,11 +12,16 @@ module Docile
   # objects.
   #
   # @see Docile.dsl_eval_immutable
+  #
+  # rubocop:disable Style/MissingRespondToMissing
   class ChainingFallbackContextProxy < FallbackContextProxy
     # Proxy methods as in {FallbackContextProxy#method_missing}, replacing
     # `receiver` with the returned value.
     def method_missing(method, *args, &block)
       @__receiver__ = super(method, *args, &block)
     end
+
+    ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
   end
-end
\ No newline at end of file
+  # rubocop:enable Style/MissingRespondToMissing
+end
diff --git a/lib/docile/execution.rb b/lib/docile/execution.rb
index f5696b9..e8a6408 100644
--- a/lib/docile/execution.rb
+++ b/lib/docile/execution.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Docile
   # @api private
   #
@@ -15,21 +17,37 @@ module Docile
     # @param block      [Proc]   the block of DSL commands to be executed
     # @return           [Object] the return value of the block
     def exec_in_proxy_context(dsl, proxy_type, *args, &block)
-      block_context = eval('self', block.binding)
+      block_context = eval("self", block.binding) # rubocop:disable Style/EvalWithLocation
+
+      # Use #equal? to test strict object identity (assuming that this dictum
+      # from the Ruby docs holds: "[u]nlike ==, the equal? method should never
+      # be overridden by subclasses as it is used to determine object
+      # identity")
+      return dsl.instance_exec(*args, &block) if dsl.equal?(block_context)
+
       proxy_context = proxy_type.new(dsl, block_context)
       begin
         block_context.instance_variables.each do |ivar|
           value_from_block = block_context.instance_variable_get(ivar)
           proxy_context.instance_variable_set(ivar, value_from_block)
         end
+
         proxy_context.instance_exec(*args, &block)
       ensure
+        if block_context.respond_to?(:__docile_undo_fallback__)
+          block_context.send(:__docile_undo_fallback__)
+        end
+
         block_context.instance_variables.each do |ivar|
+          next unless proxy_context.instance_variables.include?(ivar)
+
           value_from_dsl_proxy = proxy_context.instance_variable_get(ivar)
           block_context.instance_variable_set(ivar, value_from_dsl_proxy)
         end
       end
     end
+
+    ruby2_keywords :exec_in_proxy_context if respond_to?(:ruby2_keywords, true)
     module_function :exec_in_proxy_context
   end
-end
\ No newline at end of file
+end
diff --git a/lib/docile/fallback_context_proxy.rb b/lib/docile/fallback_context_proxy.rb
index a865d93..18a2e0e 100644
--- a/lib/docile/fallback_context_proxy.rb
+++ b/lib/docile/fallback_context_proxy.rb
@@ -1,4 +1,6 @@
-require 'set'
+# frozen_string_literal: true
+
+require "set"
 
 module Docile
   # @api private
@@ -13,14 +15,20 @@ module Docile
   # This is useful for implementing DSL evaluation in the context of an object.
   #
   # @see Docile.dsl_eval
+  #
+  # rubocop:disable Style/MissingRespondToMissing
   class FallbackContextProxy
     # The set of methods which will **not** be proxied, but instead answered
     # by this object directly.
     NON_PROXIED_METHODS = Set[:__send__, :object_id, :__id__, :==, :equal?,
-                              :'!', :'!=', :instance_exec, :instance_variables,
+                              :!, :!=, :instance_exec, :instance_variables,
                               :instance_variable_get, :instance_variable_set,
                               :remove_instance_variable]
 
+    # The set of methods which will **not** fallback from the block's context
+    # to the dsl object.
+    NON_FALLBACK_METHODS = Set[:class, :self, :respond_to?, :instance_of?]
+
     # The set of instance variables which are local to this object and hidden.
     # All other instance variables will be copied in and out of this object
     # from the scope in which this proxy was created.
@@ -38,16 +46,44 @@ module Docile
     def initialize(receiver, fallback)
       @__receiver__ = receiver
       @__fallback__ = fallback
+
+      # Enables calling DSL methods from helper methods in the block's context
+      unless fallback.respond_to?(:method_missing)
+        # NOTE: We could switch to {#define_singleton_method} on current Rubies
+        singleton_class = (class << fallback; self; end)
+
+        # instrument {#method_missing} on the block's context to fallback to
+        # the DSL object. This allows helper methods in the block's context to
+        # contain calls to methods on the DSL object.
+        singleton_class.
+          send(:define_method, :method_missing) do |method, *args, &block|
+            m = method.to_sym
+            if !NON_FALLBACK_METHODS.member?(m) &&
+               !fallback.respond_to?(m) &&
+               receiver.respond_to?(m)
+              receiver.__send__(method.to_sym, *args, &block)
+            else
+              super(method, *args, &block)
+            end
+          end
+
+        if singleton_class.respond_to?(:ruby2_keywords, true)
+          singleton_class.send(:ruby2_keywords, :method_missing)
+        end
+
+        # instrument a helper method to remove the above instrumentation
+        singleton_class.
+          send(:define_method, :__docile_undo_fallback__) do
+            singleton_class.send(:remove_method, :method_missing)
+            singleton_class.send(:remove_method, :__docile_undo_fallback__)
+          end
+      end
     end
 
     # @return [Array<Symbol>]  Instance variable names, excluding
     #                            {NON_PROXIED_INSTANCE_VARIABLES}
-    #
-    # @note on Ruby 1.8.x, the instance variable names are actually of
-    #   type `String`.
     def instance_variables
-      # Ruby 1.8.x returns string names, convert to symbols for compatibility
-      super.select { |v| !NON_PROXIED_INSTANCE_VARIABLES.include?(v.to_sym) }
+      super.reject { |v| NON_PROXIED_INSTANCE_VARIABLES.include?(v) }
     end
 
     # Proxy all methods, excluding {NON_PROXIED_METHODS}, first to `receiver`
@@ -56,8 +92,16 @@ module Docile
       if @__receiver__.respond_to?(method.to_sym)
         @__receiver__.__send__(method.to_sym, *args, &block)
       else
-        @__fallback__.__send__(method.to_sym, *args, &block)
+        begin
+          @__fallback__.__send__(method.to_sym, *args, &block)
+        rescue NoMethodError => e
+          e.extend(BacktraceFilter)
+          raise e
+        end
       end
     end
+
+    ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
   end
+  # rubocop:enable Style/MissingRespondToMissing
 end
diff --git a/lib/docile/version.rb b/lib/docile/version.rb
index 9614b34..02b3354 100644
--- a/lib/docile/version.rb
+++ b/lib/docile/version.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
 module Docile
   # The current version of this library
-  VERSION = '1.1.5'
+  VERSION = "1.4.0"
 end
diff --git a/metadata.yml b/metadata.yml
deleted file mode 100644
index 4196e9e..0000000
--- a/metadata.yml
+++ /dev/null
@@ -1,151 +0,0 @@
---- !ruby/object:Gem::Specification
-name: docile
-version: !ruby/object:Gem::Version
-  version: 1.1.5
-platform: ruby
-authors:
-- Marc Siegel
-autorequire: 
-bindir: bin
-cert_chain: []
-date: 2014-06-15 00:00:00.000000000 Z
-dependencies:
-- !ruby/object:Gem::Dependency
-  name: rake
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '0'
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '0'
-- !ruby/object:Gem::Dependency
-  name: rspec
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - "~>"
-      - !ruby/object:Gem::Version
-        version: 3.0.0
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - "~>"
-      - !ruby/object:Gem::Version
-        version: 3.0.0
-- !ruby/object:Gem::Dependency
-  name: yard
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '0'
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '0'
-- !ruby/object:Gem::Dependency
-  name: redcarpet
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '0'
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '0'
-- !ruby/object:Gem::Dependency
-  name: github-markup
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '0'
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '0'
-- !ruby/object:Gem::Dependency
-  name: coveralls
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '0'
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: '0'
-description: Docile turns any Ruby object into a DSL. Especially useful with the Builder
-  pattern.
-email: marc@usainnov.com
-executables: []
-extensions: []
-extra_rdoc_files: []
-files:
-- ".gitignore"
-- ".rspec"
-- ".ruby-gemset"
-- ".ruby-version"
-- ".travis.yml"
-- ".yardopts"
-- Gemfile
-- HISTORY.md
-- LICENSE
-- README.md
-- Rakefile
-- docile.gemspec
-- lib/docile.rb
-- lib/docile/chaining_fallback_context_proxy.rb
-- lib/docile/execution.rb
-- lib/docile/fallback_context_proxy.rb
-- lib/docile/version.rb
-- on_what.rb
-- spec/docile_spec.rb
-- spec/spec_helper.rb
-homepage: https://ms-ati.github.io/docile/
-licenses:
-- MIT
-metadata: {}
-post_install_message: 
-rdoc_options: []
-require_paths:
-- lib
-required_ruby_version: !ruby/object:Gem::Requirement
-  requirements:
-  - - ">="
-    - !ruby/object:Gem::Version
-      version: 1.8.7
-required_rubygems_version: !ruby/object:Gem::Requirement
-  requirements:
-  - - ">="
-    - !ruby/object:Gem::Version
-      version: '0'
-requirements: []
-rubyforge_project: 
-rubygems_version: 2.2.2
-signing_key: 
-specification_version: 4
-summary: Docile keeps your Ruby DSLs tame and well-behaved
-test_files:
-- spec/docile_spec.rb
-- spec/spec_helper.rb
-has_rdoc: 
diff --git a/on_what.rb b/on_what.rb
deleted file mode 100644
index b1ffa28..0000000
--- a/on_what.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# NOTE: Very simple tests for what system we are on, extracted for sharing
-#   between Rakefile, gemspec, and spec_helper. Not for use in actual library.
-
-def on_travis?
-  ENV['CI'] == 'true'
-end
-
-def on_jruby?
-  (defined?(RUBY_ENGINE) && 'jruby' == RUBY_ENGINE)
-end
-
-def on_1_8?
-  RUBY_VERSION.start_with? '1.8'
-end
\ No newline at end of file
diff --git a/spec/docile_spec.rb b/spec/docile_spec.rb
index 33bc896..e912f9b 100644
--- a/spec/docile_spec.rb
+++ b/spec/docile_spec.rb
@@ -1,11 +1,66 @@
-require 'spec_helper'
-require 'singleton'
+# frozen_string_literal: true
 
+require "spec_helper"
+
+# TODO: Factor single spec file into multiple specs
+# rubocop:disable RSpec/MultipleDescribes
 describe Docile do
+  describe ".dsl_eval" do
+    before :each do
+      stub_const("Pizza",
+                 Struct.new(:cheese, :pepperoni, :bacon, :sauce))
+
+      stub_const("PizzaBuilder", Class.new do
+        # rubocop:disable all
+        def cheese(v=true);    @cheese    = v; end
+        def pepperoni(v=true); @pepperoni = v; end
+        def bacon(v=true);     @bacon     = v; end
+        def sauce(v=nil);      @sauce     = v; end
+        # rubocop:enable all
+
+        def build
+          Pizza.new(!!@cheese, !!@pepperoni, !!@bacon, @sauce)
+        end
+      end)
+
+      stub_const("InnerDSL", Class.new do
+        def initialize
+          @b = "b"
+        end
 
-  describe '.dsl_eval' do
+        attr_accessor :b
+
+        def c
+          "inner c"
+        end
+      end)
+
+      stub_const("OuterDSL", Class.new do
+        def initialize
+          @a = "a"
+        end
 
-    context 'when DSL context object is an Array' do
+        attr_accessor :a
+
+        def c
+          "outer c"
+        end
+
+        def inner(&block)
+          Docile.dsl_eval(InnerDSL.new, &block)
+        end
+
+        def inner_with_params(param, &block)
+          Docile.dsl_eval(InnerDSL.new, param, :foo, &block)
+        end
+      end)
+    end
+
+    def outer(&block)
+      Docile.dsl_eval(OuterDSL.new, &block)
+    end
+
+    context "when DSL context object is an Array" do
       let(:array) { [] }
       let!(:result) { execute_dsl_against_array }
 
@@ -18,36 +73,28 @@ describe Docile do
         end
       end
 
-      it 'executes the block against the DSL context object' do
+      it "executes the block against the DSL context object" do
         expect(array).to eq([1, 3])
       end
 
-      it 'returns the DSL object after executing block against it' do
+      it "returns the DSL object after executing block against it" do
         expect(result).to eq(array)
       end
 
       it "doesn't proxy #__id__" do
-        Docile.dsl_eval(array) { expect(__id__).not_to eq(array.__id__) }
-      end
-
-      it "raises NoMethodError if the DSL object doesn't implement the method" do
-        expect { Docile.dsl_eval(array) { no_such_method } }.to raise_error(NoMethodError)
+        described_class.dsl_eval(array) do
+          expect(__id__).not_to eq(array.__id__)
+        end
       end
-    end
 
-    Pizza = Struct.new(:cheese, :pepperoni, :bacon, :sauce)
-
-    class PizzaBuilder
-      def cheese(v=true);    @cheese    = v; end
-      def pepperoni(v=true); @pepperoni = v; end
-      def bacon(v=true);     @bacon     = v; end
-      def sauce(v=nil);      @sauce     = v; end
-      def build
-        Pizza.new(!!@cheese, !!@pepperoni, !!@bacon, @sauce)
+      it "raises NoMethodError if DSL object doesn't implement the method" do
+        expect do
+          described_class.dsl_eval(array) { no_such_method }
+        end.to raise_error(NoMethodError)
       end
     end
 
-    context 'when DSL context object is a Builder pattern' do
+    context "when DSL context object is a Builder pattern" do
       let(:builder) { PizzaBuilder.new }
       let(:result) { execute_dsl_against_builder_and_call_build }
 
@@ -56,57 +103,35 @@ describe Docile do
         Docile.dsl_eval(builder) do
           bacon
           cheese
-          sauce @sauce
+          sauce @sauce # rubocop:disable RSpec/InstanceVariable
         end.build
       end
 
-      it 'returns correctly built object' do
+      it "returns correctly built object" do
         expect(result).to eq(Pizza.new(true, false, true, :extra))
       end
     end
 
-    class InnerDSL
-      def initialize; @b = 'b'; end
-      attr_accessor :b
-    end
-
-    class OuterDSL
-      def initialize; @a = 'a'; end
-      attr_accessor :a
-
-      def inner(&block)
-        Docile.dsl_eval(InnerDSL.new, &block)
-      end
-
-      def inner_with_params(param, &block)
-        Docile.dsl_eval(InnerDSL.new, param, :foo, &block)
-      end
-    end
-
-    def outer(&block)
-      Docile.dsl_eval(OuterDSL.new, &block)
-    end
-
-    context 'when given parameters for the DSL block' do
+    context "when given parameters for the DSL block" do
       def parameterized(*args, &block)
         Docile.dsl_eval(OuterDSL.new, *args, &block)
       end
 
-      it 'passes parameters to the block' do
-        parameterized(1,2,3) do |x,y,z|
+      it "passes parameters to the block" do
+        parameterized(1, 2, 3) do |x, y, z|
           expect(x).to eq(1)
           expect(y).to eq(2)
           expect(z).to eq(3)
         end
       end
 
-      it 'finds parameters before methods' do
+      it "finds parameters before methods" do
         parameterized(1) { |a| expect(a).to eq(1) }
       end
 
-      it 'find outer dsl parameters in inner dsl scope' do
-        parameterized(1,2,3) do |a,b,c|
-          inner_with_params(c) do |d,e|
+      it "find outer dsl parameters in inner dsl scope" do
+        parameterized(1, 2, 3) do |a, b, c|
+          inner_with_params(c) do |d, e|
             expect(a).to eq(1)
             expect(b).to eq(2)
             expect(c).to eq(3)
@@ -117,156 +142,448 @@ describe Docile do
       end
     end
 
-    class DSLWithNoMethod
-      def initialize(b); @b = b; end
-      attr_accessor :b
-      def push_element
-        @b.push 1
+    context "when block's context has helper methods which call DSL methods" do
+      subject { context.method(:factorial_as_dsl_against_array) }
+
+      let(:context) do
+        class_block_context_with_helper_methods.new(array_as_dsl)
       end
-    end
 
-    context 'when DSL have NoMethod error inside' do
-      it 'raise error from nil' do
-        Docile.dsl_eval(DSLWithNoMethod.new(nil)) do
-          expect { push_element }.to raise_error(NoMethodError, /undefined method `push' (for|on) nil:NilClass/)
+      let(:array_as_dsl) { [1, 1] }
+
+      let(:class_block_context_with_helper_methods) do
+        Class.new do
+          def initialize(array_as_dsl)
+            @array_as_dsl = array_as_dsl
+          end
+
+          # Classic dynamic programming factorial, using the methods of {Array}
+          # as a DSL to implement it, via helper methods {#calculate_factorials}
+          # and {#save_factorials} which are defined in this class, so therefore
+          # outside the block.
+          def factorial_as_dsl_against_array(num)
+            Docile.dsl_eval(@array_as_dsl) { calculate_factorials(num) }.last
+          end
+
+          # Uses the helper method {#save_factorials} below.
+          def calculate_factorials(num)
+            (2..num).each { |i| save_factorial(i) }
+          end
+
+          # Uses the methods {Array#push} and {Array#at} as a DSL from a helper
+          # method defined in the block's context. Successfully calling this
+          # proves that we can find helper methods from outside the block, and
+          # then find DSL methods from inside those helper methods.
+          def save_factorial(num)
+            push(num * at(num - 1))
+          end
+        end
+      end
+
+      it "finds DSL methods within helper method defined in block's context" do
+        # see https://en.wikipedia.org/wiki/Factorial
+        # rubocop:disable Layout/ExtraSpacing
+        [
+          [0,                  1],
+          [1,                  1],
+          [2,                  2],
+          [3,                  6],
+          [4,                 24],
+          [5,                120],
+          [6,                720],
+          [7,              5_040],
+          [8,             40_320],
+          [9,            362_880],
+          [10,         3_628_800],
+          [11,        39_916_800],
+          [12,       479_001_600],
+          [13,     6_227_020_800],
+          [14,    87_178_291_200],
+          [15, 1_307_674_368_000]
+        ].each do |n, expected_factorial|
+          array_as_dsl.replace([1, 1])
+          expect(subject.call(n)).to eq expected_factorial
+        end
+        # rubocop:enable Layout/ExtraSpacing
+      end
+
+      it "removes fallback instrumentation from the DSL object after block" do
+        expect { subject.call(5) }.
+          not_to change { context.respond_to?(:method_missing) }.
+          from(false)
+      end
+
+      it "removes method to remove fallbacl from the DSL object after block" do
+        expect { subject.call(5) }.
+          not_to change { context.respond_to?(:__docile_undo_fallback__) }.
+          from(false)
+      end
+
+      context "when helper methods call methods that are undefined" do
+        let(:array_as_dsl) { "not an array" }
+
+        it "raises NoMethodError" do
+          expect { subject.call(5) }.
+            to raise_error(NoMethodError, /method `at' /)
+        end
+
+        it "removes fallback instrumentation from the DSL object after block" do
+          expect { subject.call(5) rescue nil }. # rubocop:disable Style/RescueModifier
+            not_to change { context.respond_to?(:method_missing) }.
+            from(false)
         end
       end
     end
 
-    context 'when DSL blocks are nested' do
+    context "when DSL have NoMethod error inside" do
+      let(:class_dsl_with_no_method) do
+        Class.new do
+          def push_element
+            nil.push(1)
+          end
+        end
+      end
+
+      it "raise NoMethodError error from nil" do
+        described_class.dsl_eval(class_dsl_with_no_method.new) do
+          expect { push_element }.
+            to raise_error(
+              NoMethodError,
+              /undefined method `push' (for|on) nil:NilClass/
+            )
+        end
+      end
+    end
 
-      context 'method lookup' do
-        it 'finds method of outer dsl in outer dsl scope' do
-          outer { expect(a).to eq('a') }
+    context "when DSL blocks are nested" do
+      describe "method lookup" do
+        # rubocop:disable Style/SingleLineMethods
+        it "finds method of outer dsl in outer dsl scope" do
+          outer { expect(a).to eq("a") }
+          outer { inner {}; expect(c).to eq("outer c") } # rubocop:disable Style/Semicolon
         end
 
-        it 'finds method of inner dsl in inner dsl scope' do
-          outer { inner { expect(b).to eq('b') } }
+        it "finds method of inner dsl in inner dsl scope" do
+          outer { inner { expect(b).to eq("b") } }
+          outer { inner { expect(c).to eq("inner c") } }
         end
 
-        it 'finds method of outer dsl in inner dsl scope' do
-          outer { inner { expect(a).to eq('a') } }
+        it "finds method of outer dsl in inner dsl scope" do
+          outer { inner { expect(a).to eq("a") } }
         end
 
         it "finds method of block's context in outer dsl scope" do
-          def c; 'c'; end
-          outer { expect(c).to eq('c') }
+          def d; "d"; end
+          outer { expect(d).to eq("d") }
         end
 
         it "finds method of block's context in inner dsl scope" do
-          def c; 'c'; end
-          outer { inner { expect(c).to eq('c') } }
+          def d; "d"; end
+          outer { inner { expect(d).to eq("d") } }
         end
 
-        it 'finds method of outer dsl in preference to block context' do
-          def a; 'not a'; end
-          outer { expect(a).to eq('a') }
-          outer { inner { expect(a).to eq('a') } }
+        it "finds method of outer dsl in preference to block context" do
+          def a; "not a"; end
+          outer { expect(a).to eq("a") }
+          outer { inner { expect(a).to eq("a") } }
         end
+        # rubocop:enable Style/SingleLineMethods
       end
 
-      context 'local variable lookup' do
-        it 'finds local variable from block context in outer dsl scope' do
-          foo = 'foo'
-          outer { expect(foo).to eq('foo') }
+      describe "local variable lookup" do
+        it "finds local variable from block context in outer dsl scope" do
+          foo = "foo"
+          outer { expect(foo).to eq("foo") }
         end
 
-        it 'finds local variable from block definition in inner dsl scope' do
-          bar = 'bar'
-          outer { inner { expect(bar).to eq('bar') } }
+        it "finds local variable from block definition in inner dsl scope" do
+          bar = "bar"
+          outer { inner { expect(bar).to eq("bar") } }
         end
       end
 
-      context 'instance variable lookup' do
-        it 'finds instance variable from block definition in outer dsl scope' do
-          @iv1 = 'iv1'; outer { expect(@iv1).to eq('iv1') }
+      describe "instance variable lookup" do
+        # rubocop:disable RSpec/InstanceVariable
+        it "finds instance variable from block definition in outer dsl scope" do
+          @iv1 = "iv1"
+          outer { expect(@iv1).to eq("iv1") }
         end
 
-        it "proxies instance variable assignments in block in outer dsl scope back into block's context" do
-          @iv1 = 'foo'; outer { @iv1 = 'bar' }; expect(@iv1).to eq('bar')
+        it "proxies instance variable assignments in block in outer dsl scope "\
+           "back into block's context" do
+          @iv1 = "foo"
+          outer { @iv1 = "bar" }
+          expect(@iv1).to eq("bar")
         end
 
-        it 'finds instance variable from block definition in inner dsl scope' do
-          @iv2 = 'iv2'; outer { inner { expect(@iv2).to eq('iv2') } }
+        it "finds instance variable from block definition in inner dsl scope" do
+          @iv2 = "iv2"
+          outer { inner { expect(@iv2).to eq("iv2") } }
         end
 
-        it "proxies instance variable assignments in block in inner dsl scope back into block's context" do
-          @iv2 = 'foo'; outer { inner { @iv2 = 'bar' } }; expect(@iv2).to eq('bar')
+        it "proxies instance variable assignments in block in inner dsl scope "\
+           "back into block's context" do
+          @iv2 = "foo"
+          outer { inner { @iv2 = "bar" } }
+          expect(@iv2).to eq("bar")
         end
+        # rubocop:enable RSpec/InstanceVariable
       end
 
-    end
+      describe "identity of 'self' inside nested dsl blocks" do
+        # see https://github.com/ms-ati/docile/issues/31
+        subject do
+          identified_selves = {}
+
+          outer do
+            identified_selves[:a] = self
+
+            inner do
+              identified_selves[:b] = self
+            end
+
+            identified_selves[:c] = self
+          end
 
-    context 'when DSL context object is a Dispatch pattern' do
-      class DispatchScope
-        def params
-          { :a => 1, :b => 2, :c => 3 }
+          identified_selves
         end
-      end
 
-      class MessageDispatch
-        include Singleton
+        it "identifies self inside outer dsl block" do
+          expect(subject[:a]).to be_instance_of(OuterDSL)
+        end
 
-        def initialize
-          @responders = {}
+        it "replaces self inside inner dsl block" do
+          expect(subject[:b]).to be_instance_of(InnerDSL)
         end
 
-        def add_responder path, &block
-          @responders[path] = block
+        it "restores self to the outer dsl object after the inner dsl block" do
+          expect(subject[:c]).to be_instance_of(OuterDSL)
+          expect(subject[:c]).to be(subject[:a])
         end
+      end
+    end
 
-        def dispatch path, request
-          Docile.dsl_eval(DispatchScope.new, request, &@responders[path])
+    context "when DSL context object is a Dispatch pattern" do
+      let(:class_message_dispatcher) do
+        Class.new do
+          def initialize
+            @responders = {}
+          end
+
+          def add_responder(path, &block)
+            @responders[path] = block
+          end
+
+          def dispatch(path, request)
+            Docile.
+              dsl_eval(class_dispatch_scope.new, request, &@responders[path])
+          end
+
+          def class_dispatch_scope
+            Class.new do
+              def params
+                { a: 1, b: 2, c: 3 }
+              end
+            end
+          end
         end
       end
 
+      let(:message_dispatcher_instance) do
+        class_message_dispatcher.new
+      end
+
       def respond(path, &block)
-        MessageDispatch.instance.add_responder(path, &block)
+        message_dispatcher_instance.add_responder(path, &block)
       end
 
       def send_request(path, request)
-        MessageDispatch.instance.dispatch(path, request)
+        message_dispatcher_instance.dispatch(path, request)
       end
 
-      it 'dispatches correctly' do
+      # rubocop:disable RSpec/InstanceVariable
+      it "dispatches correctly" do
         @first = @second = nil
 
-        respond '/path' do |request|
+        respond "/path" do |request|
           @first = request
         end
 
-        respond '/new_bike' do |bike|
+        respond "/new_bike" do |bike|
           @second = "Got a new #{bike}"
         end
 
-        def x(y) ; "Got a #{y}"; end
-        respond '/third' do |third|
-          expect(x(third)).to eq('Got a third thing')
+        def third(val)
+          "Got a #{val}"
+        end
+
+        respond "/third" do |arg|
+          expect(third(arg)).to eq("Got a third thing")
         end
 
         fourth = nil
-        respond '/params' do |arg|
+
+        respond "/params" do |arg|
           fourth = params[arg]
         end
 
-        send_request '/path', 1
-        send_request '/new_bike', 'ten speed'
-        send_request '/third', 'third thing'
-        send_request '/params', :b
+        send_request "/path", 1
+        send_request "/new_bike", "ten speed"
+        send_request "/third", "third thing"
+        send_request "/params", :b
 
         expect(@first).to eq(1)
-        expect(@second).to eq('Got a new ten speed')
+        expect(@second).to eq("Got a new ten speed")
         expect(fourth).to eq(2)
       end
+      # rubocop:enable RSpec/InstanceVariable
+    end
 
+    context "when DSL context object is same as the block's context object" do
+      let(:class_context_same_as_block_context) do
+        Class.new do
+          def foo(val = nil)
+            @foo = val if val
+            @foo
+          end
+
+          def bar(val = nil)
+            @bar = val if val
+            @bar
+          end
+
+          def dsl_eval(block)
+            Docile.dsl_eval(self, &block)
+          end
+
+          def dsl_eval_string(string)
+            block = binding.eval("proc { #{string} }") # rubocop:disable all
+            dsl_eval(block)
+          end
+        end
+      end
+
+      let(:dsl) { class_context_same_as_block_context.new }
+
+      it "calls DSL methods and sets state on the DSL context object" do
+        dsl.dsl_eval_string("foo 0; bar 1")
+        expect(dsl.foo).to eq(0)
+        expect(dsl.bar).to eq(1)
+      end
+
+      context "when the DSL object is frozen" do
+        it "can call non-mutative code without raising an exception" do
+          expect { dsl.freeze.dsl_eval_string("1 + 2") }.not_to raise_error
+        end
+      end
     end
 
+    context "when NoMethodError is raised" do
+      specify "#backtrace doesn't include path to Docile's sources" do
+        described_class.dsl_eval(Object.new) { foo }
+      rescue NoMethodError => e
+        expect(e.backtrace).not_to include(match(%r{/lib/docile/}))
+      end
+
+      specify "#backtrace_locations doesn't include path to Docile's sources" do
+        described_class.dsl_eval(Object.new) { foo }
+      rescue NoMethodError => e
+        expect(e.backtrace_locations.map(&:absolute_path)).
+          not_to include(match(%r{/lib/docile/}))
+      end
+    end
+
+    context "when a DSL method has a keyword argument" do
+      let(:class_with_method_with_keyword_arg) do
+        Class.new do
+          attr_reader :v0, :v1, :v2
+
+          def set(arg, kw1:, kw2:)
+            @v0 = arg
+            @v1 = kw1
+            @v2 = kw2
+          end
+        end
+      end
+
+      let(:dsl) { class_with_method_with_keyword_arg.new }
+
+      it "calls such DSL methods with no stderr output" do
+        # This is to check warnings related to keyword argument is not output.
+        # See: https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
+        expect do
+          described_class.dsl_eval(dsl) { set(0, kw2: 2, kw1: 1) }
+        end.
+          not_to output.to_stderr
+
+        expect(dsl.v0).to eq 0
+        expect(dsl.v1).to eq 1
+        expect(dsl.v2).to eq 2
+      end
+    end
+
+    context "when a DSL method has a double splat" do
+      let(:class_with_method_with_double_splat) do
+        Class.new do
+          attr_reader :arguments, :options
+
+          def configure(*arguments, **options)
+            @arguments = arguments.dup
+            @options = options.dup
+          end
+        end
+      end
+
+      let(:dsl) { class_with_method_with_double_splat.new }
+
+      it "correctly passes keyword arguments" do
+        described_class.dsl_eval(dsl) { configure(1, a: 1) }
+
+        expect(dsl.arguments).to eq [1]
+        expect(dsl.options).to eq({ a: 1 })
+      end
+
+      it "correctly passes hash arguments" do
+        described_class.dsl_eval(dsl) { configure(1, { a: 1 }) }
+
+        # TruffleRuby 22.0.0.2 has RUBY_VERSION of 3.0.2, but behaves as 2.x
+        if RUBY_VERSION >= "3.0.0" && RUBY_ENGINE != "truffleruby"
+          expect(dsl.arguments).to eq [1, { a: 1 }]
+          expect(dsl.options).to eq({})
+        else
+          expect(dsl.arguments).to eq [1]
+          expect(dsl.options).to eq({ a: 1 })
+        end
+      end
+    end
   end
 
-  describe '.dsl_eval_immutable' do
+  describe ".dsl_eval_with_block_return" do
+    let(:array) { [] }
+    let!(:result) { execute_dsl_against_array }
+
+    def execute_dsl_against_array
+      Docile.dsl_eval_with_block_return(array) do
+        push 1
+        push 2
+        pop
+        push 3
+        "Return me!"
+      end
+    end
+
+    it "executes the block against the DSL context object" do
+      expect(array).to eq([1, 3])
+    end
 
-    context 'when DSL context object is a frozen String' do
-      let(:original) { "I'm immutable!".freeze }
+    it "returns the block's return value" do
+      expect(result).to eq("Return me!")
+    end
+  end
+
+  describe ".dsl_eval_immutable" do
+    context "when DSL context object is a frozen String" do
+      let(:original) { "I'm immutable!".freeze } # rubocop:disable Style/RedundantFreeze
       let!(:result) { execute_non_mutating_dsl_against_string }
 
       def execute_non_mutating_dsl_against_string
@@ -280,12 +597,12 @@ describe Docile do
         expect(original).to eq("I'm immutable!")
       end
 
-      it 'chains the commands in the block against the DSL context object' do
+      it "chains the commands in the block against the DSL context object" do
         expect(result).to eq("!ELBATUMMI M'I")
       end
     end
 
-    context 'when DSL context object is a number' do
+    context "when DSL context object is a number" do
       let(:original) { 84.5 }
       let!(:result) { execute_non_mutating_dsl_against_number }
 
@@ -296,25 +613,26 @@ describe Docile do
         end
       end
 
-      it 'chains the commands in the block against the DSL context object' do
+      it "chains the commands in the block against the DSL context object" do
         expect(result).to eq(42)
       end
     end
   end
-
 end
 
 describe Docile::FallbackContextProxy do
-
-  describe '#instance_variables' do
+  describe "#instance_variables" do
     subject { create_fcp_and_set_one_instance_variable.instance_variables }
+
     let(:expected_type_of_names) { type_of_ivar_names_on_this_ruby }
     let(:actual_type_of_names) { subject.first.class }
-    let(:excluded) { Docile::FallbackContextProxy::NON_PROXIED_INSTANCE_VARIABLES }
+    let(:excluded) do
+      Docile::FallbackContextProxy::NON_PROXIED_INSTANCE_VARIABLES
+    end
 
     def create_fcp_and_set_one_instance_variable
       fcp = Docile::FallbackContextProxy.new(nil, nil)
-      fcp.instance_variable_set(:@foo, 'foo')
+      fcp.instance_variable_set(:@foo, "foo")
       fcp
     end
 
@@ -323,7 +641,7 @@ describe Docile::FallbackContextProxy do
       instance_variables.first.class
     end
 
-    it 'returns proxied instance variables' do
+    it "returns proxied instance variables" do
       expect(subject.map(&:to_sym)).to include(:@foo)
     end
 
@@ -331,9 +649,8 @@ describe Docile::FallbackContextProxy do
       expect(subject.map(&:to_sym)).not_to include(*excluded)
     end
 
-    it 'preserves the type (String or Symbol) of names on this ruby version' do
+    it "preserves the type (String or Symbol) of names on this ruby version" do
       expect(actual_type_of_names).to eq(expected_type_of_names)
     end
   end
-
 end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 756491a..b235409 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,30 +1,30 @@
-require File.expand_path('on_what', File.dirname(File.dirname(__FILE__)))
+# frozen_string_literal: true
 
-begin
-  require 'simplecov'
-  require 'coveralls'
+# Code coverage (via SimpleCov)
+require "simplecov"
 
-  # On Ruby 1.9+ use SimpleCov and publish to Coveralls.io
-  if !on_1_8?
-    SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
-      SimpleCov::Formatter::HTMLFormatter,
-      Coveralls::SimpleCov::Formatter
-    ]
-    SimpleCov.start do
-      add_filter '/spec/'    # exclude test code
-      add_filter '/vendor/'  # exclude gems which are vendored on Travis CI
-    end
+SimpleCov.start do
+  add_filter "/spec/"      # exclude test code
+  add_filter "/vendor/"    # exclude gems which are cached in CI
+end
 
-    # Remove Docile, which was required by SimpleCov, to require again later
-    Object.send(:remove_const, :Docile)
-    $LOADED_FEATURES.reject! { |f| f =~ /\/docile\// }
+# On CI we publish coverage to codecov.io
+# To use the codecov action, we need to generate XML based coverage report
+if ENV.fetch("CI", nil) == "true"
+  begin
+    require "simplecov-cobertura"
+    SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
+  rescue LoadError
+    warn "simplecov-cobertura gem not found - not generating XML for codecov.io"
   end
-rescue LoadError
-  warn 'warning: simplecov/coveralls gems not found; skipping coverage'
 end
 
-lib_dir = File.join(File.dirname(File.dirname(__FILE__)), 'lib')
-$LOAD_PATH.unshift lib_dir unless $LOAD_PATH.include? lib_dir
+# Due to circular dependency (SimpleCov depends on Docile), remove docile and
+# then require the docile gem again below.
+Object.send(:remove_const, :Docile)
+$LOADED_FEATURES.reject! { |f| f.include?("/lib/docile") }
 
-# Require Docile again, now with coverage enabled on 1.9+
-require 'docile'
+# Require Docile again, now with coverage enabled
+lib_dir = File.join(File.dirname(File.dirname(__FILE__)), "lib")
+$LOAD_PATH.unshift lib_dir unless $LOAD_PATH.include? lib_dir
+require "docile"

More details

Full run details