Class: Idxfence::ModelParser

Inherits:
Object
  • Object
show all
Defined in:
lib/idxfence/model_parser.rb

Overview

Parses one Ruby source file with Ripper (structural boundaries -- class start/end -- come from the s-expression, never from do/end counting; a narrower regex pass then runs only over the text of a class body already located structurally, the same hybrid technique this workspace's mailstall/jobclash/lockstall packages use) looking for a Rails model class (superclass ApplicationRecord or ActiveRecord::Base) and, within its own body:

- an explicit `self.table_name = "..."` override, if present
- every `validates :col[, :col2, ...], uniqueness: true` (or
`uniqueness: { scope: ..., ... }`) and
`validates_uniqueness_of :col[, :col2, ...][, scope: ...]`
declaration, one UniquenessValidation per column named.

This is deliberately one file's worth of information -- resolving it against a database-level unique index requires a second pass over db/schema.rb, done separately by Idxfence::SchemaParser and joined by Idxfence::Checker. See DETAILS.md's "two passes" section.

Defined Under Namespace

Classes: ParsedModel, UniquenessValidation

Constant Summary collapse

MODEL_SUPERCLASS =
/\A(?:ApplicationRecord|ActiveRecord::Base)\z/.freeze
LEADING_COLUMNS =

One validates/validates_uniqueness_of statement's leading list of bare :symbol column arguments, e.g. the :email, :username in validates :email, :username, uniqueness: true.

/\A\s*(?:validates(?:_uniqueness_of)?)\s+((?::\w+\s*,\s*)*:\w+)/.freeze
SCOPE_SYMBOL =
/scope:\s*:(\w+)/.freeze
SCOPE_ARRAY =
/scope:\s*\[([^\]]*)\]/.freeze
SCOPE_PCT_I =
/scope:\s*%i\[([^\]]*)\]/.freeze
SELF_TABLE_NAME =
/self\.table_name\s*=\s*["']([^"']+)["']/.freeze

Instance Method Summary collapse

Constructor Details

#initialize(path:) ⇒ ModelParser

Returns a new instance of ModelParser.



39
40
41
# File 'lib/idxfence/model_parser.rb', line 39

def initialize(path:)
  @path = path
end

Instance Method Details

#call ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/idxfence/model_parser.rb', line 43

def call
  source = File.read(path)
  sexp = Ripper.sexp(source)
  return [] unless sexp

  lines = source.lines
  models = []

  each_class_node(sexp) do |class_name, class_node|
    next unless model_class?(class_node)

    start_line = min_line(class_node)
    end_line = max_line(class_node)
    next unless start_line && end_line

    body_source = lines[(start_line - 1)...end_line].join

    models << ParsedModel.new(
      class_name: class_name,
      table_name_override: (m = body_source.match(SELF_TABLE_NAME)) && m[1],
      validations: uniqueness_validations(body_source, lines, start_line),
      file: path
    )
  end

  models
end