1111
1212module AppMap
1313 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.
1516 #
1617 # Options:
1718 #
19+ # * +path+ indicates a relative path to a code folder.
1820 # * +gem+ may indicate a gem name that "owns" the path
1921 # * +require_name+ can be used to make sure that the code is required so that it can be loaded. This is generally used with
2022 # builtins, or when the path to be required is not automatically required when bundler requires the gem.
2123 # * +exclude+ can be used used to exclude sub-paths. Generally not used with +gem+.
2224 # * +labels+ is used to apply labels to matching code. This is really only useful when the package will be applied to
2325 # specific functions, via TargetMethods.
2426 # * +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
2628 # This is for internal use only.
2729 private_methods :gem
2830
@@ -45,7 +47,7 @@ class << self
4547 # Builds a package for a path, such as `app/models` in a Rails app. Generally corresponds to a `path:` entry
4648 # in appmap.yml. Also used for mapping specific methods via TargetMethods.
4749 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 )
4951 end
5052
5153 # 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: [
5759 end
5860 path = gem_path ( gem , optional )
5961 if path
60- Package . new ( path , gem , require_name , exclude , labels , shallow )
62+ Package . new ( gem , path , gem , require_name , exclude , labels , shallow )
6163 else
6264 AppMap ::Util . startup_message "#{ gem } is not available in the bundle"
6365 end
@@ -75,19 +77,16 @@ def gem_path(gem, optional)
7577 end
7678 end
7779
78- def name
79- gem || path
80- end
81-
8280 def to_h
8381 {
82+ name : name ,
8483 path : path ,
85- require_name : require_name ,
8684 gem : gem ,
87- handler_class : handler_class . name ,
85+ require_name : require_name ,
86+ handler_class : handler_class ? handler_class . name : nil ,
8887 exclude : Util . blank? ( exclude ) ? nil : exclude ,
8988 labels : Util . blank? ( labels ) ? nil : labels ,
90- shallow : shallow
89+ shallow : shallow . nil? ? nil : shallow ,
9190 } . compact
9291 end
9392 end
@@ -97,12 +96,12 @@ class TargetMethods # :nodoc:
9796 attr_reader :method_names , :package
9897
9998 def initialize ( method_names , package )
100- @method_names = method_names
99+ @method_names = Array ( method_names ) . map ( & :to_sym )
101100 @package = package
102101 end
103102
104103 def include_method? ( method_name )
105- Array ( method_names ) . include? ( method_name )
104+ method_names . include? ( method_name . to_sym )
106105 end
107106
108107 def to_h
@@ -139,9 +138,13 @@ def to_h
139138 private_constant :MethodHook
140139
141140 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 )
143142 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
145148 next unless package
146149
147150 package . handler_class = handler_class if handler_class
@@ -152,85 +155,63 @@ def package_hooks(gem_name, methods, handler_class: nil, require_name: nil)
152155 def method_hook ( cls , method_names , labels )
153156 MethodHook . new ( cls , method_names , labels )
154157 end
155- end
156158
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
234215
235216 attr_reader :name , :appmap_dir , :packages , :exclude , :swagger_config , :depends_config , :hooked_methods , :builtin_hooks
236217
@@ -247,10 +228,13 @@ def initialize(name,
247228 @depends_config = depends_config
248229 @hook_paths = Set . new ( packages . map ( &:path ) )
249230 @exclude = exclude
250- @builtin_hooks = BUILTIN_HOOKS . dup
251231 @functions = functions
252232
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 |
254238 hooked_methods [ cls_target_methods . cls ] << cls_target_methods . target_methods
255239 end
256240
@@ -269,9 +253,7 @@ def initialize(name,
269253 end
270254
271255 @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
275257 end
276258 end
277259
@@ -422,8 +404,8 @@ def package
422404
423405 # Hook a method which is specified by class and method name.
424406 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 ] )
427409 . find { |hook | hook . include_method? ( method . name ) }
428410 &.package
429411 end
0 commit comments