Пролог

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

Порядок объявления функций должен совпадать с порядком определения функций.

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

Понятное дело, что это требование ну никак не влияет на поведение программы во время исполнения. Не делает исполнение кода ни лучше, ни хуже. Зато требует дополнительные серьезные усилия, ресурсы и время для его исполнения. И всё это вообще похоже на классическую IT-муштру. Как будто у нас других забот больше нет, как перетасовывать порядок Си-функций.

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

  1. У компилятора GCC нет таких ключей, которые бы выявили разный порядок в объявлении и определении функций.

  2. В статическом анализаторе CppCheck нет таких ключей, чтобы выявить разный порядок в объявлении и определении функций

  3. В платном статическом анализаторе Understand (scitools) тоже нет таких ключей, чтобы выявить разный порядок в объявлении и определении функций

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

Поэтому я и решил написать такую утилиту. В этом есть потребность. И у меня это получилось.

Постановка задачи

Написать консольную утилиту, которая будет сообщать программисту о нарушениях соответствия последовательности объявления и определения функций в программах на Си.

Работать с утилитой должно быть просто. Буквально даешь ей *.с файл,

prototype_check.exe cgp dds.c

а утилита сама находит одноименный *.h файл, вычитывает последовательности объявления и вычитывает последовательности определения функций, сравнивает их и сигнализирует об ошибке в виде return кода (0 - успех 1 ошибка).

Терминология

Прежде чем двигаться дальше надо кое‑что запомнить.

тэг (токен) — это текстовая строка, которая может быть либо названием функции, названием переменной.

СygWin — набор Unix утилит для операционной системы Windows.

Что надо из софвера?

Я собираюсь решить эту задачу самым обыкновенным инструментарием из CygWin

Название утилиты

Назначение

1

сtags

Создает файл со списком тэгов для данного языка программирования. Эдакий индексатор исходных кодов.

2

awk/gawk

программируемый анализатор текстовых строчек

3

sed

утилита для авто удаления или авто замены строчек в текстовых файлах

5

cmp

утилита для сравнения текстовых файлов

6

rm

утилита для удаления файлов

Каков план?

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

Реализация

Есть одна старая и очень полезная утилита. Называется ctags. Это по сути анализатор токенов в разных языках программирования. В частности получить список функций внутри *.с файла можно как раз утилитой сtags. Утилиту сtags можно извлечь из CygWin

После установки ctags надо прописать путь к утилитам СygWin (C:\cygwin64\bin) в переменную PATH. После этого утилита where должна находить утилиту ctags

C:\Users\Name>where ctags
C:\cygwin64\bin\ctags.exe

С какими ключами надо запускать ctags?

Ключ утилиты

Действие ключа

1

--sort=no

Не сортировать строчки в выходной таблице с отчётом

2

-fxxxxx

Писать отчет в файл xxxxx

3

-x --c-types=f

Сгенерировать отчет по функциям языка программирования Си

Фаза 1: Получить список всех функций в Си файле

После отработки по *.с файлу с такими ключами появляется вот такой отчет в виде таблицы

вывод утилиты ctags.exe для *.с файла
DDS_GetNode      function    151 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c DDS_HANDLE* DDS_GetNode(const U8 num)
DDS_CalcSinSample function    187 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c FLOAT32 DDS_CalcSinSample(const U64 upTimeUs,
DDS_Ctrl         function    216 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c STD_RESULT DDS_Ctrl(const U8 num,
DDS_Init         function    249 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c STD_RESULT DDS_Init(void)
DDS_InitOne      function    289 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c STD_RESULT DDS_InitOne(const U8 num)
DDS_Play         function    339 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c STD_RESULT DDS_Play(const U8 num,
DDS_Play1kHz     function    388 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c STD_RESULT DDS_Play1kHz(const U8 num,
DDS_Proc         function    414 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c STD_RESULT DDS_Proc(void)
DDS_SetArray     function    454 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c STD_RESULT DDS_SetArray(const U8 num,
DDS_SetFence     function    513 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c STD_RESULT DDS_SetFence(const U8 num,
DDS_SetFramePerSec function    546 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c STD_RESULT DDS_SetFramePerSec(const U8 num,
DDS_SetPattern   function    572 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c STD_RESULT DDS_SetPattern(const U8 num,
DDS_SetPwm       function    602 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c STD_RESULT DDS_SetPwm(const U8 num,
DDS_SetSaw       function    641 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c STD_RESULT DDS_SetSaw(const U8 num,
DDS_SetSin       function    676 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c STD_RESULT DDS_SetSin(const U8 num,
DDS_Stop         function    717 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c STD_RESULT DDS_Stop(const U8 num)
DDS_GetConfig    function    753 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c const DDS_CONFIG* DDS_GetConfig(const U8 num)
DDS_ProcOne      function    791 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c static STD_RESULT DDS_ProcOne(const U8 num)
DDS_OnOffToState function    838 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c static DDS_STATE DDS_OnOffToState(const U8 onOff)
DDS_IsValidPlayer function    868 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c static STD_RESULT DDS_IsValidPlayer(const DDS_PLAYER player)
DDS_IsValidSignal function    919 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c static STD_RESULT DDS_IsValidSignal(const DDS_SIGNAL ddsSignal)
DDS_IsValidFramePattern function    967 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c static STD_RESULT DDS_IsValidFramePattern(const DDS_SAMPLE_PATTERN samplePattern)
DDS_IsValidConfig function   1005 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c static STD_RESULT DDS_IsValidConfig(const DDS_CONFIG* const Config)
DDS_IsValidSampleBitness function   1119 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c static STD_RESULT DDS_IsValidSampleBitness(const U8 sampleBitness)
DDS_CalcMaxTimeNs function   1156 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c static U32 DDS_CalcMaxTimeNs(DDS_HANDLE* const Node,
DDS_CalcOneSampleLowLevel function   1182 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c static DDS_SAMPLE_TYPE DDS_CalcOneSampleLowLevel(DDS_HANDLE* const Node,
DDS_CalcStoreOneSampleLowLevel function   1231 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c static STD_RESULT DDS_CalcStoreOneSampleLowLevel(DDS_HANDLE* const Node,
DDS_PlayerToI2sNum function   1275 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c static S16 DDS_PlayerToI2sNum(const DDS_PLAYER player)
DDS_SetValidFreq function   1317 C:/projects/code_base_workspace/era_test/source/third_party/computing/dds/dds.c static FLOAT32 DDS_SetValidFreq(const FLOAT32 frequencyHz)

Как можно убедиться, порядок перечисления функций в отчете совпадает с порядком их определения в исходном Си файле.

Фаза 2: Удалить Static функции

Из отчета надо удалить строчки которые отвечают за локальные функции. Это можно сделать утилитой sed

sed -i '/static/d' cTagFunctionReport.txt

Фаза 3: Выделить только имена функций

Из отчета надо удалить всяческую вспомогательную информацию: номер строчки, путь к файлу, кусок текста. Это можно сделать утилитой awk. Вот так.

gawk '{print $1}' cTagFunctionReport.txt > ctags_function_report_c_functions.txt

после этого получается чистый файл со списком имен функций.

список функций с сохранением порядка
DDS_GetNode
DDS_CalcSinSample
DDS_Ctrl
DDS_Init
DDS_InitOne
DDS_Play
DDS_Play1kHz
DDS_Proc
DDS_SetArray
DDS_SetFence
DDS_SetFramePerSec
DDS_SetPattern
DDS_SetPwm
DDS_SetSaw
DDS_SetSin
DDS_Stop
DDS_GetConfig

Теперь надо проделать то же самое только для h файла.

Фаза 4: Сгенерировать ctags отчет для *.h файла

Сформировать отчет по функциям для *.h файла. Заметьте тут опция другая (--kinds-c=fp).

ctags.exe  --sort=no --kinds-c=fp -fctagsReport.txt dds.h

Фаза 5: Удалить преамбулу

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

sed -i '/!/d' ctagsReport.txt

Фаза 6: Выделить только функции

Выделить из отчёта только функции. Это по сути первая колонка.

gawk '{print $1}' dds.txt > dds_h_functions.txt

Фаза 7: Сравнить последовательности объявления и определения

Так как в файлах dds_h_functions.txt dds_c_functions.txt кристаллизовались фактические последовательности объявлений и определений, то задача свелась к простому сравнению текстовых файлов.

cmp -s dds_h_functions.txt dds_c_functions.txt

Если 0, то файлы одинаковые.

Полный скрипт

Скрипт на CMD выглядит вот так:

set file_h=dds.h
set cTagFile=cTag.txt
"" > %cTagFile%
set FunctionListInC=cFunctions.txt
set file_c=dds.c
set options=--sort=no
set options=%options% -x --c-types=f
set options=%options% -w
set options=%options% -f%cTagFile%
ctags.exe   %options%  %file_c%
sed -i '/static/d' %cTagFile%
gawk '{print $1}' %cTagFile% > %FunctionListInC%
set FunctionListInH=hFunctions.txt
set hTagFile=hTag.txt
"" > %hTagFile%
set options_h=--sort=no
set options_h=%options_h% --kinds-c=fp
set options_h=%options_h% -f%hTagFile%
ctags.exe   %options_h%  %file_h%
sed -i '/!/d' %hTagFile%
gawk '{print $1}' %hTagFile% > %FunctionListInH%
cmp -s %FunctionListInH% %FunctionListInC%
echo errorlevel=%errorlevel%
if "%errorlevel%"=="0" (echo same) else (echo diff)

Однако скриптовая реализация мне не очень нравится. В скрипт можно залезть ногами и натоптать там так, что он перестанет работать. Поэтому я написал на Си программную смесь чтобы решить конкретно эту задачу. Не больше ни меньше. Я назвал утилиту prototype_check. Утилита просто вызывает консольные команды и печатает лог.

Отладка утилиты

Вот тут утилита prototype_check нашла рассинхрон между последовательностью декларации и определения функций.

Утилита prototype_check сгенерировала файлы nau8814_driver_c_functions.txt и nau8814_driver_h_functions.txt в которых можно увидеть какие именно функции сбились из строя

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

Итоги

Удалось сделать консольную утилиту, которая позволяет проверить, что в *.h файле порядок объявления функций тот же самый, что и порядок определения функций в *.с файле.

Эта утилита позволит автоматически контролировать нарушение вот такого пресловутого требования к оформлению кода.

Заметьте что при разработке tool(ы) были использованы существующие технологии. Утилиты ctags, sed, awk, rm, cmp, cmd и gcc.

Если Вам нужна такая утилита, то пишите. Я пришлю *.exe бинарь.

Ссылки

Комментарии (28)


  1. cls0
    19.09.2024 16:37

    В проекте LLVM есть инструменты для source-to-source преобразований. Выглядит как очень подходяще для того, чтобы не только выявить нарушения соответствия порядка объявление-определение, но и сразу перестроить на нужный порядок.


    1. aabzel Автор
      19.09.2024 16:37

      Вы @cls0 можете привести скрипт, который решает вот эту конкретную задачу средствами LLVM?


  1. viordash
    19.09.2024 16:37
    +1

    а что указано в правилах если h файл общий для нескольких модулей?


    1. aabzel Автор
      19.09.2024 16:37

      в правилах это запрещено


  1. apevzner
    19.09.2024 16:37
    +1

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


    1. aabzel Автор
      19.09.2024 16:37

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

      Очевидно кто-то очень сильно заинтересован, чтобы высоко технологические проекты в России, скажем так, не слишком быстро доходили до серийного производства.


    1. aabzel Автор
      19.09.2024 16:37


    1. aabzel Автор
      19.09.2024 16:37

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

      Вот как это правило прокомментировал его автор:


      1. apevzner
        19.09.2024 16:37
        +1

        А разве поддержание одинакового порядка объявлений и реализаций функций - это "очевидная фундаментальная вещь"?


        1. aabzel Автор
          19.09.2024 16:37

          Вот то -то же. Автор требования по ходу аутист.


    1. aabzel Автор
      19.09.2024 16:37

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


  1. simplepersonru
    19.09.2024 16:37
    +1

    Такое добавили в QtCreator, можете подсмотреть у них в исходниках. Вот связанная задача


    1. aabzel Автор
      19.09.2024 16:37

      Ок, а шапки с комментариями перед функциями он не забудет тоже переместить?


      1. simplepersonru
        19.09.2024 16:37
        +1

        Если имеются ввиду шапки в .h, то они перемещены не будут, т.к. определения приводятся к порядку объявлений, а не наоборот

        Если у вас в .cpp есть шапки с комментами, не знаю, такие сценарии видел крайне редко


        1. apevzner
          19.09.2024 16:37

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


          1. simplepersonru
            19.09.2024 16:37
            +2

            Вот вам тема для еще одной статьи.

            1. Придумываем правило: комментарий документации в .h не должен иметь расхождений с дубликатом в .cpp.

            2. Пишем утилиту по автоматической синхронизации шапок комментариев
              .h -> .cpp (подсказка, как это можно сделать по-взрослому -> clang умеет привязывать doxygen комменты к узлам AST C++)

            3. profit


            1. artptr86
              19.09.2024 16:37

              По идее тогда надо тогда уже сделать не синхронизацию .h -> .cpp, а по по более свежей правке согласно истории коммитов.


          1. OldFashionedEngineer
            19.09.2024 16:37

            Ещё дублировать приходится потому, что в эклипсе всплывающая подсказка показывает текст функции из сурса. А программист пользуется хидером. Вот и приходится шапку писать и там и там


  1. redfox0
    19.09.2024 16:37

    Если вы программист на Си, то почему бы не написать собственный линтер на Си и не городить десятки make-файлов? Так уж и быть, линтер вызывать из make-файла.


  1. Panzerschrek
    19.09.2024 16:37
    +2

    Начинание хорошее, но реализация странная.
    Раз существующие инструменты не имеют нужного функционала, почему бы их не доработать? Исходники то ведь открыты.

    К тому же парсить C++ код чем-то отличным от компилятора C++ - так себе идея, ибо неизбежно парсинг будет ломаться на каких-то хитрых случаях с макросами.


    1. redfox0
      19.09.2024 16:37

      Отличное замечание. PVS-Studio не устаёт повторять, что нужно учитывать настройки компилятора, флаги компилятор и параметры сборки (какие-нибудь добавляемые макросы).

      Так что правильнее уже парсить препроцессированный исходник (после выхлопа препроцессора).


      1. redfox0
        19.09.2024 16:37

        А можно пойти проще: полностью сгенерированные заголовочные файлы. Даже самописный парсер грамматики должен оказаться не слишком сложным. Берётся cpp-файл и парсится сверху вниз. Встретили функцию - сгенерировали для неё объявление с комментарием. Если функия статическая - пропускаем.

        Другие разработчики могут продолжать добавлять в h-файл свой код, просто при следующем запуске генератора файл перезатрётся.


        1. apevzner
          19.09.2024 16:37
          +1

          Грамматика C++ очень сложная, и очень зависит от контекста. Например, одна и та же строка может обозначать объявление переменной, а может - вызов функции. Не порезолвив символы с учетом контекста и области видимости, этого даже и не поймешь.

          Qt-ный moc ведь так и работает, пытается понять C++ и генерирует метаданные. Но он не полностью C++ понимает и в каких-то сложных случаях ошибается. А там его не один человек пишет, и очень давно.


  1. Azeront
    19.09.2024 16:37

    1) Почему бы сразу не написать утилиту сортировки h-файлов под порядок определения c-файлов? Или, как указали в комментариях выше, почему бы не сделать утилиту для генерации h-файлов из c-файлов, с правильным порядком, комментариями, и прочее? Зачем ограничиваться полумерами? И еще, я так понял, упорядоченность "экспортируемых" переменных не контролируется?

    2) Что в вашем понимании “топтаться ногами в скрипте"? Это предъявляемое условие информационной безопасности, или же реально бывали случаи, когда программисты в состоянии аффекта ломали свои же инструменты?


    1. aabzel Автор
      19.09.2024 16:37

      Почему бы сразу не написать утилиту сортировки h-файлов под порядок определения c-файлов?

      Это сложная задача. Вот попробуйте сами такую сделать...


    1. aabzel Автор
      19.09.2024 16:37

      Или, как указали в комментариях выше, почему бы не сделать утилиту для генерации h-файлов из c-файлов, с правильным порядком, комментариями, и прочее?

      Дело в том что в си файле аргументы по требованиям перечислены в столбик. А такое оформление очень сложно парсить кодом.
      Это сложная задача. Вот попробуйте сами такую сделать...


      1. devprodest
        19.09.2024 16:37

        Если уж на то пошло, то с таким шаблоном форматирования можно справится регулярками, благо у вас си, а не плюсы.

        Заодно можно и легко отсеять все статические функции.


    1. aabzel Автор
      19.09.2024 16:37

       Что в вашем понимании “топтаться ногами в скрипте"?

      Можно случайно так поставить лишние пробел, что скрипт перестанет работать.