Module: TheSchemaIs::Cops::Parser
- Defined in:
- lib/the_schema_is/cops/parser.rb
Defined Under Namespace
Constant Summary collapse
- STANDARD_COLUMN_TYPES =
See github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L217 TODO: numeric is just an alias for decimal TODO: different adapters can add another types edgeguides.rubyonrails.org/active_record_postgresql.html
%i[bigint binary boolean date datetime decimal numeric float integer json string text time timestamp virtual].freeze
- POSTGRES_COLUMN_TYPES =
%i[jsonb inet cidr macaddr hstore uuid].freeze
- COLUMN_DEFS =
(STANDARD_COLUMN_TYPES + POSTGRES_COLUMN_TYPES + %i[enum column]).freeze
Class Method Summary collapse
- .base_classes_query(classes) ⇒ Object
- .columns(ast) ⇒ Object
- .model(ast, base_classes: %w[ActiveRecord::Base ApplicationRecord],, table_prefix: nil) ⇒ Object
-
.node2model(name_node, definition_node, table_prefix) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity.
- .parse(code) ⇒ Object
-
.remove_attributes(ast, attrs_to_remove) ⇒ Object
Removes unnecessary column definitions from further comparison, using schema source tree editing.
- .schema(path, remove_definition_attrs: []) ⇒ Object
Class Method Details
.base_classes_query(classes) ⇒ Object
81 82 83 84 85 86 |
# File 'lib/the_schema_is/cops/parser.rb', line 81 def self.base_classes_query(classes) classes .map { |cls| cls.split('::').inject('nil?') { |res, str| "(const #{res} :#{str})" } } .join(' ') .then { |str| "{#{str}}" } end |
.columns(ast) ⇒ Object
88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/the_schema_is/cops/parser.rb', line 88 def self.columns(ast) ast.arraify.map { |node| # FIXME: Of course it should be easier to say "optional additional params" if (type, name, defs = node.ast_match('(send {(send nil? :t) (lvar :t)} $_ (str $_) $_)')) Column.new(name: name, type: type, definition: defs, source: node) \ if COLUMN_DEFS.include?(type) elsif (type, name = node.ast_match('(send {(send nil? :t) (lvar :t)} $_ (str $_))')) Column.new(name: name, type: type, source: node) if COLUMN_DEFS.include?(type) end }.compact end |
.model(ast, base_classes: %w[ActiveRecord::Base ApplicationRecord],, table_prefix: nil) ⇒ Object
44 45 46 47 48 49 50 |
# File 'lib/the_schema_is/cops/parser.rb', line 44 def self.model(ast, base_classes: %w[ActiveRecord::Base ApplicationRecord], table_prefix: nil) base = base_classes_query(base_classes) ast.ast_search("$(class $_ #{base} _)") .map { |node, name| node2model(name, node, table_prefix.to_s) } .compact .first end |
.node2model(name_node, definition_node, table_prefix) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/the_schema_is/cops/parser.rb', line 52 def self.node2model(name_node, definition_node, table_prefix) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity return if definition_node.ast_search('(send self :abstract_class= true)')&.any? # If all children are classes/modules -- model is here only as a namespace, shouldn't be # parsed/have the_schema_is if definition_node.children&.dig(2)&.arraify&.all? { |n| %i[class module].include?(n.type) } return end class_name = name_node.loc.expression.source schema_node, name_node = definition_node.ast_search('$(block (send nil? :the_schema_is $_?) _ ...)')&.last # TODO: https://api.rubyonrails.org/classes/ActiveRecord/ModelSchema/ClassMethods.html#method-i-table_name # * consider table_prefix/table_suffix settings # * also, consider engines! table_name = definition_node.ast_search('(send self :table_name= (str $_))')&.last Model.new( class_name: class_name, table_name: table_name || table_prefix.+(ActiveSupport::Inflector.tableize(class_name)), source: definition_node, schema: schema_node, table_name_node: name_node&.first ) end |
.parse(code) ⇒ Object
31 32 33 34 |
# File 'lib/the_schema_is/cops/parser.rb', line 31 def self.parse(code) # TODO: Some kind of "current version" (ask Rubocop!) RuboCop::AST::ProcessedSource.new(code, 2.7).ast end |
.remove_attributes(ast, attrs_to_remove) ⇒ Object
Removes unnecessary column definitions from further comparison, using schema source tree editing
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/the_schema_is/cops/parser.rb', line 102 def self.remove_attributes(ast, attrs_to_remove) buf = ast.loc.expression.source_buffer src = buf.source rewriter = ::Parser::Source::TreeRewriter.new(buf) # FIXME: Two nested cycles can be simplifid to just look for column definition, probably ast.ast_search('(block (send nil? :create_table (str _) _) _ $_)').each do |table_def| table_def.children.each do |col| dfn = col.children[3] or next dfn.children .select { |c| attrs_to_remove.include?(c.children[0].children[0]) } .each do |c| prev_comma = c.source_range.begin_pos.step(by: -1).find { |pos| src[pos] == ',' } range = ::Parser::Source::Range.new(buf, prev_comma, c.source_range.end_pos) rewriter.remove(range) end end end RuboCop::AST::ProcessedSource.new(rewriter.process, 2.7).ast end |
.schema(path, remove_definition_attrs: []) ⇒ Object
36 37 38 39 40 41 42 |
# File 'lib/the_schema_is/cops/parser.rb', line 36 def self.schema(path, remove_definition_attrs: []) ast = parse(File.read(path)) ast = remove_attributes(ast, remove_definition_attrs) unless remove_definition_attrs.empty? ast.ast_search('(block (send nil? :create_table (str $_) _) _ $_)').to_h end |