the_schema_is 0.0.1 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24d4d7e0d7d99a14a9d924af7efbbfbe4ee0fb9eef3b89537fb11378b4e5495e
4
- data.tar.gz: e41ee7faf7ee3620dc655c3ec3468df3cd6d89a5eb93a6b130ceabad7554b99f
3
+ metadata.gz: 8735d476459a14d53240641bec87d03d7dbcd7bd5d3c8508135bde7739a04e80
4
+ data.tar.gz: 5cb9c6f68830db4c556aabe538611f448c581fd2c94efb15bf6eda0c0ce04511
5
5
  SHA512:
6
- metadata.gz: ce06ad394d6cec9c357af68eb370ec667707a7ea2c5df57c412a3b0e86d3122cc3865035f9464729b1548477aa23dc9350b5c7700f42d668cd72a90c487fdeeb
7
- data.tar.gz: 745a19300e80a00177abad59d4c702a304b1eb1c6f5b94c2ed3eb3b63519c5438b4cdb762c752ac9b71ef8e8d665fb3180b729ab8fa72262131e11a287d84465
6
+ metadata.gz: c4ad68960b63c360ec9becc3f733e157a6547d55e318b7de37ac9d9ff3f872c436a07729759eeba126613fb812a291f7b76a1b782fa83a82c85977e91f066493
7
+ data.tar.gz: c37d853e5dfbb790264e146cd07849e959880b46559a92e5b578036b0cb906de4a4db74d4d744c4b8013763782c67f5bb0592f8bd2bb22ed702348ab42fdb1b3
data/Changelog.md ADDED
@@ -0,0 +1,16 @@
1
+ # the-schema-is changes
2
+
3
+ ## 2021-11-04 - 0.0.5
4
+
5
+ * Support `enum` column type;
6
+ * Add `RemoveDefinitions` config key for leaner `the_schema_is` definitions (avoiding huge index descriptions in models).
7
+
8
+ ## 2021-09-15 - 0.0.4
9
+
10
+ * Get rid of [Fast](https://jonatas.github.io/fast/) dependency. It is cool, but we switched to use Rubocop's own `NodePattern` to lessen the dependency burden (Fast was depending on [astrolabe](https://github.com/yujinakayama/astrolabe) which wasn't updated in 6 years, locking parser dependency to old version and making Fast incompatible with newer Rubocop);
11
+ * Introduce mandatory table name in the `the_schema_is` DSL (and the `WrongTableName` cop to check it);
12
+ * Internally, change cop classes to comply to newer (> 1.0) Rubocop API.
13
+
14
+ ## 2020-05-07 - 0.0.3
15
+
16
+ First really working release.
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # The schema is ...
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/the_schema_is.svg)](http://badge.fury.io/rb/the_schema_is)
4
- [![Build Status](https://travis-ci.org/zverok/the_schema_is.svg?branch=master)](https://travis-ci.org/zverok/the_schema_is)
4
+ ![Build Status](https://github.com/zverok/the_schema_is/workflows/CI/badge.svg?branch=master)
5
5
 
6
- `the_schema_is` is a model schema annotation DSL in ActiveSupport.
6
+ `the_schema_is` is a model schema annotation DSL for ActiveSupport models, enforced by Rubocop. [Jump to detailed description →](#so-how-your-approach-is-different).
7
7
 
8
8
  ### Why annotate?
9
9
 
@@ -34,10 +34,10 @@ Annotate gem provides a very powerful and configurable CLI/rake task which allow
34
34
 
35
35
  It kinda achieves the goal, but in our experience, it also brings some problems:
36
36
 
37
- * annotation regeneration is disruptive, just replacing the whole block with a new one, which produces a lot of "false changes" (e.g. one field with a bit longer name was added → spacing of all fields were changed);
38
- * if on different developer's machines column order or defaults is different on dev. DB, annotate also decides to rewrite all the annotations, sometimes adding tens files "changed" to PR;
39
- * regeneration makes it hard to use schema annotation for commenting/explaining some fields: because regeneration will lose them, and because comments-between-comments will be hard to distinguish;
40
- * the syntax of annotations is kinda ad-hoc, which makes it harder to add them by hand, so regeneration becomes the _only_ way to add them.
37
+ * annotation **regeneration is disruptive**, just replacing the whole block with a new one, which produces a lot of "false changes" (e.g. one field with a bit longer name was added → spacing of all fields were changed);
38
+ * if on different developer's machines **column order or defaults is different** in dev. DB, annotate also decides to rewrite all the annotations, sometimes adding tens files "changed" to PR;
39
+ * regeneration makes it **hard to use schema annotation for commenting/explaining** some fields: because regeneration will lose them, and because comments-between-comments will be hard to distinguish;
40
+ * the **syntax of annotations is kinda ad-hoc**, which makes it harder to add them by hand, so regeneration becomes the _only_ way to add them.
41
41
 
42
42
  ### So, how your approach is different?..
43
43
 
@@ -88,7 +88,7 @@ Using existing Rubocop's infrastructure brings several great benefits:
88
88
 
89
89
  * you can include checking "if all annotations are actual" in your CI/pre-commit hooks easily;
90
90
  * you can preview problems found, and then fix them automatically (with `rubocop -a`) or manually however you see suitable;
91
- * the changes made with auto-fix is very local (just add/remove/change line related to relevant column), so your custom structuring, like separating groups of related columns with empty lines and comments, will be preserved;
91
+ * the changes made with auto-correct is very local (just add/remove/change line related to relevant column), so your custom structuring, like separating groups of related columns with empty lines and comments, will be preserved;
92
92
  * rubocop is easy to run on some sub-folder or one file, or files corresponding to some pattern; or exclude permanently for some file or folder.
93
93
 
94
94
  ### But what the block itself does?
@@ -110,11 +110,12 @@ The block isn't even evaluated at all (so potentially can contain any code, and
110
110
  - the_schema_is/cops
111
111
  ```
112
112
  3. Run `rubocop` and see what it now says about your models.
113
- 4. Now you can add schema definitions manually, or allow `rubocop --auto-fix` (or `-a`) to do its job! NB: you can always use `rubocop --auto-fix --only TheSchemaIs` to auto-fix ONLY this schema thing
113
+ 4. Now you can add schema definitions manually, or allow `rubocop --auto-correct` (or `-a`) to do its job! NB: you can always use `rubocop --auto-correct --only TheSchemaIs` to auto-correct ONLY this schema thing
114
114
 
115
115
  To make reporting cleaner, all cops are split into:
116
116
 
117
117
  * `Presence`
118
+ * `WrongTableName`
118
119
  * `MissingColumn`
119
120
  * `UnknownColumn`
120
121
  * `WrongColumnDefinition`
@@ -133,8 +134,9 @@ TheSchemaIs:
133
134
  Currently available settings are:
134
135
 
135
136
  * `TablePrefix` to help `the_schema_is` deduce table name from class name;
136
- * `Schema` to set path to schema (by default `db/schema.rb`)
137
- * `BaseClass` to help `the_schema_is` guess what is a model class (by default `ApplicationRecord` and `ActiveRecord::Base`).
137
+ * `Schema` to set path to schema (by default `db/schema.rb`);
138
+ * `BaseClass` to help `the_schema_is` guess what is a model class (by default `ApplicationRecord` and `ActiveRecord::Base`);
139
+ * `RemoveDefinitions`: list of definition keys to remove (for example, `[index, foreign_key, limit]`) when copying definitions into models; this might be desirable for leaner `the_schema_is` statements, displaying only field types/names.
138
140
 
139
141
  So, if you have your custom-named base class, you should do:
140
142
 
@@ -143,12 +145,12 @@ TheSchemaIs:
143
145
  BaseClass: OurOwnBase
144
146
  ```
145
147
 
146
- Note that Rubocop allows per-folder settings out of the box, which allows TheSchemaIs even at the tender version of 0.0.1, support complicated configurations with multiple databases and engines.
148
+ Note that Rubocop allows per-folder settings out of the box, which allows TheSchemaIs to support complicated configurations with multiple databases and engines.
147
149
 
148
150
  For example, consider your models are split into `app/models/users/` and `app/models/products` which are stored in the different databases, then you probably have different schemas and base classes for them. So, to configure it properly, you might want to do in `app/models/users/.rubocop.yml`:
149
151
 
150
152
  ```yaml
151
- # Don't forget this for all other cops to not be ignored
153
+ # Don't forget this for all other cop settings to not be ignored
152
154
  inherit_from: ../../../.rubocop.yml
153
155
 
154
156
  TheSchemaIs:
@@ -158,13 +160,18 @@ TheSchemaIs:
158
160
 
159
161
  ## Some Q&A
160
162
 
161
- * **Q: It doesn't check the actual DB?** A: No, it does not! At the current moment, our belief is that in a healthy Rails codebase `schema.rb` is always corresponding to DB state, so checking against it is enough. This approach makes the tooling much easier (with existing Rubocop's ecosystem of parsers/offenses/configurations).
162
- * **Q: What if I don't use Rubocop?** A: You may want to try, at least? Do you know that you may disable or configure most of its checks to your liking? And auto-correct any code to your preferences?.. Or automatically create "TODO" config-file (which disables all the cops currently raising offenses, and allows to review them and later setup one-by-one)?.. It is much more than "linter making your code to complain about some rigid style guide".
163
- * **Q: Cool, but I still don't want to.** ...OK, then you can disable all cops _except_ for `TheSchemaIs` namespace :)
164
- * **How do I annotate my fabrics, model specs, routes, controllers, ... (which `annotate` allows)?** You don't. The same way you don't copy-paste the whole definition of the class into spec file which tests this class: Definition is in one place, tests and other code using this definition is another. DRY!
165
- * **Rubocop is unhappy with the code `TheSchemaIs` generated**. There are two known things in generated `the_schema_is` blocks that Rubocop may complain about:
166
- * Usage of double quotes for strings, if your config insists on single quotes: that's because we just copy code objects from `schema.rb`. Rubocop's auto-fix will fix it :) (Even in one run: "fixing TheSchemaIs, then fixing quotes");
167
- * Too long blocks (if you have tables with dozens of columns, God forbid... as we do). It can be fixed by adding this to `.rubocop.yml`:
163
+ * **Q: It doesn't check the actual DB?**
164
+ * A: No, it does not! At the current moment, our belief is that in a healthy Rails codebase `schema.rb` is always corresponding to DB state, so checking against it is enough. This approach makes the tooling much easier (with existing Rubocop's ecosystem of parsers/offenses/configurations).
165
+ * **Q: What if I don't use Rubocop?**
166
+ * A: You may want to try, at least? Do you know that you may disable or configure most of its checks to your liking? And auto-correct any code to your preferences?.. Or automatically create "TODO" config-file (which disables all the cops currently raising offenses, and allows to review them and later setup one-by-one)?.. It is much more than "linter making your code to complain about some rigid style guide".
167
+ * **Q: Cool, but I still don't want to.**
168
+ * A: ...OK, then you can disable all cops _except_ for `TheSchemaIs` namespace :)
169
+ * **How do I annotate my fabrics, model specs, routes, controllers, ... (which `annotate` allows)?**
170
+ * A: You don't. The same way you don't copy-paste the whole definition of the class into spec file which tests this class: Definition is in one place, tests and other code using this definition is another. DRY!
171
+ * **Rubocop is unhappy with the code `TheSchemaIs` generated**.
172
+ * A: There are two known things in generated `the_schema_is` blocks that Rubocop may complain about:
173
+ * Usage of double quotes for strings, if your config insists on single quotes: that's because we just copy code objects from `schema.rb`. Rubocop's auto-correct will fix it :) (Even in one run: "fixing TheSchemaIs, then fixing quotes");
174
+ * Too long blocks (if you have tables with dozens of columns, God forbid... as we do). It can be fixed by adding this to `.rubocop.yml`:
168
175
  ```yaml
169
176
  Metrics/BlockLength:
170
177
  ExcludedMethods:
@@ -0,0 +1,29 @@
1
+ ---
2
+ # For all TheSchemaIs cops the settings are the same.
3
+ # But note that Rubocop's behavior is slightly weird with --show-cops (it will not show that
4
+ # cop has those three setting)
5
+ TheSchemaIs:
6
+ Schema: db/schema.rb
7
+ BaseClass: ['ActiveRecord::Base', 'ApplicationRecord']
8
+ TablePrefix: ''
9
+ RemoveDefinitions: []
10
+
11
+ TheSchemaIs/Presence:
12
+ Description: "Check presence of the_schema_is statement in ActiveRecord models"
13
+ Enabled: true
14
+
15
+ TheSchemaIs/WrongTableName:
16
+ Description: "Check table name define by the_schema_is statement against one in schema.rb"
17
+ Enabled: true
18
+
19
+ TheSchemaIs/MissingColumn:
20
+ Description: "Check columns missing in the_schema_is definition (but present in schema.rb)"
21
+ Enabled: true
22
+
23
+ TheSchemaIs/UnknownColumn:
24
+ Description: "Check unknown column in the_schema_is definition (not defined in schema.rb)"
25
+ Enabled: true
26
+
27
+ TheSchemaIs/WrongColumnDefinition:
28
+ Description: "Check column definition in the_schema_is statement against schema.rb"
29
+ Enabled: true
@@ -1,15 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'singleton'
4
+
3
5
  module TheSchemaIs
4
6
  module Cops
7
+ # This module mitigates usage of RuboCop's NodePattern in a more flexible manner. NodePattern
8
+ # (targeting RuboCop's goals) was only available as a metaprogramming macro, requiring to
9
+ # `def_node_search :some_method, pattern` before the pattern can be used, while we wanted to
10
+ # just do `ast.ast_search(some_pattern)`; so this method defines a method for each used pattern
11
+ # on the fly and hides this discrepancy. Used by NodeRefinements (mixed into parser's node) to
12
+ # provide `Node#ast_search` and `Node#ast_match`.
13
+ class Patterns
14
+ include Singleton
15
+ extend RuboCop::AST::NodePattern::Macros
16
+
17
+ class << self
18
+ extend Memoist
19
+
20
+ def search(pattern, node)
21
+ search_methods[pattern].then { |m| instance.send(m, node) }
22
+ end
23
+
24
+ def match(pattern, node)
25
+ match_methods[pattern].then { |m| instance.send(m, node) }
26
+ end
27
+
28
+ private
29
+
30
+ memoize def search_methods
31
+ Hash.new { |h, pattern|
32
+ method_name = "search_#{h.size}"
33
+ def_node_search method_name, pattern
34
+ h[pattern] = method_name
35
+ }
36
+ end
37
+
38
+ memoize def match_methods
39
+ Hash.new { |h, pattern|
40
+ method_name = "match_#{h.size}"
41
+ def_node_search method_name, pattern
42
+ h[pattern] = method_name
43
+ }
44
+ end
45
+ end
46
+ end
47
+
5
48
  module NodeRefinements
6
49
  refine ::Parser::AST::Node do
7
- def ffast(expr)
8
- Fast.search(expr, self)
50
+ def ast_search(expr)
51
+ Patterns.search(expr, self).to_a
9
52
  end
10
53
 
11
- def ffast_match?(expr)
12
- Fast.match?(expr, self)
54
+ def ast_match(expr)
55
+ Patterns.match(expr, self).to_a.first
13
56
  end
14
57
 
15
58
  def arraify
@@ -23,7 +66,7 @@ module TheSchemaIs
23
66
  end
24
67
 
25
68
  def find_parent(type)
26
- Enumerator.produce(parent, &:parent).slice_after { |n| n && n.type == type }.first.last
69
+ Enumerator.produce(parent, &:parent).find { |n| n && n.type == type }
27
70
  end
28
71
  end
29
72
  end
@@ -15,66 +15,72 @@ module TheSchemaIs
15
15
  float integer json string text time timestamp virtual].freeze
16
16
  POSTGRES_COLUMN_TYPES = %i[jsonb inet cidr macaddr hstore uuid].freeze
17
17
 
18
- COLUMN_DEFS = (STANDARD_COLUMN_TYPES + POSTGRES_COLUMN_TYPES + %i[column]).freeze
18
+ COLUMN_DEFS = (STANDARD_COLUMN_TYPES + POSTGRES_COLUMN_TYPES + %i[enum column]).freeze
19
19
 
20
- Model = Struct.new(:class_name, :table_name, :source, :schema, keyword_init: true)
20
+ Model = Struct.new(:class_name, :table_name, :source, :schema, :table_name_node,
21
+ keyword_init: true)
21
22
 
22
23
  Column = Struct.new(:name, :type, :definition, :source, keyword_init: true) do
23
24
  def definition_source
24
25
  return unless definition
25
26
 
26
- eval('{' + definition.loc.expression.source + '}') # rubocop:disable Security/Eval
27
+ eval('{' + definition.loc.expression.source + '}') # rubocop:disable Security/Eval,Style/StringConcatenation
27
28
  end
28
29
  end
29
30
 
30
- def self.schema(path)
31
- ast = Fast.ast(File.read(path))
31
+ def self.parse(code)
32
+ # TODO: Some kind of "current version" (ask Rubocop!)
33
+ RuboCop::AST::ProcessedSource.new(code, 2.7).ast
34
+ end
35
+
36
+ def self.schema(path, remove_definition_attrs: [])
37
+ ast = parse(File.read(path))
38
+
39
+ ast = remove_attributes(ast, remove_definition_attrs) unless remove_definition_attrs.empty?
32
40
 
33
- content = Fast.search(
34
- '(block (send (const (const nil :ActiveRecord) :Schema) :define) _ $_)',
35
- ast
36
- ).last.first
37
- Fast.search('(block (send nil :create_table (str $_)) _ _)', content)
38
- .each_slice(2).to_h { |t, name| [Array(name).first, t] } # FIXME: Why it sometimes makes arrays, and sometimes not?..
41
+ ast.ast_search('(block (send nil? :create_table (str $_) _) _ $_)').to_h
39
42
  end
40
43
 
41
44
  def self.model(ast, base_classes: %w[ActiveRecord::Base ApplicationRecord], table_prefix: nil)
42
45
  base = base_classes_query(base_classes)
43
- ast.ffast("(class $_ #{base})").each_slice(2)
46
+ ast.ast_search("$(class $_ #{base} _)")
44
47
  .map { |node, name| node2model(name, node, table_prefix.to_s) }
45
48
  .compact
46
49
  .first
47
50
  end
48
51
 
49
- def self.node2model(name_node, definition_node, table_prefix)
50
- return if definition_node.ffast('(send self abstract_class= true)').any?
52
+ def self.node2model(name_node, definition_node, table_prefix) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
53
+ return if definition_node.ast_search('(send self :abstract_class= true)')&.any?
51
54
 
52
55
  # If all children are classes/modules -- model is here only as a namespace, shouldn't be
53
56
  # parsed/have the_schema_is
54
- return if definition_node
55
- .children[2]&.arraify&.all? { |n| %i[class module].include?(n.type) }
57
+ if definition_node.children&.dig(2)&.arraify&.all? { |n| %i[class module].include?(n.type) }
58
+ return
59
+ end
56
60
 
57
- class_name = name_node.first.loc.expression.source
61
+ class_name = name_node.loc.expression.source
58
62
 
59
- schema = definition_node.ffast('$(block (send nil :the_schema_is) _ ...')&.last
63
+ schema_node, name_node =
64
+ definition_node.ast_search('$(block (send nil? :the_schema_is $_?) _ ...)')&.last
60
65
 
61
66
  # TODO: https://api.rubyonrails.org/classes/ActiveRecord/ModelSchema/ClassMethods.html#method-i-table_name
62
67
  # * consider table_prefix/table_suffix settings
63
68
  # * also, consider engines!
64
- table_name = definition_node.ffast('(send self table_name= (str $_)')&.last
69
+ table_name = definition_node.ast_search('(send self :table_name= (str $_))')&.last
65
70
 
66
71
  Model.new(
67
72
  class_name: class_name,
68
73
  table_name: table_name ||
69
74
  table_prefix.+(ActiveSupport::Inflector.tableize(class_name)),
70
75
  source: definition_node,
71
- schema: schema
76
+ schema: schema_node,
77
+ table_name_node: name_node&.first
72
78
  )
73
79
  end
74
80
 
75
81
  def self.base_classes_query(classes)
76
82
  classes
77
- .map { |cls| cls.split('::').inject('nil') { |res, str| "(const #{res} :#{str})" } }
83
+ .map { |cls| cls.split('::').inject('nil?') { |res, str| "(const #{res} :#{str})" } }
78
84
  .join(' ')
79
85
  .then { |str| "{#{str}}" }
80
86
  end
@@ -83,14 +89,37 @@ module TheSchemaIs
83
89
  ast.arraify.map { |node|
84
90
  # FIXME: Of course it should be easier to say "optional additional params"
85
91
  if (type, name, defs =
86
- node.ffast_match?('(send {(send nil t) (lvar t)} $_ (str $_) $...'))
92
+ node.ast_match('(send {(send nil? :t) (lvar :t)} $_ (str $_) $_)'))
87
93
  Column.new(name: name, type: type, definition: defs, source: node) \
88
94
  if COLUMN_DEFS.include?(type)
89
- elsif (type, name = Fast.match?('(send {(send nil t) (lvar t)} $_ (str $_)', node))
95
+ elsif (type, name = node.ast_match('(send {(send nil? :t) (lvar :t)} $_ (str $_))'))
90
96
  Column.new(name: name, type: type, source: node) if COLUMN_DEFS.include?(type)
91
97
  end
92
98
  }.compact
93
99
  end
100
+
101
+ # Removes unnecessary column definitions from further comparison, using schema source tree editing
102
+ def self.remove_attributes(ast, attrs_to_remove)
103
+ buf = ast.loc.expression.source_buffer
104
+ src = buf.source
105
+ rewriter = ::Parser::Source::TreeRewriter.new(buf)
106
+
107
+ # FIXME: Two nested cycles can be simplifid to just look for column definition, probably
108
+ ast.ast_search('(block (send nil? :create_table (str _) _) _ $_)').each do |table_def|
109
+ table_def.children.each do |col|
110
+ dfn = col.children[3] or next
111
+ dfn.children
112
+ .select { |c| attrs_to_remove.include?(c.children[0].children[0]) }
113
+ .each do |c|
114
+ prev_comma = c.source_range.begin_pos.step(by: -1).find { |pos| src[pos] == ',' }
115
+ range = ::Parser::Source::Range.new(buf, prev_comma, c.source_range.end_pos)
116
+ rewriter.remove(range)
117
+ end
118
+ end
119
+ end
120
+
121
+ RuboCop::AST::ProcessedSource.new(rewriter.process, 2.7).ast
122
+ end
94
123
  end
95
124
  end
96
125
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'memoist'
4
4
  require 'rubocop'
5
- require 'fast'
6
5
  require 'backports/latest'
7
6
 
8
7
  require_relative 'cops/node_util'
@@ -16,8 +15,12 @@ module TheSchemaIs
16
15
  using Cops::NodeRefinements
17
16
 
18
17
  module Cops
19
- def self.schema_cache
20
- @schema_cache ||= Hash.new { |h, path| h[path] = Cops::Parser.schema(path) }
18
+ class << self
19
+ extend Memoist
20
+
21
+ memoize def fetch_schema(path, remove_definition_attrs: [])
22
+ Cops::Parser.schema(path, remove_definition_attrs: remove_definition_attrs)
23
+ end
21
24
  end
22
25
  end
23
26
 
@@ -25,9 +28,10 @@ module TheSchemaIs
25
28
  extend Memoist
26
29
 
27
30
  def self.included(cls)
28
- cls.define_singleton_method(:badge) {
31
+ cls.define_singleton_method(:badge) do
29
32
  RuboCop::Cop::Badge.for("TheSchemaIs::#{name.split('::').last}")
30
- }
33
+ end
34
+ super
31
35
  end
32
36
 
33
37
  def on_class(node)
@@ -35,14 +39,22 @@ module TheSchemaIs
35
39
  base_classes: cop_config.fetch('BaseClass'),
36
40
  table_prefix: cop_config['TablePrefix']) or return
37
41
 
38
- validate
42
+ register_offense(node)
43
+ end
44
+
45
+ # We need this method to tell Rubocop that EVEN if app/models/user.rb haven't changed, and
46
+ # .rubocop.yml haven't changed, we STILL may need to rerun the cop if schema.rb have changed.
47
+ def external_dependency_checksum
48
+ return unless schema_path
49
+
50
+ Digest::SHA1.hexdigest(File.read(schema_path))
39
51
  end
40
52
 
41
53
  private
42
54
 
43
55
  attr_reader :model
44
56
 
45
- def validate
57
+ def register_offense(node)
46
58
  fail NotImplementedError
47
59
  end
48
60
 
@@ -51,38 +63,40 @@ module TheSchemaIs
51
63
  end
52
64
 
53
65
  memoize def schema
54
- Cops.schema_cache.dig(schema_path, model.table_name)
66
+ attrs_to_remove = cop_config['RemoveDefinitions']&.map(&:to_sym) || []
67
+ # It is OK if it returns Nil, just will be handled by "schema is absent" cop
68
+ Cops.fetch_schema(schema_path, remove_definition_attrs: attrs_to_remove)[model.table_name]
55
69
  end
56
70
 
57
71
  memoize def model_columns
58
- statements = model.schema.ffast('(block (send nil :the_schema_is) (args) $...)').last.last
72
+ statements = model.schema.ast_search('(block (send nil? :the_schema_is _?) _ $...)')
73
+ .last.last
59
74
 
60
75
  Cops::Parser.columns(statements).to_h { |col| [col.name, col] }
61
76
  end
62
77
 
63
78
  memoize def schema_columns
64
- # FIXME: should be already done in Parser.schema, probably!
65
- statements = schema.ffast('(block (send nil :create_table) (args) $...)').last.last
66
-
67
- Cops::Parser.columns(statements).to_h { |col| [col.name, col] }
79
+ Cops::Parser.columns(schema).to_h { |col| [col.name, col] }
68
80
  end
69
81
  end
70
82
 
71
- class Presence < RuboCop::Cop::Cop
83
+ class Presence < RuboCop::Cop::Base
72
84
  include Common
85
+ extend RuboCop::Cop::AutoCorrector
73
86
 
74
87
  MSG_NO_MODEL_SCHEMA = 'The schema is not specified in the model (use the_schema_is statement)'
75
88
  MSG_NO_DB_SCHEMA = 'Table "%s" is not defined in %s'
76
89
 
77
- def autocorrect(node)
78
- return unless schema
90
+ private
79
91
 
80
- m = model
92
+ def register_offense(node)
93
+ schema.nil? and
94
+ add_offense(model.source, message: MSG_NO_DB_SCHEMA % [model.table_name, schema_path])
81
95
 
82
- lambda do |corrector|
96
+ model.schema.nil? and add_offense(model.source, message: MSG_NO_MODEL_SCHEMA) do |corrector|
83
97
  indent = node.loc.expression.column + 2
84
98
  code = [
85
- "the_schema_is #{m.table_name.to_s.inspect} do |t|",
99
+ "the_schema_is #{model.table_name.to_s.inspect} do |t|",
86
100
  *schema_columns.map { |_, col| " #{col.source.loc.expression.source}" },
87
101
  'end'
88
102
  ].map { |s| ' ' * indent + s }.join("\n").then { |s| "\n#{s}\n" }
@@ -91,52 +105,71 @@ module TheSchemaIs
91
105
  corrector.insert_after(node.children[1].loc.expression, code)
92
106
  end
93
107
  end
108
+ end
109
+
110
+ class WrongTableName < RuboCop::Cop::Base
111
+ include Common
112
+ extend RuboCop::Cop::AutoCorrector
113
+
114
+ MSG_WRONG_TABLE_NAME = 'The real table name should be %p'
115
+ MSG_NO_TABLE_NAME = 'Table name is not specified'
94
116
 
95
117
  private
96
118
 
97
- def validate
98
- schema.nil? and
99
- add_offense(model.source, message: MSG_NO_DB_SCHEMA % [model.table_name, schema_path])
100
- model.schema.nil? and add_offense(model.source, message: MSG_NO_MODEL_SCHEMA)
119
+ def register_offense(_node)
120
+ return if model.schema.nil? || schema.nil?
121
+
122
+ pp
123
+
124
+ if model.table_name_node.nil?
125
+ add_offense(model.schema, message: MSG_NO_TABLE_NAME) do |corrector|
126
+ corrector.insert_after(model.schema.children[0].loc.expression, " #{model.table_name.to_s.inspect}")
127
+ end
128
+ elsif model.table_name_node.children[0] != model.table_name
129
+ add_offense(model.table_name_node,
130
+ message: MSG_WRONG_TABLE_NAME % model.table_name) do |corrector|
131
+ corrector.replace(model.table_name_node.loc.expression, model.table_name.to_s.inspect)
132
+ end
133
+ end
101
134
  end
102
135
  end
103
136
 
104
- class MissingColumn < RuboCop::Cop::Cop
137
+ class MissingColumn < RuboCop::Cop::Base
105
138
  include Common
139
+ extend RuboCop::Cop::AutoCorrector
106
140
 
107
141
  MSG = 'Column "%s" definition is missing'
108
142
 
109
- def autocorrect(_node)
110
- lambda do |corrector|
111
- missing_columns.each { |name, col|
112
- prev_statement = model_columns
113
- .slice(*schema_columns.keys[0...schema_columns.keys.index(name)])
114
- .values.last&.source
115
- if prev_statement
116
- indent = prev_statement.loc.expression.column
117
- corrector.insert_after(
118
- prev_statement.loc.expression,
119
- "\n#{' ' * indent}#{col.source.loc.expression.source}"
120
- )
121
- else
122
- indent = model.schema.loc.expression.column + 2
123
- corrector.insert_after(
124
- # of "the_schema_is do |t|" -- children[1] is "|t|""
125
- model.schema.children[1].loc.expression,
126
- "\n#{' ' * indent}#{col.source.loc.expression.source}"
127
- )
128
- end
129
- }
130
- end
131
- end
132
-
133
143
  private
134
144
 
135
- def validate
145
+ def register_offense(_node)
136
146
  return if model.schema.nil? || schema.nil?
137
147
 
138
- missing_columns.each do |_, col|
139
- add_offense(model.schema, message: MSG % col.name)
148
+ missing_columns.each do |name, col|
149
+ add_offense(model.schema, message: MSG % col.name) do |corrector|
150
+ insert_column(corrector, name, col)
151
+ end
152
+ end
153
+ end
154
+
155
+ def insert_column(corrector, name, col)
156
+ prev_statement = model_columns
157
+ .slice(*schema_columns.keys[0...schema_columns.keys.index(name)])
158
+ .values.last&.source
159
+
160
+ if prev_statement
161
+ indent = prev_statement.loc.expression.column
162
+ corrector.insert_after(
163
+ prev_statement.loc.expression,
164
+ "\n#{' ' * indent}#{col.source.loc.expression.source}"
165
+ )
166
+ else
167
+ indent = model.schema.loc.expression.column + 2
168
+ corrector.insert_after(
169
+ # of "the_schema_is do |t|" -- children[1] is "|t|""
170
+ model.schema.children[1].loc.expression,
171
+ "\n#{' ' * indent}#{col.source.loc.expression.source}"
172
+ )
140
173
  end
141
174
  end
142
175
 
@@ -145,14 +178,19 @@ module TheSchemaIs
145
178
  end
146
179
  end
147
180
 
148
- class UnknownColumn < RuboCop::Cop::Cop
181
+ class UnknownColumn < RuboCop::Cop::Base
149
182
  include Common
183
+ extend RuboCop::Cop::AutoCorrector
150
184
 
151
185
  MSG = 'Uknown column "%s"'
152
186
 
153
- def autocorrect(_node)
154
- lambda do |corrector|
155
- extra_columns.each do |_, col|
187
+ private
188
+
189
+ def register_offense(_node)
190
+ return if model.schema.nil? || schema.nil?
191
+
192
+ extra_columns.each do |_, col|
193
+ add_offense(col.source, message: MSG % col.name) do |corrector|
156
194
  src_range = col.source.loc.expression
157
195
  end_pos = col.source.next_sibling.then { |n|
158
196
  n ? n.loc.expression.begin_pos - 2 : col.source.find_parent(:block).loc.end.begin_pos
@@ -164,42 +202,27 @@ module TheSchemaIs
164
202
  end
165
203
  end
166
204
 
167
- private
168
-
169
- def validate
170
- return if model.schema.nil? || schema.nil?
171
-
172
- extra_columns.each do |_, col|
173
- add_offense(col.source, message: MSG % col.name)
174
- end
175
- end
176
-
177
205
  def extra_columns
178
206
  model_columns.reject { |name,| schema_columns.keys.include?(name) }
179
207
  end
180
208
  end
181
209
 
182
- class WrongColumnDefinition < RuboCop::Cop::Cop
210
+ class WrongColumnDefinition < RuboCop::Cop::Base
183
211
  include Common
212
+ extend RuboCop::Cop::AutoCorrector
184
213
 
185
214
  MSG = 'Wrong column definition: expected `%s`'
186
215
 
187
- def autocorrect(_node)
188
- lambda do |corrector|
189
- wrong_columns.each do |mcol, scol|
190
- corrector.replace(mcol.source.loc.expression, scol.source.loc.expression.source)
191
- end
192
- end
193
- end
194
-
195
216
  private
196
217
 
197
- def validate
218
+ def register_offense(_node)
198
219
  return if model.schema.nil? || schema.nil?
199
220
 
200
221
  wrong_columns
201
222
  .each do |mcol, scol|
202
- add_offense(mcol.source, message: MSG % scol.source.loc.expression.source)
223
+ add_offense(mcol.source, message: MSG % scol.source.loc.expression.source) do |corrector|
224
+ corrector.replace(mcol.source.loc.expression, scol.source.loc.expression.source)
225
+ end
203
226
  end
204
227
  end
205
228
 
@@ -207,7 +230,9 @@ module TheSchemaIs
207
230
  model_columns
208
231
  .map { |name, col| [col, schema_columns[name]] }
209
232
  .reject { |mcol, scol|
210
- mcol.type == scol.type && mcol.definition_source == scol.definition_source
233
+ # When column is not in schema, we shouldn't try to check it: UnknownColumn cop will
234
+ # handle.
235
+ !scol || mcol.type == scol.type && mcol.definition_source == scol.definition_source
211
236
  }
212
237
  end
213
238
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: the_schema_is
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Victor Shepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-22 00:00:00.000000000 Z
11
+ date: 2021-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: backports
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: 1.0.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: 1.0.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: activesupport
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: ffast
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: 0.1.8
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: 0.1.8
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: memoist
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -130,8 +116,10 @@ executables: []
130
116
  extensions: []
131
117
  extra_rdoc_files: []
132
118
  files:
119
+ - Changelog.md
133
120
  - LICENSE.txt
134
121
  - README.md
122
+ - config/defaults.yml
135
123
  - lib/the_schema_is.rb
136
124
  - lib/the_schema_is/cops.rb
137
125
  - lib/the_schema_is/cops/inject.rb
@@ -149,14 +137,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
149
137
  requirements:
150
138
  - - ">="
151
139
  - !ruby/object:Gem::Version
152
- version: 2.6.0
140
+ version: 2.5.0
153
141
  required_rubygems_version: !ruby/object:Gem::Requirement
154
142
  requirements:
155
143
  - - ">="
156
144
  - !ruby/object:Gem::Version
157
145
  version: '0'
158
146
  requirements: []
159
- rubygems_version: 3.0.3
147
+ rubygems_version: 3.1.6
160
148
  signing_key:
161
149
  specification_version: 4
162
150
  summary: ActiveRecord model annotations done right