Стилистический-Анализатор: Проверка Наличия Комментария в Конце Фигурной Скобки

90a286b74e53438c0c524f99feee421c.png

Пролог

Настал тот первый день, когда в программировании микроконтроллеров наконец пригодилась такая структура данных как стек LIFO. Он же стек.

На бытовом уровне все мы так или иначе имели дело со стеком. Стек это, по сути, стопка игральных карт на столе, пули в обойме штурмовой винтовки AK-47, стопка тарелок на кухне, стопка книг на столе, RAM память для локальных функций внутри компьютерных программ тоже растет и уменьшается по правилу LIFO.

Всё это примеры стека (LIFO). Это когда брать и класть «чиво-либо» можно только с одной стороны.

Сейчас объясню при каких именно обстоятельствах мне понадобился стек на работе …

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

блок кода xxx () {} должен заканчиваться комментарием «end of …» (см. шаблон)

В переводе на кухонный язык это значит, что в конце каждого блока if (…) {…} ; switch (…) {…} ; for (…) {…} и т.п. необходимо пиcать комментарий

// end of if (…). end of switch (…) end of for (…) соответственно.

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

Вот так выглядит это художество в образцовом куске кода: Это тот самый «см. шаблон», который надо как попугай всегда и везде повторять.


//**************************************************************************************************
//! [Description of MODULE_FunctionTwo]
//!
//! \note       [text]
//!
//! \param[in]  parameterZero - [description of parameterZero]
//! \param[in]  parameterOne - [description of parameterOne]
//!
//! \return     [Description of return value]
//**************************************************************************************************
static DATA_TYPE MODULE_FunctionTwo(DATA_TYPE parameterZero,
                                    DATA_TYPE parameterOne)
{
    DATA_TYPE returnValue;
    
    // [Description...]
    switch (expression)
    {
        case CASE_ONE:
            caseOneCnt++;
            break;

        case CASE_TWO:
            caseTwoCnt++;
            break;

        default:
            caseDefaultCnt++;
            break;
    } // end of switch (expression)
    
    return returnValue;
} // end of MODULE_FunctionTwo()

Как можно заметить, тут после switch присутствует комментарий // end of switch (expression). И в конце имени функции тоже // end of MODULE_FunctionTwo ().

У нас на цензуре в Gerrit коммит просто не примут в ветку main, если хотя бы в одном месте нет этого комментария // end of xxx (). Для этого у нас в организации есть специальный надзиратель — апологет именно правила end of xxx (), который даже не глядя на функционал программного компонента и модульные тесты (про которые он, к слову, даже ничего не слышал) просто банит коммиты, если хотя бы в одном месте нет этого текстового комментария // end of xxx (…) .

А теперь внимание…

При этом сам программист апологет требования комментариев // end of xxx () это требование в своих исходниках сам игнорирует!

Вот так… Правила да не для всех…

Даже язык не поворачивается назвать такие порядки словом «инспекция программ». Это самая настоящая цензура.

Вот такие пирожки с капустой… Понимаете? А мы с этим живем…

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

Однако, требование такое есть. Раз надо, так надо…

В чём проблема?

Проблема в том, что вручную прослеживать везде исполнение этого нелепого правила очевидно очень утомительно. Особенно в файлах, где уже 5000–7000+ строк кода.

Поэтому, как ни крути, тут нужна волшебная палочка — консольная утилита-локатор, которая сама будет находить все места, где отсутствуют комментарии // end of xxx () после закрывающейся фигурной скобочки }.

При этом за 30 лет существования этого правила в этой организации никто из программистов эту утилиту тут так не написал. За 30 лет! Вот так…

До сегодняшнего дня.

Любая разработка начинается только тогда, когда появляются полноценные средства для отладки.

Реализация

Текстовое описание алгоритма

Решить эту задачу можно при помощи такой классической структуры данных как LIFO (стек). Идея в следующем.

Открыть *.c файл и читать его строчка за строчкой и символ за символом. Как только встретится символ { положить в стек структуру, которая запомнит номер строки и тип скобки. Когда встретиться } тоже запомнить в стек структуру с информацией про скобку.

Переменная

Возможные значение

1

тип скобки

{ или }

2

номер строки

натуральное число

После каждого добавления в стек скобки } следует проверять можно ли сократить скобки. Если предпоследний элемент в стеке это {, а последний элемент в стеке это }, то можно сократить.

475757f03868545fe3575f42185df687.png

В этом случае мы извлекаем два последние элемента из LIFO. Вычисляем разницу номеров строк. Если значение больше 4 — проверяем есть ли комментарий // end of xxx (). Если есть, то идем дальше. Если нет, то выдаём красное сообщение ошибки, что на строке N отсутствует комментарий // end of xxx () и увеличивает счетчик ошибок.

Вот так просто и не затейливо. Между делом утилита ещё и проверяет баланс открытых и закрытых фигурных скобочек.

И так до конца *.c файла.

При этом утилиту я написал на Си за два вечера и ядро функционала составило менее чем 250 строк

ядро утилиты

#include "end_of_block.h"

#include 
#include 
#include 

#include "log.h"
#include "file_pc.h"
#include "lifo_array.h"
#include "lifo_array_diag.h"
#include "str_utils_ex.h"
#include "csv.h"


bool end_of_block_mcal_init(void) {
    bool res = true;
    log_level_get_set(LINE, LOG_LEVEL_INFO);
    log_level_get_set(END_OF_BLOCK, LOG_LEVEL_INFO);
    LOG_INFO(END_OF_BLOCK, "END_OF_BLOCK_VERSION:%u", END_OF_BLOCK_DRIVER_VERSION);
    return res;
}

static bool end_of_block_proc_brace_open(EndOfBlockHandle_t *const Node) {
    bool res = false;
    //push to stack
    BraceInfo_t* Brace = (BraceInfo_t*) malloc(sizeof(BraceInfo_t));
    if(Brace) {
        Brace->dir = BRACE_DIR_OPEN;
        Brace->line_number = Node->cur_line;
        Brace->code = END_OF_BLOCK_ID;

        Array_t Elem = {0};
        Elem.pArr = (uint8_t*)Brace;
        Elem.size = sizeof(BraceInfo_t);
        res = lifo_arr_push(&(Node->LifoArray), Elem);
        if(res){
            LOG_DEBUG(END_OF_BLOCK, "Line:  %7u  ,LifoArrayPush {",Node->cur_line);
        }else{
            LOG_ERROR(END_OF_BLOCK,"ErrPush");
        }
    }else{
        LOG_ERROR(END_OF_BLOCK,"ErrMalloc");
    }
    return res;
}

static bool EndOfBlockIsValidBrace(BraceInfo_t* Node){
    bool res = false;
    if(Node) {
        if((BRACE_DIR_CLOSE==Node->dir) || (BRACE_DIR_OPEN==Node->dir))
        {
            if(0line_number){
                if(END_OF_BLOCK_ID==Node->code){
                    res = true;
                }else{
                    LOG_ERROR(END_OF_BLOCK,"NoCodeID");
                }
            }else{
                LOG_ERROR(END_OF_BLOCK,"NotLine");
            }
        }else{
            LOG_ERROR(END_OF_BLOCK,"NotBrase");
        }
    }

    if(false==res){
        LOG_ERROR(END_OF_BLOCK,"%s",BraceInfoToStr(Node));
    }
    return res;
}

bool end_of_block_try_reduce(EndOfBlockHandle_t *const Node) {
    bool res = false;
    LOG_DEBUG(END_OF_BLOCK,  "TryReduce");
    Array_t PrevNode={0};
    res = lifo_arr_peek_num(&(Node->LifoArray),   0, &PrevNode);
    if(res) {
        res = LivoIsValidItem(&PrevNode);

        BraceInfo_t PrevBrace={0};
        PrevBrace =    *((BraceInfo_t*)     PrevNode.pArr);
        //memcpy((void *)&PrevBrace,(void *)PrevNode.pArr,sizeof(BraceInfo_t));
        //memcpy((void *)&PrevBrace,(void *)PrevNode.pArr,sizeof(BraceInfo_t));

         Array_t PrevPrevNode={0};
        res = lifo_arr_peek_num(&(Node->LifoArray),   1, &PrevPrevNode);
        if (res) {
            res = LivoIsValidItem(&PrevPrevNode);

            BraceInfo_t PrevPrevBrace;
            PrevPrevBrace =    *((BraceInfo_t*)     PrevPrevNode.pArr);
            //memcpy((void *)&PrevPrevBrace,(void *)PrevPrevNode.pArr,sizeof(BraceInfo_t));

            res = EndOfBlockIsValidBrace(&PrevBrace);
            res = EndOfBlockIsValidBrace(&PrevPrevBrace);

            if(BRACE_DIR_CLOSE==PrevBrace.dir) {
                if(BRACE_DIR_OPEN==PrevPrevBrace.dir) {
                    LOG_DEBUG(END_OF_BLOCK,  "Spot{} pair");
                    Node->pair_cnt++;
                    uint32_t line_diff = PrevBrace.line_number - PrevPrevBrace.line_number;
                    if (Node->line_threshold < line_diff) {
                        char* subStr = strstr(Node->curLine,"end of");
                        if(subStr) {
                            res = true;
                            Node->ok_counter++;
                        } else {
                            Node->violation_counter++;
                            LOG_ERROR(END_OF_BLOCK,"Err:%3u,%s:Line: %7u   ,lack[ // end of xxx() ]",
                                    Node->violation_counter,
                                    Node->fileShortName,
                                    PrevBrace.line_number
                                    );
                            res = true;
                        }
                    }
                    res = lifo_arr_delete_cnt(&(Node->LifoArray), 2) ;
                }
            }
        }else{
            LOG_ERROR(END_OF_BLOCK,"PeekErr");
        }
    }else{
        LOG_ERROR(END_OF_BLOCK,"PeekErr");
    }
    return res;
}

static bool end_of_block_proc_brace_close(EndOfBlockHandle_t *const Node) {
    bool res = false;
    //push to stack
    BraceInfo_t *Brace = (BraceInfo_t*) malloc(sizeof(BraceInfo_t));
    if(Brace) {
        Brace->dir = BRACE_DIR_CLOSE;
        Brace->line_number = Node->cur_line;
        Brace->code = END_OF_BLOCK_ID;

        Array_t Elem;
        Elem.pArr = (uint8_t*)Brace;
        Elem.size = sizeof(BraceInfo_t);

        res = lifo_arr_push(&(Node->LifoArray), Elem);
        if(res){
            LOG_DEBUG(END_OF_BLOCK,  "Line:%3u,LifoArrayPush }",Node->cur_line);
            res = end_of_block_try_reduce(Node);
        }else{
            LOG_ERROR(END_OF_BLOCK,"ErrPush");
        }
    }else{
        LOG_ERROR(END_OF_BLOCK,"ErrMalloc");
    }
    return res;
}

static bool end_of_block_proc_byte(EndOfBlockHandle_t* Node, char letter) {
    bool res = false ;
    switch(letter){
        case '{': {
            res = end_of_block_proc_brace_open(Node);
        } break;
        case '}': {
            res = end_of_block_proc_brace_close(Node);
            //try reduce
            res = true;
        } break;
        case '\n': {res = true;} break;
        case '\r': {res = true;} break;
        default: {res = true;} break;
    }
    return res;
}

static bool end_of_block_proc_line(EndOfBlockHandle_t* Node){
    bool res = true;
    uint32_t len=strlen(Node->curLine);
    uint32_t i = 0 ;
    uint32_t ok_cnt = 0 ;
    for(i=0;icurLine[i]);
        if (res) {
            ok_cnt++;
        } else {
            LOG_ERROR(END_OF_BLOCK, "ProcByteErr:[%c]",Node->curLine[i]);
        }
    }
    if(len==ok_cnt) {
        res = true;
    }else{
        LOG_ERROR(END_OF_BLOCK, "ProcLineErr:[%s]",Node->curLine);
        res = false;
    }
    return res;
}

bool end_of_block_check(const char *const file_name_c, uint32_t lines ) {
    bool res = false;
    if (file_name_c) {
        if( 0 < lines) {
            EndOfBlockHandle_t EndOfBlock={0};
            EndOfBlock.line_threshold = lines;
            res = file_pc_realpath(file_name_c, EndOfBlock.fileNameC);
            if(res) {
                Array_t LifoArray[800] = {0};
                res = csv_parse_last_text(EndOfBlock.fileNameC, '/',
                                          EndOfBlock.fileShortName,
                                          sizeof(EndOfBlock.fileShortName) );
                if(res) {
                    res = lifo_arr_init(&EndOfBlock.LifoArray, LifoArray, ARRAY_SIZE(LifoArray));
                    log_res(END_OF_BLOCK, res, "LiFoInit");
                    if(res) {
                        LOG_DEBUG(END_OF_BLOCK, "CheckEndOfBlockCommentIn:[%s]", EndOfBlock.fileNameC);
                        EndOfBlock.filePtr = fopen(EndOfBlock.fileNameC, "r");
                        if(EndOfBlock.filePtr) {
                            LOG_DEBUG(END_OF_BLOCK, "OpenOkFile:[%s]", EndOfBlock.fileNameC);
                            while(NULL != fgets(EndOfBlock.curLine, END_OF_BLOCK_MAX_LINE_SIZE, EndOfBlock.filePtr)) {
                                EndOfBlock.cur_line++;
                                LOG_PARN(END_OF_BLOCK, "%u,%s",EndOfBlock.cur_line, EndOfBlock.curLine);
                                res = end_of_block_proc_line(&EndOfBlock);
                                if(res) {
                                    EndOfBlock.ok_cnt++;
                                }else{
                                    EndOfBlock.err_cnt++;
                                }
                            }
                            fclose(EndOfBlock.filePtr);
                        }
                    }
                }

            }else{
                LOG_ERROR(END_OF_BLOCK, "OpenFileErr:[%s]", EndOfBlock.fileNameC);
            }

            if(0==EndOfBlock.err_cnt) {
                res = true;
            } else {
                LOG_ERROR(END_OF_BLOCK, "Err:%u", EndOfBlock.err_cnt);
            }

            LOG_INFO(END_OF_BLOCK, "%s", EndOfBlockNodeReportToStr(&EndOfBlock));
        }
    }
    return res;
}

Однако в организации 600+ программистов за 30 лет никто даже этого не сделал. Это как?

Отладка

Мне удалось написать на Си консольную утилиту, которая находит в Си-коде все места, где отсутствуют комментарий после закрывающейся фигурной скобки }.

Взводится утилита следующим образом. Надо просто осуществить пуск *.bat файла с таким содержимым.

:: Windows cmd script
cls

set line_threshold=5
set file_name=C:/project/HAL/GPIO/gpio_drv.c 
code_style_check.exe eob %line_threshold% %file_name%
::  eob - end of block

Вот такой лог метаданных выдает утилита:

d4476f2a26edbd76b1d8a3f48a06cd6c.png

Вот и в другом файле с исходниками утилита обнаружила многочисленные нарушения нашего внутреннего code-style. Непорядок…

86476e72a6c9e992bd30d9092a9d33ce.png

Благодаря этому логу можно смело откопать все места и ликвидировать ошибки, добавив там комментарии // end of xxx ().

Дистрибутив утилиты

Если вы являетесь коллегой по несчастью, то можете тоже взять у меня готовую утилиту code_style_check.exe для решения задачи поиска отсутствующих комментариев.

Скачать tool (у) можно у меня с github

f3a39d3d50a150794e93a4b979df8707.png

Итог

Удалось автоматизировать процесс проверки правила №456 нашего внутреннего code-style при помощи отдельной специально разработанной консольной радар-утилиты целеуказания. Утилита сама локально выявит недочеты в коде ещё до комита в общак.

Надеюсь этот текст и выложенная утилита помогут Вам в прохождении цензуры при разработке программ для микроконтроллеров в России.

Ссылки

У меня присутствуют утилиты для выявления и других правил нашего внутреннего code-style. Про них можно почитать тут

© Habrahabr.ru