Язык программирования с препроцессорными директивами сложен для обработки, поскольку в этом случае необходимо вычислять значения директив, вырезать некомпилируемые фрагменты кода, а затем производить парсинг очищенного кода. Обработка директив может осуществляться во время парсинга обычного кода. Данная статья подробно описывает оба подхода применительно к языку Objective-C, а также раскрывает их достоинства и недостатки. Эти подходы существуют не только в теории, но уже реализованы и используются на практике в таких веб-сервисах, как Swiftify и Codebeat. 
Swiftify — веб-сервис для преобразования исходников на Objective-C в Swift. На данный момент сервис поддерживает обработку как одиночных файлов, так и целых проектов. Таким образом, он может сэкономить время разработчикам, желающим освоить новый язык от Apple. 
Codebeat — автоматизированная система для подсчета метрик кода и проведения анализа различных языков программирования, в том числе и Objective-C. Обработка директив препроцессора осуществляется во время парсинга кода. Базовые понятия парсинга мы описывать не будем, однако здесь будут использоваться термины из статьи по теории и парсингу исходного кода с помощью ANTLR и Roslyn. В качестве генератора парсера в обоих сервисах используется ANTLR, а сами грамматики Objective-C выложены в официальный репозиторий грамматик ANTLR (Objective-C grammar). Нами было выделено два способа обработки препроцессорных директив: Одноэтапная обработка подразумевает одновременный парсинг директив и токенов основного языка. В ANTLR существует механизм каналов, позволяющий изолировать токены различных типов: например, токенов основного языка и скрытых токенов (комментариев и пробелов). Токены директив также могут быть помещены в отдельный именованный канал. Обычно токены директив начинаются со знака решетки (# или шарп) и заканчиваются символами разрыва строк (\r\n). Таким образом, для захвата подобных токенов целесообразно иметь другой режим распознавания лексем. ANTLR поддерживает такие режимы, они описываются так: mode DIRECTIVE_MODE;. Фрагмент лексера с секцией mode для препроцессорных директив выглядит следующим образом: Часть препроцессорных директив Objective-C преобразуется в определенный код на языке Swift (например, с использованием синтаксиса let): какие-то остаются в неизмененном виде, а остальные преобразуются в комментарии. Таблица ниже содержит примеры: Комментарии также нужно помещать в правильную позицию в результирующем Swift коде. Однако, как уже упоминалось, в дереве разбора отсутствуют сами скрытые токены. Действительно, скрытые токены можно включать в грамматику, но из-за этого она станет слишком сложной и избыточной, т.к. токены COMMENT и DIRECTIVE будут содержаться в каждом правиле между значимыми токенами: Поэтому о таком подходе можно сразу забыть. Возникает вопрос: как же все же извлекать такие токены при обходе дерева разбора? Как оказалось, существует несколько вариантов решения такой задачи, при котором скрытые токены связываются с нетерминальными или же терминальными (конечными) узлами дерева разбора. Данный способ заимствован из относительно старой статьи 2012 года по ANTLR 3. В этом случае все скрытые токены разбиваются на множества следующих типов: Чтобы лучше понять что означают эти типы рассмотрим простое правило, в котором фигурные скобки — терминальные символы, а в качестве statement может быть любое выражение, содержащее точку с запятой на конце, например присваивание a = b;. В таком случае все комментарии из следующего фрагмента кода попадут в список precending, т.е. первый токен в файле или токены перед нетерминальными узлами дерева разбора. Если комментарий является последним в файле, или же комментарий вставлен после всех statement (после него идет терминальная скобка), то он попадает в список following. Все остальные комментарии попадают в список orphans (все они по сути обособлены токенами, в данном случае фигурными скобками): Благодаря такому разбиению, все скрытые токены можно обрабатывать в общем методе Visit. Данный способ и сейчас используется в Swiftify, однако он достаточно сложный и строить достоверное (fidelity) дерево разбора с помощью него проблематично. Достоверность дерева заключается в том, что оно может быть преобразовано обратно в код символ в символ, включая пробелы, комментарии и директивы препроцессора. В будущем мы планируем перейти на использование способа для обработки препроцессорных директив и других скрытых токенов, описание которого вы увидите ниже. В данном случае скрытые токены связываются с определенным значимыми токенами. При этом скрытые токены могут быть лидирующими (LeadingTrivia) и замыкающими (TrailingTrivia). Этот способ сейчас используется в Roslyn парсере (для C# и Visual Basic), а скрытые токены в нем называются тривиями (Trivia). Во множество замыкающих токенов попадают все тривии на той же самой строчке от значимого токена до следующего значимого токена. Все остальные скрытые токены попадают в множество лидирующих и связываются со следующим значимым токеном. Первый значимый токен содержит в себе начальные тривии файла. Скрытые токены, замыкающие файл, связываются с последним специальным end-of-file токеном нулевой длины. Более детально о типах дерева разбора и тривиях написано в официальной документации по Roslyn. В ANTLR для токена с индексом i существует метод, который возвращает все токены из определенного канала слева или справа: getHiddenTokensToLeft(int tokenIndex, int channel), getHiddenTokensToRight(int tokenIndex, int channel). Таким образом, можно сделать так, чтобы парсер на основе ANTLR формировал достоверное дерево разбора, схожое с деревом разбора Roslyn. Так как при одноэтапной обработке макросы не заменяются на фрагменты кода Objective-C, их можно игнорировать или помещать в отдельный изолированный канал. Это позволяет избежать проблем при парсинге обычного кода Objective-C и необходимости включать макросы в узлы грамматики (по аналогии с комментариями). Это касается и макросов по умолчанию, таких как NS_ASSUME_NONNULL_BEGIN, NS_AVAILABLE_IOS(3_0) и других: Алгоритм двухэтапной обработки может быть представлен в виде следующей последовательности шагов: Третий шаг может быть пропущен, и макросы могут быть включены непосредственно в грамматику, по крайней мере частично. Однако данный метод все равно сложнее реализовать, чем одноэтапную обработку: в этом случае после первого шага необходимо заменять код препроцессорных директив на пробелы, если существует потребность в сохранении правильных позиций токенов обычного исходного кода. Тем не менее данный алгоритм обработки препроцессорных директив в свое время также был реализован и сейчас используется в Codebeat. Грамматики выложены на GitHub вместе с визитором, обрабатывающим препроцессорные директивы. Дополнительным плюсом такого метода является представление грамматик в более структурированной форме. Для двухэтапной обработки используются следующие компоненты: Напомним, что лексер группирует символы исходного кода в значимые последовательности, которые называются лексемами или токенами. А парсер строит из потока токенов связную древовидную структуру, которая называется деревом разбора. Визитор (Visitor) — паттерн проектирования, позволяющий выносить логику обработки каждого узла дерева в отдельный метод. Лексер, отделяющий токены препроцессорных директив и обычного Objective-C кода. Для токенов обычного кода используется DEFAULT_MODE, а для кода директив — DIRECTIVE_MODE. Ниже приведены токены из DEFAULT_MODE. При взгляде на этот фрагмент кода может возникнуть вопрос о необходимости дополнительных токенов (COMMENT, QUOTE_STRING и прочих), тогда как для кода Objective-C используется всего один токен — CODE. Дело в том, что символ # может быть спрятан внутрь обычных строк и комментариев. Поэтому такие токены необходимо выделять отдельно. Но это не является проблемой, поскольку их тип все равно изменяется на CODE, а в препроцессорном парсере для отделения токенов существуют следующие правила: Парсер, отделяющий токены кода Objective-C и обрабатывающий токены препроцессорных директив. Полученное дерево разбора затем передается препроцессору. Визитор, вычисляющий значения препроцессорных директив. Каждый метод обхода узла возвращает строку. Если вычисленное значение директивы принимает значение true, то возвращается последующий фрагмент кода Objective-C. В противном случае код Objective-C заменяется на пробелы. Как уже говорилось ранее, это необходимо для того, чтобы сохранить правильные позиции токенов основного кода. Для облегчения понимания приведем в качестве примера следующий фрагмент кода Objective-C: Этот фрагмент будет преобразован в следующий код на Objective-C при заданном условном символе DEBUG при использовании двухэтапной обработки. Стоит обратить внимание, что все директивы и некомпилируемый код превратились в пробелы. Директивы также могут быть вложенными друг в друга: Лексер обычного Objective-C без токенов, распознающих препроцессорные директивы. Если директив в исходном файле нет, то на вход поступает тот же самый оригинальный файл. Парсер обычного Objective-C кода. Грамматика данного парсера совпадает с грамматикой парсера из одноэтапной обработки. Существуют и другие способы обработки препроцессорных директив, например можно использовать безлексерный парсер. Теоретически в таком парсере можно будет совмещать достоинства как одноэтапной, так и двухэтапной обработки, а именно: парсер будет вычислять значения директив и определять некомпилируемые блоки кода, причем за один проход. Однако такие парсеры также имеют и недостатки: их сложнее понимать и отлаживать. Так как ANTLR очень сильно завязан на процесс токенизации, то подобные решения не рассматривались. Хотя возможность создания безлексерых грамматик сейчас уже существует и будет дорабатываться в будущем (см. обсуждение). В настоящей статье были рассмотрены подходы по обработке препроцессорных директив, которые могут использоваться при парсинге C-подобных языков. Эти подходы уже реализованы для обработки кода Objective-C и используются в коммерческих сервисах, таких как Swiftify и Codebeat. Парсер с двухэтапной обработкой протестирован на 20 проектах, в которых количество безошибочно обработанных файлов составляет более 95% от общего числа. Кроме того, одноэтапная обработка также реализована для парсинга C# и выложена в Open Source: C# grammar. В Swiftify используется одноэтапная обработка препроцессорных директив, так как наша задача — не выполнить работу препроцессора, а транслировать препроцессорные директивы в соответствующие языковые конструкции Swift, несмотря на потенциально возможные ошибки парсинга. Например, директивы #define в Objective-C обычно используются для объявления глобальных констант и макросов. В Swift для этих же целей используются константы (let) и функции (func). Software Developer at JetBrains (Kotlin Compiler)