Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение9 мин
Количество просмотров1.9K

Пролог

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

На бытовом уровне все мы так или иначе имели дело со стеком. Стек это, по сути, стопка игральных карт на столе, патроны в магазине штурмовой винтовки AK-47, стопка тарелок на кухне, свежеиспечённые блинчики на тарелке, стопка книг на столе, RAM память для локальных переменных внутри компьютерных программ тоже растет и уменьшается по правилу LIFO, говорят про стек протоколов в модели ISO-7. Синтаксический разбор XML файла требует работы LIFO. Даже детская дошкольная игрушка Ханойская башня - это тоже 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

номер строки

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

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

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

Если видит }, или }; то пропускать такие строчки. Это не конец функции или оператора, а конец структуры или массива.

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

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

ядро утилиты
#include "end_of_block.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "log.h"
#include "file_pc.h"
#include "lifo_array.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(0<Node->line_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;i<len;i++){
        res = end_of_block_proc_byte(Node, Node->curLine[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;
}

Как можно заметить, эта утилита использует такие программные зависимости как LIFO, CSV, LOG, STRING и FILE. Подразумевается, что у Вас в репозитории уже присутствует реализация на Си этих программных компонентов SWC.

Однако в организации 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

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

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

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

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

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

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

Итог

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

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

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

Словарь

Акроним

Расшифровка

SWC

SoftWare Component

LIFO

last-in-first-out

ISO

International Organization for Standardization

CSV

Comma-separated values

Ссылки

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

Название текста

URL

1

Стилистический анализатор: синхронизация объявлений и определений static функций

https://habr.com/ru/articles/846020/

2

Стилистический Анализатор: Синхронизация порядка объявлений и определений функций

https://habr.com/ru/articles/844436/

3

Интеграция Стилистического Анализа в общий Make Скрипт Сборки Проекта

https://habr.com/ru/articles/843746/

4

Нельзя Просто Так Пойти и Купить Овцу (или Потёмкинская Деревня в Коде)

https://habr.com/ru/articles/837396/

5

Интеграция clang-format в Процесс Сборки

https://habr.com/ru/articles/833500/

6

Почему Сборка с Помощью Есlipse ARM GCC Плагинов это Тупиковый Путь

https://habr.com/ru/articles/794206/

7

Дистрибутив утилиты code_style_check.exe

https://github.com/aabzel/Artifacts/tree/main/end_of_xxx

8

Синтаксический разбор CSV строчек

https://habr.com/ru/articles/765066/

Вопросы

1
-- Зачем в конце if(...) {} писать if(...) {} // end of if(...)?

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
У Вас в организации есть обязательное требование подписывать закрывающиеся фигурные скобки?
6.78% да4
93.22% нет55
Проголосовали 59 пользователей. Воздержались 3 пользователя.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вам приходилось использовать абстрактную структуру данных LIFO(стек) в программировании микроконтроллеров?
39.47% да15
60.53% нет23
Проголосовали 38 пользователей. Воздержались 10 пользователей.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
У Вас заработала утилита code_style_check.exe?
100% да1
0% нет0
Проголосовал 1 пользователь. Воздержались 4 пользователя.
Теги:
Хабы:
Всего голосов 14: ↑7 и ↓7+3
Комментарии99
8

Публикации

Истории

Работа

DevOps инженер
23 вакансии
Программист С
30 вакансий

Ближайшие события

25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань