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,27 @@
1
+ ![Tests](https://github.com/dunkelbraun/turbo_test_static_analysis/workflows/Tests/badge.svg?branch=main)
2
+ [![Maintainability](https://api.codeclimate.com/v1/badges/b80b14e60467094e6c92/maintainability)](https://codeclimate.com/github/dunkelbraun/turbo_test_static_analysis/maintainability)
3
+ [![Coverage Status](https://coveralls.io/repos/github/dunkelbraun/turbo_test_static_analysis/badge.svg?branch=main)](https://coveralls.io/github/dunkelbraun/turbo_test_static_analysis?branch=main)
4
+
5
+ # TurboTestStaticAnalysis
6
+
7
+
8
+ Pending description.
9
+
10
+ ## Development
11
+
12
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
13
+
14
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
15
+
16
+ ## Contributing
17
+
18
+ Bug reports and pull requests are welcome on GitHub at https://github.com/dunkelbraun/turbo_test_static_analysis. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/dunkelbraun/turbo_test_static_analysis/blob/master/CODE_OF_CONDUCT.md).
19
+
20
+
21
+ ## License
22
+
23
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
24
+
25
+ ## Code of Conduct
26
+
27
+ Everyone interacting in the TurboTestStaticAnalysis project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/dunkelbraun/turbo_test_static_analysis/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ ENV["TESTOPTS"] = "#{ENV['TESTOPTS']} --verbose"
7
+
8
+ Rake::TestTask.new(:test) do |t|
9
+ t.libs << "test"
10
+ t.libs << "lib"
11
+ t.test_files = FileList["test/**/*_test.rb"]
12
+ end
13
+
14
+ task default: :test
15
+
16
+ if ENV["CI"]
17
+ require "coveralls/rake/task"
18
+ Coveralls::RakeTask.new
19
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "turbo_test_events"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "turbo_test_static_analysis/version"
4
+
5
+ require_relative "turbo_test_static_analysis/active_record_schema"
6
+ require_relative "turbo_test_static_analysis/constants"
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "active_record_schema/diff"
4
+ require_relative "active_record_schema/map"
5
+ require_relative "active_record_schema/snapshot"
6
+ require_relative "active_record_schema/diff_compute"
7
+ require_relative "active_record_schema/constructor"
8
+ require_relative "active_record_schema/sexp_builder/line_column_stack"
9
+ require_relative "active_record_schema/sexp_builder/sexp_builder"
10
+ require_relative "active_record_schema/sexp_builder/line_stack_sexp_builder"
11
+
12
+ module TurboTest
13
+ module StaticAnalysis
14
+ module ActiveRecord
15
+ class << self
16
+ def schema_changes(new_schema, old_schema)
17
+ current_snapshot = SexpBuilder.snapshot_from_source(new_schema)
18
+ previous_snapshot = SexpBuilder.snapshot_from_source(old_schema)
19
+ changes = DiffCompute.new(current_snapshot, previous_snapshot).calc
20
+ {
21
+ extensions: changes[:extensions].to_h,
22
+ tables: changes[:tables].to_h
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboTest
4
+ module StaticAnalysis
5
+ module ActiveRecord
6
+ class Constructor
7
+ def initialize
8
+ @schema = Snapshot.new
9
+ @fingerprints = {}
10
+ end
11
+
12
+ def enable_extension(name, content)
13
+ extensions[name] = Digest::MD5.hexdigest(content)
14
+ end
15
+
16
+ def fingerprint(table_name, content)
17
+ @fingerprints[table_name] ||= []
18
+ @fingerprints[table_name] << content
19
+ end
20
+
21
+ alias add_index fingerprint
22
+ alias add_foreign_key fingerprint
23
+ alias create_table fingerprint
24
+ alias create_trigger fingerprint
25
+
26
+ remove_method :fingerprint
27
+
28
+ def snapshot
29
+ add_fingerprints
30
+ @schema
31
+ end
32
+
33
+ private
34
+
35
+ def extensions
36
+ @schema[:extensions]
37
+ end
38
+
39
+ def tables
40
+ @schema[:tables]
41
+ end
42
+
43
+ def add_fingerprints
44
+ @fingerprints.each_pair.each_with_object(tables) do |(key, val), memo|
45
+ memo[key] = Digest::MD5.hexdigest(val.sort.join)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboTest
4
+ module StaticAnalysis
5
+ module ActiveRecord
6
+ Diff = Struct.new(:changed, :added, :deleted) do
7
+ def initialize(*)
8
+ super
9
+ self.changed ||= []
10
+ self.added ||= []
11
+ self.deleted ||= []
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboTest
4
+ module StaticAnalysis
5
+ module ActiveRecord
6
+ class DiffCompute
7
+ def initialize(new_snapshot, old_snapshot)
8
+ @data = { new: new_snapshot, old: old_snapshot }
9
+ end
10
+
11
+ def calc
12
+ empty_diff.tap do |diff|
13
+ compute(:extensions, diff[:extensions])
14
+ compute(:tables, diff[:tables])
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def empty_diff
21
+ {
22
+ extensions: Diff.new,
23
+ tables: Diff.new
24
+ }
25
+ end
26
+
27
+ def compute(type, diff)
28
+ compute_added_changed type, diff
29
+ compute_deleted type, diff
30
+ end
31
+
32
+ def compute_added_changed(type, diff)
33
+ iterator = @data.dig(:new, type).each_pair
34
+ iterator.each_with_object(diff) do |(name, fgpt), memo|
35
+ old_data = @data.dig(:old, type)
36
+ unless old_data[name]
37
+ memo.added << name
38
+ next memo
39
+ end
40
+ memo.changed << name if fgpt != old_data[name]
41
+ end
42
+ end
43
+
44
+ def compute_deleted(type, diff)
45
+ @data.dig(:old, type).each_key.each_with_object(diff) do |name, memo|
46
+ memo.deleted << name unless @data.dig(:new, type)[name]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboTest
4
+ module StaticAnalysis
5
+ module ActiveRecord
6
+ Map = 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,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module TurboTest
6
+ module StaticAnalysis
7
+ module ActiveRecord
8
+ class LineColumnStack
9
+ extend Forwardable
10
+
11
+ def_delegators :@_stack, :pop, :last, :any?
12
+
13
+ def initialize
14
+ @_stack = []
15
+ end
16
+
17
+ def push(line, column)
18
+ line_column = [line, column]
19
+ @_stack.push line_column unless @_stack.last == line_column
20
+ end
21
+
22
+ def remove_greater_than(line_column)
23
+ first_pop = nil
24
+ while last && greater(last, line_column)
25
+ line = pop
26
+ first_pop ||= line
27
+ end
28
+ first_pop
29
+ end
30
+
31
+ private
32
+
33
+ def greater(line_column_a, line_column_b)
34
+ line_column_a[0] > line_column_b[0] ||
35
+ (line_column_a[0] == line_column_b[0] &&
36
+ line_column_a[1] >= line_column_b[1])
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ripper"
4
+
5
+ module TurboTest
6
+ module StaticAnalysis
7
+ module ActiveRecord
8
+ class LineStackSexpBuilder < Ripper::SexpBuilder
9
+ EVENTS_TO_REJECT = [:magic_comment].freeze
10
+
11
+ ARITIES = ["()", "(a)", "(a,b)", "(a,b,c)", "(a,b,c,d)",
12
+ "(a,b,c,d,e)", "(a,b,c,d,e,f)", "(a,b,c,d,e,f,g)",
13
+ "(a,b,c,d,e,f,g,h)"].freeze
14
+
15
+ Ripper::PARSER_EVENT_TABLE.each do |method, arity|
16
+ next if EVENTS_TO_REJECT.include?(method)
17
+
18
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
19
+ def on_#{method}#{ARITIES[arity]}
20
+ super.tap do |result|
21
+ stack_line
22
+ end
23
+ end
24
+ RUBY
25
+ end
26
+
27
+ attr_reader :stack
28
+
29
+ def initialize(path, filename = "-", lineno = 1)
30
+ super
31
+ @stack = LineColumnStack.new
32
+ end
33
+
34
+ def stack_line
35
+ @stack.push lineno, column
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "sorcerer"
5
+ require_relative "line_stack_sexp_builder"
6
+
7
+ module TurboTest
8
+ module StaticAnalysis
9
+ module ActiveRecord
10
+ class SexpBuilder < LineStackSexpBuilder
11
+ COMMANDS = %w[add_index add_foreign_key enable_extension].freeze
12
+ TABLE_REGEXP = /create_table\s"(\w+)"/.freeze
13
+ TRIGGER_REGEXP = /create_trigger\("(\w+)".+on\("(\w+)"/.freeze
14
+ COMMAND_REGEXP = /\b(\w+)\b/.freeze
15
+
16
+ attr_accessor :schema
17
+
18
+ def initialize(path, filename = "-", lineno = 1)
19
+ super
20
+ @schema_file = path.split("\n")
21
+ @schema = Constructor.new
22
+ end
23
+
24
+ def self.snapshot_from_file(path)
25
+ builder = new(File.read(path), path)
26
+ builder.parse
27
+ builder.snapshot
28
+ end
29
+
30
+ def self.snapshot_from_source(source)
31
+ builder = new(source)
32
+ builder.parse
33
+ builder.snapshot
34
+ end
35
+
36
+ def snapshot
37
+ @schema.snapshot
38
+ end
39
+
40
+ def on_command(token_one, token_two)
41
+ super.tap do |_result|
42
+ name = token_one[1]
43
+ next unless COMMANDS.include? name
44
+
45
+ last_line = @stack.remove_greater_than([lineno, column])
46
+ content = method_content(lineno, last_line[0])
47
+ @schema.send(name.to_sym, table_name(:command, token_two), content)
48
+ end
49
+ end
50
+
51
+ def on_method_add_block(token_one, token_two)
52
+ super.tap do |_result|
53
+ next unless (type = type_for_token(token_one[0]))
54
+
55
+ first_line, last_line = send("#{type}_lines", token_one)
56
+ handle_create(type, token_one, first_line, last_line)
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def type_for_token(token)
63
+ case token
64
+ when :command
65
+ :table
66
+ when :method_add_arg
67
+ :trigger
68
+ end
69
+ end
70
+
71
+ def table_lines(token)
72
+ return unless token[1][1] == "create_table"
73
+
74
+ last_line = @stack.remove_greater_than(token[1][2])
75
+ [token[1][2][0], last_line[0]]
76
+ end
77
+
78
+ def trigger_lines(token)
79
+ fcall = extract_fcall(token[1])
80
+ return unless fcall[0] == "create_trigger"
81
+
82
+ first_line = fcall[1]
83
+ last_line = @stack.remove_greater_than(first_line)
84
+ [first_line[0], last_line[0]]
85
+ end
86
+
87
+ def handle_create(type, token, line_start, line_end)
88
+ return unless line_start && line_end
89
+
90
+ table = table_name(type, token)
91
+ content = method_content(line_start, line_end)
92
+ @schema.send("create_#{type}".to_sym, table, content)
93
+ end
94
+
95
+ def table_name(type, token)
96
+ regexp = self.class.const_get "#{type.upcase}_REGEXP"
97
+ match = Sorcerer.source(token).match(regexp)
98
+ case type
99
+ when :trigger
100
+ match[1..2][1]
101
+ else
102
+ match[1]
103
+ end
104
+ end
105
+
106
+ def method_content(start_line, end_line)
107
+ lines_str(start_line, end_line)
108
+ end
109
+
110
+ def extract_fcall(token)
111
+ case token[0]
112
+ when :fcall
113
+ [token[1][1], token[1][2]]
114
+ when :call
115
+ extract_fcall(token[1][1])
116
+ else
117
+ []
118
+ end
119
+ end
120
+
121
+ def lines_str(from, to)
122
+ from -= 1
123
+ to -= 1
124
+ @schema_file[from..to].join("")
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end