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