appmap 0.23.0 → 0.27.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +17 -8
  4. data/.travis.yml +6 -0
  5. data/CHANGELOG.md +43 -0
  6. data/README.md +33 -21
  7. data/Rakefile +3 -3
  8. data/appmap.gemspec +3 -1
  9. data/exe/appmap +5 -73
  10. data/lib/appmap.rb +61 -6
  11. data/lib/appmap/algorithm/prune_class_map.rb +2 -0
  12. data/lib/appmap/algorithm/stats.rb +4 -2
  13. data/lib/appmap/class_map.rb +143 -0
  14. data/lib/appmap/command/record.rb +8 -6
  15. data/lib/appmap/command/stats.rb +2 -0
  16. data/lib/appmap/event.rb +168 -0
  17. data/lib/appmap/hook.rb +152 -0
  18. data/lib/appmap/middleware/remote_recording.rb +14 -21
  19. data/lib/appmap/rails/action_handler.rb +10 -6
  20. data/lib/appmap/rails/sql_handler.rb +10 -13
  21. data/lib/appmap/railtie.rb +31 -18
  22. data/lib/appmap/rspec.rb +247 -260
  23. data/lib/appmap/trace.rb +88 -0
  24. data/lib/appmap/version.rb +1 -1
  25. data/package-lock.json +90 -92
  26. data/spec/abstract_controller4_base_spec.rb +1 -1
  27. data/spec/abstract_controller_base_spec.rb +7 -3
  28. data/spec/config_spec.rb +25 -0
  29. data/spec/fixtures/hook/attr_accessor.rb +5 -0
  30. data/spec/fixtures/hook/class_method.rb +17 -0
  31. data/spec/fixtures/hook/constructor.rb +7 -0
  32. data/spec/fixtures/hook/exception_method.rb +11 -0
  33. data/spec/fixtures/hook/instance_method.rb +23 -0
  34. data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +3 -3
  35. data/spec/fixtures/rails4_users_app/config/database.yml +2 -1
  36. data/spec/fixtures/rails4_users_app/docker-compose.yml +2 -0
  37. data/spec/fixtures/rails_users_app/.ruby-version +1 -1
  38. data/spec/fixtures/rails_users_app/app/controllers/api/users_controller.rb +2 -2
  39. data/spec/fixtures/rails_users_app/config/database.yml +2 -1
  40. data/spec/fixtures/rails_users_app/create_app +1 -0
  41. data/spec/fixtures/rails_users_app/docker-compose.yml +4 -0
  42. data/spec/fixtures/rails_users_app/spec/models/user_spec.rb +1 -1
  43. data/spec/hook_spec.rb +369 -0
  44. data/spec/rails_spec_helper.rb +25 -16
  45. data/spec/railtie_spec.rb +1 -1
  46. data/spec/record_sql_rails_pg_spec.rb +1 -2
  47. data/spec/remote_recording_spec.rb +117 -0
  48. data/spec/spec_helper.rb +5 -0
  49. data/test/cli_test.rb +4 -46
  50. data/test/fixtures/cli_record_test/appmap.yml +2 -1
  51. data/test/fixtures/cli_record_test/lib/cli_record_test/main.rb +4 -2
  52. data/test/fixtures/rspec_recorder/Gemfile +1 -1
  53. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
  54. data/test/rspec_test.rb +5 -0
  55. data/test/test_helper.rb +0 -42
  56. metadata +46 -63
  57. data/exe/_appmap-record-self +0 -49
  58. data/lib/appmap/command/inspect.rb +0 -14
  59. data/lib/appmap/command/upload.rb +0 -99
  60. data/lib/appmap/config.rb +0 -65
  61. data/lib/appmap/config/directory.rb +0 -65
  62. data/lib/appmap/config/file.rb +0 -13
  63. data/lib/appmap/config/named_function.rb +0 -21
  64. data/lib/appmap/config/package_dir.rb +0 -52
  65. data/lib/appmap/config/path.rb +0 -25
  66. data/lib/appmap/feature.rb +0 -262
  67. data/lib/appmap/inspect.rb +0 -91
  68. data/lib/appmap/inspect/inspector.rb +0 -99
  69. data/lib/appmap/inspect/parse_node.rb +0 -170
  70. data/lib/appmap/inspect/parser.rb +0 -15
  71. data/lib/appmap/parser.rb +0 -60
  72. data/lib/appmap/rspec/parse_node.rb +0 -41
  73. data/lib/appmap/rspec/parser.rb +0 -15
  74. data/lib/appmap/trace/event_handler/rack_handler_webrick.rb +0 -65
  75. data/lib/appmap/trace/tracer.rb +0 -356
  76. data/spec/fixtures/rails_users_app/bin/_appmap-record-self +0 -29
  77. data/spec/rack_handler_webrick_spec.rb +0 -59
  78. data/test/config_test.rb +0 -149
  79. data/test/explict_inspect_test.rb +0 -29
  80. data/test/fixtures/active_record_like/active_record.rb +0 -2
  81. data/test/fixtures/active_record_like/active_record/aggregations.rb +0 -4
  82. data/test/fixtures/active_record_like/active_record/association.rb +0 -4
  83. data/test/fixtures/active_record_like/active_record/associations/join_dependency.rb +0 -6
  84. data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_base.rb +0 -8
  85. data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_part.rb +0 -8
  86. data/test/fixtures/active_record_like/active_record/caps/caps.rb +0 -4
  87. data/test/fixtures/ignore_non_ruby_file/class.rb +0 -3
  88. data/test/fixtures/ignore_non_ruby_file/non-ruby.txt +0 -1
  89. data/test/fixtures/includes_excludes/lib/a/a_1.rb +0 -6
  90. data/test/fixtures/includes_excludes/lib/a/a_2.rb +0 -6
  91. data/test/fixtures/includes_excludes/lib/a/x/x_1.rb +0 -8
  92. data/test/fixtures/includes_excludes/lib/b/b_1.rb +0 -6
  93. data/test/fixtures/includes_excludes/lib/root_1.rb +0 -4
  94. data/test/fixtures/inspect_multiple_subdirs/module_a.rb +0 -2
  95. data/test/fixtures/inspect_multiple_subdirs/module_a/class_a.rb +0 -5
  96. data/test/fixtures/inspect_multiple_subdirs/module_b.rb +0 -2
  97. data/test/fixtures/inspect_multiple_subdirs/module_b/class_b.rb +0 -5
  98. data/test/fixtures/inspect_multiple_subdirs/module_b/class_c.rb +0 -5
  99. data/test/fixtures/inspect_package/module_a/module_b/class_in_module.rb +0 -6
  100. data/test/fixtures/parse_file/defs_static_function.rb +0 -96
  101. data/test/fixtures/parse_file/function_within_class.rb +0 -36
  102. data/test/fixtures/parse_file/include_public_methods.rb +0 -127
  103. data/test/fixtures/parse_file/instance_function.rb +0 -17
  104. data/test/fixtures/parse_file/modules.rb +0 -71
  105. data/test/fixtures/parse_file/sclass_static_function.rb +0 -88
  106. data/test/fixtures/parse_file/toplevel_class.rb +0 -13
  107. data/test/fixtures/parse_file/toplevel_function.rb +0 -14
  108. data/test/fixtures/trace_test/trace_program_1.rb +0 -44
  109. data/test/implicit_inspect_test.rb +0 -33
  110. data/test/include_exclude_test.rb +0 -48
  111. data/test/prerecorded_trace_test.rb +0 -76
  112. data/test/trace_test.rb +0 -92
@@ -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
@@ -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
@@ -1,13 +0,0 @@
1
- module AppMap
2
- module Config
3
- # Scan a specific file for AppMap features.
4
- #
5
- # @appmap
6
- class File < Path
7
- # @appmap
8
- def initialize(path)
9
- super
10
- end
11
- end
12
- end
13
- end
@@ -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
@@ -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
@@ -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