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,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