appmap 0.18.1

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