Skip to content

Commit 70d2e43

Browse files
committed
Lazyload DeadEnd internals on syntax error
Instead of having to load all dead end code on every invocation of Ruby, we can delay requiring the files until they're actually needed (on SyntaxError). Resolves this comment ruby/ruby#5859 (review) This requirement makes the library a little unusual in that `dead_end/version` no longer defines `DeadEnd::VERSION` but rather a placeholder value in another constant so the gem isn't eagerly loaded when using the project's gemspec in local tests.
1 parent a4cc0ed commit 70d2e43

File tree

9 files changed

+65
-8
lines changed

9 files changed

+65
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## HEAD (unreleased)
22

3+
- [Breaking] Lazy load DeadEnd internals only if there is a Syntax error. Use `require "dead_end"; require "dead_end/api"` to load eagerly all internals. Otherwise `require "dead_end"` will set up an autoload for the first time the DeadEnd module is used in code. This should only happen on a syntax error. (https://github.com/zombocom/dead_end/pull/142)
34
- Monkeypatch `SyntaxError#detailed_message` in Ruby 3.2+ instead of `require`, `load`, and `require_relative` (https://github.com/zombocom/dead_end/pull/139)
45

56
## 3.1.2

dead_end.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ end
88

99
Gem::Specification.new do |spec|
1010
spec.name = "dead_end"
11-
spec.version = DeadEnd::VERSION
11+
spec.version = UnloadedDeadEnd::VERSION
1212
spec.authors = ["schneems"]
1313
spec.email = ["richard.schneeman+foo@gmail.com"]
1414

exe/dead_end

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env ruby
22

3-
require_relative "../lib/dead_end"
3+
require_relative "../lib/dead_end/api"
44

55
DeadEnd::Cli.new(
66
argv: ARGV

lib/dead_end.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
# frozen_string_literal: true
22

3-
require_relative "dead_end/api"
43
require_relative "dead_end/core_ext"

lib/dead_end/api.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
require_relative "version"
24

35
require "tmpdir"
@@ -7,6 +9,8 @@
79
require "timeout"
810

911
module DeadEnd
12+
VERSION = UnloadedDeadEnd::VERSION
13+
1014
# Used to indicate a default value that cannot
1115
# be confused with another input.
1216
DEFAULT_VALUE = Object.new.freeze

lib/dead_end/core_ext.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
# frozen_string_literal: true
22

3+
# Allow lazy loading, only load code if/when there's a syntax error
4+
autoload :DeadEnd, "dead_end/api"
5+
36
# Ruby 3.2+ has a cleaner way to hook into Ruby that doesn't use `require`
47
if SyntaxError.new.respond_to?(:detailed_message)
5-
module DeadEnd
8+
module DeadEndUnloaded
69
class MiniStringIO
710
def initialize(isatty: $stderr.isatty)
811
@string = +""
912
@isatty = isatty
1013
end
1114

1215
attr_reader :isatty
13-
1416
def puts(value = $/, **)
1517
@string << value
1618
end
@@ -23,7 +25,7 @@ def puts(value = $/, **)
2325
def detailed_message(highlight: nil, **)
2426
message = super
2527
file = DeadEnd::PathnameFromMessage.new(message).call.name
26-
io = DeadEnd::MiniStringIO.new
28+
io = DeadEndUnloaded::MiniStringIO.new
2729

2830
if file
2931
DeadEnd.call(
@@ -47,6 +49,8 @@ def detailed_message(highlight: nil, **)
4749
end
4850
}
4951
else
52+
autoload :Pathname, "pathname"
53+
5054
# Monkey patch kernel to ensure that all `require` calls call the same
5155
# method
5256
module Kernel

lib/dead_end/version.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# frozen_string_literal: true
22

3-
module DeadEnd
3+
# Calling `DeadEnd::VERSION` forces an eager load due to
4+
# an `autoload` on the `DeadEnd` constant.
5+
#
6+
# This is used for gemspec access in tests
7+
module UnloadedDeadEnd
48
VERSION = "3.1.2"
59
end

spec/integration/ruby_command_line_spec.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,50 @@ module DeadEnd
101101
expect(out).to include('❯ 5 it "flerg"').once
102102
end
103103
end
104+
105+
it "does not load internals into memory if no syntax error" do
106+
Dir.mktmpdir do |dir|
107+
tmpdir = Pathname(dir)
108+
script = tmpdir.join("script.rb")
109+
script.write <<~EOM
110+
class Dog
111+
end
112+
113+
# When a constant is defined through an autoload
114+
# then Object.autoload? will return the name of the
115+
# require only until it has been loaded.
116+
#
117+
# We can use this to detect if DeadEnd internals
118+
# have been fully loaded yet or not.
119+
#
120+
# Example:
121+
#
122+
# Object.autoload?("Cat") # => nil
123+
# autoload :Cat, "animals/cat
124+
# Object.autoload?("Cat") # => "animals/cat
125+
# Object.autoload?("Cat") # => "animals/cat
126+
#
127+
# # Once required, `autoload?` returns falsey
128+
# puts Cat.meow # invoke autoload
129+
# Object.autoload?("Cat") # => nil
130+
#
131+
if Object.autoload?("DeadEnd")
132+
puts "DeadEnd is NOT loaded"
133+
else
134+
puts "DeadEnd is loaded"
135+
end
136+
EOM
137+
138+
require_rb = tmpdir.join("require.rb")
139+
require_rb.write <<~EOM
140+
load "#{script.expand_path}"
141+
EOM
142+
143+
out = `ruby -I#{lib_dir} -rdead_end #{require_rb} 2>&1`
144+
145+
expect($?.success?).to be_truthy
146+
expect(out).to include("DeadEnd is NOT loaded").once
147+
end
148+
end
104149
end
105150
end

spec/spec_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
require "bundler/setup"
4-
require "dead_end"
4+
require "dead_end/api"
55

66
require "benchmark"
77
require "tempfile"

0 commit comments

Comments
 (0)