appmap 0.23.0 → 0.27.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +17 -8
- data/.travis.yml +6 -0
- data/CHANGELOG.md +43 -0
- data/README.md +33 -21
- data/Rakefile +3 -3
- data/appmap.gemspec +3 -1
- data/exe/appmap +5 -73
- data/lib/appmap.rb +61 -6
- data/lib/appmap/algorithm/prune_class_map.rb +2 -0
- data/lib/appmap/algorithm/stats.rb +4 -2
- data/lib/appmap/class_map.rb +143 -0
- data/lib/appmap/command/record.rb +8 -6
- data/lib/appmap/command/stats.rb +2 -0
- data/lib/appmap/event.rb +168 -0
- data/lib/appmap/hook.rb +152 -0
- data/lib/appmap/middleware/remote_recording.rb +14 -21
- data/lib/appmap/rails/action_handler.rb +10 -6
- data/lib/appmap/rails/sql_handler.rb +10 -13
- data/lib/appmap/railtie.rb +31 -18
- data/lib/appmap/rspec.rb +247 -260
- data/lib/appmap/trace.rb +88 -0
- data/lib/appmap/version.rb +1 -1
- data/package-lock.json +90 -92
- data/spec/abstract_controller4_base_spec.rb +1 -1
- data/spec/abstract_controller_base_spec.rb +7 -3
- data/spec/config_spec.rb +25 -0
- data/spec/fixtures/hook/attr_accessor.rb +5 -0
- data/spec/fixtures/hook/class_method.rb +17 -0
- data/spec/fixtures/hook/constructor.rb +7 -0
- data/spec/fixtures/hook/exception_method.rb +11 -0
- data/spec/fixtures/hook/instance_method.rb +23 -0
- data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +3 -3
- data/spec/fixtures/rails4_users_app/config/database.yml +2 -1
- data/spec/fixtures/rails4_users_app/docker-compose.yml +2 -0
- data/spec/fixtures/rails_users_app/.ruby-version +1 -1
- data/spec/fixtures/rails_users_app/app/controllers/api/users_controller.rb +2 -2
- data/spec/fixtures/rails_users_app/config/database.yml +2 -1
- data/spec/fixtures/rails_users_app/create_app +1 -0
- data/spec/fixtures/rails_users_app/docker-compose.yml +4 -0
- data/spec/fixtures/rails_users_app/spec/models/user_spec.rb +1 -1
- data/spec/hook_spec.rb +369 -0
- data/spec/rails_spec_helper.rb +25 -16
- data/spec/railtie_spec.rb +1 -1
- data/spec/record_sql_rails_pg_spec.rb +1 -2
- data/spec/remote_recording_spec.rb +117 -0
- data/spec/spec_helper.rb +5 -0
- data/test/cli_test.rb +4 -46
- data/test/fixtures/cli_record_test/appmap.yml +2 -1
- data/test/fixtures/cli_record_test/lib/cli_record_test/main.rb +4 -2
- data/test/fixtures/rspec_recorder/Gemfile +1 -1
- data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
- data/test/rspec_test.rb +5 -0
- data/test/test_helper.rb +0 -42
- metadata +46 -63
- data/exe/_appmap-record-self +0 -49
- data/lib/appmap/command/inspect.rb +0 -14
- data/lib/appmap/command/upload.rb +0 -99
- data/lib/appmap/config.rb +0 -65
- data/lib/appmap/config/directory.rb +0 -65
- data/lib/appmap/config/file.rb +0 -13
- data/lib/appmap/config/named_function.rb +0 -21
- data/lib/appmap/config/package_dir.rb +0 -52
- data/lib/appmap/config/path.rb +0 -25
- data/lib/appmap/feature.rb +0 -262
- data/lib/appmap/inspect.rb +0 -91
- data/lib/appmap/inspect/inspector.rb +0 -99
- data/lib/appmap/inspect/parse_node.rb +0 -170
- data/lib/appmap/inspect/parser.rb +0 -15
- data/lib/appmap/parser.rb +0 -60
- data/lib/appmap/rspec/parse_node.rb +0 -41
- data/lib/appmap/rspec/parser.rb +0 -15
- data/lib/appmap/trace/event_handler/rack_handler_webrick.rb +0 -65
- data/lib/appmap/trace/tracer.rb +0 -356
- data/spec/fixtures/rails_users_app/bin/_appmap-record-self +0 -29
- data/spec/rack_handler_webrick_spec.rb +0 -59
- data/test/config_test.rb +0 -149
- data/test/explict_inspect_test.rb +0 -29
- data/test/fixtures/active_record_like/active_record.rb +0 -2
- data/test/fixtures/active_record_like/active_record/aggregations.rb +0 -4
- data/test/fixtures/active_record_like/active_record/association.rb +0 -4
- data/test/fixtures/active_record_like/active_record/associations/join_dependency.rb +0 -6
- data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_base.rb +0 -8
- data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_part.rb +0 -8
- data/test/fixtures/active_record_like/active_record/caps/caps.rb +0 -4
- data/test/fixtures/ignore_non_ruby_file/class.rb +0 -3
- data/test/fixtures/ignore_non_ruby_file/non-ruby.txt +0 -1
- data/test/fixtures/includes_excludes/lib/a/a_1.rb +0 -6
- data/test/fixtures/includes_excludes/lib/a/a_2.rb +0 -6
- data/test/fixtures/includes_excludes/lib/a/x/x_1.rb +0 -8
- data/test/fixtures/includes_excludes/lib/b/b_1.rb +0 -6
- data/test/fixtures/includes_excludes/lib/root_1.rb +0 -4
- data/test/fixtures/inspect_multiple_subdirs/module_a.rb +0 -2
- data/test/fixtures/inspect_multiple_subdirs/module_a/class_a.rb +0 -5
- data/test/fixtures/inspect_multiple_subdirs/module_b.rb +0 -2
- data/test/fixtures/inspect_multiple_subdirs/module_b/class_b.rb +0 -5
- data/test/fixtures/inspect_multiple_subdirs/module_b/class_c.rb +0 -5
- data/test/fixtures/inspect_package/module_a/module_b/class_in_module.rb +0 -6
- data/test/fixtures/parse_file/defs_static_function.rb +0 -96
- data/test/fixtures/parse_file/function_within_class.rb +0 -36
- data/test/fixtures/parse_file/include_public_methods.rb +0 -127
- data/test/fixtures/parse_file/instance_function.rb +0 -17
- data/test/fixtures/parse_file/modules.rb +0 -71
- data/test/fixtures/parse_file/sclass_static_function.rb +0 -88
- data/test/fixtures/parse_file/toplevel_class.rb +0 -13
- data/test/fixtures/parse_file/toplevel_function.rb +0 -14
- data/test/fixtures/trace_test/trace_program_1.rb +0 -44
- data/test/implicit_inspect_test.rb +0 -33
- data/test/include_exclude_test.rb +0 -48
- data/test/prerecorded_trace_test.rb +0 -76
- data/test/trace_test.rb +0 -92
data/exe/_appmap-record-self
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require 'json'
|
4
|
-
|
5
|
-
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '../lib')
|
6
|
-
|
7
|
-
require 'appmap'
|
8
|
-
require 'appmap/feature'
|
9
|
-
require 'shellwords'
|
10
|
-
|
11
|
-
def usage
|
12
|
-
warn 'Usage: trace-self <trace-file>'
|
13
|
-
exit 1
|
14
|
-
end
|
15
|
-
|
16
|
-
trace_file = ARGV.shift || usage
|
17
|
-
usage unless ARGV.empty?
|
18
|
-
|
19
|
-
replay_events = File.read(trace_file)
|
20
|
-
.split("\n")
|
21
|
-
.map(&:strip)
|
22
|
-
.reject(&:empty?)
|
23
|
-
.map(&JSON.method(:parse))
|
24
|
-
.map { |te| te['event'] = te['event'].intern; te }
|
25
|
-
.map { |te| OpenStruct.new(te) }
|
26
|
-
|
27
|
-
require 'appmap/trace/tracer'
|
28
|
-
|
29
|
-
def method_call_from_event(evt)
|
30
|
-
AppMap::Trace::MethodCall.new(evt.id, evt.event.intern, evt.defined_class, evt.method_id, evt.path, evt.lineno, evt.static, evt.thread_id, evt.variables)
|
31
|
-
end
|
32
|
-
|
33
|
-
# _parent_id and _elapsed are ignored since they are already specified in the
|
34
|
-
# data being replayed.
|
35
|
-
def method_return_from_event(evt, _parent_id, _elapsed)
|
36
|
-
AppMap::Trace::MethodReturn.new(evt.id, evt.event.intern, evt.defined_class, evt.method_id, evt.path, evt.lineno, evt.static, evt.thread_id, evt.variables).tap do |mr|
|
37
|
-
mr.parent_id = evt.parent_id
|
38
|
-
mr.elapsed = evt.elapsed
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
tracer = AppMap::Trace.tracer
|
43
|
-
handler = AppMap::Trace::TracePointHandler.new(tracer)
|
44
|
-
handler.call_constructor = method(:method_call_from_event)
|
45
|
-
handler.return_constructor = method(:method_return_from_event)
|
46
|
-
|
47
|
-
replay_events.each do |evt|
|
48
|
-
handler.handle evt
|
49
|
-
end
|
@@ -1,14 +0,0 @@
|
|
1
|
-
module AppMap
|
2
|
-
module Command
|
3
|
-
InspectStruct = Struct.new(:config)
|
4
|
-
|
5
|
-
class Inspect < InspectStruct
|
6
|
-
def perform
|
7
|
-
require 'appmap/command/record'
|
8
|
-
|
9
|
-
features = AppMap.inspect(config)
|
10
|
-
{ version: AppMap::APPMAP_FORMAT_VERSION, metadata: AppMap::Command::Record.detect_metadata, classMap: features }
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
@@ -1,99 +0,0 @@
|
|
1
|
-
require 'json'
|
2
|
-
require 'faraday'
|
3
|
-
|
4
|
-
module AppMap
|
5
|
-
module Command
|
6
|
-
UploadResponse = Struct.new(:batch_id, :scenario_uuid)
|
7
|
-
|
8
|
-
UploadStruct = Struct.new(:config, :data, :url, :user, :org)
|
9
|
-
class Upload < UploadStruct
|
10
|
-
MAX_DEPTH = 12
|
11
|
-
|
12
|
-
attr_accessor :batch_id
|
13
|
-
|
14
|
-
def initialize(config, data, url, user, org)
|
15
|
-
super
|
16
|
-
|
17
|
-
# TODO: Make this an option
|
18
|
-
@max_depth = MAX_DEPTH
|
19
|
-
end
|
20
|
-
|
21
|
-
def perform
|
22
|
-
appmap = data.clone
|
23
|
-
|
24
|
-
events = data.delete('events')
|
25
|
-
class_map = data.delete('classMap') || []
|
26
|
-
|
27
|
-
unless events.blank?
|
28
|
-
pruned_events = []
|
29
|
-
stack = []
|
30
|
-
events.each do |evt|
|
31
|
-
if evt['event'] == 'call'
|
32
|
-
stack << evt
|
33
|
-
stack_depth = stack.length
|
34
|
-
else
|
35
|
-
stack_depth = stack.length
|
36
|
-
stack.pop
|
37
|
-
end
|
38
|
-
|
39
|
-
prune = stack_depth > @max_depth
|
40
|
-
|
41
|
-
pruned_events << evt unless prune
|
42
|
-
end
|
43
|
-
|
44
|
-
warn "Pruned events to #{pruned_events.length}" if events.length > pruned_events.length
|
45
|
-
|
46
|
-
appmap[:events] = pruned_events
|
47
|
-
appmap[:classMap] = prune(class_map, events: pruned_events)
|
48
|
-
else
|
49
|
-
appmap[:events] = []
|
50
|
-
appmap[:classMap] = prune(class_map)
|
51
|
-
end
|
52
|
-
|
53
|
-
upload_file = { user: user, org: org, data: appmap }.compact
|
54
|
-
|
55
|
-
conn = Faraday.new(url: url)
|
56
|
-
response = conn.post do |req|
|
57
|
-
req.url '/api/scenarios'
|
58
|
-
req.headers['Content-Type'] = 'application/json'
|
59
|
-
req.headers[AppMap::BATCH_HEADER_NAME] = @batch_id if @batch_id
|
60
|
-
req.body = JSON.generate(upload_file)
|
61
|
-
end
|
62
|
-
|
63
|
-
unless response.body.blank?
|
64
|
-
message = begin
|
65
|
-
JSON.parse(response.body)
|
66
|
-
rescue JSON::ParserError => e
|
67
|
-
warn "Response is not valid JSON (#{e.message})"
|
68
|
-
nil
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
unless response.success?
|
73
|
-
error = [ 'Upload failed' ]
|
74
|
-
error << ": #{message}" if message
|
75
|
-
raise error.join
|
76
|
-
end
|
77
|
-
|
78
|
-
batch_id = @batch_id || response.headers[AppMap::BATCH_HEADER_NAME]
|
79
|
-
|
80
|
-
uuid = message['uuid']
|
81
|
-
UploadResponse.new(batch_id, uuid)
|
82
|
-
end
|
83
|
-
|
84
|
-
protected
|
85
|
-
|
86
|
-
def debug?
|
87
|
-
ENV['DEBUG'] == 'true' || ENV['GLI_DEBUG'] == 'true'
|
88
|
-
end
|
89
|
-
|
90
|
-
def prune(class_map, events: nil)
|
91
|
-
require 'appmap/algorithm/prune_class_map'
|
92
|
-
Algorithm::PruneClassMap.new(class_map).tap do |alg|
|
93
|
-
alg.events = events if events
|
94
|
-
alg.logger = ->(msg) { warn msg } if debug?
|
95
|
-
end.perform
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
data/lib/appmap/config.rb
DELETED
@@ -1,65 +0,0 @@
|
|
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
|
@@ -1,65 +0,0 @@
|
|
1
|
-
module AppMap
|
2
|
-
module Config
|
3
|
-
# A normal directory is scanned for AppMap features without interpreting the
|
4
|
-
# directory as a 'package'.
|
5
|
-
#
|
6
|
-
# @appmap
|
7
|
-
class Directory < Path
|
8
|
-
# @appmap
|
9
|
-
def initialize(path)
|
10
|
-
super
|
11
|
-
end
|
12
|
-
|
13
|
-
# @appmap
|
14
|
-
def children
|
15
|
-
child_files.sort + child_directories.sort
|
16
|
-
end
|
17
|
-
|
18
|
-
protected
|
19
|
-
|
20
|
-
def ruby_file?(path)
|
21
|
-
::File.file?(path) && (path =~ /\.rb$/ || ruby_shebang?(path))
|
22
|
-
end
|
23
|
-
|
24
|
-
def ruby_shebang?(path)
|
25
|
-
lines = begin
|
26
|
-
::File.read(path).split("\n")
|
27
|
-
rescue ArgumentError => e
|
28
|
-
if e.message.index 'invalid byte sequence'
|
29
|
-
warn "Unable to load file #{path.inspect} : #{e.message}"
|
30
|
-
return false
|
31
|
-
end
|
32
|
-
raise
|
33
|
-
end
|
34
|
-
lines[0] && lines[0].index('#!/usr/bin/env ruby') == 0
|
35
|
-
end
|
36
|
-
|
37
|
-
def child_files
|
38
|
-
expand_path = ->(fname) { ::File.join(path, fname) }
|
39
|
-
Dir.new(path).entries.select do |fname|
|
40
|
-
::File.file?(expand_path.call(fname)) &&
|
41
|
-
!::File.symlink?(expand_path.call(fname)) &&
|
42
|
-
ruby_file?(expand_path.call(fname))
|
43
|
-
end.select do |fname|
|
44
|
-
!exclude?(::File.join(path, fname))
|
45
|
-
end.map do |fname|
|
46
|
-
File.new(expand_path.call(fname)).tap do |f|
|
47
|
-
f.mode = mode
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def child_directories
|
53
|
-
File.new(path).entries.select do |fname|
|
54
|
-
!%w[. ..].include?(fname) && !::File.directory?(fname)
|
55
|
-
end.select do |dir|
|
56
|
-
!exclude?(::File.join(path, dir))
|
57
|
-
end.map do |dir|
|
58
|
-
PackageDir.new(dir, [module_name, dir].join('/')).tap do |m|
|
59
|
-
m.mode = mode
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
data/lib/appmap/config/file.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
module AppMap
|
2
|
-
module Config
|
3
|
-
NamedFunctionStruct = Struct.new(:id, :gem_name, :file_path, :class_names, :method_name, :static)
|
4
|
-
|
5
|
-
# Identifies a specific function within a Gem to be instrumented.
|
6
|
-
#
|
7
|
-
# * `id` A unique identifier for the named function. This is used to associate custom logic with the
|
8
|
-
# named function when the trace events are being handled.
|
9
|
-
# * `gem_name` Name of the Gem.
|
10
|
-
# * `file_path` Name of the file within the Gem in which the function is located.
|
11
|
-
# * `class_names` Array of the module/class name scope which contains the function. For example,
|
12
|
-
# `%w[Rack Handler WEBrick]`.
|
13
|
-
# * `method_name` Name of the method within the class name scope.
|
14
|
-
# * `static` Whether it's a static or instance method.
|
15
|
-
class NamedFunction < NamedFunctionStruct
|
16
|
-
def children
|
17
|
-
[]
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
@@ -1,52 +0,0 @@
|
|
1
|
-
require 'pathname'
|
2
|
-
|
3
|
-
module AppMap
|
4
|
-
module Config
|
5
|
-
# Scan a directory for AppMap features, treating it as a package and its
|
6
|
-
# sub-folders as sub-packages.
|
7
|
-
#
|
8
|
-
# @appmap
|
9
|
-
class PackageDir < Directory
|
10
|
-
attr_accessor :package_name, :base_path, :exclude
|
11
|
-
|
12
|
-
# @appmap
|
13
|
-
def initialize(path, package_name = Pathname.new(path || '').basename.to_s)
|
14
|
-
super(path)
|
15
|
-
|
16
|
-
@package_name = package_name
|
17
|
-
@base_path = path
|
18
|
-
@exclude = []
|
19
|
-
end
|
20
|
-
|
21
|
-
def sub_package_dir(dir)
|
22
|
-
PackageDir.new(::File.join(path, dir), dir).tap do |m|
|
23
|
-
m.base_path = base_path
|
24
|
-
m.exclude = exclude
|
25
|
-
m.mode = mode
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def exclude?(path)
|
30
|
-
relative_path = path.gsub("#{base_path}/", '')
|
31
|
-
exclude.member?(relative_path)
|
32
|
-
end
|
33
|
-
|
34
|
-
# @appmap
|
35
|
-
def children
|
36
|
-
child_files.sort + child_packages.sort
|
37
|
-
end
|
38
|
-
|
39
|
-
protected
|
40
|
-
|
41
|
-
def child_packages
|
42
|
-
::Dir.new(path).entries.select do |fname|
|
43
|
-
!%w[. ..].include?(fname) && ::File.directory?(::File.join(path, fname))
|
44
|
-
end.select do |dir|
|
45
|
-
!exclude?(::File.join(path, dir))
|
46
|
-
end.map do |dir|
|
47
|
-
sub_package_dir(dir)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
data/lib/appmap/config/path.rb
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
module AppMap
|
2
|
-
module Config
|
3
|
-
PathStruct = Struct.new(:path)
|
4
|
-
|
5
|
-
# Path is an abstract configuration of a file, directory, or package.
|
6
|
-
class Path < PathStruct
|
7
|
-
attr_accessor :mode
|
8
|
-
|
9
|
-
def initialize(path)
|
10
|
-
super(path)
|
11
|
-
|
12
|
-
@mode = :implicit
|
13
|
-
end
|
14
|
-
|
15
|
-
def <=>(other)
|
16
|
-
path <=> other.path
|
17
|
-
end
|
18
|
-
|
19
|
-
# Automatically determined configurations of child file/package paths.
|
20
|
-
def children
|
21
|
-
[]
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
data/lib/appmap/feature.rb
DELETED
@@ -1,262 +0,0 @@
|
|
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
|