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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +17 -8
  4. data/.travis.yml +6 -0
  5. data/CHANGELOG.md +19 -0
  6. data/README.md +29 -12
  7. data/Rakefile +3 -3
  8. data/appmap.gemspec +3 -1
  9. data/exe/appmap +6 -18
  10. data/lib/appmap.rb +47 -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/command/upload.rb +4 -2
  17. data/lib/appmap/event.rb +168 -0
  18. data/lib/appmap/hook.rb +151 -0
  19. data/lib/appmap/middleware/remote_recording.rb +14 -20
  20. data/lib/appmap/rails/action_handler.rb +10 -6
  21. data/lib/appmap/rails/sql_handler.rb +10 -8
  22. data/lib/appmap/railtie.rb +31 -18
  23. data/lib/appmap/rspec.rb +238 -261
  24. data/lib/appmap/trace.rb +88 -0
  25. data/lib/appmap/version.rb +1 -1
  26. data/package-lock.json +90 -92
  27. data/spec/abstract_controller4_base_spec.rb +1 -1
  28. data/spec/abstract_controller_base_spec.rb +7 -3
  29. data/spec/config_spec.rb +25 -0
  30. data/spec/fixtures/hook/attr_accessor.rb +5 -0
  31. data/spec/fixtures/hook/class_method.rb +17 -0
  32. data/spec/fixtures/hook/constructor.rb +7 -0
  33. data/spec/fixtures/hook/exception_method.rb +11 -0
  34. data/spec/fixtures/hook/instance_method.rb +23 -0
  35. data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +3 -3
  36. data/spec/fixtures/rails4_users_app/config/database.yml +2 -1
  37. data/spec/fixtures/rails4_users_app/docker-compose.yml +2 -0
  38. data/spec/fixtures/rails_users_app/.ruby-version +1 -1
  39. data/spec/fixtures/rails_users_app/app/controllers/api/users_controller.rb +2 -2
  40. data/spec/fixtures/rails_users_app/config/database.yml +2 -1
  41. data/spec/fixtures/rails_users_app/create_app +1 -0
  42. data/spec/fixtures/rails_users_app/docker-compose.yml +4 -0
  43. data/spec/fixtures/rails_users_app/spec/models/user_spec.rb +1 -1
  44. data/spec/hook_spec.rb +357 -0
  45. data/spec/rails_spec_helper.rb +25 -16
  46. data/spec/railtie_spec.rb +1 -1
  47. data/spec/record_sql_rails_pg_spec.rb +1 -2
  48. data/spec/remote_recording_spec.rb +117 -0
  49. data/spec/spec_helper.rb +1 -0
  50. data/test/cli_test.rb +7 -36
  51. data/test/fixtures/cli_record_test/appmap.yml +2 -1
  52. data/test/fixtures/cli_record_test/lib/cli_record_test/main.rb +4 -2
  53. data/test/test_helper.rb +0 -42
  54. metadata +46 -62
  55. data/exe/_appmap-record-self +0 -49
  56. data/lib/appmap/command/inspect.rb +0 -14
  57. data/lib/appmap/config.rb +0 -65
  58. data/lib/appmap/config/directory.rb +0 -65
  59. data/lib/appmap/config/file.rb +0 -13
  60. data/lib/appmap/config/named_function.rb +0 -21
  61. data/lib/appmap/config/package_dir.rb +0 -52
  62. data/lib/appmap/config/path.rb +0 -25
  63. data/lib/appmap/feature.rb +0 -262
  64. data/lib/appmap/inspect.rb +0 -91
  65. data/lib/appmap/inspect/inspector.rb +0 -99
  66. data/lib/appmap/inspect/parse_node.rb +0 -170
  67. data/lib/appmap/inspect/parser.rb +0 -15
  68. data/lib/appmap/parser.rb +0 -60
  69. data/lib/appmap/rspec/parse_node.rb +0 -41
  70. data/lib/appmap/rspec/parser.rb +0 -15
  71. data/lib/appmap/trace/event_handler/rack_handler_webrick.rb +0 -65
  72. data/lib/appmap/trace/tracer.rb +0 -356
  73. data/spec/fixtures/rails_users_app/bin/_appmap-record-self +0 -29
  74. data/spec/rack_handler_webrick_spec.rb +0 -59
  75. data/test/config_test.rb +0 -149
  76. data/test/explict_inspect_test.rb +0 -29
  77. data/test/fixtures/active_record_like/active_record.rb +0 -2
  78. data/test/fixtures/active_record_like/active_record/aggregations.rb +0 -4
  79. data/test/fixtures/active_record_like/active_record/association.rb +0 -4
  80. data/test/fixtures/active_record_like/active_record/associations/join_dependency.rb +0 -6
  81. data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_base.rb +0 -8
  82. data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_part.rb +0 -8
  83. data/test/fixtures/active_record_like/active_record/caps/caps.rb +0 -4
  84. data/test/fixtures/ignore_non_ruby_file/class.rb +0 -3
  85. data/test/fixtures/ignore_non_ruby_file/non-ruby.txt +0 -1
  86. data/test/fixtures/includes_excludes/lib/a/a_1.rb +0 -6
  87. data/test/fixtures/includes_excludes/lib/a/a_2.rb +0 -6
  88. data/test/fixtures/includes_excludes/lib/a/x/x_1.rb +0 -8
  89. data/test/fixtures/includes_excludes/lib/b/b_1.rb +0 -6
  90. data/test/fixtures/includes_excludes/lib/root_1.rb +0 -4
  91. data/test/fixtures/inspect_multiple_subdirs/module_a.rb +0 -2
  92. data/test/fixtures/inspect_multiple_subdirs/module_a/class_a.rb +0 -5
  93. data/test/fixtures/inspect_multiple_subdirs/module_b.rb +0 -2
  94. data/test/fixtures/inspect_multiple_subdirs/module_b/class_b.rb +0 -5
  95. data/test/fixtures/inspect_multiple_subdirs/module_b/class_c.rb +0 -5
  96. data/test/fixtures/inspect_package/module_a/module_b/class_in_module.rb +0 -6
  97. data/test/fixtures/parse_file/defs_static_function.rb +0 -96
  98. data/test/fixtures/parse_file/function_within_class.rb +0 -36
  99. data/test/fixtures/parse_file/include_public_methods.rb +0 -127
  100. data/test/fixtures/parse_file/instance_function.rb +0 -17
  101. data/test/fixtures/parse_file/modules.rb +0 -71
  102. data/test/fixtures/parse_file/sclass_static_function.rb +0 -88
  103. data/test/fixtures/parse_file/toplevel_class.rb +0 -13
  104. data/test/fixtures/parse_file/toplevel_function.rb +0 -14
  105. data/test/fixtures/trace_test/trace_program_1.rb +0 -44
  106. data/test/implicit_inspect_test.rb +0 -33
  107. data/test/include_exclude_test.rb +0 -48
  108. data/test/prerecorded_trace_test.rb +0 -76
  109. data/test/trace_test.rb +0 -92
@@ -1,91 +0,0 @@
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
@@ -1,99 +0,0 @@
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
@@ -1,170 +0,0 @@
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
@@ -1,15 +0,0 @@
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
data/lib/appmap/parser.rb DELETED
@@ -1,60 +0,0 @@
1
- require 'parser/current'
2
- require 'set'
3
-
4
- module AppMap
5
- class Parser
6
- def initialize(file_path: nil, code: nil)
7
- @file_path = file_path
8
- @code = code
9
- end
10
-
11
- def to_s
12
- "Parse code #{file_path.inspect}"
13
- end
14
-
15
- # Parse the contents of a file into a list of features.
16
- def parse
17
- parse_tree, comments = parse_code_and_comments
18
- parse_nodes = build_parse_nodes parse_tree
19
- [ parse_nodes, comments ]
20
- end
21
-
22
- protected
23
-
24
- def file_path
25
- @file_path || '<inline>'
26
- end
27
-
28
- def code
29
- @code ||= File.read(file_path)
30
- end
31
-
32
- def parse_code_and_comments
33
- ::Parser::CurrentRuby.parse_with_comments(code)
34
- rescue ::Parser::SyntaxError, EncodingError
35
- warn "Unable to parse #{file_path.inspect} : #{$!.message}"
36
- [ [], [] ]
37
- end
38
-
39
- # rubocop:disable Metrics/MethodLength
40
- def build_parse_nodes(parse_tree)
41
- parse_nodes = []
42
- visit_methods = lambda do |node, ancestors|
43
- return unless node.respond_to?(:type)
44
-
45
- parse_node = build_parse_node(node, file_path, ancestors)
46
- parse_nodes << parse_node if parse_node
47
-
48
- ancestors << node
49
- node.children.each do |child|
50
- visit_methods.call(child, ancestors)
51
- end
52
- ancestors.pop
53
- end
54
- visit_methods.call(parse_tree, [])
55
- parse_nodes
56
- end
57
- end
58
- # rubocop:enable Metrics/MethodLength
59
- end
60
-
@@ -1,41 +0,0 @@
1
- require 'forwardable'
2
-
3
- module AppMap
4
- module RSpec
5
- # ParseNodeStruct wraps a generic AST parse node.
6
- ParseNodeStruct = Struct.new(:node, :file_path, :ancestors) do
7
- end
8
-
9
- # ParseNode wraps a generic AST parse node.
10
- class ParseNode < ParseNodeStruct
11
- extend Forwardable
12
-
13
- def_delegators :node, :type, :location
14
-
15
- class << self
16
- # Build a ParseNode from an AST node.
17
- def from_node(node, file_path, ancestors)
18
- case node.type
19
- when :block
20
- BlockParseNode.new(node, file_path, ancestors.dup)
21
- end
22
- end
23
- end
24
- end
25
-
26
- # A Ruby block.
27
- class BlockParseNode < ParseNode
28
- def to_s
29
- "RSpec block at #{file_path} #{first_line}:#{last_line}"
30
- end
31
-
32
- def first_line
33
- node.location.first_line
34
- end
35
-
36
- def last_line
37
- node.location.last_line
38
- end
39
- end
40
- end
41
- end