the_schema_is 0.0.1 → 0.0.5

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