appmap 0.77.4 → 0.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +7 -0
- data/ext/appmap/appmap.c +21 -2
- data/lib/appmap/builtin_hooks/ruby.yml +6 -3
- data/lib/appmap/handler/eval.rb +41 -0
- data/lib/appmap/version.rb +1 -1
- data/spec/handler/eval_spec.rb +66 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8dce970e998105e2e322410b204af7a0ff48790cd177225468e9b49c75651fdb
|
4
|
+
data.tar.gz: d93b8f4f0690bf5ade69b2c08e4354d3043567fc4bf6018d6e68084eece08be9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bea7fecb6bbae7feb68fa649ecba6276b3d497312ff99054c527d2f74117b584dd7119fa8e4a346b0091089786e90a0a2f2f375f1bc94a0e5437dfa8cff99e20
|
7
|
+
data.tar.gz: 913ab70d04467c584ecf5edfe4c8d60807974cf3d01eb9f2107ceaaa8677c157dc3b0e4130b6e90dcdaba3ba055b4a5268b40e94d0054eb6c41d1146205b0eff
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
# [0.78.0](https://github.com/applandinc/appmap-ruby/compare/v0.77.4...v0.78.0) (2022-04-04)
|
2
|
+
|
3
|
+
|
4
|
+
### Features
|
5
|
+
|
6
|
+
* Hook and label Kernel#eval ([e0c151d](https://github.com/applandinc/appmap-ruby/commit/e0c151d1371f5bed5597ffd0d3bfebb8bca247c2))
|
7
|
+
|
1
8
|
## [0.77.4](https://github.com/applandinc/appmap-ruby/compare/v0.77.3...v0.77.4) (2022-04-04)
|
2
9
|
|
3
10
|
|
data/ext/appmap/appmap.c
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#include <ruby.h>
|
2
|
+
#include <ruby/debug.h>
|
2
3
|
#include <ruby/intern.h>
|
3
4
|
|
4
5
|
// Seems like CLASS_OR_MODULE_P should really be in a header file in
|
@@ -39,7 +40,7 @@ am_define_method_with_arity(VALUE mod, VALUE name, VALUE arity, VALUE proc)
|
|
39
40
|
{
|
40
41
|
VALUE arities_key = rb_intern(ARITIES_KEY);
|
41
42
|
VALUE arities = rb_ivar_get(mod, arities_key);
|
42
|
-
|
43
|
+
|
43
44
|
if (arities == Qundef || NIL_P(arities)) {
|
44
45
|
arities = rb_hash_new();
|
45
46
|
rb_ivar_set(mod, arities_key, arities);
|
@@ -62,7 +63,7 @@ am_get_method_arity(VALUE method, VALUE orig_arity_method)
|
|
62
63
|
}
|
63
64
|
// Didn't find one, call the original method.
|
64
65
|
if (NIL_P(arity)) {
|
65
|
-
VALUE bound_method = rb_funcall(orig_arity_method, rb_intern("bind"), 1, method);
|
66
|
+
VALUE bound_method = rb_funcall(orig_arity_method, rb_intern("bind"), 1, method);
|
66
67
|
arity = rb_funcall(bound_method, rb_intern("call"), 0);
|
67
68
|
}
|
68
69
|
|
@@ -83,11 +84,29 @@ am_method_arity(VALUE method)
|
|
83
84
|
return am_get_method_arity(method, orig_method_arity);
|
84
85
|
}
|
85
86
|
|
87
|
+
static VALUE
|
88
|
+
bindings_callback(const rb_debug_inspector_t *dbg_context, void *level)
|
89
|
+
{
|
90
|
+
const int i = NUM2INT((VALUE) level);
|
91
|
+
if (i >= RARRAY_LEN(rb_debug_inspector_backtrace_locations(dbg_context))) {
|
92
|
+
return Qnil;
|
93
|
+
} else {
|
94
|
+
return rb_debug_inspector_frame_binding_get(dbg_context, i);
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
static VALUE
|
99
|
+
am_previous_bindings(VALUE self, VALUE level)
|
100
|
+
{
|
101
|
+
return rb_debug_inspector_open(bindings_callback, (void *) level);
|
102
|
+
}
|
103
|
+
|
86
104
|
void Init_appmap() {
|
87
105
|
VALUE appmap = rb_define_module("AppMap");
|
88
106
|
am_AppMapHook = rb_define_class_under(appmap, "Hook", rb_cObject);
|
89
107
|
|
90
108
|
rb_define_singleton_method(am_AppMapHook, "singleton_method_owner_name", singleton_method_owner_name, 1);
|
109
|
+
rb_define_module_function(appmap, "caller_binding", am_previous_bindings, 1);
|
91
110
|
|
92
111
|
rb_define_method(rb_cModule, "define_method_with_arity", am_define_method_with_arity, 3);
|
93
112
|
rb_define_method(rb_cUnboundMethod, "arity", am_unbound_method_arity, 0);
|
@@ -14,14 +14,17 @@
|
|
14
14
|
- String#unpack1
|
15
15
|
require_name: ruby
|
16
16
|
label: string.unpack
|
17
|
-
|
17
|
+
- methods:
|
18
|
+
- Kernel#eval
|
19
|
+
require_name: ruby
|
20
|
+
label: lang.eval
|
21
|
+
handler_class: AppMap::Handler::Eval
|
18
22
|
# TODO: eval does not happen in the right context, and therefore any new constants
|
19
23
|
# which are defined are placed on the wrong module/class.
|
20
|
-
# - Kernel#eval
|
21
24
|
# - Binding#eval
|
22
25
|
# - BasicObject#instance_eval
|
23
26
|
# These methods cannot be hooked as far as I can tell.
|
24
|
-
# Why? When calling one of these functions, the context at the point of
|
27
|
+
# Why? When calling one of these functions, the context at the point of
|
25
28
|
# definition is used. It's not possible to bind class_eval to a new context.
|
26
29
|
# - Module#class_eval
|
27
30
|
# - Module#module_eval
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'appmap/handler/function'
|
4
|
+
|
5
|
+
module AppMap
|
6
|
+
module Handler
|
7
|
+
# Handler class for Kernel#eval.
|
8
|
+
#
|
9
|
+
# Second argument to eval is a Binding, which despite the name (and
|
10
|
+
# the accessible methods) in addition to locals and receiver also
|
11
|
+
# encapsulates the entire execution context, in particular including
|
12
|
+
# the lexical scope. This is especially important for constant lookup
|
13
|
+
# and definition.
|
14
|
+
#
|
15
|
+
# If the binding is not provided, by default eval will run in the
|
16
|
+
# current frame. Since we call it here, this will mean the #do_call
|
17
|
+
# frame, which would make AppMap::Handler::Eval the lexical scope
|
18
|
+
# for constant lookup and definition; as a consequence
|
19
|
+
# eg. `eval "class Foo; end"` would define
|
20
|
+
# AppMap::Handler::Eval::Foo instead of defining it in
|
21
|
+
# the module where the original call was made.
|
22
|
+
#
|
23
|
+
# To avoid this, we explicitly substitute the correct execution
|
24
|
+
# context, up several stack frames.
|
25
|
+
class Eval < Function
|
26
|
+
# The depth of the frame we need to pluck out:
|
27
|
+
# 1. Hook::Method#do_call
|
28
|
+
# 2. Hook::Method#trace_call
|
29
|
+
# 3. Hook::Method#call
|
30
|
+
# 4. proc generated by Hook::Method#hook_method_def
|
31
|
+
# 5. the (intended) frame of the original eval that we hooked
|
32
|
+
# Note it needs to be adjusted if this call sequence changes.
|
33
|
+
FRAME_DEPTH = 5
|
34
|
+
|
35
|
+
def do_call(receiver, src = nil, context = nil, *rest)
|
36
|
+
context ||= AppMap.caller_binding FRAME_DEPTH
|
37
|
+
hook_method.bind(receiver).call(src, context, *rest)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/appmap/version.rb
CHANGED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Security/Eval, Style/EvalWithLocation
|
4
|
+
|
5
|
+
require 'spec_helper'
|
6
|
+
require 'appmap/config'
|
7
|
+
|
8
|
+
describe 'AppMap::Handler::Eval' do
|
9
|
+
include_context 'collect events'
|
10
|
+
let!(:config) { AppMap::Config.new('hook_spec') }
|
11
|
+
before { AppMap.configuration = config }
|
12
|
+
after { AppMap.configuration = nil }
|
13
|
+
|
14
|
+
def record_block
|
15
|
+
AppMap::Hook.new(config).enable do
|
16
|
+
tracer = AppMap.tracing.trace
|
17
|
+
AppMap::Event.reset_id_counter
|
18
|
+
begin
|
19
|
+
yield
|
20
|
+
ensure
|
21
|
+
AppMap.tracing.delete(tracer)
|
22
|
+
end
|
23
|
+
tracer
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'produces a simple result' do
|
28
|
+
tracer = record_block do
|
29
|
+
expect(eval('12')).to eq(12)
|
30
|
+
end
|
31
|
+
events = collect_events(tracer)
|
32
|
+
expect(events[0]).to match hash_including \
|
33
|
+
defined_class: 'Kernel',
|
34
|
+
method_id: 'eval',
|
35
|
+
parameters: [{ class: 'Array', kind: :rest, name: 'arg', value: '[12]' }]
|
36
|
+
end
|
37
|
+
|
38
|
+
# a la Ruby 2.6.3 ruby-token.rb
|
39
|
+
# token_c = eval("class #{token_n} < #{super_token}; end; #{token_n}")
|
40
|
+
it 'can define a new class' do
|
41
|
+
num = (Random.random_number * 10_000).to_i
|
42
|
+
class_name = "Cls_#{num}"
|
43
|
+
m = ClassMaker
|
44
|
+
cls = nil
|
45
|
+
record_block do
|
46
|
+
cls = m.make_class class_name
|
47
|
+
end
|
48
|
+
expect(cls).to be_instance_of(Class)
|
49
|
+
# If execution context wasn't substituted, the class would be defined as
|
50
|
+
# eg. AppMap::Handler::Eval::Cls_7566
|
51
|
+
expect { AppMap::Handler::Eval.const_get(class_name) }.to raise_error(NameError)
|
52
|
+
# This would be the right behavior
|
53
|
+
expect(m.const_get(class_name)).to be_instance_of(Class)
|
54
|
+
expect(m.const_get(class_name)).to eq(cls)
|
55
|
+
new_cls = Class.new do
|
56
|
+
include m
|
57
|
+
end
|
58
|
+
expect(new_cls.const_get(class_name)).to eq(cls)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
module ClassMaker
|
63
|
+
def self.make_class(class_name)
|
64
|
+
eval "class #{class_name}; end; #{class_name}"
|
65
|
+
end
|
66
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: appmap
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.78.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Gilpin
|
@@ -376,6 +376,7 @@ files:
|
|
376
376
|
- lib/appmap/gem_hooks/sidekiq.yml
|
377
377
|
- lib/appmap/gem_hooks/sprockets.yml
|
378
378
|
- lib/appmap/handler.rb
|
379
|
+
- lib/appmap/handler/eval.rb
|
379
380
|
- lib/appmap/handler/function.rb
|
380
381
|
- lib/appmap/handler/net_http.rb
|
381
382
|
- lib/appmap/handler/rails/request_handler.rb
|
@@ -703,6 +704,7 @@ files:
|
|
703
704
|
- spec/fixtures/rails7_users_app/test/models/instructor_test.rb
|
704
705
|
- spec/fixtures/rails7_users_app/test/system/.keep
|
705
706
|
- spec/fixtures/rails7_users_app/test/test_helper.rb
|
707
|
+
- spec/handler/eval_spec.rb
|
706
708
|
- spec/hook_spec.rb
|
707
709
|
- spec/open_spec.rb
|
708
710
|
- spec/rails_recording_spec.rb
|