delfos 0.0.1.pre.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+ require "pathname"
3
+ require "forwardable"
4
+ require "binding_of_caller"
5
+ require_relative "common_path"
6
+ require_relative "method_logging/klass_determination"
7
+ require_relative "method_logging/code_location"
8
+ require_relative "method_logging/args"
9
+ require_relative "execution_chain"
10
+
11
+ module Delfos
12
+ class << self
13
+ attr_accessor :logger, :application_directories
14
+ end
15
+
16
+ class ApplicationDirectoriesNotDefined < StandardError
17
+ def initialize(*_args)
18
+ super "Please set Delfos.application_directories"
19
+ end
20
+ end
21
+
22
+ module MethodLogging
23
+ class << self
24
+ def check_setup!
25
+ raise Delfos::ApplicationDirectoriesNotDefined unless Delfos.application_directories
26
+ end
27
+
28
+ def log(called_object,
29
+ args, keyword_args, _block,
30
+ class_method,
31
+ stack, call_site_binding,
32
+ called_method)
33
+ check_setup!
34
+
35
+ call_site = CodeLocation.from_call_site(stack, call_site_binding)
36
+ Delfos::ExecutionChain.push(call_site)
37
+
38
+ return unless call_site
39
+
40
+ args = Args.new(args.dup, keyword_args.dup)
41
+
42
+ called_code = CodeLocation.from_called(called_object, called_method, class_method)
43
+
44
+ Delfos.logger.debug(args, call_site, called_code)
45
+ end
46
+
47
+ def include_any_path_in_logging?(paths)
48
+ paths.inject(false) do |result, path|
49
+ result || include_file_in_logging?(path)
50
+ end
51
+ end
52
+
53
+ def exclude_from_logging?(method)
54
+ file, _line_number = method.source_location
55
+ return true unless file
56
+
57
+ exclude_file_from_logging?(File.expand_path(file))
58
+ end
59
+
60
+ def include_file_in_logging?(file)
61
+ !exclude_file_from_logging?(file)
62
+ end
63
+
64
+ def exclude_file_from_logging?(file)
65
+ check_setup!
66
+ path = Pathname.new(File.expand_path file)
67
+ !CommonPath.included_in?(path, Delfos.application_directories)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../../delfos"
3
+ require_relative "../common_path"
4
+ require_relative "klass_determination"
5
+
6
+ module Delfos
7
+ module MethodLogging
8
+ class Args
9
+ include KlassDetermination
10
+
11
+ def initialize(args, keyword_args)
12
+ @raw_args = args
13
+ @raw_keyword_args = keyword_args
14
+ end
15
+
16
+ def args
17
+ @args ||= calculate_args(@raw_args)
18
+ end
19
+
20
+ def calculate_args(args)
21
+ klass_file_locations(args).select do |_klass, locations|
22
+ Delfos::MethodLogging.include_any_path_in_logging?(locations)
23
+ end.map(&:first)
24
+ end
25
+
26
+ def keyword_args
27
+ @keyword_args ||= calculate_args(@raw_keyword_args.values)
28
+ end
29
+
30
+ def klass_locations(klass)
31
+ files = method_sources(klass)
32
+
33
+ files.flatten.compact.uniq
34
+ end
35
+
36
+ private
37
+
38
+ def klass_file_locations(args)
39
+ klasses(args).each_with_object({}) { |k, result| result[k] = klass_locations(k) }
40
+ end
41
+
42
+ def klasses(args)
43
+ args.map do |k|
44
+ klass_for(k)
45
+ end
46
+ end
47
+
48
+ def method_sources(klass)
49
+ (Delfos::Patching.added_methods[klass.to_s] || {}).values.map(&:first)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+ require_relative "klass_determination"
3
+
4
+ module Delfos
5
+ module MethodLogging
6
+ class CodeLocation
7
+ include KlassDetermination
8
+
9
+ class << self
10
+ def from_call_site(stack, call_site_binding)
11
+ CallSiteParsing.new(stack, call_site_binding).perform
12
+ end
13
+
14
+ def from_called(object, called_method, class_method)
15
+ file, line_number = called_method.source_location
16
+ return unless file && line_number
17
+
18
+ new(object, called_method.name.to_s, class_method, file, line_number)
19
+ end
20
+
21
+ def method_type_from(class_method)
22
+ class_method ? "ClassMethod" : "InstanceMethod"
23
+ end
24
+
25
+ def method_definition_for(klass, class_method, method_name)
26
+ key = key_from(class_method, method_name)
27
+
28
+ Delfos::Patching.method_definition_for(klass, key)
29
+ end
30
+
31
+ private
32
+
33
+ def key_from(class_method, name)
34
+ "#{method_type_from(class_method)}_#{name}"
35
+ end
36
+ end
37
+
38
+ attr_reader :object, :method_name, :class_method, :method_type, :file, :line_number
39
+
40
+ def initialize(object, method_name, class_method, file, line_number)
41
+ @object = object
42
+ @method_name = method_name
43
+ @class_method = class_method
44
+ @method_type = self.class.method_type_from class_method
45
+ @line_number = line_number.to_i
46
+ @file = file
47
+
48
+ end
49
+
50
+ def file
51
+ file = @file.to_s
52
+
53
+ if file
54
+ Delfos.application_directories.map do |d|
55
+ file = relative_path(file, d)
56
+ end
57
+ end
58
+
59
+ file
60
+ end
61
+
62
+ def relative_path(file, dir)
63
+ match = dir.to_s.split("/")[0..-2].join("/")
64
+
65
+ if file[match]
66
+ file = file.gsub(match, "").
67
+ gsub(%r{^/}, "")
68
+ end
69
+
70
+ file
71
+ end
72
+
73
+ def klass
74
+ klass_for(object)
75
+ end
76
+
77
+ def klass_name
78
+ name = klass.name || "__AnonymousClass"
79
+ name.tr ":", "_"
80
+ end
81
+
82
+ def method_definition_file
83
+ method_definition[0].to_s
84
+ end
85
+
86
+ def method_definition_line
87
+ method_definition[1].to_i
88
+ end
89
+
90
+ private
91
+
92
+ def method_key
93
+ "#{method_type}_#{method_name}"
94
+ end
95
+
96
+ def method_definition
97
+ (@method_definition ||= self.class.method_definition_for(klass, class_method, method_name)) || {}
98
+ end
99
+ end
100
+
101
+ class CallSiteParsing
102
+ # This magic number is determined based on the specific implementation now
103
+ # E.g. if the line
104
+ # where we call this `call_site_binding.of_caller(stack_index + STACK_OFFSET).eval('self')`
105
+ # is to be extracted into another method we will get a failing test and have to increment
106
+ # the value
107
+ STACK_OFFSET = 5
108
+
109
+ attr_reader :stack, :call_site_binding
110
+
111
+ def initialize(stack, call_site_binding)
112
+ @stack = stack
113
+ @call_site_binding = call_site_binding
114
+ end
115
+
116
+ def perform
117
+ file, line_number, method_name = method_details
118
+ return unless current && file && line_number && method_name
119
+
120
+ CodeLocation.new(object, method_name.to_s, class_method, file, line_number)
121
+ end
122
+
123
+ private
124
+
125
+ def class_method
126
+ object.is_a? Module
127
+ end
128
+
129
+ def current
130
+ stack.detect do |s|
131
+ file = s.split(":")[0]
132
+ Delfos::MethodLogging.include_file_in_logging?(file)
133
+ end
134
+ end
135
+
136
+ def object
137
+ @object ||= call_site_binding.of_caller(stack_index + STACK_OFFSET).receiver
138
+ end
139
+
140
+ def stack_index
141
+ stack.index { |c| c == current }
142
+ end
143
+
144
+ def method_details
145
+ return unless current
146
+ file, line_number, rest = current.split(":")
147
+ method_name = rest[/`.*'$/]
148
+ return unless method_name && file && line_number
149
+
150
+ method_name.delete!("`").delete!("'")
151
+
152
+ [file, line_number.to_i, method_name]
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module Delfos
3
+ module MethodLogging
4
+ module KlassDetermination
5
+ private
6
+
7
+ def klass_for(object)
8
+ if object.is_a?(Class)
9
+ object
10
+ else
11
+ object.class
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../distance/calculation"
3
+ require_relative "query_execution"
4
+ require "json"
5
+
6
+ module Delfos
7
+ module Neo4j
8
+ class DistanceUpdate
9
+ def perform
10
+ query = <<-QUERY
11
+ MATCH
12
+ (klass) - [:OWNS] -> (method),
13
+ (method) - [:CONTAINS] -> (call_site),
14
+ (call_site) - [:CALLS] -> (called),
15
+ (called_klass) - [:OWNS] -> (called)
16
+
17
+ RETURN
18
+ head(labels(klass)),
19
+ call_site, id(call_site),
20
+ method,
21
+ called, id(called),
22
+ head(labels(called_klass))
23
+ QUERY
24
+
25
+ results = Delfos::Neo4j::QueryExecution.execute(query)
26
+
27
+ update(results)
28
+ end
29
+
30
+ def determine_full_path(f)
31
+ f = Pathname.new f
32
+ return f.realpath if File.exist?(f)
33
+
34
+ Delfos.application_directories.map do |d|
35
+ path = Pathname.new(d + f.to_s.gsub(%r{[^/]*/}, ""))
36
+ path if path.exist?
37
+ end.compact.first
38
+ end
39
+
40
+ private
41
+
42
+ def update(results)
43
+ results.map do |klass, call_site, call_site_id, meth, called, called_id, called_klass|
44
+ start = determine_full_path call_site["file"]
45
+ finish = determine_full_path called["file"]
46
+
47
+ calc = Delfos::Distance::Calculation.new(start, finish)
48
+
49
+ perform_query(calc, call_site_id, called_id)
50
+ end
51
+ end
52
+
53
+ def perform_query(calc, call_site_id, called_id)
54
+ Delfos::Neo4j::QueryExecution.execute <<-QUERY
55
+ START call_site = node(#{call_site_id}),
56
+ called = node(#{called_id})
57
+
58
+ MERGE (call_site) - #{rel_for(calc)} -> (called)
59
+ QUERY
60
+ end
61
+
62
+ def rel_for(calc)
63
+ <<-REL
64
+ [:EFFERENT_COUPLING{
65
+ distance: #{calc.sum_traversals},
66
+ possible_distance: #{calc.sum_possible_traversals}
67
+ }
68
+ ]
69
+ REL
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,63 @@
1
+ require_relative "query_execution"
2
+
3
+ module Delfos
4
+ module Neo4j
5
+ class ExecutionPersistence
6
+ def self.save!(other)
7
+ new(other.call_sites, other.execution_count).save!
8
+ end
9
+
10
+ def save!
11
+ Neo4j::QueryExecution.execute query
12
+ end
13
+
14
+ private
15
+
16
+ def initialize(call_sites, execution_count)
17
+ @call_sites = call_sites
18
+
19
+ @execution_count = execution_count
20
+ end
21
+
22
+ attr_reader :call_sites, :execution_count
23
+
24
+ def query
25
+ call_sites.compact.map.with_index do |c, i|
26
+ call_site_query(c, i)
27
+ end.join("\n")
28
+ end
29
+
30
+ def call_site_query(cs, i)
31
+ <<-QUERY
32
+ #{method_query(cs, i)}
33
+
34
+ MERGE
35
+ (m#{i})
36
+
37
+ -[:CONTAINS]->
38
+
39
+ (cs#{i}:CallSite {file: "#{cs.file}", line_number: #{cs.line_number}})
40
+
41
+ #{execution_chain_query(cs, i)}
42
+ QUERY
43
+ end
44
+
45
+ def execution_chain_query(cs, i)
46
+ <<-QUERY
47
+ MERGE (e#{i}:ExecutionChain{number: #{execution_count}})
48
+
49
+ MERGE e#{i}-[:STEP{number: #{i + 1}}]-> (cs#{i})
50
+ QUERY
51
+ end
52
+
53
+ def method_query(cs, i)
54
+ <<-QUERY
55
+ MERGE (k#{i}:#{cs.klass})
56
+ - [:OWNS] ->
57
+
58
+ (m#{i} :#{cs.method_type} {name: "#{cs.method_name}", file: "#{cs.method_definition_file}", line_number: #{cs.method_definition_line}})
59
+ QUERY
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+ require "delfos"
3
+ require_relative "query_execution"
4
+
5
+ module Delfos
6
+ module Neo4j
7
+ class Informer
8
+ def debug(args, call_site, called_code)
9
+ execute_query(args, call_site, called_code)
10
+ end
11
+
12
+ def execute_query(*args)
13
+ query = query_for(*args)
14
+
15
+ QueryExecution.execute(query)
16
+ end
17
+
18
+ def query_for(args, call_site, called_code)
19
+ assign_query_variables(args, call_site, called_code)
20
+
21
+ klasses_query = query_variables.map do |klass, name|
22
+ "MERGE (#{name}:#{klass})"
23
+ end.join("\n")
24
+
25
+ <<-QUERY
26
+ #{klasses_query}
27
+
28
+ #{merge_query(call_site, called_code)}
29
+ #{args_query args}
30
+ QUERY
31
+ end
32
+
33
+ def assign_query_variables(args, call_site, called_code)
34
+ query_variables.assign(call_site.klass, "k")
35
+ query_variables.assign(called_code.klass, "k")
36
+
37
+ (args.args + args.keyword_args).uniq.each do |k|
38
+ query_variables.assign(k, "k")
39
+ end
40
+ end
41
+
42
+ def merge_query(call_site, called_code)
43
+ <<-MERGE_QUERY
44
+ #{method_definition call_site, "m1"}
45
+
46
+ MERGE (m1) - [:CONTAINS] -> (cs:CallSite{file: "#{call_site.file}", line_number: #{call_site.line_number}})
47
+
48
+ #{method_definition called_code, "m2"}
49
+
50
+ MERGE (cs) - [:CALLS] -> m2
51
+ MERGE_QUERY
52
+ end
53
+
54
+ def method_definition(code, id)
55
+ <<-METHOD
56
+ MERGE (#{query_variable(code.klass)}) - [:OWNS] -> #{method_node(code, id)}
57
+ METHOD
58
+ end
59
+
60
+ def method_node(code, id)
61
+ if code.method_definition_file.length > 0 && code.method_definition_line > 0
62
+ <<-NODE
63
+ (#{id}:#{code.method_type}{name: "#{code.method_name}", file: #{code.method_definition_file.inspect}, line_number: #{code.method_definition_line}})
64
+ NODE
65
+ else
66
+ <<-NODE
67
+ (#{id}:#{code.method_type}{name: "#{code.method_name}"})
68
+ NODE
69
+ end
70
+ end
71
+
72
+ def args_query(args)
73
+ (args.args + args.keyword_args).map do |k|
74
+ name = query_variable(k)
75
+ "MERGE (cs) - [:ARG] -> (#{name})"
76
+ end.join("\n")
77
+ end
78
+
79
+ def query_variable(k)
80
+ query_variables[k.to_s]
81
+ end
82
+
83
+ def query_variables
84
+ @query_variables ||= QueryVariables.new
85
+ end
86
+
87
+ def code_execution_query
88
+ Delfos::Patching.method_chain
89
+ end
90
+
91
+ class QueryVariables < Hash
92
+ def initialize(*args)
93
+ super(*args)
94
+ @counters = Hash.new(1)
95
+ end
96
+
97
+ def assign(klass, prefix)
98
+ klass = klass.to_s
99
+ val = self[klass]
100
+ return val if val
101
+
102
+ "#{prefix}#{@counters[prefix]}".tap do |v|
103
+ self[klass] = v
104
+ @counters[prefix] += 1
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end