Skip to content

Commit 175f489

Browse files
committed
fix: Add the proper template function hooks for Rails 6.0.7
Ignore methods named 'call'
1 parent 2c701ec commit 175f489

File tree

4 files changed

+108
-96
lines changed

4 files changed

+108
-96
lines changed

lib/appmap/config.rb

Lines changed: 93 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,7 @@ def to_h
8585
end
8686
end
8787

88-
Function = Struct.new(:package, :cls, :labels, :function_names) do # :nodoc:
89-
def to_h
90-
{
91-
package: package,
92-
class: cls,
93-
labels: labels,
94-
functions: function_names.map(&:to_sym)
95-
}.compact
96-
end
97-
end
98-
private_constant :Function
99-
88+
# Identifies specific methods within a package which should be hooked.
10089
class TargetMethods # :nodoc:
10190
attr_reader :method_names, :package
10291

@@ -118,28 +107,91 @@ def to_h
118107
end
119108
private_constant :TargetMethods
120109

121-
OPENSSL_PACKAGES = ->(labels) { Package.build_from_path('openssl', package_name: 'openssl', labels: labels) }
110+
# Function represents a specific function configured for hooking by the +functions+
111+
# entry in appmap.yml. When the Config is initialized, each Function is converted into
112+
# a Package and TargetMethods. It's called a Function rather than a Method, because Function
113+
# is the AppMap terminology.
114+
Function = Struct.new(:package, :cls, :labels, :function_names) do # :nodoc:
115+
def to_h
116+
{
117+
package: package,
118+
class: cls,
119+
labels: labels,
120+
functions: function_names.map(&:to_sym)
121+
}.compact
122+
end
123+
end
124+
private_constant :Function
122125

123-
# Methods that should always be hooked, with their containing
124-
# package and labels that should be applied to them.
125-
HOOKED_METHODS = {
126-
'ActionView::Renderer' => TargetMethods.new(:render, Package.build_from_gem('actionview', shallow: false, package_name: 'action_view', labels: %w[mvc.view], optional: true).tap do |package|
127-
package.handler_class = AppMap::Handler::Rails::Template::RenderHandler if package
128-
end),
129-
'ActionView::Resolver' => TargetMethods.new(%i[find_all find_all_anywhere], Package.build_from_gem('actionview', shallow: false, package_name: 'action_view', labels: %w[mvc.template.resolver], optional: true).tap do |package|
130-
package.handler_class = AppMap::Handler::Rails::Template::ResolverHandler if package
131-
end),
132-
'ActionDispatch::Request::Session' => TargetMethods.new(%i[destroy [] dig values []= clear update delete fetch merge], Package.build_from_gem('actionpack', shallow: false, package_name: 'action_dispatch', labels: %w[http.session], optional: true)),
133-
'ActionDispatch::Cookies::CookieJar' => TargetMethods.new(%i[[]= clear update delete recycle], Package.build_from_gem('actionpack', shallow: false, package_name: 'action_dispatch', labels: %w[http.cookie], optional: true)),
134-
'ActionDispatch::Cookies::EncryptedCookieJar' => TargetMethods.new(%i[[]=], Package.build_from_gem('actionpack', shallow: false, package_name: 'action_dispatch', labels: %w[http.cookie crypto.encrypt], optional: true)),
135-
'CanCan::ControllerAdditions' => TargetMethods.new(%i[authorize! can? cannot?], Package.build_from_gem('cancancan', shallow: false, labels: %w[security.authorization], optional: true)),
136-
'CanCan::Ability' => TargetMethods.new(%i[authorize!], Package.build_from_gem('cancancan', shallow: false, labels: %w[security.authorization], optional: true)),
137-
'ActionController::Instrumentation' => [
138-
TargetMethods.new(%i[process_action send_file send_data redirect_to], Package.build_from_gem('actionpack', shallow: false, package_name: 'action_controller', labels: %w[mvc.controller], optional: true))
139-
]
140-
}.freeze
126+
ClassTargetMethods = Struct.new(:cls, :target_methods) # :nodoc:
127+
private_constant :ClassTargetMethods
128+
129+
MethodHook = Struct.new(:cls, :method_names, :labels) # :nodoc:
130+
private_constant :MethodHook
131+
132+
class << self
133+
def package_hooks(gem_name, methods, handler_class: nil, package_name: nil)
134+
Array(methods).map do |method|
135+
package = Package.build_from_gem(gem_name, package_name: package_name, labels: method.labels, shallow: false, optional: true)
136+
next unless package
137+
138+
package.handler_class = handler_class if handler_class
139+
ClassTargetMethods.new(method.cls, TargetMethods.new(Array(method.method_names), package))
140+
end.compact
141+
end
142+
143+
def method_hook(cls, method_names, labels)
144+
MethodHook.new(cls, method_names, labels)
145+
end
146+
end
147+
148+
# Hook well-known functions. When a function configured here is available in the bundle, it will be hooked with the
149+
# predefined labels specified here. If any of these hooks are not desired, they can be disabled in the +exclude+ section
150+
# of appmap.yml.
151+
METHOD_HOOKS = [
152+
package_hooks('actionview',
153+
[
154+
method_hook('ActionView::Renderer', :render, %w[mvc.view]),
155+
method_hook('ActionView::TemplateRenderer', :render, %w[mvc.view]),
156+
method_hook('ActionView::PartialRenderer', :render, %w[mvc.view])
157+
],
158+
handler_class: AppMap::Handler::Rails::Template::RenderHandler,
159+
package_name: 'action_view'
160+
),
161+
package_hooks('actionview',
162+
[
163+
method_hook('ActionView::Resolver', %i[find_all find_all_anywhere], %w[mvc.template.resolver])
164+
],
165+
handler_class: AppMap::Handler::Rails::Template::ResolverHandler,
166+
package_name: 'action_view'
167+
),
168+
package_hooks('actionpack',
169+
[
170+
method_hook('ActionDispatch::Request::Session', %i[destroy [] dig values []= clear update delete fetch merge], %w[http.session]),
171+
method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session]),
172+
method_hook('ActionDispatch::Cookies::EncryptedCookieJar', %i[[]= clear update delete recycle], %w[http.cookie crypto.encrypt])
173+
],
174+
package_name: 'action_dispatch'
175+
),
176+
package_hooks('cancancan',
177+
[
178+
method_hook('CanCan::ControllerAdditions', %i[authorize! can? cannot?], %w[security.authorization]),
179+
method_hook('CanCan::Ability', %i[authorize?], %w[security.authorization])
180+
]
181+
),
182+
package_hooks('actionpack',
183+
[
184+
method_hook('ActionController::Instrumentation', %i[process_action send_file send_data redirect_to], %w[mvc.controller])
185+
],
186+
package_name: 'action_controller'
187+
)
188+
].flatten.freeze
141189

142-
BUILTIN_METHODS = {
190+
OPENSSL_PACKAGES = ->(labels) { Package.build_from_path('openssl', package_name: 'openssl', labels: labels) }
191+
192+
# Hook functions which are builtin to Ruby. Because they are builtins, they may be loaded before appmap.
193+
# Therefore, we can't rely on TracePoint to report the loading of this code.
194+
BUILTIN_HOOKS = {
143195
'OpenSSL::PKey::PKey' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.pkey])),
144196
'OpenSSL::X509::Request' => TargetMethods.new(%i[sign verify], OPENSSL_PACKAGES.(%w[crypto.x509])),
145197
'OpenSSL::PKCS5' => TargetMethods.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES.(%w[crypto.pkcs5])),
@@ -166,26 +218,29 @@ def to_h
166218
'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
167219
}.freeze
168220

169-
attr_reader :name, :packages, :exclude, :hooked_methods, :builtin_methods
221+
attr_reader :name, :packages, :exclude, :hooked_methods, :builtin_hooks
170222

171223
def initialize(name, packages, exclude: [], functions: [])
172224
@name = name
173225
@packages = packages
174-
@hook_paths = packages.map(&:path)
226+
@hook_paths = Set.new(packages.map(&:path))
175227
@exclude = exclude
176-
@builtin_methods = BUILTIN_METHODS
228+
@builtin_hooks = BUILTIN_HOOKS
177229
@functions = functions
178-
@hooked_methods = HOOKED_METHODS.dup
230+
231+
@hooked_methods = METHOD_HOOKS.each_with_object(Hash.new { |h,k| h[k] = [] }) do |cls_target_methods, hooked_methods|
232+
hooked_methods[cls_target_methods.cls] << cls_target_methods.target_methods
233+
end
234+
179235
functions.each do |func|
180236
package_options = {}
181237
package_options[:labels] = func.labels if func.labels
182-
@hooked_methods[func.cls] ||= []
183238
@hooked_methods[func.cls] << TargetMethods.new(func.function_names, Package.build_from_path(func.package, package_options))
184239
end
185240

186241
@hooked_methods.each_value do |hooks|
187242
Array(hooks).each do |hook|
188-
@hook_paths << hook.package.path if hook.package
243+
@hook_paths << hook.package.path
189244
end
190245
end
191246
end

lib/appmap/handler/rails/template.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,18 @@ def handle_call(defined_class, hook_method, receiver, args)
105105
# If so, populate the template path. In all cases, add a TemplateMethod so that the
106106
# template will be recorded in the classMap.
107107
def handle_return(call_event_id, elapsed, return_value, exception)
108-
warn "Resolver return: #{return_value.inspect}" if LOG
109-
110108
renderer = Array(Thread.current[TEMPLATE_RENDERER]).last
111-
path = Array(return_value).first&.inspect
109+
path_obj = Array(return_value).first
110+
111+
warn "Resolver return: #{path_obj}" if LOG
112112

113-
if path
113+
if path_obj
114+
path = if path_obj.respond_to?(:identifier) && path_obj.inspect.index('#<')
115+
path_obj.identifier
116+
else
117+
path_obj.inspect
118+
end
119+
path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
114120
AppMap.tracing.record_method(TemplateMethod.new(path))
115121
renderer.path ||= path if renderer
116122
end

lib/appmap/hook.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def hook_builtins
6464
end
6565
end
6666

67-
config.builtin_methods.each do |class_name, hooks|
67+
config.builtin_hooks.each do |class_name, hooks|
6868
Array(hooks).each do |hook|
6969
require hook.package.package_name if hook.package.package_name
7070
Array(hook.method_names).each do |method_name|
@@ -139,6 +139,8 @@ def trace_end(trace_point)
139139
# a stack overflow in the defined hook method.
140140
next if %w[Marshal AppMap ActiveSupport].member?((hook_cls&.name || '').split('::')[0])
141141

142+
next if method_id == :call
143+
142144
method = begin
143145
hook_cls.public_instance_method(method_id)
144146
rescue NameError

spec/hook_spec.rb

Lines changed: 2 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -64,65 +64,14 @@ def test_hook_behavior(file, events_yaml, setup: nil, &block)
6464
expect(config.never_hook?(ExcludeTest, ExcludeTest.method(:cls_method))).to be_truthy
6565
end
6666

67-
it "handles an instance method named 'call' without issues" do
67+
it "an instance method named 'call' will be ignored" do
6868
events_yaml = <<~YAML
69-
---
70-
- :id: 1
71-
:event: :call
72-
:defined_class: MethodNamedCall
73-
:method_id: call
74-
:path: spec/fixtures/hook/method_named_call.rb
75-
:lineno: 8
76-
:static: false
77-
:parameters:
78-
- :name: :a
79-
:class: Integer
80-
:value: '1'
81-
:kind: :req
82-
- :name: :b
83-
:class: Integer
84-
:value: '2'
85-
:kind: :req
86-
- :name: :c
87-
:class: Integer
88-
:value: '3'
89-
:kind: :req
90-
- :name: :d
91-
:class: Integer
92-
:value: '4'
93-
:kind: :req
94-
- :name: :e
95-
:class: Integer
96-
:value: '5'
97-
:kind: :req
98-
:receiver:
99-
:class: MethodNamedCall
100-
:value: MethodNamedCall
101-
- :id: 2
102-
:event: :return
103-
:parent_id: 1
104-
:return_value:
105-
:class: String
106-
:value: 1 2 3 4 5
69+
--- []
10770
YAML
10871

10972
_, tracer = test_hook_behavior 'spec/fixtures/hook/method_named_call.rb', events_yaml do
11073
expect(MethodNamedCall.new.call(1, 2, 3, 4, 5)).to eq('1 2 3 4 5')
11174
end
112-
class_map = AppMap.class_map(tracer.event_methods)
113-
expect(Diffy::Diff.new(<<~CLASSMAP, YAML.dump(class_map)).to_s).to eq('')
114-
---
115-
- :name: spec/fixtures/hook/method_named_call.rb
116-
:type: package
117-
:children:
118-
- :name: MethodNamedCall
119-
:type: class
120-
:children:
121-
- :name: call
122-
:type: function
123-
:location: spec/fixtures/hook/method_named_call.rb:8
124-
:static: false
125-
CLASSMAP
12675
end
12776

12877
it 'can custom hook and label a function' do

0 commit comments

Comments
 (0)