turbo_test_static_analysis 0.1.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.
- checksums.yaml +7 -0
- data/.github/workflows/tests.yml +42 -0
- data/.gitignore +66 -0
- data/.rubocop.yml +44 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +10 -0
- data/Gemfile.common +3 -0
- data/Gemfile.lock +54 -0
- data/GemfileCI +9 -0
- data/GemfileCI.lock +69 -0
- data/LICENSE.txt +21 -0
- data/Makefile +44 -0
- data/README.md +27 -0
- data/Rakefile +19 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/turbo_test_static_analysis.rb +6 -0
- data/lib/turbo_test_static_analysis/active_record_schema.rb +28 -0
- data/lib/turbo_test_static_analysis/active_record_schema/constructor.rb +51 -0
- data/lib/turbo_test_static_analysis/active_record_schema/diff.rb +16 -0
- data/lib/turbo_test_static_analysis/active_record_schema/diff_compute.rb +52 -0
- data/lib/turbo_test_static_analysis/active_record_schema/map.rb +15 -0
- data/lib/turbo_test_static_analysis/active_record_schema/sexp_builder/line_column_stack.rb +41 -0
- data/lib/turbo_test_static_analysis/active_record_schema/sexp_builder/line_stack_sexp_builder.rb +40 -0
- data/lib/turbo_test_static_analysis/active_record_schema/sexp_builder/sexp_builder.rb +129 -0
- data/lib/turbo_test_static_analysis/active_record_schema/snapshot.rb +15 -0
- data/lib/turbo_test_static_analysis/constants.rb +13 -0
- data/lib/turbo_test_static_analysis/constants/node.rb +66 -0
- data/lib/turbo_test_static_analysis/constants/node_maker.rb +118 -0
- data/lib/turbo_test_static_analysis/constants/node_processor.rb +115 -0
- data/lib/turbo_test_static_analysis/constants/sexp_builder.rb +109 -0
- data/lib/turbo_test_static_analysis/constants/token_matcher.rb +43 -0
- data/lib/turbo_test_static_analysis/version.rb +7 -0
- data/turbo_test_static_analysis.gemspec +30 -0
- metadata +120 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TurboTest
|
4
|
+
module StaticAnalysis
|
5
|
+
module ActiveRecord
|
6
|
+
Snapshot = Struct.new(:extensions, :tables) do
|
7
|
+
def initialize(*)
|
8
|
+
super
|
9
|
+
self.extensions ||= {}
|
10
|
+
self.tables ||= {}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TurboTest
|
4
|
+
module StaticAnalysis
|
5
|
+
module Constants
|
6
|
+
class Node
|
7
|
+
attr_reader :name, :start_pos, :parent, :children, :singleton
|
8
|
+
attr_accessor :end_pos, :top_level, :definition
|
9
|
+
|
10
|
+
def initialize(name:, start_pos:, end_pos:, singleton: false, definition: false)
|
11
|
+
@name = name
|
12
|
+
@children = []
|
13
|
+
@start_pos = start_pos
|
14
|
+
@end_pos = end_pos
|
15
|
+
@parent = nil
|
16
|
+
@singleton = singleton
|
17
|
+
@definition = definition
|
18
|
+
@top_level = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_child(child)
|
22
|
+
return if child.start_pos == start_pos && child.name == name
|
23
|
+
return unless contains?(child)
|
24
|
+
|
25
|
+
@children << child
|
26
|
+
child.instance_variable_set(:@parent, self)
|
27
|
+
child.instance_variable_set(:@definition, definition)
|
28
|
+
child
|
29
|
+
end
|
30
|
+
|
31
|
+
def contains?(node)
|
32
|
+
((@start_pos <=> node.start_pos) == -1 && (@end_pos <=> node.end_pos) == 1) ||
|
33
|
+
((@start_pos <=> node.start_pos).zero? &&
|
34
|
+
((@end_pos <=> node.end_pos).zero? || (@end_pos <=> node.end_pos) == 1))
|
35
|
+
end
|
36
|
+
|
37
|
+
def full_name
|
38
|
+
return @name unless @parent
|
39
|
+
|
40
|
+
[@parent.full_name, @name].compact.join("::")
|
41
|
+
end
|
42
|
+
|
43
|
+
def root
|
44
|
+
return self if parent.nil?
|
45
|
+
|
46
|
+
parent.root
|
47
|
+
end
|
48
|
+
|
49
|
+
def parent_is_named_singleton?
|
50
|
+
return false unless parent
|
51
|
+
|
52
|
+
parent.singleton && parent.name != "singleton_class"
|
53
|
+
end
|
54
|
+
|
55
|
+
def named_singleton_with_parent?
|
56
|
+
return false unless parent
|
57
|
+
|
58
|
+
singleton && name != "singleton_class"
|
59
|
+
end
|
60
|
+
|
61
|
+
class << self
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TurboTest
|
4
|
+
module StaticAnalysis
|
5
|
+
module Constants
|
6
|
+
module NodeMaker
|
7
|
+
private
|
8
|
+
|
9
|
+
def create_top_singleton_class_node(token)
|
10
|
+
parent = new_node_from_token(token[1], [lineno, column])
|
11
|
+
@class_nodes.push parent
|
12
|
+
node = Node.new(
|
13
|
+
name: "singleton_class",
|
14
|
+
start_pos: node_start_pos(token),
|
15
|
+
end_pos: [lineno, column]
|
16
|
+
)
|
17
|
+
parent.add_child node
|
18
|
+
node
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_var_ref_singleton_class_node(token)
|
22
|
+
node = new_node_from_token(token[1], [lineno, column], singleton_class: true)
|
23
|
+
@class_nodes.push node
|
24
|
+
child = Node.new(
|
25
|
+
name: "singleton_class",
|
26
|
+
start_pos: node_start_pos(token),
|
27
|
+
end_pos: [lineno, column]
|
28
|
+
)
|
29
|
+
node.add_child child
|
30
|
+
node
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_singleton_class_node(token)
|
34
|
+
new_node_from_token(token[1], [lineno, column], singleton_class: true)
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_singleton_method_definition_with_self_node(token)
|
38
|
+
node = Node.new(
|
39
|
+
name: "self",
|
40
|
+
start_pos: node_start_pos(token),
|
41
|
+
end_pos: [lineno, column], definition: true
|
42
|
+
)
|
43
|
+
@class_nodes.push node
|
44
|
+
@all_nodes.unshift(node)
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_singleton_method_definition_with_constant_node(token)
|
48
|
+
Node.new(
|
49
|
+
name: token[1][1],
|
50
|
+
start_pos: node_start_pos(token),
|
51
|
+
end_pos: [lineno, column],
|
52
|
+
definition: true
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_block_node(token)
|
57
|
+
return unless (start_pos = token[1][3]&.last)
|
58
|
+
|
59
|
+
node = Node.new(
|
60
|
+
name: "block",
|
61
|
+
start_pos: start_pos,
|
62
|
+
end_pos: [lineno, column]
|
63
|
+
)
|
64
|
+
@block_nodes << node
|
65
|
+
end
|
66
|
+
|
67
|
+
def find_or_new_node_for_const_class_or_module_eval(token)
|
68
|
+
start_pos = token[1][1][1]
|
69
|
+
end_pos = token[1][1][2]
|
70
|
+
find_and_update_node([nil, [nil, start_pos, end_pos]]).tap do |node|
|
71
|
+
break const_class_or_module_eval_node(start_pos, end_pos) if node.nil?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def const_class_or_module_eval_node(start_pos, end_pos)
|
76
|
+
Node.new(
|
77
|
+
name: start_pos,
|
78
|
+
start_pos: end_pos,
|
79
|
+
end_pos: [lineno, column],
|
80
|
+
definition: true
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
def ident_class_or_module_eval_node(token)
|
85
|
+
Node.new(
|
86
|
+
name: token[1][1][1],
|
87
|
+
start_pos: token[1][1][2],
|
88
|
+
end_pos: [lineno, column],
|
89
|
+
definition: true
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
def new_node_from_token(token, end_pos = [0, 0], singleton_class: false)
|
94
|
+
name = node_name(token)
|
95
|
+
start_pos = node_start_pos(token)
|
96
|
+
Node.new(name: name, start_pos: start_pos, end_pos: end_pos, singleton: singleton_class)
|
97
|
+
end
|
98
|
+
|
99
|
+
def node_name(token)
|
100
|
+
case token[0]
|
101
|
+
when :@kw
|
102
|
+
"singleton_class"
|
103
|
+
when :@const
|
104
|
+
token[1]
|
105
|
+
else
|
106
|
+
"unknown_ref"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def node_start_pos(token)
|
111
|
+
start_pos = token.last
|
112
|
+
start_pos = start_pos.last while start_pos.last.is_a?(Array)
|
113
|
+
start_pos
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TurboTest
|
4
|
+
module StaticAnalysis
|
5
|
+
module Constants
|
6
|
+
module NodeProcessor
|
7
|
+
private
|
8
|
+
|
9
|
+
def node_key(token)
|
10
|
+
"#{token[1]}-#{token[2].join('-')}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def process_module(node)
|
14
|
+
@var_refs.reject! do |var_ref|
|
15
|
+
(var_ref.name == node.name && var_ref.start_pos == node.start_pos) ||
|
16
|
+
node.contains?(var_ref).tap do |condition|
|
17
|
+
@referenced_constants[var_ref.name] = true if condition
|
18
|
+
end
|
19
|
+
end
|
20
|
+
@class_nodes.push node
|
21
|
+
assign_children(node)
|
22
|
+
end
|
23
|
+
|
24
|
+
def process_class_nodes
|
25
|
+
@class_nodes.reject! do |class_node|
|
26
|
+
next process_definition_class_node(class_node) if class_node.root.definition
|
27
|
+
|
28
|
+
@referenced_constants[class_node.name] = true if class_node.named_singleton_with_parent?
|
29
|
+
class_node.parent_is_named_singleton? || class_node.named_singleton_with_parent?
|
30
|
+
end
|
31
|
+
|
32
|
+
@defined_classes = class_nodes_names_without_singletons_and_unknowns
|
33
|
+
end
|
34
|
+
|
35
|
+
def process_definition_class_node(node)
|
36
|
+
container_node = @class_nodes.find do |class_node|
|
37
|
+
class_node != node.root && class_node.contains?(node.root)
|
38
|
+
end
|
39
|
+
return false if container_node.nil?
|
40
|
+
|
41
|
+
@referenced_constants[node.full_name] = true if node.name != "self"
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def process_var_refs
|
46
|
+
@var_refs.each do |var_ref|
|
47
|
+
if @block_nodes.find { |node| node.contains?(var_ref) }
|
48
|
+
@referenced_constants[var_ref.name] = true
|
49
|
+
else
|
50
|
+
@referenced_top_constants[var_ref.name] = true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def class_nodes_names_without_singletons_and_unknowns
|
56
|
+
@class_nodes.map(&:full_name).reject do |full_name|
|
57
|
+
full_name.end_with?("singleton_class") || full_name.include?("unknown_ref")
|
58
|
+
end.uniq
|
59
|
+
end
|
60
|
+
|
61
|
+
def reject_unknown_constants
|
62
|
+
@referenced_top_constants.reject! do |key, _value|
|
63
|
+
key.include?("unknown_ref")
|
64
|
+
end
|
65
|
+
|
66
|
+
@referenced_constants.reject! do |key, _value|
|
67
|
+
key.include?("unknown_ref")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def assign_children(node)
|
72
|
+
if @referenced_top_constants.delete(node.name)
|
73
|
+
@all_nodes.unshift(node)
|
74
|
+
add_children(node)
|
75
|
+
else
|
76
|
+
add_children(node)
|
77
|
+
@all_nodes.unshift(node)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_children(node)
|
82
|
+
@all_nodes.reject! do |children_candidate|
|
83
|
+
node.contains?(children_candidate).tap do |child|
|
84
|
+
node.add_child(children_candidate) if child && children_candidate.definition == false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_const_ref(token, top_level: false)
|
90
|
+
node = new_node_from_token(token)
|
91
|
+
node.top_level = top_level
|
92
|
+
@const_refs[node_key(token)] = node
|
93
|
+
end
|
94
|
+
|
95
|
+
def add_referenced_top_constant(name)
|
96
|
+
@referenced_top_constants[name] = true
|
97
|
+
end
|
98
|
+
|
99
|
+
def find_and_update_node(token)
|
100
|
+
@const_refs.delete(node_key(token[1])).tap do |node|
|
101
|
+
node.end_pos = [lineno, column] if node
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def reject_children_in_class_nodes(node)
|
106
|
+
@class_nodes.reject! do |class_node|
|
107
|
+
node.contains?(class_node).tap do |child|
|
108
|
+
@referenced_constants[class_node.full_name] = true if child
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ripper"
|
4
|
+
require_relative "node"
|
5
|
+
require_relative "token_matcher"
|
6
|
+
require_relative "node_maker"
|
7
|
+
require_relative "node_processor"
|
8
|
+
|
9
|
+
module TurboTest
|
10
|
+
module StaticAnalysis
|
11
|
+
module Constants
|
12
|
+
class SexpBuilder < Ripper::SexpBuilder
|
13
|
+
attr_reader :defined_classes, :referenced_top_constants, :referenced_constants
|
14
|
+
|
15
|
+
include TokenMatcher
|
16
|
+
include NodeMaker
|
17
|
+
include NodeProcessor
|
18
|
+
|
19
|
+
def initialize(path, filename = "-", lineno = 1)
|
20
|
+
@const_refs = {}
|
21
|
+
@referenced_top_constants = {}
|
22
|
+
@referenced_constants = {}
|
23
|
+
@class_nodes = []
|
24
|
+
@defined_classes = []
|
25
|
+
@all_nodes = []
|
26
|
+
@var_refs = []
|
27
|
+
@block_nodes = []
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_sclass(token_one, token_two)
|
32
|
+
node = if top_const_ref?(token_one)
|
33
|
+
create_top_singleton_class_node(token_one)
|
34
|
+
elsif var_ref?(token_one)
|
35
|
+
create_var_ref_singleton_class_node(token_one)
|
36
|
+
else
|
37
|
+
create_singleton_class_node(token_one)
|
38
|
+
end
|
39
|
+
process_module(node)
|
40
|
+
@all_nodes.shift if top_const_ref?(token_one)
|
41
|
+
super
|
42
|
+
end
|
43
|
+
|
44
|
+
def on_module(token_one, token_two, token_three = nil)
|
45
|
+
process_module find_and_update_node(token_one)
|
46
|
+
super
|
47
|
+
end
|
48
|
+
alias on_class on_module
|
49
|
+
|
50
|
+
def on_defs(token_one, token_two, token_three, token_four, token_five)
|
51
|
+
if singleton_method_definition_with_self?(token_one)
|
52
|
+
create_singleton_method_definition_with_self_node(token_one)
|
53
|
+
elsif singleton_method_definition_with_constant?(token_one)
|
54
|
+
node = create_singleton_method_definition_with_constant_node(token_one)
|
55
|
+
process_module(node)
|
56
|
+
end
|
57
|
+
super
|
58
|
+
end
|
59
|
+
|
60
|
+
def on_top_const_ref(token)
|
61
|
+
add_const_ref token, top_level: true
|
62
|
+
add_referenced_top_constant token[1]
|
63
|
+
super
|
64
|
+
end
|
65
|
+
|
66
|
+
def on_const_ref(token)
|
67
|
+
add_const_ref token
|
68
|
+
super
|
69
|
+
end
|
70
|
+
|
71
|
+
def on_var_ref(token)
|
72
|
+
@var_refs << new_node_from_token(token, [lineno, column]) if token[0] == :@const
|
73
|
+
super
|
74
|
+
end
|
75
|
+
alias on_var_field on_var_ref
|
76
|
+
|
77
|
+
def on_top_const_field(token)
|
78
|
+
add_referenced_top_constant token[1]
|
79
|
+
super
|
80
|
+
end
|
81
|
+
|
82
|
+
def on_def(token_one, token_two, token_three)
|
83
|
+
super
|
84
|
+
end
|
85
|
+
|
86
|
+
def on_method_add_block(token_one, token_two)
|
87
|
+
add_block_node(token_one)
|
88
|
+
if const_class_or_module_eval?(token_one)
|
89
|
+
node = find_or_new_node_for_const_class_or_module_eval(token_one)
|
90
|
+
process_module node
|
91
|
+
elsif ident_class_or_module_eval?(token_one)
|
92
|
+
node = ident_class_or_module_eval_node(token_one)
|
93
|
+
reject_children_in_class_nodes(node)
|
94
|
+
end
|
95
|
+
super
|
96
|
+
end
|
97
|
+
|
98
|
+
def on_program(token_one)
|
99
|
+
process_class_nodes
|
100
|
+
process_var_refs
|
101
|
+
reject_unknown_constants
|
102
|
+
super
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Check https://fili.pp.ru/leaky-constants.html
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TurboTest
|
4
|
+
module StaticAnalysis
|
5
|
+
module Constants
|
6
|
+
module TokenMatcher
|
7
|
+
private
|
8
|
+
|
9
|
+
def top_const_ref?(token)
|
10
|
+
token[0] == :top_const_ref
|
11
|
+
end
|
12
|
+
|
13
|
+
def var_ref?(token)
|
14
|
+
token[0] == :var_ref
|
15
|
+
end
|
16
|
+
|
17
|
+
def const_class_or_module_eval?(token)
|
18
|
+
token.last[0] == :@ident &&
|
19
|
+
%w[class_eval module_eval].include?(token.last[1]) &&
|
20
|
+
token[1][1][0] == :@const
|
21
|
+
end
|
22
|
+
|
23
|
+
def ident_class_or_module_eval?(token)
|
24
|
+
token.last[0] == :@ident &&
|
25
|
+
%w[class_eval module_eval].include?(token.last[1]) &&
|
26
|
+
token[1][1][0] == :@ident
|
27
|
+
end
|
28
|
+
|
29
|
+
def singleton_method_definition_with_self?(token)
|
30
|
+
token[1] &&
|
31
|
+
token[1][0] == :@kw &&
|
32
|
+
token[1][1] == "self"
|
33
|
+
end
|
34
|
+
|
35
|
+
def singleton_method_definition_with_constant?(token)
|
36
|
+
token[0] == :var_ref &&
|
37
|
+
token[1] &&
|
38
|
+
token[1][0] == :@const
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|