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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a3775abc9bff6d949fa377e2da5121414500735c37d9c5b9d6f03a7a2ae31a7
4
- data.tar.gz: 7254e2e5c3f25f431cdb25e8d0512d55a155afd8b64647b255b3a8223b663f3f
3
+ metadata.gz: 8dce970e998105e2e322410b204af7a0ff48790cd177225468e9b49c75651fdb
4
+ data.tar.gz: d93b8f4f0690bf5ade69b2c08e4354d3043567fc4bf6018d6e68084eece08be9
5
5
  SHA512:
6
- metadata.gz: cf54fd237e40750eff85fb4723651a747ba6e69214ea04acae1a95964a09e4a800ea1de31dbc4f7dec7d26c585ef7fb9167a3e97eb36e9b0c14003240696d6d6
7
- data.tar.gz: 81f413c5e21379b145a99f9882db9faea3d27e75ef56df7a8a928ce0b74f141c26025300cf0449fbf00128eb4991d99c94316f4fb522c5bd25042fb6ad2cfff3
6
+ metadata.gz: bea7fecb6bbae7feb68fa649ecba6276b3d497312ff99054c527d2f74117b584dd7119fa8e4a346b0091089786e90a0a2f2f375f1bc94a0e5437dfa8cff99e20
7
+ data.tar.gz: 913ab70d04467c584ecf5edfe4c8d60807974cf3d01eb9f2107ceaaa8677c157dc3b0e4130b6e90dcdaba3ba055b4a5268b40e94d0054eb6c41d1146205b0eff
data/.rubocop.yml CHANGED
@@ -19,6 +19,7 @@ Layout/LineLength:
19
19
 
20
20
  Metrics/BlockLength:
21
21
  ExcludedMethods:
22
+ - describe
22
23
  - it
23
24
  - context
24
25
 
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
- #- methods:
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
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.77.4'
6
+ VERSION = '0.78.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.5.1'
9
9
 
@@ -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.77.4
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