Tesseract — бесплатная программа для распознавания текста, разработанная Google. Согласно описанию проекта, «Tesseract, вероятно, является наиболее точным доступным механизмом распознавания текста с открытым исходным кодом». А что, если мы попытаемся отловить там какие-то баги с помощью анализатора PVS-Studio?

Тессеракт

Tesseract — это механизм оптического распознавания символов для различных операционных систем и представляет собой бесплатное программное обеспечение, первоначально разработанное как проприетарное программное обеспечение в лабораториях Hewlett Packard в период с 1985 по 1994 год, с некоторыми дополнительными изменениями, внесенными в 1996 году для переноса на Windows, и некоторым переходом с C на C++ в 1998 году. , Много кода было написано на C, а затем еще немного на C++. С тех пор весь код был преобразован, по крайней мере, для компиляции с помощью компилятора C++. В следующее десятилетие было сделано очень мало. Затем он был выпущен с открытым исходным кодом в 2005 году компанией Hewlett Packard и Университетом Невады в Лас-Вегасе (UNLV). Разработка Tesseract спонсируется Google с 2006 года. [взято из Википедии]

Исходный код проекта доступен в Google Code: https://code.google.com/p/tesseract-ocr/

Размер исходного кода составляет около 16 Мбайт.

Результаты анализа

Ниже я приведу те фрагменты кода, которые привлекли мое внимание при изучении отчета анализа PVS-Studio. Возможно, я мог что-то упустить, поэтому авторам Tesseract следует провести собственный анализ. Пробная версия активна в течение 7 дней, что более чем достаточно для такого небольшого проекта. Затем они сами решат, хотят ли они использовать инструмент регулярно и выявлять опечатки или нет.

Как обычно, напомню основной закон: методология статического анализа сводится к тому, чтобы использовать ее регулярно, а не в редких случаях.

Плохое подразделение

void LanguageModel::FillConsistencyInfo(....)
{
  ....
  float gap_ratio = expected_gap / actual_gap;
  if (gap_ratio < 1/2 || gap_ratio > 2) {
    consistency_info->num_inconsistent_spaces++;
  ....
}

Диагностические сообщения PVS-Studio: V636 Выражение «1 / 2» было неявно приведено из типа «int» в тип «float». Рассмотрите возможность использования явного приведения типа, чтобы избежать потери дробной части. Пример: двойной A = (двойной)(X)/Y;. language_model.cpp 1163

Программист хотел сравнить переменную gap_ratio со значением 0,5. К сожалению, он выбрал плохой способ записи 0.5. 1/2 — это целочисленное деление, которое дает 0.

Правильный код должен выглядеть так:

if (gap_ratio < 1.0f/2 || gap_ratio > 2) {

или это:

if (gap_ratio < 0.5f || gap_ratio > 2) {

Есть и другие фрагменты с подозрительным целочисленным делением. Некоторые из них также могут содержать действительно неприятные ошибки.

Ниже приведены фрагменты кода, которые необходимо проверить:

  • baselinedetect.cpp 110
  • bmp_8.cpp 983
  • cjkpitch.cpp 553
  • cjkpitch.cpp 564
  • mfoutline.cpp 392
  • mfoutline.cpp 393
  • normalis.cpp 454

Опечатка в сравнении

uintmax_t streamtoumax(FILE* s, int base) {
  int d, c = 0;
  ....
  c = fgetc(s);
  if (c == 'x' && c == 'X') c = fgetc(s);
  ....
}

Диагностическое сообщение PVS-Studio: V547 Выражение ‘c == ‘x’ && c == ‘X’’ всегда ложно. Вероятно, здесь следует использовать оператор «||». scanutils.cpp 135

Фиксированный чек:

if (c == 'x' || c == 'X') c = fgetc(s);

Неопределенное поведение

Я обнаружил одну интересную конструкцию, которую никогда раньше не видел:

void TabVector::Evaluate(....) {
  ....
  int num_deleted_boxes = 0;
  ....
  ++num_deleted_boxes = true;
  ....
}

Диагностическое сообщение PVS-Studio: V567 Неопределенное поведение. Переменная «num_deleted_boxes» изменена, поскольку используется дважды между точками последовательности. tabvector.cpp 735

Непонятно, что автор имел в виду под этим кодом; это должно быть результатом опечатки.

Результат этого выражения нельзя предсказать: переменная num_deleted_boxes может увеличиваться как до, так и после присваивания. Причина в том, что переменная изменяется дважды в одной точке следования.

Другие ошибки, вызывающие неопределенное поведение, связаны со сменами. Например:

void Dawg::init(....)
{
  ....
  letter_mask_ = ~(~0 << flag_start_bit_);
  ....
}

Диагностическое сообщение V610 Неопределенное поведение. Проверьте оператора сдвига ‘‹‹. Левый операнд ‘~0’ отрицательный. dawg.cpp 187

Выражение «~0» имеет тип «int» и оценивается как «-1». Сдвиг отрицательных значений приводит к неопределенному поведению, так что это просто удача, что программа работает хорошо. Чтобы исправить ошибку, нам нужно сделать «0» беззнаковым:

letter_mask_ = ~(~0u << flag_start_bit_);

Но это не все. Эта строка также вызывает еще одно предупреждение:

V629 Рассмотрите возможность проверки выражения ‘~0 ‹‹ flag_start_bit_’. Битовый сдвиг 32-битного значения с последующим расширением до 64-битного типа. dawg.cpp 187

Дело в том, что переменная letter_mask_ имеет тип uinT64. Насколько я понимаю, может потребоваться запись единиц в старшие 32 бита. В этом случае реализованное выражение некорректно, так как оно может обрабатывать только младшие значащие биты.

Нам нужно сделать «0» 64-битного типа:

letter_mask_ = ~(~0ull << flag_start_bit_);

Вот список других фрагментов кода, в которых сдвигаются отрицательные числа:

  • dawg.cpp 188
  • intmatcher.cpp 172
  • intmatcher.cpp 174
  • intmatcher.cpp 176
  • intmatcher.cpp 178
  • intmatcher.cpp 180
  • intmatcher.cpp 182
  • intmatcher.cpp 184
  • intmatcher.cpp 186
  • intmatcher.cpp 188
  • intmatcher.cpp 190
  • intmatcher.cpp 192
  • intmatcher.cpp 194
  • intmatcher.cpp 196
  • intmatcher.cpp 198
  • intmatcher.cpp 200
  • intmatcher.cpp 202
  • intmatcher.cpp 323
  • intmatcher.cpp 347
  • intmatcher.cpp 366

Подозрительное двойное назначение

TESSLINE* ApproximateOutline(....) {
  EDGEPT *edgept;
  ....
  edgept = edgesteps_to_edgepts(c_outline, edgepts);
  fix2(edgepts, area);
  edgept = poly2 (edgepts, area);  // 2nd approximation.
  ....
}

Диагностическое сообщение PVS-Studio: V519 Переменной edge дважды подряд присваиваются значения. Возможно, это ошибка. Проверить строки: 76, 78. polyaprx.cpp 78

Еще похожая ошибка:

inT32 row_words2(....)
{
  ....
  this_valid = blob_box.width () >= min_width;
  this_valid = TRUE;
  ....
}

Диагностическое сообщение PVS-Studio: V519 Переменной this_valid дважды подряд присваиваются значения. Возможно, это ошибка. Проверить строки: 396, 397. wordseg.cpp 397

Неправильный порядок инициализации члена класса

Давайте сначала рассмотрим класс «MasterTrainer». Обратите внимание, что элемент «samples_» записывается перед элементом «fontinfo_table_»:

class MasterTrainer {
  ....
  TrainingSampleSet samples_;
  ....
  FontInfoTable fontinfo_table_;
  ....
};

По стандарту члены класса инициализируются в конструкторе в том же порядке, в каком они объявлены внутри класса. Это означает, что «samples_» будет инициализирован ДО «fontinfo_table_».

Теперь давайте рассмотрим конструктор:

MasterTrainer::MasterTrainer(NormalizationMode norm_mode,
                             bool shape_analysis,
                             bool replicate_samples,
                             int debug_level)
  : norm_mode_(norm_mode), samples_(fontinfo_table_),
    junk_samples_(fontinfo_table_),
    verify_samples_(fontinfo_table_),
    charsetsize_(0),
    enable_shape_anaylsis_(shape_analysis),
    enable_replication_(replicate_samples),
    fragments_(NULL), prev_unichar_id_(-1),
    debug_level_(debug_level)
{
}

Проблема заключается в использовании еще не инициализированной переменной «fontinfo_table_» для инициализации «samples_».

Аналогичная проблема в этом классе связана с инициализацией полей «junk_samples_» и «verify_samples_».

Я не могу точно сказать, что делать с этим классом. Возможно, было бы достаточно просто переместить объявление «fontinfo_table_» в самое начало класса.

Опечатка в условии

Эта опечатка не очень хорошо видна, но анализатор всегда начеку.

class ScriptDetector {
  ....
  int korean_id_;
  int japanese_id_;
  int katakana_id_;
  int hiragana_id_;
  int han_id_;
  int hangul_id_;
  int latin_id_;
  int fraktur_id_;
  ....
};
void ScriptDetector::detect_blob(BLOB_CHOICE_LIST* scores) {
  ....
  if (prev_id == katakana_id_)
    osr_->scripts_na[i][japanese_id_] += 1.0;
  if (prev_id == hiragana_id_)
    osr_->scripts_na[i][japanese_id_] += 1.0;
  if (prev_id == hangul_id_)
    osr_->scripts_na[i][korean_id_] += 1.0;
  if (prev_id == han_id_)
    osr_->scripts_na[i][korean_id_] += kHanRatioInKorean;
  if (prev_id == han_id_)             <<<<====
    osr_->scripts_na[i][japanese_id_] += kHanRatioInJapanese;
  ....
}

Диагностическое сообщение PVS-Studio: V581 Условные выражения операторов if, расположенных рядом друг с другом, идентичны. Проверить строки: 551, 553. osdetect.cpp 553

Самое последнее сравнение, скорее всего, будет выглядеть так:

if (prev_id == japanese_id_)

Ненужные проверки

Нет необходимости проверять возвращаемый результат нового оператора. Если память не может быть выделена, будет выдано исключение. Можно, конечно, реализовать специальный оператор new, возвращающий нулевые указатели, но это частный случай (подробнее).

Имея это в виду, мы можем упростить следующую функцию:

void SetLabel(char_32 label) {
  if (label32_ != NULL) {
    delete []label32_;
  }
  label32_ = new char_32[2];
  if (label32_ != NULL) {
    label32_[0] = label;
    label32_[1] = 0;
  }
}

Диагностическое сообщение PVS-Studio: V668 Проверять указатель label32_ на null нет смысла, так как память была выделена с помощью оператора new. Исключение будет сгенерировано в случае ошибки выделения памяти. char_samp.h 73

Существует 101другой фрагмент, в котором проверяется указатель, возвращаемый оператором «новый». Перечислять их все здесь не вижу смысла — лучше запустите PVS-Studio и найдите их сами.

Вывод

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

И не забудьте подписаться на меня в Твиттере: @Code_Analysis. Я регулярно публикую там ссылки на интересные статьи по C++.