appmap 0.23.0 → 0.25.0

Sign up to get free protection for your applications and to get access to all the features.
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