Class: Recheck::Command::Setup

Inherits:
Object
  • Object
show all
Defined in:
lib/recheck/commands.rb,
lib/recheck/rails/setup.rb

Overview

Run

Instance Method Summary collapse

Constructor Details

#initialize(argv: []) ⇒ Setup



129
130
131
132
133
134
135
# File 'lib/recheck/commands.rb', line 129

def initialize(argv: [])
  @argv = argv
  @options = Optimist.options(@argv) do
    banner "recheck setup: create a check suite"
  end
  @files_created = []
end

Instance Method Details

#detect_linterObject



153
154
155
156
157
# File 'lib/recheck/commands.rb', line 153

def detect_linter
  return "bundle exec standardrb --fix-unsafely recheck" if File.exist?(".standard.yml") || gemfile_includes?("standard")
  return "bundle exec rubocop --autocorrect-all recheck" if File.exist?(".rubocop.yml") || gemfile_includes?("rubocop")
  nil
end

#gemfile_includes?(gem_name) ⇒ Boolean



159
160
161
162
163
# File 'lib/recheck/commands.rb', line 159

def gemfile_includes?(gem_name)
  File.readlines("Gemfile").any? { |line| line.include?(gem_name) }
rescue Errno::ENOENT
  false
end

#queries(model:) ⇒ Object



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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/recheck/rails/setup.rb', line 55

def queries model:
  model.validators.map do |validator|
    # validators take a list of attributes but operate on them individually
    validator.attributes.map do |attr|
      name = "query_#{validator.kind}_#{attr}"
      # remove memory addresses; just noise
      inspect = validator.inspect.gsub(/(#<[\w:]+):0x[0-9a-f]+ /, '\1 ')

      column = model.columns_hash[attr.to_s]
      next Placeholder.new inspect:, comment: "Can't query attribute #{attr}, it's not a database column" if column.nil?
      type = column..type

      if validator.options[:if] || validator.options[:unless]
        next Placeholder.new inspect:, comment: "Can't automatically translate this validation's :if or :unless into a query"
      end

      # normalizing - if not specified or [], on: means [:create, :udpate]
      on = validator.options[:on] || []
      on = [:create, :update] if on.empty?
      if validator.options[:on] == [:create]
        next Placeholder.new inspect:, comment: "Only validates on: :create, so there's nothing to validate for persisted records"
      end

      warning = if validator.options[:allow_nil] && !column.null
        "Warning: model #{model.name} validates #{attr} with :allow_nil but column #{column.name} is NOT NULL, so a 'valid' record can't be saved.\nRemove :allow_nil or make the column nullable."
      end

      case validator
      when ActiveModel::BlockValidator
        Placeholder.new inspect:, comment: "Can't automatically translate a Ruby block into a query."
      when ActiveModel::Validations::ConfirmationValidator
        FunctionPlaceholder.new inspect:, name:, comment: "Coming soon to Recheck beta"
      when ActiveModel::Validations::FormatValidator
        FunctionPlaceholder.new inspect:, name:, comment: "Coming soon to Recheck beta"
      when ActiveModel::Validations::InclusionValidator
        FunctionPlaceholder.new inspect:, name:, comment: "Coming soon to Recheck beta"
      when ActiveRecord::Validations::AbsenceValidator
        FunctionPlaceholder.new inspect:, name:, comment: "Coming soon to Recheck beta"
      when ActiveRecord::Validations::AssociatedValidator
        FunctionPlaceholder.new inspect:, name:, comment: "Coming soon to Recheck beta"
      when ActiveRecord::Validations::LengthValidator
        or_clauses = []
        if type == :string || type == :text || type == :integer
          if validator.options[:is]
            or_clauses << %{"LENGTH(`#{column.name}`) != #{validator.options[:is]}"}
          end
          if validator.options[:minimum] && validator.options[:maximum]
            or_clauses << %{"LENGTH(`#{column.name}`) < #{validator.options[:minimum]} and LENGTH(`#{column.name}`) > #{validator.options[:maximum]}"}
          elsif validator.options[:minimum]
            or_clauses << %{"LENGTH(`#{column.name}`) < #{validator.options[:minimum]}"}
          elsif validator.options[:maximum]
            or_clauses << %{"LENGTH(`#{column.name}`) > #{validator.options[:maximum]}"}
          end
          if validator.options[:allow_blank]
            or_clauses = or_clauses.map { |clause| clause[..-2] + %( and `#{column.name}` != ''") }
          end
        elsif type == :boolean
          comment = "Validating length of a boolean is backend-dependent and a strange idea."
        else
          comment = "Recheck doesn't know how to handle length on a #{type}, please report."
        end
        if !validator.options[:allow_nil] && !validator.options[:allow_blank]
          or_clauses << "#{column.name}: nil"
        end
        Query.new inspect:, name:, warning:, comment:, or_clauses:
      when ActiveRecord::Validations::NumericalityValidator
        FunctionPlaceholder.new inspect:, name:, comment: "Coming soon to Recheck beta"
      when ActiveRecord::Validations::PresenceValidator
        if validator.options[:allow_blank]
          next Placeholder.new inspect:, comment: "Validates presence of #{attr} with :allow_blank, which can never fail."
        end
        or_clauses = []
        if !validator.options[:allow_nil]
          or_clauses << "#{column.name}: nil"
        end
        case type
        when :string
          or_clauses << %{"TRIM(`#{column.name}`) = ''"}
        when :boolean
          or_clauses << "#{column.name}: false"
        else
          comment = "Recheck doesn't know how to handle presence on a #{type}, please report."
        end
        Query.new inspect:, name:, warning:, comment:, or_clauses:
      when ActiveRecord::Validations::UniquenessValidator
        FunctionPlaceholder.new inspect:, name:, comment: "Coming soon to Recheck beta"
      end
    end
  end.flatten
end

#runObject



137
138
139
140
141
142
143
144
# File 'lib/recheck/commands.rb', line 137

def run
  create_helper
  create_samples
  create_site_checks
  setup_model_checks
  run_linter
  vcs_message
end

#run_linterObject



146
147
148
149
150
151
# File 'lib/recheck/commands.rb', line 146

def run_linter
  if (linter_command = detect_linter)
    puts "Detected linter, running `#{linter_command}` on created files..."
    system("#{linter_command} #{@files_created.join(" ")}")
  end
end

#setup_model_checksObject

override the base gem's method; with the Rails env booted we can introspect the models to create much better initial checks



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/recheck/commands.rb', line 198

def setup_model_checks
  puts "Scanning for ActiveRecord models..."
  model_files.each do |mf|
    if mf.readonly
      puts "  #{mf.path} -> Skipped (readonly model, likely a view or uneditable record)"
    elsif mf.pk_info.nil?
      puts "  #{mf.path} -> Skipped (model without primary key, unable to report on it)"
    else
      FileUtils.mkdir_p(File.dirname(mf.checker_path))
      File.write(mf.checker_path, model_check_content(mf.class_name, mf.pk_info))
      @files_created << mf.checker_path
      puts "  #{mf.path} -> #{mf.checker_path}"
    end
  end
end