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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/tests.yml +42 -0
  3. data/.gitignore +66 -0
  4. data/.rubocop.yml +44 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +10 -0
  7. data/Gemfile.common +3 -0
  8. data/Gemfile.lock +54 -0
  9. data/GemfileCI +9 -0
  10. data/GemfileCI.lock +69 -0
  11. data/LICENSE.txt +21 -0
  12. data/Makefile +44 -0
  13. data/README.md +27 -0
  14. data/Rakefile +19 -0
  15. data/bin/console +14 -0
  16. data/bin/setup +8 -0
  17. data/lib/turbo_test_static_analysis.rb +6 -0
  18. data/lib/turbo_test_static_analysis/active_record_schema.rb +28 -0
  19. data/lib/turbo_test_static_analysis/active_record_schema/constructor.rb +51 -0
  20. data/lib/turbo_test_static_analysis/active_record_schema/diff.rb +16 -0
  21. data/lib/turbo_test_static_analysis/active_record_schema/diff_compute.rb +52 -0
  22. data/lib/turbo_test_static_analysis/active_record_schema/map.rb +15 -0
  23. data/lib/turbo_test_static_analysis/active_record_schema/sexp_builder/line_column_stack.rb +41 -0
  24. data/lib/turbo_test_static_analysis/active_record_schema/sexp_builder/line_stack_sexp_builder.rb +40 -0
  25. data/lib/turbo_test_static_analysis/active_record_schema/sexp_builder/sexp_builder.rb +129 -0
  26. data/lib/turbo_test_static_analysis/active_record_schema/snapshot.rb +15 -0
  27. data/lib/turbo_test_static_analysis/constants.rb +13 -0
  28. data/lib/turbo_test_static_analysis/constants/node.rb +66 -0
  29. data/lib/turbo_test_static_analysis/constants/node_maker.rb +118 -0
  30. data/lib/turbo_test_static_analysis/constants/node_processor.rb +115 -0
  31. data/lib/turbo_test_static_analysis/constants/sexp_builder.rb +109 -0
  32. data/lib/turbo_test_static_analysis/constants/token_matcher.rb +43 -0
  33. data/lib/turbo_test_static_analysis/version.rb +7 -0
  34. data/turbo_test_static_analysis.gemspec +30 -0
  35. 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "constants/sexp_builder"
4
+
5
+ module TurboTest
6
+ module StaticAnalysis
7
+ module Constants
8
+ def self.parse(source)
9
+ SexpBuilder.new(source).tap(&:parse)
10
+ end
11
+ end
12
+ end
13
+ 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