Categories

Как я патчил и рефакторил плагин what_column для rails

Ничего не предвещало беды

В текущем проекте у нас используется довольно полезный плагин what_column, который после каждой миграции обновляет файлы в app/models, добавляя в коментарии название атрибутов и типы полей. Примерно вот так:

class Post < ActiveRecord::Base
  # === List of columns ===
  #   id         : integer 
  #   title      : string 
  #   body       : string 
  #   created_at : datetime 
  #   updated_at : datetime 
  # =======================
end

Очень удобно видеть какие поля присутствуют у той или иной модели, не залезая в db/scheme.rb

Но не все с плагином оказалось хорошо

Недавно, пришлось добавить исключение (exception) в модель

class Post < ActiveRecord::Base
  class SomeOneIsLookingAtMyBeautifulPostException < ::StandardError; end
end

И после очередной миграции начались проблемы

rake db:migrate
uninitialized constant SomeOneIsLookingAtMyBeautifulPostException

Коментирование вложенного класса

# class SomeOneIsLookingAtMyBeautifulPostException < ::StandardError; end

не дало никакого результата.

Начинаем копать

Идем в исходники плагина и после небольшого изучения видим следующее

# vendor/plugins/what_column/lib/what_column.rb:41

if line.match(/class (.*)\</) and $1.strip.constantize == ar_class

Опа! Регулярка матчит любое вхождение class, не учитывая начало строки, то есть перед class может стоять все что угодно! Именно поэтому, когда я закоментировал определение класса, то получил ту же ошибку с uninitialized constant

Осталось понять почему же так происходит. Текст ошибки очень похож на NameError. Его может генерировать $1.strip.constantize

$1 содержит имя класса выдернутое регуляркой, #strip просто убирает пробелы по краям строки, значит смотрим код #constantize

# active_support-2.3.2/lib/inflector.rb:355-364

def constantize(camel_cased_word)
  names = camel_cased_word.split('::')
  names.shift if names.empty? || names.first.empty?

  constant = Object
  names.each do |name|
    constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
  end
  constant
end

Код говорит — чтобы превратить строку “Post::SomeOneIsLookingAtMyBeautifulPostException” в константу, нужно у Object‘a запросить Post, а затем у Post‘a запросить SomeOneIsLookingAtMyBeautifulPostException, то есть, так как определение SomeOneIsLookingAtMyBeautifulPostException находится внутри класса Post, значит напрямую запросив у Object‘a SomeOneIsLookingAtMyBeautifulPostException (что у нас и происходит) мы получим исключение

uninitialized constant SomeOneIsLookingAtMyBeautifulPostException

Бинго! С корнем проблемы разобрались.

Пишем патч

Добавим наши “случаи” в тесты, благо они в плагине присутствуют.

class Shop::Product < ActiveRecord::Base
  class ProductError < ::StandardError; end # <========== 
end

# class Shop::HotProduct < Product          # <==========

Пофиксим сначала случай с коментариями. Для этого дополним регулярку (она встречается в 2-х местах) символами ^\s*

/class (.*)\</ 
/^\s*class (.*)\</

^\s* означает, что в начале строки могут быть только пробелы или ничего, проверяем и видим, что закоментированные классы игнорируются — уже хорошо :-) пол дела сделали.

Добавим теперь rescue NameError чтобы перехватывать наши “неудавшиеся” классы

klass = begin
  line.match(/^\s*class (.*)\</) && $1.strip.constantize
rescue NameError
  # it means that we have defined subclasses in the class
  # for example
  #
  # class Order < ActiveRecord::Base
  #   class TrackingError < StandardError; end
  # end
  # 
  # "TrackingError".constantize will raise NameError because TrackingError doesn't exists in Object space
end

if klass == ar_class

Вуа-ля! теперь все работает!

Точнее почти все, еще возможен случай, что имя вложенного класса будет совпадать с именем в общем пространстве имен, но такие случаи редки, и самое худшее, что может сделать плагин — это добавить пару лишних коментариев, что не критично. Поэтому забьем.

Выводы

  1. Баги есть везде, даже в плагинах (на самом деле, глючных плагинов очень много)
  2. Изучайте исходники перед тем как добавлять плагин в проект, даже беглый взгляд по коду может выявить его неадекватность
  3. В опенсорсе баг вседа можно пофиксить, написать свой патч или форкнуться

Форк и рефакторинг

Я написал небольшой патч, который фиксит what_column, но после того как я внимательно посмотрел код плагина, мне он очень не понравился, я и форкнулся на гитхабе и потратил почти всю ночь на рефакторинг. Можете сравнить что было и что стало

P.S. Спасибо Крису (автору плагина) за то, что в проекте были тесты, они очень, нет, даже ОЧЕНЬ облегчили рефакторинг.

Пишите тесты :-)

Leave a Reply

 

 

 

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>