turbo_test_static_analysis 0.1.0

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