Tesseract е безплатна софтуерна програма за разпознаване на текст, разработена от Google. Според описанието на проекта, „Tesseract е може би най-точният наличен OCR двигател с отворен код“. И какво, ако се опитаме да хванем някои грешки там с помощта на анализатора на PVS-Studio?

Тесеракт

Tesseract е механизъм за оптично разпознаване на знаци за различни операционни системи и е безплатен софтуер, първоначално разработен като патентован софтуер в лабораториите на Hewlett Packard между 1985 и 1994 г., с още някои промени, направени през 1996 г. за пренасяне към Windows и известна миграция от C към C++ през 1998 г. Голяма част от кода беше написан на C, а след това още малко на C++. Оттогава целият код е преобразуван поне за компилиране с C++ компилатор. През следващото десетилетие беше свършена много малко работа. След това беше пуснат като отворен код през 2005 г. от Hewlett Packard и Университета на Невада, Лас Вегас (UNLV). Разработката на Tesseract е спонсорирана от Google от 2006 г. [взето от Wikipedia]

Изходният код на проекта е достъпен в Google Code: https://code.google.com/p/tesseract-ocr/

Размерът на изходния код е около 16 Mbytes.

Резултати от анализа

По-долу ще цитирам онези фрагменти от код, които привлякоха вниманието ми, докато разглеждах доклада за анализ на 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’’ винаги е false. Вероятно тук трябва да се използва операторът „||“. 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 На променливата ‘edgept’ се присвояват стойности два пъти последователно. Може би това е грешка. Проверете редове: 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_)

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

Няма нужда да проверявате върнатия резултат от оператора „нов“. Ако паметта не може да бъде разпределена, тя ще хвърли изключение. Можете, разбира се, да приложите специален „нов“ оператор, който връща нулеви указатели, но това е специален случай („научете повече“).

Имайки това предвид, можем да опростим следната функция:

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_“ срещу нула, тъй като паметта е разпределена с помощта на оператора „нов“. Изключението ще бъде генерирано в случай на грешка при разпределяне на паметта. char_samp.h 73

Има 101други фрагмента, където се проверява указател, върнат от оператора „нов“. Не намирам за разумно да ги изброявам всички тук - по-добре стартирайте PVS-Studio и ги намерете сами.

Заключение

Моля, използвайте статичен анализ редовно — това ще ви помогне да спестите много време, което да отделите за решаване на по-полезни задачи, отколкото за улавяне на глупави грешки и правописни грешки.

И не забравяйте да ме последвате в Twitter: @Code_Analysis. Там редовно публикувам връзки към интересни статии за C++.