Ничего не предвещало беды
В текущем проекте у нас используется довольно полезный плагин 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
Вуа-ля! теперь все работает!
Точнее почти все, еще возможен случай, что имя вложенного класса будет совпадать с именем в общем пространстве имен,
но такие случаи редки, и самое худшее, что может сделать плагин — это добавить пару лишних коментариев, что не критично.
Поэтому забьем.
Выводы
- Баги есть везде, даже в плагинах (на самом деле, глючных плагинов очень много)
- Изучайте исходники перед тем как добавлять плагин в проект,
даже беглый взгляд по коду может выявить его неадекватность
- В опенсорсе баг вседа можно пофиксить, написать свой патч или форкнуться
Форк и рефакторинг
Я написал небольшой патч, который фиксит what_column, но после того как я внимательно посмотрел код плагина,
мне он очень не понравился, я и форкнулся на гитхабе и потратил почти всю ночь на рефакторинг.
Можете сравнить что было и что стало
P.S. Спасибо Крису (автору плагина) за то, что в проекте были тесты, они очень, нет, даже ОЧЕНЬ облегчили рефакторинг.
Пишите тесты