appmap 0.18.1

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.
Files changed (262) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +5 -0
  3. data/.gitignore +17 -0
  4. data/.rubocop.yml +18 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +100 -0
  7. data/Dockerfile.appmap +5 -0
  8. data/Gemfile +5 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +133 -0
  11. data/Rakefile +117 -0
  12. data/appmap.gemspec +41 -0
  13. data/appmap.yml +8 -0
  14. data/examples/install.rb +76 -0
  15. data/examples/mock_webapp/Gemfile +1 -0
  16. data/examples/mock_webapp/appmap.yml +2 -0
  17. data/examples/mock_webapp/exe/mock_webapp_request +12 -0
  18. data/examples/mock_webapp/lib/mock_webapp/controller.rb +23 -0
  19. data/examples/mock_webapp/lib/mock_webapp/request.rb +12 -0
  20. data/examples/mock_webapp/lib/mock_webapp/user.rb +18 -0
  21. data/exe/_appmap-record-self +49 -0
  22. data/exe/appmap +168 -0
  23. data/lib/appmap/algorithm/prune_class_map.rb +65 -0
  24. data/lib/appmap/command/inspect.rb +11 -0
  25. data/lib/appmap/command/record.rb +91 -0
  26. data/lib/appmap/command/upload.rb +103 -0
  27. data/lib/appmap/config/directory.rb +65 -0
  28. data/lib/appmap/config/file.rb +13 -0
  29. data/lib/appmap/config/named_function.rb +21 -0
  30. data/lib/appmap/config/package_dir.rb +52 -0
  31. data/lib/appmap/config/path.rb +25 -0
  32. data/lib/appmap/config.rb +65 -0
  33. data/lib/appmap/feature.rb +262 -0
  34. data/lib/appmap/inspect/inspector.rb +99 -0
  35. data/lib/appmap/inspect/parse_node.rb +170 -0
  36. data/lib/appmap/inspect/parser.rb +15 -0
  37. data/lib/appmap/inspect.rb +91 -0
  38. data/lib/appmap/middleware/remote_recording.rb +122 -0
  39. data/lib/appmap/parser.rb +60 -0
  40. data/lib/appmap/rails/action_handler.rb +77 -0
  41. data/lib/appmap/rails/sql_handler.rb +148 -0
  42. data/lib/appmap/railtie.rb +32 -0
  43. data/lib/appmap/rspec/parse_node.rb +41 -0
  44. data/lib/appmap/rspec/parser.rb +15 -0
  45. data/lib/appmap/rspec.rb +288 -0
  46. data/lib/appmap/trace/event_handler/rack_handler_webrick.rb +65 -0
  47. data/lib/appmap/trace/tracer.rb +347 -0
  48. data/lib/appmap/version.rb +5 -0
  49. data/lib/appmap.rb +26 -0
  50. data/lore/pages/2019-05-21-install-and-record/index.pug +51 -0
  51. data/lore/pages/2019-05-21-install-and-record/install_example_appmap.png +0 -0
  52. data/lore/pages/2019-05-21-install-and-record/metadata.yml +5 -0
  53. data/lore/pages/layout.pug +66 -0
  54. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.css +1912 -0
  55. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.css.map +1 -0
  56. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.min.css +7 -0
  57. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.min.css.map +1 -0
  58. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.css +331 -0
  59. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.css.map +1 -0
  60. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.min.css +8 -0
  61. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.min.css.map +1 -0
  62. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.css +9030 -0
  63. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.css.map +1 -0
  64. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.min.css +7 -0
  65. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.min.css.map +1 -0
  66. data/lore/public/stylesheets/style.css +8 -0
  67. data/package-lock.json +1066 -0
  68. data/package.json +24 -0
  69. data/spec/abstract_controller4_base_spec.rb +58 -0
  70. data/spec/abstract_controller_base_spec.rb +59 -0
  71. data/spec/fixtures/rack_users_app/.dockerignore +2 -0
  72. data/spec/fixtures/rack_users_app/.gitignore +2 -0
  73. data/spec/fixtures/rack_users_app/Dockerfile +32 -0
  74. data/spec/fixtures/rack_users_app/Gemfile +10 -0
  75. data/spec/fixtures/rack_users_app/appmap.yml +3 -0
  76. data/spec/fixtures/rack_users_app/config.ru +2 -0
  77. data/spec/fixtures/rack_users_app/docker-compose.yml +9 -0
  78. data/spec/fixtures/rack_users_app/lib/app.rb +36 -0
  79. data/spec/fixtures/rails4_users_app/.gitignore +13 -0
  80. data/spec/fixtures/rails4_users_app/.rbenv-gemsets +2 -0
  81. data/spec/fixtures/rails4_users_app/.ruby-version +1 -0
  82. data/spec/fixtures/rails4_users_app/Dockerfile +30 -0
  83. data/spec/fixtures/rails4_users_app/Dockerfile.pg +3 -0
  84. data/spec/fixtures/rails4_users_app/Gemfile +77 -0
  85. data/spec/fixtures/rails4_users_app/README.rdoc +28 -0
  86. data/spec/fixtures/rails4_users_app/Rakefile +6 -0
  87. data/spec/fixtures/rails4_users_app/app/assets/images/.keep +0 -0
  88. data/spec/fixtures/rails4_users_app/app/assets/javascripts/application.js +16 -0
  89. data/spec/fixtures/rails4_users_app/app/assets/stylesheets/application.css +15 -0
  90. data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +27 -0
  91. data/spec/fixtures/rails4_users_app/app/controllers/application_controller.rb +5 -0
  92. data/spec/fixtures/rails4_users_app/app/controllers/concerns/.keep +0 -0
  93. data/spec/fixtures/rails4_users_app/app/controllers/health_controller.rb +5 -0
  94. data/spec/fixtures/rails4_users_app/app/controllers/users_controller.rb +5 -0
  95. data/spec/fixtures/rails4_users_app/app/helpers/application_helper.rb +2 -0
  96. data/spec/fixtures/rails4_users_app/app/mailers/.keep +0 -0
  97. data/spec/fixtures/rails4_users_app/app/models/.keep +0 -0
  98. data/spec/fixtures/rails4_users_app/app/models/concerns/.keep +0 -0
  99. data/spec/fixtures/rails4_users_app/app/models/user.rb +18 -0
  100. data/spec/fixtures/rails4_users_app/app/views/layouts/application.html.haml +7 -0
  101. data/spec/fixtures/rails4_users_app/app/views/users/index.html.haml +7 -0
  102. data/spec/fixtures/rails4_users_app/appmap.yml +3 -0
  103. data/spec/fixtures/rails4_users_app/bin/rails +9 -0
  104. data/spec/fixtures/rails4_users_app/bin/setup +29 -0
  105. data/spec/fixtures/rails4_users_app/bin/spring +17 -0
  106. data/spec/fixtures/rails4_users_app/config/application.rb +26 -0
  107. data/spec/fixtures/rails4_users_app/config/boot.rb +3 -0
  108. data/spec/fixtures/rails4_users_app/config/database.yml +17 -0
  109. data/spec/fixtures/rails4_users_app/config/environment.rb +5 -0
  110. data/spec/fixtures/rails4_users_app/config/environments/development.rb +41 -0
  111. data/spec/fixtures/rails4_users_app/config/environments/production.rb +79 -0
  112. data/spec/fixtures/rails4_users_app/config/environments/test.rb +42 -0
  113. data/spec/fixtures/rails4_users_app/config/initializers/assets.rb +11 -0
  114. data/spec/fixtures/rails4_users_app/config/initializers/backtrace_silencers.rb +7 -0
  115. data/spec/fixtures/rails4_users_app/config/initializers/cookies_serializer.rb +3 -0
  116. data/spec/fixtures/rails4_users_app/config/initializers/filter_parameter_logging.rb +4 -0
  117. data/spec/fixtures/rails4_users_app/config/initializers/inflections.rb +16 -0
  118. data/spec/fixtures/rails4_users_app/config/initializers/mime_types.rb +4 -0
  119. data/spec/fixtures/rails4_users_app/config/initializers/session_store.rb +3 -0
  120. data/spec/fixtures/rails4_users_app/config/initializers/to_time_preserves_timezone.rb +10 -0
  121. data/spec/fixtures/rails4_users_app/config/initializers/wrap_parameters.rb +14 -0
  122. data/spec/fixtures/rails4_users_app/config/locales/en.yml +23 -0
  123. data/spec/fixtures/rails4_users_app/config/routes.rb +12 -0
  124. data/spec/fixtures/rails4_users_app/config/secrets.yml +22 -0
  125. data/spec/fixtures/rails4_users_app/config.ru +4 -0
  126. data/spec/fixtures/rails4_users_app/create_app +23 -0
  127. data/spec/fixtures/rails4_users_app/db/migrate/20191127112304_create_users.rb +10 -0
  128. data/spec/fixtures/rails4_users_app/db/schema.rb +26 -0
  129. data/spec/fixtures/rails4_users_app/db/seeds.rb +7 -0
  130. data/spec/fixtures/rails4_users_app/docker-compose.yml +24 -0
  131. data/spec/fixtures/rails4_users_app/lib/assets/.keep +0 -0
  132. data/spec/fixtures/rails4_users_app/lib/tasks/.keep +0 -0
  133. data/spec/fixtures/rails4_users_app/log/.keep +0 -0
  134. data/spec/fixtures/rails4_users_app/public/404.html +67 -0
  135. data/spec/fixtures/rails4_users_app/public/422.html +67 -0
  136. data/spec/fixtures/rails4_users_app/public/500.html +66 -0
  137. data/spec/fixtures/rails4_users_app/public/favicon.ico +0 -0
  138. data/spec/fixtures/rails4_users_app/public/robots.txt +5 -0
  139. data/spec/fixtures/rails4_users_app/spec/controllers/users_controller_api_spec.rb +49 -0
  140. data/spec/fixtures/rails4_users_app/spec/rails_helper.rb +95 -0
  141. data/spec/fixtures/rails4_users_app/spec/spec_helper.rb +96 -0
  142. data/spec/fixtures/rails4_users_app/test/fixtures/users.yml +9 -0
  143. data/spec/fixtures/rails_users_app/.dockerignore +1 -0
  144. data/spec/fixtures/rails_users_app/.gitignore +39 -0
  145. data/spec/fixtures/rails_users_app/.rspec +1 -0
  146. data/spec/fixtures/rails_users_app/.ruby-version +1 -0
  147. data/spec/fixtures/rails_users_app/Dockerfile +29 -0
  148. data/spec/fixtures/rails_users_app/Dockerfile.pg +3 -0
  149. data/spec/fixtures/rails_users_app/Gemfile +51 -0
  150. data/spec/fixtures/rails_users_app/Rakefile +6 -0
  151. data/spec/fixtures/rails_users_app/app/controllers/api/users_controller.rb +27 -0
  152. data/spec/fixtures/rails_users_app/app/controllers/application_controller.rb +2 -0
  153. data/spec/fixtures/rails_users_app/app/controllers/concerns/.keep +0 -0
  154. data/spec/fixtures/rails_users_app/app/controllers/health_controller.rb +5 -0
  155. data/spec/fixtures/rails_users_app/app/controllers/users_controller.rb +5 -0
  156. data/spec/fixtures/rails_users_app/app/models/activerecord/user.rb +18 -0
  157. data/spec/fixtures/rails_users_app/app/models/concerns/.keep +0 -0
  158. data/spec/fixtures/rails_users_app/app/models/sequel/user.rb +25 -0
  159. data/spec/fixtures/rails_users_app/app/views/layouts/application.html.haml +7 -0
  160. data/spec/fixtures/rails_users_app/app/views/users/index.html.haml +7 -0
  161. data/spec/fixtures/rails_users_app/appmap.yml +3 -0
  162. data/spec/fixtures/rails_users_app/bin/_appmap-record-self +29 -0
  163. data/spec/fixtures/rails_users_app/bin/appmap +29 -0
  164. data/spec/fixtures/rails_users_app/bin/byebug +29 -0
  165. data/spec/fixtures/rails_users_app/bin/gli +29 -0
  166. data/spec/fixtures/rails_users_app/bin/htmldiff +29 -0
  167. data/spec/fixtures/rails_users_app/bin/ldiff +29 -0
  168. data/spec/fixtures/rails_users_app/bin/nokogiri +29 -0
  169. data/spec/fixtures/rails_users_app/bin/rackup +29 -0
  170. data/spec/fixtures/rails_users_app/bin/rails +4 -0
  171. data/spec/fixtures/rails_users_app/bin/rake +29 -0
  172. data/spec/fixtures/rails_users_app/bin/rspec +29 -0
  173. data/spec/fixtures/rails_users_app/bin/ruby-parse +29 -0
  174. data/spec/fixtures/rails_users_app/bin/ruby-rewrite +29 -0
  175. data/spec/fixtures/rails_users_app/bin/sequel +29 -0
  176. data/spec/fixtures/rails_users_app/bin/setup +25 -0
  177. data/spec/fixtures/rails_users_app/bin/sprockets +29 -0
  178. data/spec/fixtures/rails_users_app/bin/thor +29 -0
  179. data/spec/fixtures/rails_users_app/bin/update +25 -0
  180. data/spec/fixtures/rails_users_app/config/application.rb +51 -0
  181. data/spec/fixtures/rails_users_app/config/boot.rb +3 -0
  182. data/spec/fixtures/rails_users_app/config/credentials.yml.enc +1 -0
  183. data/spec/fixtures/rails_users_app/config/database.yml +17 -0
  184. data/spec/fixtures/rails_users_app/config/environment.rb +5 -0
  185. data/spec/fixtures/rails_users_app/config/environments/development.rb +40 -0
  186. data/spec/fixtures/rails_users_app/config/environments/production.rb +68 -0
  187. data/spec/fixtures/rails_users_app/config/environments/test.rb +36 -0
  188. data/spec/fixtures/rails_users_app/config/initializers/application_controller_renderer.rb +8 -0
  189. data/spec/fixtures/rails_users_app/config/initializers/backtrace_silencers.rb +7 -0
  190. data/spec/fixtures/rails_users_app/config/initializers/cors.rb +16 -0
  191. data/spec/fixtures/rails_users_app/config/initializers/filter_parameter_logging.rb +4 -0
  192. data/spec/fixtures/rails_users_app/config/initializers/inflections.rb +16 -0
  193. data/spec/fixtures/rails_users_app/config/initializers/mime_types.rb +4 -0
  194. data/spec/fixtures/rails_users_app/config/initializers/record_button.rb +3 -0
  195. data/spec/fixtures/rails_users_app/config/initializers/wrap_parameters.rb +9 -0
  196. data/spec/fixtures/rails_users_app/config/locales/en.yml +33 -0
  197. data/spec/fixtures/rails_users_app/config/routes.rb +11 -0
  198. data/spec/fixtures/rails_users_app/config.ru +5 -0
  199. data/spec/fixtures/rails_users_app/create_app +11 -0
  200. data/spec/fixtures/rails_users_app/db/migrate/20190728211408_create_users.rb +9 -0
  201. data/spec/fixtures/rails_users_app/db/schema.rb +23 -0
  202. data/spec/fixtures/rails_users_app/docker-compose.yml +24 -0
  203. data/spec/fixtures/rails_users_app/lib/tasks/.keep +0 -0
  204. data/spec/fixtures/rails_users_app/log/.keep +0 -0
  205. data/spec/fixtures/rails_users_app/public/robots.txt +1 -0
  206. data/spec/fixtures/rails_users_app/spec/controllers/users_controller_api_spec.rb +29 -0
  207. data/spec/fixtures/rails_users_app/spec/models/user_spec.rb +39 -0
  208. data/spec/fixtures/rails_users_app/spec/rails_helper.rb +66 -0
  209. data/spec/fixtures/rails_users_app/spec/spec_helper.rb +96 -0
  210. data/spec/fixtures/rails_users_app/users_app/.gitignore +20 -0
  211. data/spec/rack_handler_webrick_spec.rb +59 -0
  212. data/spec/rails_spec_helper.rb +34 -0
  213. data/spec/railtie_spec.rb +35 -0
  214. data/spec/record_sql_rails4_pg_spec.rb +76 -0
  215. data/spec/record_sql_rails_pg_spec.rb +68 -0
  216. data/spec/rspec_feature_metadata_spec.rb +30 -0
  217. data/spec/spec_helper.rb +6 -0
  218. data/test/cli_test.rb +81 -0
  219. data/test/config_test.rb +149 -0
  220. data/test/explict_inspect_test.rb +29 -0
  221. data/test/fixtures/active_record_like/active_record/aggregations.rb +4 -0
  222. data/test/fixtures/active_record_like/active_record/association.rb +4 -0
  223. data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_base.rb +8 -0
  224. data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_part.rb +8 -0
  225. data/test/fixtures/active_record_like/active_record/associations/join_dependency.rb +6 -0
  226. data/test/fixtures/active_record_like/active_record/caps/caps.rb +4 -0
  227. data/test/fixtures/active_record_like/active_record.rb +2 -0
  228. data/test/fixtures/cli_record_test/appmap.yml +2 -0
  229. data/test/fixtures/cli_record_test/lib/cli_record_test/main.rb +7 -0
  230. data/test/fixtures/ignore_non_ruby_file/class.rb +3 -0
  231. data/test/fixtures/ignore_non_ruby_file/non-ruby.txt +1 -0
  232. data/test/fixtures/includes_excludes/lib/a/a_1.rb +6 -0
  233. data/test/fixtures/includes_excludes/lib/a/a_2.rb +6 -0
  234. data/test/fixtures/includes_excludes/lib/a/x/x_1.rb +8 -0
  235. data/test/fixtures/includes_excludes/lib/b/b_1.rb +6 -0
  236. data/test/fixtures/includes_excludes/lib/root_1.rb +4 -0
  237. data/test/fixtures/inspect_multiple_subdirs/module_a/class_a.rb +5 -0
  238. data/test/fixtures/inspect_multiple_subdirs/module_a.rb +2 -0
  239. data/test/fixtures/inspect_multiple_subdirs/module_b/class_b.rb +5 -0
  240. data/test/fixtures/inspect_multiple_subdirs/module_b/class_c.rb +5 -0
  241. data/test/fixtures/inspect_multiple_subdirs/module_b.rb +2 -0
  242. data/test/fixtures/inspect_package/module_a/module_b/class_in_module.rb +6 -0
  243. data/test/fixtures/parse_file/defs_static_function.rb +96 -0
  244. data/test/fixtures/parse_file/function_within_class.rb +36 -0
  245. data/test/fixtures/parse_file/include_public_methods.rb +127 -0
  246. data/test/fixtures/parse_file/instance_function.rb +17 -0
  247. data/test/fixtures/parse_file/modules.rb +71 -0
  248. data/test/fixtures/parse_file/sclass_static_function.rb +88 -0
  249. data/test/fixtures/parse_file/toplevel_class.rb +13 -0
  250. data/test/fixtures/parse_file/toplevel_function.rb +14 -0
  251. data/test/fixtures/rspec_recorder/Gemfile +5 -0
  252. data/test/fixtures/rspec_recorder/appmap.yml +3 -0
  253. data/test/fixtures/rspec_recorder/lib/hello.rb +5 -0
  254. data/test/fixtures/rspec_recorder/spec/hello_spec.rb +9 -0
  255. data/test/fixtures/trace_test/trace_program_1.rb +44 -0
  256. data/test/implicit_inspect_test.rb +33 -0
  257. data/test/include_exclude_test.rb +48 -0
  258. data/test/prerecorded_trace_test.rb +76 -0
  259. data/test/rspec_test.rb +22 -0
  260. data/test/test_helper.rb +46 -0
  261. data/test/trace_test.rb +92 -0
  262. metadata +501 -0
@@ -0,0 +1,65 @@
1
+ require 'appmap/config/path'
2
+ require 'appmap/config/file'
3
+ require 'appmap/config/directory'
4
+ require 'appmap/config/package_dir'
5
+ require 'appmap/config/named_function'
6
+
7
+ module AppMap
8
+ module Config
9
+ class Configuration
10
+ attr_reader :name, :packages, :files, :named_functions
11
+
12
+ def initialize(name)
13
+ @name = name
14
+ @packages = []
15
+ @files = []
16
+ @named_functions = []
17
+ end
18
+
19
+ def source_locations
20
+ packages + files + named_functions
21
+ end
22
+ end
23
+
24
+ class << self
25
+ NAMED_FUNCTIONS = [
26
+ Config::NamedFunction.new(:rack_handler_webrick, 'rack', 'lib/rack/handler/webrick.rb',
27
+ %w[Rack Handler WEBrick], 'service', false)
28
+ ].freeze
29
+
30
+ # Loads configuration data from a file, specified by the file name.
31
+ def load_from_file(config_file_name)
32
+ require 'yaml'
33
+ load YAML.safe_load(::File.read(config_file_name))
34
+ end
35
+
36
+ # Loads configuration from a Hash.
37
+ def load(config_data)
38
+ Configuration.new(config_data['name']).tap do |config|
39
+ builders = Hash.new { |_, key| raise "Unknown config type #{key.inspect}" }
40
+ builders[:packages] = lambda { |path, options|
41
+ AppMap::Config::PackageDir.new(path).tap do |pdir|
42
+ pdir.package_name = options['name'] if options['name']
43
+ pdir.exclude = options['exclude'] if options['exclude']
44
+ end
45
+ }
46
+ builders[:files] = ->(path, _) { AppMap::Config::File.new(path) }
47
+
48
+ %i[packages files].each do |kind|
49
+ next unless (members = config_data[kind.to_s])
50
+ members.each do |member|
51
+ path = member.delete('path')
52
+ config.send(kind) << builders[kind].call(path, member)
53
+ end
54
+ end
55
+
56
+ NAMED_FUNCTIONS.each do |dep|
57
+ next if config_data['named_functions'] && !config_data['named_functions'].member?(dep.gem_name)
58
+
59
+ config.named_functions << dep
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,262 @@
1
+ module AppMap
2
+ # A Feature is a construct within the code that will be observed. Examples features include
3
+ # modules, classes and functions.
4
+ module Feature
5
+ TYPE_MAP = {
6
+ 'cls' => 'class'
7
+ }.freeze
8
+
9
+ class << self
10
+ FEATURE_BUILDERS = {
11
+ module: ->(_) { Module.new },
12
+ class: ->(_) { Cls.new },
13
+ function: lambda do |hash|
14
+ static = hash.delete('static')
15
+ class_name = hash.delete('class_name')
16
+ Function.new.tap do |e|
17
+ e.static = static
18
+ e.class_name = class_name
19
+ end
20
+ end
21
+ }.freeze
22
+
23
+ # Deserialize a feature from a Hash. The Hash is typically a deserialized JSON dump of the feature.
24
+ def from_hash(hash)
25
+ builder = FEATURE_BUILDERS[hash['type'].to_sym]
26
+ raise "Unrecognized type of feature: #{type.inspect}" unless builder
27
+
28
+ feature = builder.call(hash)
29
+ feature.name = hash['name']
30
+ feature.location = hash['location']
31
+ feature.attributes = hash['attributes'] || {}
32
+ feature.children = (hash['children'] || []).map { |child| from_hash(child) }
33
+ feature
34
+ end
35
+ end
36
+
37
+ FeatureStruct = Struct.new(:name, :location, :attributes)
38
+
39
+ # Base is an abstract base class for features.
40
+ class Base < FeatureStruct
41
+ class << self
42
+ def expand_path(location)
43
+ path, lineno = location.split(':')
44
+ [ path, lineno ].compact.join(':')
45
+ end
46
+ end
47
+
48
+ attr_reader :parent, :children
49
+
50
+ def initialize(name, location, attributes)
51
+ super(name, self.class.expand_path(location), attributes)
52
+
53
+ @parent = nil
54
+ @children = []
55
+ end
56
+
57
+ def remove_child(child)
58
+ # TODO: Encountered this indexing appland with active_dispatch
59
+ children.delete(child) or warn "Unable to remove #{name.inspect} from parent" # or raise "No such child : #{child}"
60
+ child.instance_variable_set('@parent', nil)
61
+ end
62
+
63
+ def add_child(child)
64
+ @children << child
65
+ child.instance_variable_set('@parent', self)
66
+ end
67
+
68
+ # Gets an array containing the type names which enclose this feature.
69
+ def enclosing_type_name
70
+ @enclosing_type_name ||= [].tap do |names|
71
+ p = self
72
+ while (p = p.parent) && p.type?
73
+ names << p.name
74
+ end
75
+ end.reverse
76
+ end
77
+
78
+ # true iff this feature has an enclosing type. An example of when this is false: when
79
+ # the parent of the feature is not a type (e.g. it's a location).
80
+ def enclosing_type_name?
81
+ !enclosing_type_name.empty?
82
+ end
83
+
84
+ # The 'include' attribute can indicate which elements of the parse subtree
85
+ # to automatically add as features. For example: public_classes, public_modules,
86
+ # public_methods.
87
+ def include_option
88
+ (attributes[:include] || '').split(',')
89
+ end
90
+
91
+ # yield each function to a block.
92
+ def collect_functions(accumulator = [])
93
+ accumulator.tap do |_|
94
+ accumulator << self if is_a?(Function)
95
+ children.each { |child| child.collect_functions(accumulator) }
96
+ end
97
+ end
98
+
99
+ def type?
100
+ false
101
+ end
102
+
103
+ def valid?
104
+ !name.blank? && !location.blank?
105
+ end
106
+
107
+ def to_json(*opts)
108
+ to_h.to_json(*opts)
109
+ end
110
+
111
+ def to_h
112
+ super.tap do |map|
113
+ map.delete(:parent)
114
+ class_name = self.class.name.underscore.split('/')[-1]
115
+ map[:type] = TYPE_MAP[class_name] || class_name
116
+ map[:children] = @children.map(&:to_h) unless @children.empty?
117
+ map.delete(:attributes) if map[:attributes].empty?
118
+ end
119
+ end
120
+
121
+ # Determines if this feature should be dropped from the feature tree.
122
+ # A feature is dropped from the feature tree if it doesn't add useful information for the user.
123
+ # Performing this operation removes feature nodes that don't add anything useful to the user.
124
+ # For example, empty classes.
125
+ def prune(parent = nil)
126
+ should_prune = prune? && !parent.nil?
127
+ parent = self unless should_prune
128
+ children.dup.each do |child|
129
+ child.prune(parent)
130
+ end
131
+
132
+ # Perform the prune in post-fix traversal order, otherwise the
133
+ # features will get confused about whether they should prune or not.
134
+ if should_prune
135
+ parent.remove_child(self)
136
+ children.each do |child|
137
+ parent.add_child(child)
138
+ end
139
+ end
140
+ end
141
+
142
+ # Determines if this feature should be re-parented as a child of a different feature.
143
+ #
144
+ # A feature is re-parented if the enclosing type of the feature has already been defined in the tree.
145
+ #
146
+ # @param parent the parent of this feature in the compacted tree.
147
+ def reparent(parent = nil, features_by_type = {})
148
+ # Determine if the enclosing type of the feature is defined.
149
+ # Generally, it should be.
150
+
151
+ existing_enclosing_type = features_by_type[enclosing_type_name] if enclosing_type_name?
152
+ if existing_enclosing_type
153
+ parent = existing_enclosing_type
154
+ end
155
+
156
+ # Determine if this feature is a type which is already defined.
157
+ type_exists = true if type? && features_by_type.key?(type_name)
158
+
159
+ # If this feature is a type that's already defined, skip over it and
160
+ # add the children to the existing feature. Otherwise, clone this feature
161
+ # under the parent and use the cloned object as the parent of the compacted
162
+ # children.
163
+ if type_exists
164
+ features_by_type[type_name]
165
+ else
166
+ clone.tap do |f|
167
+ parent.add_child(f) if parent
168
+ features_by_type[type_name] = f if type?
169
+ end
170
+ end.tap do |updated_parent|
171
+ children.each do |child|
172
+ child.reparent(updated_parent, features_by_type)
173
+ end
174
+ end
175
+ end
176
+
177
+ def prune?
178
+ false
179
+ end
180
+
181
+ protected
182
+
183
+ def clone
184
+ self.class.new(name, location, attributes)
185
+ end
186
+
187
+ def child_classes
188
+ children.select { |c| c.is_a?(Cls) }
189
+ end
190
+
191
+ def child_nonclasses
192
+ children.reject { |c| c.is_a?(Cls) }
193
+ end
194
+ end
195
+
196
+ # Package is a feature which represents the directory containing code.
197
+ class Package < Base
198
+ # prune a package if it's empty, or if it contains anything but packages.
199
+ def prune?
200
+ children.empty? || children.any? { |c| !c.is_a?(Package) }
201
+ end
202
+ end
203
+
204
+ # Cls is a feature which represents a code class. A class defines a namespace which contains other
205
+ # features (such as member classes and functions), and it also usually encapsulates some data on which
206
+ # the member features operate.
207
+ class Cls < Base
208
+ # prune a class if it's empty.
209
+ def prune?
210
+ children.empty?
211
+ end
212
+
213
+ def type?
214
+ true
215
+ end
216
+
217
+ # Gets the type name of this class as an array.
218
+ def type_name
219
+ @type_name ||= enclosing_type_name + [ name ]
220
+ end
221
+ end
222
+
223
+ # Function is a feature which represents a code function. It can be an instance function or static (aka 'class')
224
+ # function. Instance functions operate on the instance data of the class on which they are defined. Static
225
+ # functions are used to perform operations which don't have want or need of instance data.
226
+ #
227
+ # * `handler_id` If provided, identifies a trace handler which can apply specialized logic to the
228
+ # event data which is recorded for this function. For example, if the function represents a handler
229
+ # method for a web server, the custom handler can inspect and record the HTTP request method and path info.
230
+ class Function < Base
231
+ attr_accessor :static, :class_name, :handler_id
232
+
233
+ alias static? static
234
+ def instance?
235
+ !static?
236
+ end
237
+
238
+ # Static functions must have an enclosing class defined in order to be traced.
239
+ def valid?
240
+ super && (instance? || !class_name.blank?)
241
+ end
242
+
243
+ def to_h
244
+ super.tap do |h|
245
+ # Suppress the class name when it can be inferred from the enclosing type.
246
+ h[:class_name] = class_name if class_name && class_name != enclosing_type_name.join('::')
247
+ h[:static] = static?
248
+ end
249
+ end
250
+
251
+ protected
252
+
253
+ def clone
254
+ super.tap do |obj|
255
+ obj.static = static
256
+ obj.class_name = class_name
257
+ obj.handler_id = handler_id
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,99 @@
1
+ module AppMap
2
+ module Inspect
3
+ # Inspector is an abstract class for extracting features from a Ruby program.
4
+ class Inspector
5
+ attr_reader :file_path, :parse_nodes, :comments
6
+
7
+ def initialize(file_path, parse_nodes, comments)
8
+ @file_path = file_path
9
+ @parse_nodes = parse_nodes
10
+ @comments = comments
11
+ end
12
+ end
13
+
14
+ # ImplicitInspector extracts features from a Ruby program, creating a feature for each public class and method.
15
+ class ImplicitInspector < Inspector
16
+ def inspect_file
17
+ features = []
18
+ features_by_ast_node = {}
19
+ parse_nodes.select(&:public?).each do |parse_node|
20
+ feature = parse_node.to_feature({})
21
+ features_by_ast_node[parse_node.node] = feature
22
+ if feature
23
+ if (enclosing_type_node = parse_node.enclosing_type_node) &&
24
+ (parent_feature = features_by_ast_node[enclosing_type_node])
25
+ parent_feature.add_child(feature)
26
+ else
27
+ features << feature
28
+ end
29
+ end
30
+ end
31
+
32
+ features.keep_if(&:valid?)
33
+ features
34
+ end
35
+ end
36
+
37
+ # ExplicitInspector extracts features from a Ruby program, requiring the use of @appmap annotations to mark each
38
+ # relevant class and method.
39
+ class ExplicitInspector < Inspector
40
+ def inspect_file
41
+ nodes_by_line = parse_nodes.each_with_object({}) { |node, h| h[node.node.loc.line] = node }
42
+
43
+ features_by_ast_node = {}
44
+ features = []
45
+
46
+ comments.select { |c| c.text.index('@appmap') }.each do |c|
47
+ c.text.split("\n").select { |l| l.index('@appmap') }.each do |feature|
48
+ tokens = feature.split
49
+ tokens.delete_if { |t| %w[# @appmap].member?(t) }
50
+ attributes = tokens.inject({}) do |memo, token|
51
+ key, value = token.split('=')
52
+ memo.tap do |attrs|
53
+ attrs[key.to_sym] = value
54
+ end
55
+ end
56
+
57
+ parse_node = nodes_by_line[c.location.last_line + 1]
58
+ if parse_node
59
+ feature = parse_node.to_feature(attributes)
60
+ features_by_ast_node[parse_node.node] = feature
61
+ if feature
62
+ if (enclosing_type_node = parse_node.enclosing_type_node) &&
63
+ (parent_feature = features_by_ast_node[enclosing_type_node])
64
+ parent_feature.add_child(feature)
65
+ else
66
+ features << feature
67
+ end
68
+
69
+ # At this point there's a parse_node and an associated feature.
70
+ # If the feature is a class which has the option 'include=public_methods',
71
+ # scan the rest of the class body for public methods and create features
72
+ # for them.
73
+
74
+ if parse_node.type == :class && feature.include_option.member?('public_methods')
75
+ begin_node = parse_node.node.children.find { |n| n.respond_to?(:type) && n.type == :begin }
76
+ if begin_node
77
+ public_methods = begin_node
78
+ .children
79
+ .select { |n| n.respond_to?(:type) && n.type == :def }
80
+ .map { |n| ParseNode.from_node(n, file_path, parse_node.ancestors + [ parse_node.node, begin_node ]) }
81
+ .select(&:public?)
82
+ public_methods.map { |m| m.to_feature([]) }.compact.each do |f|
83
+ feature.add_child(f)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ else
89
+ warn "No parse node found at #{file_path}:#{c.location.last_line + 1}"
90
+ end
91
+ end
92
+ end
93
+
94
+ features.keep_if(&:valid?)
95
+ features
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,170 @@
1
+ require 'appmap/feature'
2
+ require 'forwardable'
3
+
4
+ module AppMap
5
+ module Inspect
6
+ # ParseNodeStruct wraps a generic AST parse node.
7
+ ParseNodeStruct = Struct.new(:node, :file_path, :ancestors) do
8
+ end
9
+
10
+ # ParseNode wraps a generic AST parse node.
11
+ class ParseNode < ParseNodeStruct
12
+ extend Forwardable
13
+
14
+ def_delegators :node, :type, :location
15
+
16
+ class << self
17
+ # Build a ParseNode from an AST node.
18
+ def from_node(node, file_path, ancestors)
19
+ case node.type
20
+ when :class, :module
21
+ ClassParseNode.new(node, file_path, ancestors.dup)
22
+ when :def
23
+ InstanceMethodParseNode.new(node, file_path, ancestors.dup)
24
+ when :defs
25
+ StaticMethodParseNode.new(node, file_path, ancestors.dup) \
26
+ if StaticMethodParseNode.static?(node)
27
+ end
28
+ end
29
+ end
30
+
31
+ def public?
32
+ preceding_send = preceding_sibling_nodes
33
+ .reverse
34
+ .select { |n| n.respond_to?(:type) && n.type == :send }
35
+ .find { |n| %i[public protected private].member?(n.children[1]) }
36
+ preceding_send.nil? || preceding_send.children[1] == :public
37
+ end
38
+
39
+ # Gets the AST node of the module or class which encloses this node.
40
+ def enclosing_type_node
41
+ ancestors.reverse.find do |a|
42
+ %i[class module].include?(a.type)
43
+ end
44
+ end
45
+
46
+ def parent_node
47
+ ancestors[-1]
48
+ end
49
+
50
+ def preceding_sibling_nodes
51
+ return [] unless parent_node
52
+ index_of_this_node = parent_node.children.index { |c| c == node }
53
+ parent_node.children[0...index_of_this_node]
54
+ end
55
+
56
+ protected
57
+
58
+ def extract_class_name(node)
59
+ node.children[0].children[1].to_s
60
+ end
61
+
62
+ def extract_module_name(node)
63
+ node.children[0].children[1].to_s
64
+ end
65
+ end
66
+
67
+ # A Ruby class.
68
+ class ClassParseNode < ParseNode
69
+ def to_feature(attributes)
70
+ AppMap::Feature::Cls.new(extract_class_name(node), "#{file_path}:#{location.line}", attributes)
71
+ end
72
+ end
73
+
74
+ # Abstract representation of a method.
75
+ class MethodParseNode < ParseNode
76
+ def to_feature(attributes)
77
+ AppMap::Feature::Function.new(name, "#{file_path}:#{location.line}", attributes).tap do |a|
78
+ a.static = static?
79
+ a.class_name = class_name
80
+ end
81
+ end
82
+
83
+ def enclosing_names
84
+ ancestors.select do |a|
85
+ %i[class module].include?(a.type)
86
+ end.map do |a|
87
+ send("extract_#{a.type}_name", a)
88
+ end
89
+ end
90
+ end
91
+
92
+ # A method defines as a :def AST node.
93
+ class InstanceMethodParseNode < MethodParseNode
94
+ def name
95
+ node.children[0].to_s
96
+ end
97
+
98
+ # class_name should be inferred from the enclosing type.
99
+ def class_name
100
+ enclosing_names.join('::')
101
+ end
102
+
103
+ # An instance method defined in an sclass is a static method.
104
+ #
105
+ # TODO: Well, not strictly true. A singleton class can be defined on a class or
106
+ # on an instance. In fact, to Ruby a class method is really just an instance method
107
+ # on a class. So, this needs fixing to try and determine if the singleton class is
108
+ # defined on an instance or on a class. This may actually be hard (impossible?) to do
109
+ # from static parsing.
110
+ def static?
111
+ result = ancestors[-1].type == :sclass ||
112
+ (ancestors[-1].type == :begin && ancestors[-2] && ancestors[-2].type == :sclass)
113
+ !!result
114
+ end
115
+ end
116
+
117
+ # A method defines as a :defs AST node.
118
+ # For example:
119
+ #
120
+ # class Main
121
+ # def Main.main_func; end
122
+ # def explain
123
+ # some_func.tap do |s|
124
+ # def s.inspect; self; end
125
+ # end
126
+ # end
127
+ # end
128
+ class StaticMethodParseNode < MethodParseNode
129
+ class << self
130
+ def static?(node)
131
+ %i[self const].member?(node.children[0].type)
132
+ end
133
+ end
134
+
135
+ def name
136
+ node.children[1].to_s
137
+ end
138
+
139
+ # class_name is specified as `nil` if it should be inferred from the
140
+ # enclosing type.
141
+ def class_name
142
+ case (defs_type = node.children[0].type)
143
+ when :self
144
+ class_name_from_enclosing_type
145
+ when :const
146
+ class_name_from_declaration
147
+ else
148
+ raise "Unrecognized 'defs' method type : #{defs_type.inspect}"
149
+ end
150
+ end
151
+
152
+ def static?
153
+ true
154
+ end
155
+
156
+ protected
157
+
158
+ def class_name_from_enclosing_type
159
+ enclosing_names.join('::')
160
+ end
161
+
162
+ def class_name_from_declaration
163
+ ancestor_names = enclosing_names
164
+ ancestor_names.pop
165
+ ancestor_names << node.children[0].children[1]
166
+ ancestor_names.join('::')
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,15 @@
1
+ require 'appmap/parser'
2
+ require 'appmap/inspect/parse_node'
3
+
4
+ module AppMap
5
+ module Inspect
6
+ # Parser processes a Ruby into a list of parse nodes and a list of comments.
7
+ class Parser < ::AppMap::Parser
8
+ protected
9
+
10
+ def build_parse_node(node, file_path, ancestors)
11
+ ParseNode.from_node(node, file_path, ancestors)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,91 @@
1
+ require 'appmap/inspect/inspector'
2
+ require 'appmap/inspect/parser'
3
+
4
+ module AppMap
5
+ # Inspect identifies features from a Ruby file.
6
+ module Inspect
7
+ class << self
8
+ # Detect features from a source code repository. The manner in which the features are detected in the
9
+ # code is defined and tuned by a path configuration object. The path configuration tells the
10
+ # feature detector what it should do when it encounters code that may be a "sub-feature",
11
+ # for example a public instance method of a class.
12
+ #
13
+ # rubocop:disable Metrics/AbcSize
14
+ # rubocop:disable Metrics/MethodLength
15
+ # @appmap
16
+ def detect_features(config_spec)
17
+ child_features = -> { config_spec.children.map(&Inspect.method(:detect_features)).flatten.compact }
18
+ parse_file = -> { inspect_file(config_spec.mode, file_path: config_spec.path) }
19
+
20
+ feature_builders = Hash.new { |_, key| raise "Unable to build features for #{key.inspect}" }
21
+ feature_builders[AppMap::Config::Directory] = child_features
22
+ feature_builders[AppMap::Config::File] = parse_file
23
+ feature_builders[AppMap::Config::PackageDir] = lambda {
24
+ AppMap::Feature::Package.new(config_spec.package_name, config_spec.path, {}).tap do |package|
25
+ child_features.call.each do |child|
26
+ package.add_child(child)
27
+ end
28
+ end
29
+ }
30
+ feature_builders[AppMap::Config::NamedFunction] = lambda {
31
+ # Loads named functions by finding the requested gem, finding the file within the gem,
32
+ # parsing that file, and then inspecting the module/class scope for the requested method.
33
+ # We can't 'require' the specified code, because if we do that, it can change the
34
+ # behavior of the program.
35
+
36
+ gem = Gem.loaded_specs[config_spec.gem_name]
37
+ return [] unless gem
38
+
39
+ gem_dir = gem.gem_dir
40
+ file_path = File.join(gem_dir, config_spec.file_path)
41
+
42
+ parse_nodes, comments = Parser.new(file_path: file_path).parse
43
+ features = ImplicitInspector.new(file_path, parse_nodes, comments).inspect_file
44
+
45
+ class_names = config_spec.class_names.dup
46
+ until class_names.empty?
47
+ class_name = class_names.shift
48
+ feature = features.find { |f| f.to_h[:type] == 'class' && f.name == class_name }
49
+ raise "#{class_name.inspect} not found" unless feature
50
+
51
+ features = feature.children
52
+ end
53
+
54
+ function = features.find { |f| f.to_h[:type] == 'function' && f.name == config_spec.method_name && f.static == config_spec.static }
55
+
56
+ # If the configuration specifier has an id, use it as the handler id.
57
+ # This is how we can associate custom handler logic with the named function.
58
+ function.handler_id = config_spec.id.to_s if config_spec.id
59
+
60
+ AppMap::Feature::Package.new(config_spec.gem_name, "#{gem_dir}:0", {}).tap do |pkg|
61
+ parent = pkg
62
+ class_names = config_spec.class_names.dup
63
+ until class_names.empty?
64
+ class_name = class_names.shift
65
+ cls = AppMap::Feature::Cls.new(class_name, "#{gem_dir}:0", {})
66
+ parent.children << cls
67
+ parent = cls
68
+ end
69
+ parent.children << function
70
+ end
71
+ }
72
+
73
+ feature_builders[config_spec.class].call
74
+ end
75
+ # rubocop:enable Metrics/AbcSize
76
+ # rubocop:enable Metrics/MethodLength
77
+
78
+ # Inspect a specific file for features.
79
+ #
80
+ # @appmap
81
+ def inspect_file(strategy, file_path: nil)
82
+ parse_nodes, comments = Parser.new(file_path: file_path).parse
83
+ inspector_class = {
84
+ implicit: ImplicitInspector,
85
+ explicit: ExplicitInspector
86
+ }[strategy] or raise "Invalid strategy : #{strategy.inspect}"
87
+ inspector_class.new(file_path, parse_nodes, comments).inspect_file
88
+ end
89
+ end
90
+ end
91
+ end