delfos 0.0.1.pre.beta

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.
@@ -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