class Pronto::RailsMigrationsAnnotated

Runner that detects migration smells

Constants

VERSION

Public Instance Methods

run() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 11
def run
  @messages = []

  check_migrations_mixed_with_code
  check_migration_schema_version
  check_structure_migration_version_numbers
  check_large_schema_diff
  check_large_structure_diff

  @messages
end

Private Instance Methods

add_message_at_patch(patches, message, level = :warning, line: :first) click to toggle source

TODO: override def self.title ?

# File lib/pronto/rails_migrations_annotated.rb, line 27
def add_message_at_patch(patches, message, level = :warning, line: :first)
  patches = [patches] unless patches.is_a?(Array)
  patches.each do |patch|
    target_line = case line
                  when :first then patch.added_lines.first || patch.lines.first
                  when :last then patch.added_lines.last || patch.lines.last
                  else line
                  end
    @messages << Message.new(patch.delta.new_file[:path], target_line, level, message, nil, self.class)
  end
end
check_large_schema_diff() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 44
def check_large_schema_diff
  return unless diff_threshold
  return unless schema_patches.sum(&:additions) >= diff_threshold ||
                schema_patches.sum(&:deletions) >= diff_threshold

  add_message_at_patch(schema_patches.first, "Large schema diff, pay attention")
end
check_large_structure_diff() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 52
def check_large_structure_diff
  return unless diff_threshold
  return unless structure_patches.sum(&:additions) >= diff_threshold ||
                structure_patches.sum(&:deletions) >= diff_threshold

  add_message_at_patch(structure_patches.first, "Large structure diff, pay attention")
end
check_migration_schema_version() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 66
def check_migration_schema_version
  return if migration_patches.none? || !File.exist?(schema_file_name) || gitignored?(schema_file_name)

  if schema_patches.none?
    return add_message_at_patch(migration_patches, "Migration file detected, but no changes in schema.rb", :error)
  end

  migration_version_numbers.select { |version| schema_migration_version&.<(version) }.each do |wrong_version|
    add_message_at_patch(
      migration_patches.first { |patch| patch.delta.new_file[:path].include?(wrong_version) },
      "Migration version #{wrong_version} is above schema.rb version #{schema_migration_version}"
    )
  end
end
check_migrations_mixed_with_code() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 60
def check_migrations_mixed_with_code
  return unless migration_patches.any? && non_migration_related_patches.any?

  add_message_at_patch(migration_patches, "Do not mix migrations with other stuff", :fatal)
end
check_structure_ending(structure_file_lines) click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 162
def check_structure_ending(structure_file_lines)
  return if structure_patches.none?
  return if structure_file_lines.last(2) == %W[\n \n] && structure_file_lines[-3] != "\n"
  return if structure_file_lines.last.match?(/\A\s*[^\s]+\s*\n/)

  add_message_at_patch(structure_patches.last,
                       "structure.sql must end with a newline or 2 empty lines", line: :last)
end
check_structure_migration_missing() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 130
def check_structure_migration_missing
  structure_patches.each do |patch|
    patch.added_lines.select { |line| line.content.match?(migration_line_regex) }.each do |line|
      version = line.content.match(migration_line_regex)[:version]
      next if migration_version_numbers.include?(version)
      next if line.content.end_with?(",\n") &&
              patch.deleted_lines.any? { |del| del.content.include?("#{version}');") }

      add_message_at_patch(patch, "Migration #{version} is not present in this changeset", :error, line: line)
    end
  end
end
check_structure_migration_version_numbers() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 99
def check_structure_migration_version_numbers
  return unless migration_patches.any?

  structure_file_name = "db/structure.sql"
  return if !File.exist?(structure_file_name) || gitignored?(structure_file_name)

  structure_file_lines = File.readlines(structure_file_name)
  versions_from_schema = structure_file_lines.grep(migration_line_regex)

  check_structure_versions_present(versions_from_schema)
  check_structure_migration_missing

  check_structure_versions_syntax(versions_from_schema)
  check_structure_versions_sorted(versions_from_schema)
  check_structure_ending(structure_file_lines)
end
check_structure_versions_present(versions_from_schema) click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 116
def check_structure_versions_present(versions_from_schema)
  found_missing_migration = false
  migration_version_numbers.select { |version| versions_from_schema.none? { |ver| ver.include?(version) } }
                           .each do |wrong_version|
    found_missing_migration = true
    add_message_at_patch(migration_patches.first { |patch| patch.delta.new_file[:path].include?(wrong_version) },
                         "Migration #{wrong_version} is missing from structure.sql", :error)
  end

  return if structure_patches.any? || found_missing_migration

  add_message_at_patch(migration_patches, "Migration file detected, but no changes in structure.sql")
end
check_structure_versions_sorted(versions_from_schema) click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 143
def check_structure_versions_sorted(versions_from_schema)
  return if versions_from_schema == versions_from_schema.sort

  add_message_at_patch(
    structure_patches.first { |patch| patch.lines.any? { |line| line.content.match?(migration_line_regex) } },
    "Migration versions must be sorted and have correct syntax"
  )
end
check_structure_versions_syntax(versions_from_schema) click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 152
def check_structure_versions_syntax(versions_from_schema)
  return if versions_from_schema.last.end_with?("');\n") &&
            versions_from_schema.all? { |line| line.end_with?("'),\n", "');\n") }

  add_message_at_patch(
    structure_patches.first { |patch| patch.lines.any? { |line| line.content.match?(migration_line_regex) } },
    "Migration version lines must be separated by comma and end with semicolon"
  )
end
diff_threshold() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 39
def diff_threshold
  # TODO: some config for this?
  200
end
gitignored?(path) click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 171
def gitignored?(path)
  # TODO: get this from rugged (but it's not exposed to plugins) or make more compatible
  `git check-ignore #{path}` != ""
rescue Errno::ENOENT # when git not present
  nil
end
migration_line_regex() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 182
def migration_line_regex
  /\A\s*\('(?<version>[0-9]{14})'\)/
end
migration_patches() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 194
def migration_patches
  # nb: there may be engines added to migrations_paths in config or database.yml
  # but cannot check for this without more knowledge into the particular app
  # rails uses Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
  @migration_patches ||= @patches.select do |patch|
    patch.delta.added? && patch.delta.new_file[:path] =~ %r{db/migrate/.*[0-9]+_\w+.rb}
  end
end
migration_version_numbers() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 93
def migration_version_numbers
  @migration_version_numbers ||= migration_patches.map do |patch|
    patch.delta.new_file[:path].sub(/^[^0-9]*([0-9]+).+/, '\1')
  end
end
schema_file_name() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 178
def schema_file_name
  "db/schema.rb"
end
schema_migration_version() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 81
def schema_migration_version
  return @schema_migration_version if defined? @schema_migration_version

  @schema_migration_version ||= File.read(schema_file_name)
                                    .match(/ActiveRecord::Schema.define\(version:\s*(?<version>[0-9_]+)\s*\)/)
                                    &.[](:version)&.gsub(/[^0-9]/, "") ||
                                (add_message_at_patch(migration_patches.first,
                                                      "Cannot detect migration version in schema.rb. "\
                                                      "Migration versions are not checked.",
                                                      :warning) && nil)
end
schema_patches() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 215
def schema_patches
  @schema_patches = @patches.select { |patch| patch.delta.new_file[:path] =~ %r{db/schema.rb} }
end
structure_patches() click to toggle source
# File lib/pronto/rails_migrations_annotated.rb, line 219
def structure_patches
  @structure_patches = @patches.select { |patch| patch.delta.new_file[:path] =~ %r{db/structure.sql} }
end