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.
- checksums.yaml +7 -0
- data/.dockerignore +5 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +18 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +100 -0
- data/Dockerfile.appmap +5 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +133 -0
- data/Rakefile +117 -0
- data/appmap.gemspec +41 -0
- data/appmap.yml +8 -0
- data/examples/install.rb +76 -0
- data/examples/mock_webapp/Gemfile +1 -0
- data/examples/mock_webapp/appmap.yml +2 -0
- data/examples/mock_webapp/exe/mock_webapp_request +12 -0
- data/examples/mock_webapp/lib/mock_webapp/controller.rb +23 -0
- data/examples/mock_webapp/lib/mock_webapp/request.rb +12 -0
- data/examples/mock_webapp/lib/mock_webapp/user.rb +18 -0
- data/exe/_appmap-record-self +49 -0
- data/exe/appmap +168 -0
- data/lib/appmap/algorithm/prune_class_map.rb +65 -0
- data/lib/appmap/command/inspect.rb +11 -0
- data/lib/appmap/command/record.rb +91 -0
- data/lib/appmap/command/upload.rb +103 -0
- data/lib/appmap/config/directory.rb +65 -0
- data/lib/appmap/config/file.rb +13 -0
- data/lib/appmap/config/named_function.rb +21 -0
- data/lib/appmap/config/package_dir.rb +52 -0
- data/lib/appmap/config/path.rb +25 -0
- data/lib/appmap/config.rb +65 -0
- data/lib/appmap/feature.rb +262 -0
- data/lib/appmap/inspect/inspector.rb +99 -0
- data/lib/appmap/inspect/parse_node.rb +170 -0
- data/lib/appmap/inspect/parser.rb +15 -0
- data/lib/appmap/inspect.rb +91 -0
- data/lib/appmap/middleware/remote_recording.rb +122 -0
- data/lib/appmap/parser.rb +60 -0
- data/lib/appmap/rails/action_handler.rb +77 -0
- data/lib/appmap/rails/sql_handler.rb +148 -0
- data/lib/appmap/railtie.rb +32 -0
- data/lib/appmap/rspec/parse_node.rb +41 -0
- data/lib/appmap/rspec/parser.rb +15 -0
- data/lib/appmap/rspec.rb +288 -0
- data/lib/appmap/trace/event_handler/rack_handler_webrick.rb +65 -0
- data/lib/appmap/trace/tracer.rb +347 -0
- data/lib/appmap/version.rb +5 -0
- data/lib/appmap.rb +26 -0
- data/lore/pages/2019-05-21-install-and-record/index.pug +51 -0
- data/lore/pages/2019-05-21-install-and-record/install_example_appmap.png +0 -0
- data/lore/pages/2019-05-21-install-and-record/metadata.yml +5 -0
- data/lore/pages/layout.pug +66 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.css +1912 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.css.map +1 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.min.css +7 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.min.css.map +1 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.css +331 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.css.map +1 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.min.css +8 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.min.css.map +1 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.css +9030 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.css.map +1 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.min.css +7 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.min.css.map +1 -0
- data/lore/public/stylesheets/style.css +8 -0
- data/package-lock.json +1066 -0
- data/package.json +24 -0
- data/spec/abstract_controller4_base_spec.rb +58 -0
- data/spec/abstract_controller_base_spec.rb +59 -0
- data/spec/fixtures/rack_users_app/.dockerignore +2 -0
- data/spec/fixtures/rack_users_app/.gitignore +2 -0
- data/spec/fixtures/rack_users_app/Dockerfile +32 -0
- data/spec/fixtures/rack_users_app/Gemfile +10 -0
- data/spec/fixtures/rack_users_app/appmap.yml +3 -0
- data/spec/fixtures/rack_users_app/config.ru +2 -0
- data/spec/fixtures/rack_users_app/docker-compose.yml +9 -0
- data/spec/fixtures/rack_users_app/lib/app.rb +36 -0
- data/spec/fixtures/rails4_users_app/.gitignore +13 -0
- data/spec/fixtures/rails4_users_app/.rbenv-gemsets +2 -0
- data/spec/fixtures/rails4_users_app/.ruby-version +1 -0
- data/spec/fixtures/rails4_users_app/Dockerfile +30 -0
- data/spec/fixtures/rails4_users_app/Dockerfile.pg +3 -0
- data/spec/fixtures/rails4_users_app/Gemfile +77 -0
- data/spec/fixtures/rails4_users_app/README.rdoc +28 -0
- data/spec/fixtures/rails4_users_app/Rakefile +6 -0
- data/spec/fixtures/rails4_users_app/app/assets/images/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/assets/javascripts/application.js +16 -0
- data/spec/fixtures/rails4_users_app/app/assets/stylesheets/application.css +15 -0
- data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +27 -0
- data/spec/fixtures/rails4_users_app/app/controllers/application_controller.rb +5 -0
- data/spec/fixtures/rails4_users_app/app/controllers/concerns/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/controllers/health_controller.rb +5 -0
- data/spec/fixtures/rails4_users_app/app/controllers/users_controller.rb +5 -0
- data/spec/fixtures/rails4_users_app/app/helpers/application_helper.rb +2 -0
- data/spec/fixtures/rails4_users_app/app/mailers/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/models/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/models/concerns/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/models/user.rb +18 -0
- data/spec/fixtures/rails4_users_app/app/views/layouts/application.html.haml +7 -0
- data/spec/fixtures/rails4_users_app/app/views/users/index.html.haml +7 -0
- data/spec/fixtures/rails4_users_app/appmap.yml +3 -0
- data/spec/fixtures/rails4_users_app/bin/rails +9 -0
- data/spec/fixtures/rails4_users_app/bin/setup +29 -0
- data/spec/fixtures/rails4_users_app/bin/spring +17 -0
- data/spec/fixtures/rails4_users_app/config/application.rb +26 -0
- data/spec/fixtures/rails4_users_app/config/boot.rb +3 -0
- data/spec/fixtures/rails4_users_app/config/database.yml +17 -0
- data/spec/fixtures/rails4_users_app/config/environment.rb +5 -0
- data/spec/fixtures/rails4_users_app/config/environments/development.rb +41 -0
- data/spec/fixtures/rails4_users_app/config/environments/production.rb +79 -0
- data/spec/fixtures/rails4_users_app/config/environments/test.rb +42 -0
- data/spec/fixtures/rails4_users_app/config/initializers/assets.rb +11 -0
- data/spec/fixtures/rails4_users_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/fixtures/rails4_users_app/config/initializers/cookies_serializer.rb +3 -0
- data/spec/fixtures/rails4_users_app/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/fixtures/rails4_users_app/config/initializers/inflections.rb +16 -0
- data/spec/fixtures/rails4_users_app/config/initializers/mime_types.rb +4 -0
- data/spec/fixtures/rails4_users_app/config/initializers/session_store.rb +3 -0
- data/spec/fixtures/rails4_users_app/config/initializers/to_time_preserves_timezone.rb +10 -0
- data/spec/fixtures/rails4_users_app/config/initializers/wrap_parameters.rb +14 -0
- data/spec/fixtures/rails4_users_app/config/locales/en.yml +23 -0
- data/spec/fixtures/rails4_users_app/config/routes.rb +12 -0
- data/spec/fixtures/rails4_users_app/config/secrets.yml +22 -0
- data/spec/fixtures/rails4_users_app/config.ru +4 -0
- data/spec/fixtures/rails4_users_app/create_app +23 -0
- data/spec/fixtures/rails4_users_app/db/migrate/20191127112304_create_users.rb +10 -0
- data/spec/fixtures/rails4_users_app/db/schema.rb +26 -0
- data/spec/fixtures/rails4_users_app/db/seeds.rb +7 -0
- data/spec/fixtures/rails4_users_app/docker-compose.yml +24 -0
- data/spec/fixtures/rails4_users_app/lib/assets/.keep +0 -0
- data/spec/fixtures/rails4_users_app/lib/tasks/.keep +0 -0
- data/spec/fixtures/rails4_users_app/log/.keep +0 -0
- data/spec/fixtures/rails4_users_app/public/404.html +67 -0
- data/spec/fixtures/rails4_users_app/public/422.html +67 -0
- data/spec/fixtures/rails4_users_app/public/500.html +66 -0
- data/spec/fixtures/rails4_users_app/public/favicon.ico +0 -0
- data/spec/fixtures/rails4_users_app/public/robots.txt +5 -0
- data/spec/fixtures/rails4_users_app/spec/controllers/users_controller_api_spec.rb +49 -0
- data/spec/fixtures/rails4_users_app/spec/rails_helper.rb +95 -0
- data/spec/fixtures/rails4_users_app/spec/spec_helper.rb +96 -0
- data/spec/fixtures/rails4_users_app/test/fixtures/users.yml +9 -0
- data/spec/fixtures/rails_users_app/.dockerignore +1 -0
- data/spec/fixtures/rails_users_app/.gitignore +39 -0
- data/spec/fixtures/rails_users_app/.rspec +1 -0
- data/spec/fixtures/rails_users_app/.ruby-version +1 -0
- data/spec/fixtures/rails_users_app/Dockerfile +29 -0
- data/spec/fixtures/rails_users_app/Dockerfile.pg +3 -0
- data/spec/fixtures/rails_users_app/Gemfile +51 -0
- data/spec/fixtures/rails_users_app/Rakefile +6 -0
- data/spec/fixtures/rails_users_app/app/controllers/api/users_controller.rb +27 -0
- data/spec/fixtures/rails_users_app/app/controllers/application_controller.rb +2 -0
- data/spec/fixtures/rails_users_app/app/controllers/concerns/.keep +0 -0
- data/spec/fixtures/rails_users_app/app/controllers/health_controller.rb +5 -0
- data/spec/fixtures/rails_users_app/app/controllers/users_controller.rb +5 -0
- data/spec/fixtures/rails_users_app/app/models/activerecord/user.rb +18 -0
- data/spec/fixtures/rails_users_app/app/models/concerns/.keep +0 -0
- data/spec/fixtures/rails_users_app/app/models/sequel/user.rb +25 -0
- data/spec/fixtures/rails_users_app/app/views/layouts/application.html.haml +7 -0
- data/spec/fixtures/rails_users_app/app/views/users/index.html.haml +7 -0
- data/spec/fixtures/rails_users_app/appmap.yml +3 -0
- data/spec/fixtures/rails_users_app/bin/_appmap-record-self +29 -0
- data/spec/fixtures/rails_users_app/bin/appmap +29 -0
- data/spec/fixtures/rails_users_app/bin/byebug +29 -0
- data/spec/fixtures/rails_users_app/bin/gli +29 -0
- data/spec/fixtures/rails_users_app/bin/htmldiff +29 -0
- data/spec/fixtures/rails_users_app/bin/ldiff +29 -0
- data/spec/fixtures/rails_users_app/bin/nokogiri +29 -0
- data/spec/fixtures/rails_users_app/bin/rackup +29 -0
- data/spec/fixtures/rails_users_app/bin/rails +4 -0
- data/spec/fixtures/rails_users_app/bin/rake +29 -0
- data/spec/fixtures/rails_users_app/bin/rspec +29 -0
- data/spec/fixtures/rails_users_app/bin/ruby-parse +29 -0
- data/spec/fixtures/rails_users_app/bin/ruby-rewrite +29 -0
- data/spec/fixtures/rails_users_app/bin/sequel +29 -0
- data/spec/fixtures/rails_users_app/bin/setup +25 -0
- data/spec/fixtures/rails_users_app/bin/sprockets +29 -0
- data/spec/fixtures/rails_users_app/bin/thor +29 -0
- data/spec/fixtures/rails_users_app/bin/update +25 -0
- data/spec/fixtures/rails_users_app/config/application.rb +51 -0
- data/spec/fixtures/rails_users_app/config/boot.rb +3 -0
- data/spec/fixtures/rails_users_app/config/credentials.yml.enc +1 -0
- data/spec/fixtures/rails_users_app/config/database.yml +17 -0
- data/spec/fixtures/rails_users_app/config/environment.rb +5 -0
- data/spec/fixtures/rails_users_app/config/environments/development.rb +40 -0
- data/spec/fixtures/rails_users_app/config/environments/production.rb +68 -0
- data/spec/fixtures/rails_users_app/config/environments/test.rb +36 -0
- data/spec/fixtures/rails_users_app/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/fixtures/rails_users_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/fixtures/rails_users_app/config/initializers/cors.rb +16 -0
- data/spec/fixtures/rails_users_app/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/fixtures/rails_users_app/config/initializers/inflections.rb +16 -0
- data/spec/fixtures/rails_users_app/config/initializers/mime_types.rb +4 -0
- data/spec/fixtures/rails_users_app/config/initializers/record_button.rb +3 -0
- data/spec/fixtures/rails_users_app/config/initializers/wrap_parameters.rb +9 -0
- data/spec/fixtures/rails_users_app/config/locales/en.yml +33 -0
- data/spec/fixtures/rails_users_app/config/routes.rb +11 -0
- data/spec/fixtures/rails_users_app/config.ru +5 -0
- data/spec/fixtures/rails_users_app/create_app +11 -0
- data/spec/fixtures/rails_users_app/db/migrate/20190728211408_create_users.rb +9 -0
- data/spec/fixtures/rails_users_app/db/schema.rb +23 -0
- data/spec/fixtures/rails_users_app/docker-compose.yml +24 -0
- data/spec/fixtures/rails_users_app/lib/tasks/.keep +0 -0
- data/spec/fixtures/rails_users_app/log/.keep +0 -0
- data/spec/fixtures/rails_users_app/public/robots.txt +1 -0
- data/spec/fixtures/rails_users_app/spec/controllers/users_controller_api_spec.rb +29 -0
- data/spec/fixtures/rails_users_app/spec/models/user_spec.rb +39 -0
- data/spec/fixtures/rails_users_app/spec/rails_helper.rb +66 -0
- data/spec/fixtures/rails_users_app/spec/spec_helper.rb +96 -0
- data/spec/fixtures/rails_users_app/users_app/.gitignore +20 -0
- data/spec/rack_handler_webrick_spec.rb +59 -0
- data/spec/rails_spec_helper.rb +34 -0
- data/spec/railtie_spec.rb +35 -0
- data/spec/record_sql_rails4_pg_spec.rb +76 -0
- data/spec/record_sql_rails_pg_spec.rb +68 -0
- data/spec/rspec_feature_metadata_spec.rb +30 -0
- data/spec/spec_helper.rb +6 -0
- data/test/cli_test.rb +81 -0
- data/test/config_test.rb +149 -0
- data/test/explict_inspect_test.rb +29 -0
- data/test/fixtures/active_record_like/active_record/aggregations.rb +4 -0
- data/test/fixtures/active_record_like/active_record/association.rb +4 -0
- data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_base.rb +8 -0
- data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_part.rb +8 -0
- data/test/fixtures/active_record_like/active_record/associations/join_dependency.rb +6 -0
- data/test/fixtures/active_record_like/active_record/caps/caps.rb +4 -0
- data/test/fixtures/active_record_like/active_record.rb +2 -0
- data/test/fixtures/cli_record_test/appmap.yml +2 -0
- data/test/fixtures/cli_record_test/lib/cli_record_test/main.rb +7 -0
- data/test/fixtures/ignore_non_ruby_file/class.rb +3 -0
- data/test/fixtures/ignore_non_ruby_file/non-ruby.txt +1 -0
- data/test/fixtures/includes_excludes/lib/a/a_1.rb +6 -0
- data/test/fixtures/includes_excludes/lib/a/a_2.rb +6 -0
- data/test/fixtures/includes_excludes/lib/a/x/x_1.rb +8 -0
- data/test/fixtures/includes_excludes/lib/b/b_1.rb +6 -0
- data/test/fixtures/includes_excludes/lib/root_1.rb +4 -0
- data/test/fixtures/inspect_multiple_subdirs/module_a/class_a.rb +5 -0
- data/test/fixtures/inspect_multiple_subdirs/module_a.rb +2 -0
- data/test/fixtures/inspect_multiple_subdirs/module_b/class_b.rb +5 -0
- data/test/fixtures/inspect_multiple_subdirs/module_b/class_c.rb +5 -0
- data/test/fixtures/inspect_multiple_subdirs/module_b.rb +2 -0
- data/test/fixtures/inspect_package/module_a/module_b/class_in_module.rb +6 -0
- data/test/fixtures/parse_file/defs_static_function.rb +96 -0
- data/test/fixtures/parse_file/function_within_class.rb +36 -0
- data/test/fixtures/parse_file/include_public_methods.rb +127 -0
- data/test/fixtures/parse_file/instance_function.rb +17 -0
- data/test/fixtures/parse_file/modules.rb +71 -0
- data/test/fixtures/parse_file/sclass_static_function.rb +88 -0
- data/test/fixtures/parse_file/toplevel_class.rb +13 -0
- data/test/fixtures/parse_file/toplevel_function.rb +14 -0
- data/test/fixtures/rspec_recorder/Gemfile +5 -0
- data/test/fixtures/rspec_recorder/appmap.yml +3 -0
- data/test/fixtures/rspec_recorder/lib/hello.rb +5 -0
- data/test/fixtures/rspec_recorder/spec/hello_spec.rb +9 -0
- data/test/fixtures/trace_test/trace_program_1.rb +44 -0
- data/test/implicit_inspect_test.rb +33 -0
- data/test/include_exclude_test.rb +48 -0
- data/test/prerecorded_trace_test.rb +76 -0
- data/test/rspec_test.rb +22 -0
- data/test/test_helper.rb +46 -0
- data/test/trace_test.rb +92 -0
- 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
|