11
11
12
12
module AppMap
13
13
class Config
14
- # Specifies a code +path+ to be mapped.
14
+ # Specifies a logical code package be mapped.
15
+ # This can be a project source folder, a Gem, or a builtin.
15
16
#
16
17
# Options:
17
18
#
19
+ # * +path+ indicates a relative path to a code folder.
18
20
# * +gem+ may indicate a gem name that "owns" the path
19
21
# * +require_name+ can be used to make sure that the code is required so that it can be loaded. This is generally used with
20
22
# builtins, or when the path to be required is not automatically required when bundler requires the gem.
21
23
# * +exclude+ can be used used to exclude sub-paths. Generally not used with +gem+.
22
24
# * +labels+ is used to apply labels to matching code. This is really only useful when the package will be applied to
23
25
# specific functions, via TargetMethods.
24
26
# * +shallow+ indicates shallow mapping, in which only the entrypoint to a gem is recorded.
25
- Package = Struct . new ( :path , :gem , :require_name , :exclude , :labels , :shallow ) do
27
+ Package = Struct . new ( :name , : path, :gem , :require_name , :exclude , :labels , :shallow ) do
26
28
# This is for internal use only.
27
29
private_methods :gem
28
30
@@ -45,7 +47,7 @@ class << self
45
47
# Builds a package for a path, such as `app/models` in a Rails app. Generally corresponds to a `path:` entry
46
48
# in appmap.yml. Also used for mapping specific methods via TargetMethods.
47
49
def build_from_path ( path , shallow : false , require_name : nil , exclude : [ ] , labels : [ ] )
48
- Package . new ( path , nil , require_name , exclude , labels , shallow )
50
+ Package . new ( path , path , nil , require_name , exclude , labels , shallow )
49
51
end
50
52
51
53
# Builds a package for gem. Generally corresponds to a `gem:` entry in appmap.yml. Also used when mapping
@@ -57,7 +59,7 @@ def build_from_gem(gem, shallow: true, require_name: nil, exclude: [], labels: [
57
59
end
58
60
path = gem_path ( gem , optional )
59
61
if path
60
- Package . new ( path , gem , require_name , exclude , labels , shallow )
62
+ Package . new ( gem , path , gem , require_name , exclude , labels , shallow )
61
63
else
62
64
AppMap ::Util . startup_message "#{ gem } is not available in the bundle"
63
65
end
@@ -75,19 +77,16 @@ def gem_path(gem, optional)
75
77
end
76
78
end
77
79
78
- def name
79
- gem || path
80
- end
81
-
82
80
def to_h
83
81
{
82
+ name : name ,
84
83
path : path ,
85
- require_name : require_name ,
86
84
gem : gem ,
87
- handler_class : handler_class . name ,
85
+ require_name : require_name ,
86
+ handler_class : handler_class ? handler_class . name : nil ,
88
87
exclude : Util . blank? ( exclude ) ? nil : exclude ,
89
88
labels : Util . blank? ( labels ) ? nil : labels ,
90
- shallow : shallow
89
+ shallow : shallow . nil? ? nil : shallow ,
91
90
} . compact
92
91
end
93
92
end
@@ -97,12 +96,12 @@ class TargetMethods # :nodoc:
97
96
attr_reader :method_names , :package
98
97
99
98
def initialize ( method_names , package )
100
- @method_names = method_names
99
+ @method_names = Array ( method_names ) . map ( & :to_sym )
101
100
@package = package
102
101
end
103
102
104
103
def include_method? ( method_name )
105
- Array ( method_names ) . include? ( method_name )
104
+ method_names . include? ( method_name . to_sym )
106
105
end
107
106
108
107
def to_h
@@ -139,9 +138,13 @@ def to_h
139
138
private_constant :MethodHook
140
139
141
140
class << self
142
- def package_hooks ( gem_name , methods , handler_class : nil , require_name : nil )
141
+ def package_hooks ( methods , path : nil , gem : nil , force : false , handler_class : nil , require_name : nil )
143
142
Array ( methods ) . map do |method |
144
- package = Package . build_from_gem ( gem_name , require_name : require_name , labels : method . labels , shallow : false , optional : true )
143
+ package = if gem
144
+ Package . build_from_gem ( gem , require_name : require_name , labels : method . labels , shallow : false , force : force , optional : true )
145
+ elsif path
146
+ Package . build_from_path ( path , require_name : require_name , labels : method . labels , shallow : false )
147
+ end
145
148
next unless package
146
149
147
150
package . handler_class = handler_class if handler_class
@@ -152,85 +155,63 @@ def package_hooks(gem_name, methods, handler_class: nil, require_name: nil)
152
155
def method_hook ( cls , method_names , labels )
153
156
MethodHook . new ( cls , method_names , labels )
154
157
end
155
- end
156
158
157
- # Hook well-known functions. When a function configured here is available in the bundle, it will be hooked with the
158
- # predefined labels specified here. If any of these hooks are not desired, they can be disabled in the +exclude+ section
159
- # of appmap.yml.
160
- METHOD_HOOKS = [
161
- package_hooks ( 'actionview' ,
162
- [
163
- method_hook ( 'ActionView::Renderer' , :render , %w[ mvc.view ] ) ,
164
- method_hook ( 'ActionView::TemplateRenderer' , :render , %w[ mvc.view ] ) ,
165
- method_hook ( 'ActionView::PartialRenderer' , :render , %w[ mvc.view ] )
166
- ] ,
167
- handler_class : AppMap ::Handler ::Rails ::Template ::RenderHandler ,
168
- require_name : 'action_view'
169
- ) ,
170
- package_hooks ( 'actionview' ,
171
- [
172
- method_hook ( 'ActionView::Resolver' , %i[ find_all find_all_anywhere ] , %w[ mvc.template.resolver ] )
173
- ] ,
174
- handler_class : AppMap ::Handler ::Rails ::Template ::ResolverHandler ,
175
- require_name : 'action_view'
176
- ) ,
177
- package_hooks ( 'actionpack' ,
178
- [
179
- method_hook ( 'ActionDispatch::Request::Session' , %i[ [] dig values fetch ] , %w[ http.session.read ] ) ,
180
- method_hook ( 'ActionDispatch::Request::Session' , %i[ destroy []= clear update delete merge ] , %w[ http.session.write ] ) ,
181
- method_hook ( 'ActionDispatch::Cookies::CookieJar' , %i[ [] fetch ] , %w[ http.session.read ] ) ,
182
- method_hook ( 'ActionDispatch::Cookies::CookieJar' , %i[ []= clear update delete recycle ] , %w[ http.session.write ] ) ,
183
- method_hook ( 'ActionDispatch::Cookies::EncryptedCookieJar' , %i[ []= clear update delete recycle ] , %w[ http.cookie crypto.encrypt ] )
184
- ] ,
185
- require_name : 'action_dispatch'
186
- ) ,
187
- package_hooks ( 'cancancan' ,
188
- [
189
- method_hook ( 'CanCan::ControllerAdditions' , %i[ authorize! can? cannot? ] , %w[ security.authorization ] ) ,
190
- method_hook ( 'CanCan::Ability' , %i[ authorize? ] , %w[ security.authorization ] )
191
- ]
192
- ) ,
193
- package_hooks ( 'actionpack' ,
194
- [
195
- method_hook ( 'ActionController::Instrumentation' , %i[ process_action send_file send_data redirect_to ] , %w[ mvc.controller ] )
196
- ] ,
197
- require_name : 'action_controller'
198
- )
199
- ] . flatten . freeze
200
-
201
- OPENSSL_PACKAGES = -> ( labels ) { Package . build_from_path ( 'openssl' , require_name : 'openssl' , labels : labels ) }
202
-
203
- # Hook functions which are builtin to Ruby. Because they are builtins, they may be loaded before appmap.
204
- # Therefore, we can't rely on TracePoint to report the loading of this code.
205
- BUILTIN_HOOKS = {
206
- 'OpenSSL::PKey::PKey' => TargetMethods . new ( :sign , OPENSSL_PACKAGES . ( %w[ crypto.pkey ] ) ) ,
207
- 'OpenSSL::X509::Request' => TargetMethods . new ( %i[ sign verify ] , OPENSSL_PACKAGES . ( %w[ crypto.x509 ] ) ) ,
208
- 'OpenSSL::PKCS5' => TargetMethods . new ( %i[ pbkdf2_hmac_sha1 pbkdf2_hmac ] , OPENSSL_PACKAGES . ( %w[ crypto.pkcs5 ] ) ) ,
209
- 'OpenSSL::Cipher' => [
210
- TargetMethods . new ( %i[ encrypt ] , OPENSSL_PACKAGES . ( %w[ crypto.encrypt ] ) ) ,
211
- TargetMethods . new ( %i[ decrypt ] , OPENSSL_PACKAGES . ( %w[ crypto.decrypt ] ) )
212
- ] ,
213
- 'ActiveSupport::Callbacks::CallbackSequence' => [
214
- TargetMethods . new ( :invoke_before , Package . build_from_gem ( 'activesupport' , force : true , require_name : 'active_support' , labels : %w[ mvc.before_action ] ) ) ,
215
- TargetMethods . new ( :invoke_after , Package . build_from_gem ( 'activesupport' , force : true , require_name : 'active_support' , labels : %w[ mvc.after_action ] ) ) ,
216
- ] ,
217
- 'ActiveSupport::SecurityUtils' => TargetMethods . new ( :secure_compare , Package . build_from_gem ( 'activesupport' , force : true , require_name : 'active_support/security_utils' , labels : %w[ crypto.secure_compare ] ) ) ,
218
- 'OpenSSL::X509::Certificate' => TargetMethods . new ( :sign , OPENSSL_PACKAGES . ( %w[ crypto.x509 ] ) ) ,
219
- 'Net::HTTP' => TargetMethods . new ( :request , Package . build_from_path ( 'net/http' , require_name : 'net/http' , labels : %w[ protocol.http ] ) . tap do |package |
220
- package . handler_class = AppMap ::Handler ::NetHTTP
221
- end ) ,
222
- 'Net::SMTP' => TargetMethods . new ( :send , Package . build_from_path ( 'net/smtp' , require_name : 'net/smtp' , labels : %w[ protocol.email.smtp ] ) ) ,
223
- 'Net::POP3' => TargetMethods . new ( :mails , Package . build_from_path ( 'net/pop3' , require_name : 'net/pop' , labels : %w[ protocol.email.pop ] ) ) ,
224
- # This is happening: Method send_command not found on Net::IMAP
225
- # 'Net::IMAP' => TargetMethods.new(:send_command, Package.build_from_path('net/imap', require_name: 'net/imap', labels: %w[protocol.email.imap])),
226
- # 'Marshal' => TargetMethods.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
227
- 'Psych' => [
228
- TargetMethods . new ( %i[ load load_stream parse parse_stream ] , Package . build_from_path ( 'yaml' , require_name : 'psych' , labels : %w[ format.yaml.parse ] ) ) ,
229
- TargetMethods . new ( %i[ dump dump_stream ] , Package . build_from_path ( 'yaml' , require_name : 'psych' , labels : %w[ format.yaml.generate ] ) ) ,
230
- ] ,
231
- 'JSON::Ext::Parser' => TargetMethods . new ( :parse , Package . build_from_path ( 'json' , require_name : 'json' , labels : %w[ format.json.parse ] ) ) ,
232
- 'JSON::Ext::Generator::State' => TargetMethods . new ( :generate , Package . build_from_path ( 'json' , require_name : 'json' , labels : %w[ format.json.generate ] ) ) ,
233
- } . freeze
159
+ def declare_hook ( hook_decl )
160
+ hook_decl = YAML . load ( hook_decl ) if hook_decl . is_a? ( String )
161
+
162
+ methods_decl = hook_decl [ 'methods' ] || hook_decl [ 'method' ]
163
+ methods_decl = Array ( methods_decl ) unless methods_decl . is_a? ( Hash )
164
+ labels_decl = Array ( hook_decl [ 'labels' ] || hook_decl [ 'label' ] )
165
+
166
+ methods = methods_decl . map do |name |
167
+ class_name , method_name , static = name . include? ( '.' ) ? name . split ( '.' , 2 ) + [ true ] : name . split ( '#' , 2 ) + [ false ]
168
+ method_hook class_name , [ method_name ] , labels_decl
169
+ end
170
+
171
+ require_name = hook_decl [ 'require_name' ]
172
+ gem_name = hook_decl [ 'gem' ]
173
+ path = hook_decl [ 'path' ]
174
+
175
+ options = {
176
+ gem : gem_name ,
177
+ path : path ,
178
+ require_name : require_name || gem_name || path ,
179
+ force : hook_decl [ 'force' ]
180
+ } . compact
181
+
182
+ handler_class = hook_decl [ 'handler_class' ]
183
+ options [ :handler_class ] = Util ::class_from_string ( handler_class ) if handler_class
184
+
185
+ package_hooks ( methods , **options )
186
+ end
187
+
188
+ def load_builtin_hooks
189
+ load_hooks ( 'builtin_hooks' ) do |path , config |
190
+ config [ 'path' ] = path
191
+ end
192
+ end
193
+
194
+ def load_gem_hooks
195
+ load_hooks ( 'gem_hooks' ) do |path , config |
196
+ config [ 'gem' ] = path
197
+ end
198
+ end
199
+
200
+ def load_hooks ( dir , &block )
201
+ basedir = [ __dir__ , dir ] . join ( '/' )
202
+ [ ] . tap do |hooks |
203
+ Dir . glob ( "#{ basedir } /**/*.yml" ) . each do |yaml_file |
204
+ path = yaml_file [ basedir . length + 1 ...-4 ]
205
+ YAML . load ( File . read ( yaml_file ) ) . map do |config |
206
+ yield path , config
207
+ config
208
+ end . each do |config |
209
+ hooks << declare_hook ( config )
210
+ end
211
+ end
212
+ end . compact . flatten
213
+ end
214
+ end
234
215
235
216
attr_reader :name , :appmap_dir , :packages , :exclude , :swagger_config , :depends_config , :hooked_methods , :builtin_hooks
236
217
@@ -247,10 +228,13 @@ def initialize(name,
247
228
@depends_config = depends_config
248
229
@hook_paths = Set . new ( packages . map ( &:path ) )
249
230
@exclude = exclude
250
- @builtin_hooks = BUILTIN_HOOKS . dup
251
231
@functions = functions
252
232
253
- @hooked_methods = METHOD_HOOKS . each_with_object ( Hash . new { |h , k | h [ k ] = [ ] } ) do |cls_target_methods , hooked_methods |
233
+ @builtin_hooks = self . class . load_builtin_hooks . each_with_object ( Hash . new { |h , k | h [ k ] = [ ] } ) do |cls_target_methods , hooked_methods |
234
+ hooked_methods [ cls_target_methods . cls ] << cls_target_methods . target_methods
235
+ end
236
+
237
+ @hooked_methods = self . class . load_gem_hooks . each_with_object ( Hash . new { |h , k | h [ k ] = [ ] } ) do |cls_target_methods , hooked_methods |
254
238
hooked_methods [ cls_target_methods . cls ] << cls_target_methods . target_methods
255
239
end
256
240
@@ -269,9 +253,7 @@ def initialize(name,
269
253
end
270
254
271
255
@hooked_methods . each_value do |hooks |
272
- Array ( hooks ) . each do |hook |
273
- @hook_paths << hook . package . path
274
- end
256
+ @hook_paths += Array ( hooks ) . map { |hook | hook . package . path } . compact
275
257
end
276
258
end
277
259
@@ -422,8 +404,8 @@ def package
422
404
423
405
# Hook a method which is specified by class and method name.
424
406
def package_for_code_object
425
- Array ( config . hooked_methods [ cls . name ] )
426
- . compact
407
+ class_name = cls . to_s . index ( '#<Class:' ) == 0 ? cls . to_s [ '#<Class:' . length ...- 1 ] : cls . name
408
+ Array ( config . hooked_methods [ class_name ] )
427
409
. find { |hook | hook . include_method? ( method . name ) }
428
410
&.package
429
411
end
0 commit comments