Пролог
У нас в организации есть правило оформления исходников, которое звучит так:
Порядок объявления функций должен совпадать с порядком определения функций.
В чём проблема?
Понятное дело, что это требование ну никак не влияет на поведение программы во время исполнения. Не делает исполнение кода ни лучше, ни хуже. Зато требует дополнительные серьезные усилия, ресурсы и время для его исполнения. И всё это вообще похоже на классическую IT-муштру. Как будто у нас других забот больше нет, как перетасовывать порядок Си-функций.
Но раз это требование тут существует, то его, как ни крути, приходится выставлять. При этом возникает целый ряд вытекающих проблем
У компилятора GCC нет таких ключей, которые бы выявили разный порядок в объявлении и определении функций.
В статическом анализаторе CppCheck нет таких ключей, чтобы выявить разный порядок в объявлении и определении функций
В платном статическом анализаторе Understand (scitools) тоже нет таких ключей, чтобы выявить разный порядок в объявлении и определении функций
Очевидно также, что проблема и в том, что вручную глазами очень утомительно выявлять места нарушения этого странного и стерильного правила. Очевидно же, что было бы здорово составить консольную утилиту, которая сама, автоматически покажет и докажет, что этот пресловутый порядок объявления и определения не совпадает или совпадает.
Поэтому я и решил написать такую утилиту. В этом есть потребность. И у меня это получилось.
Постановка задачи
Написать консольную утилиту, которая будет сообщать программисту о нарушениях соответствия последовательности объявления и определения функций в программах на Си.
Работать с утилитой должно быть просто. Буквально даешь ей *.с файл,
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)
apevzner
19.09.2024 16:37+1Любопытно было бы узнать, откуда в организациях появляются такие правила оформления исходников...
aabzel Автор
19.09.2024 16:37Любопытно было бы узнать, откуда в организациях появляются такие правила оформления исходников...
Очевидно кто-то очень сильно заинтересован, чтобы высоко технологические проекты в России, скажем так, не слишком быстро доходили до серийного производства.
aabzel Автор
19.09.2024 16:37Любопытно было бы узнать, откуда в организациях появляются такие правила оформления исходников...
Вот как это правило прокомментировал его автор:
aabzel Автор
19.09.2024 16:37Любопытно было бы узнать, откуда в организациях появляются такие правила оформления исходников...
simplepersonru
19.09.2024 16:37+1Такое добавили в QtCreator, можете подсмотреть у них в исходниках. Вот связанная задача
aabzel Автор
19.09.2024 16:37Ок, а шапки с комментариями перед функциями он не забудет тоже переместить?
simplepersonru
19.09.2024 16:37+1Если имеются ввиду шапки в .h, то они перемещены не будут, т.к. определения приводятся к порядку объявлений, а не наоборот
Если у вас в .cpp есть шапки с комментами, не знаю, такие сценарии видел крайне редко
apevzner
19.09.2024 16:37Я обычно дублирую коментарии в сишниках и ашниках. Потому, что программист, который хочет ознакомиться с API, будет последовательно читать ашник, а программист, который идет по коду, пойдет в основном по сишникам.
simplepersonru
19.09.2024 16:37+2Вот вам тема для еще одной статьи.
Придумываем правило: комментарий документации в .h не должен иметь расхождений с дубликатом в .cpp.
Пишем утилиту по автоматической синхронизации шапок комментариев
.h -> .cpp (подсказка, как это можно сделать по-взрослому -> clang умеет привязывать doxygen комменты к узлам AST C++)profit
artptr86
19.09.2024 16:37По идее тогда надо тогда уже сделать не синхронизацию .h -> .cpp, а по по более свежей правке согласно истории коммитов.
OldFashionedEngineer
19.09.2024 16:37Ещё дублировать приходится потому, что в эклипсе всплывающая подсказка показывает текст функции из сурса. А программист пользуется хидером. Вот и приходится шапку писать и там и там
redfox0
19.09.2024 16:37Если вы программист на Си, то почему бы не написать собственный линтер на Си и не городить десятки make-файлов? Так уж и быть, линтер вызывать из make-файла.
Panzerschrek
19.09.2024 16:37+2Начинание хорошее, но реализация странная.
Раз существующие инструменты не имеют нужного функционала, почему бы их не доработать? Исходники то ведь открыты.
К тому же парсить C++ код чем-то отличным от компилятора C++ - так себе идея, ибо неизбежно парсинг будет ломаться на каких-то хитрых случаях с макросами.redfox0
19.09.2024 16:37Отличное замечание. PVS-Studio не устаёт повторять, что нужно учитывать настройки компилятора, флаги компилятор и параметры сборки (какие-нибудь добавляемые макросы).
Так что правильнее уже парсить препроцессированный исходник (после выхлопа препроцессора).
redfox0
19.09.2024 16:37А можно пойти проще: полностью сгенерированные заголовочные файлы. Даже самописный парсер грамматики должен оказаться не слишком сложным. Берётся cpp-файл и парсится сверху вниз. Встретили функцию - сгенерировали для неё объявление с комментарием. Если функия статическая - пропускаем.
Другие разработчики могут продолжать добавлять в h-файл свой код, просто при следующем запуске генератора файл перезатрётся.
apevzner
19.09.2024 16:37+1Грамматика C++ очень сложная, и очень зависит от контекста. Например, одна и та же строка может обозначать объявление переменной, а может - вызов функции. Не порезолвив символы с учетом контекста и области видимости, этого даже и не поймешь.
Qt-ный moc ведь так и работает, пытается понять C++ и генерирует метаданные. Но он не полностью C++ понимает и в каких-то сложных случаях ошибается. А там его не один человек пишет, и очень давно.
Azeront
19.09.2024 16:371) Почему бы сразу не написать утилиту сортировки h-файлов под порядок определения c-файлов? Или, как указали в комментариях выше, почему бы не сделать утилиту для генерации h-файлов из c-файлов, с правильным порядком, комментариями, и прочее? Зачем ограничиваться полумерами? И еще, я так понял, упорядоченность "экспортируемых" переменных не контролируется?
2) Что в вашем понимании “топтаться ногами в скрипте"? Это предъявляемое условие информационной безопасности, или же реально бывали случаи, когда программисты в состоянии аффекта ломали свои же инструменты?
aabzel Автор
19.09.2024 16:37Почему бы сразу не написать утилиту сортировки h-файлов под порядок определения c-файлов?
Это сложная задача. Вот попробуйте сами такую сделать...
aabzel Автор
19.09.2024 16:37Или, как указали в комментариях выше, почему бы не сделать утилиту для генерации h-файлов из c-файлов, с правильным порядком, комментариями, и прочее?
Дело в том что в си файле аргументы по требованиям перечислены в столбик. А такое оформление очень сложно парсить кодом.
Это сложная задача. Вот попробуйте сами такую сделать...devprodest
19.09.2024 16:37Если уж на то пошло, то с таким шаблоном форматирования можно справится регулярками, благо у вас си, а не плюсы.
Заодно можно и легко отсеять все статические функции.
aabzel Автор
19.09.2024 16:37Что в вашем понимании “топтаться ногами в скрипте"?
Можно случайно так поставить лишние пробел, что скрипт перестанет работать.
cls0
В проекте LLVM есть инструменты для source-to-source преобразований. Выглядит как очень подходяще для того, чтобы не только выявить нарушения соответствия порядка объявление-определение, но и сразу перестроить на нужный порядок.
aabzel Автор
Вы @cls0 можете привести скрипт, который решает вот эту конкретную задачу средствами LLVM?