appmap 0.77.4 → 0.78.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|