appmap 0.23.0 → 0.25.0
Sign up to get free protection for your applications and to get access to all the features.
- 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/lib/appmap/inspect.rb
DELETED
@@ -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
|