appmap 0.23.0 → 0.25.0
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 +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +17 -8
- data/.travis.yml +6 -0
- data/CHANGELOG.md +19 -0
- data/README.md +29 -12
- data/Rakefile +3 -3
- data/appmap.gemspec +3 -1
- data/exe/appmap +6 -18
- data/lib/appmap.rb +47 -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/command/upload.rb +4 -2
- data/lib/appmap/event.rb +168 -0
- data/lib/appmap/hook.rb +151 -0
- data/lib/appmap/middleware/remote_recording.rb +14 -20
- data/lib/appmap/rails/action_handler.rb +10 -6
- data/lib/appmap/rails/sql_handler.rb +10 -8
- data/lib/appmap/railtie.rb +31 -18
- data/lib/appmap/rspec.rb +238 -261
- 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 +357 -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 +1 -0
- data/test/cli_test.rb +7 -36
- 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/test_helper.rb +0 -42
- metadata +46 -62
- data/exe/_appmap-record-self +0 -49
- data/lib/appmap/command/inspect.rb +0 -14
- 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
|
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
|