Начало работы с Sylver — Часть 1 : Создание парсера JSON за 49 строк кода

Sylver — это платформа, не зависящая от языка, для создания пользовательских анализаторов исходного кода.
анализаторов (представьте себе eslint для любого языка).
Этого может быть много для распаковки, поэтому давайте изучим этот инструмент
на примере решения реальной проблемы: конфигурация нашего приложения хранится в
сложных документах JSON, и мы хотели бы создать инструмент для автоматической проверки
эти документы на соответствие нашим бизнес-правилам.

В этой серии уроков мы пройдем путь от нулевых знаний о Sylver или
статического анализа до создания полноценного линтера для наших конфигурационных файлов.
В качестве примера мы будем использовать JSON, но представленные инструменты и методы
применимы ко многим форматам данных и даже к целым языкам программирования!
Также обратите внимание, что хотя мы будем создавать все с нуля, используя
доменно-специфических языков (DSL) Sylver, каталог встроенных спецификаций для наиболее распространенных языков будет включен в будущие выпуски инструмента.

  • В первой части мы познакомимся с метаязыком Sylver, DSL, используемым для описания формы
    языков программирования и форматов данных. После завершения этого руководства у нас будет
    полную спецификацию для языка JSON, что позволит нам превращать документы JSON в
    деревья разбора Sylver.

  • Во второй части мы загрузим деревья разбора в механизм запросов Sylver,
    и найдем узлы, которые нарушают наши бизнес-правила, используя SQL-подобный язык запросов.
    языка.

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

Установка

Sylver распространяется в виде одного статического бинарного файла. Установить его очень просто:

  1. Перейдите по адресу https://sylver.dev, чтобы загрузить бинарный файл для вашей платформы.
  2. Распакуйте скачанный архив
  3. Переместите двоичный файл sylver в место в вашем $PATH.

Прелюдия

Давайте создадим чистое рабочее пространство для этого урока!

Мы начнем с создания новой папки и файла json.syl для записи
спецификацию Sylver для языка JSON.

mkdir sylver_getting_started
cd sylver_getting_started
touch json.syl 
Вход в полноэкранный режим Выход из полноэкранного режима

Мы также сохраним наш тестовый JSON-файл в config.json.

{
    "variables": [
        {
            "name": "country",
            "description": "Customer's country of residence",
            "values": ["us", "fr", "it"]
        },
        {
            "name": "age",
            "description": "Cusomer's age",
            "type": "number"
        }
    ]
}
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Определение типов

Sylver разбирает необработанный текст в типизированные структуры, называемые деревьями разбора. Первым шагом в определении спецификации языка является определение набора типов для узлов нашего дерева.
Мы добавим следующие объявления типов узлов в нашу спецификацию json.syl:

node JsonNode { }

node Null: JsonNode { }

node Bool: JsonNode { }

node Number: JsonNode { }

node String: JsonNode { }

node Array: JsonNode { 
    elems: List<JsonNode> 
}

node Object: JsonNode {
    members: List<Member>
}

node Member: JsonNode {
    key: String,
    value: JsonNode
}
Вход в полноэкранный режим Выход из полноэкранного режима

Эти объявления похожи на объявления типов объектов во многих основных языках.
Синтаксис : обозначает наследование.

Теперь, когда у нас есть набор типов для описания документов JSON, нам нужно указать, как построить дерево разбора из последовательности символов.
дерево разбора из последовательности символов. Этот процесс выполняется в два этапа:

  1. Лексический анализ: на этом этапе отдельные символы, образующие неделимое целое (например, цифры числа или символы строки), объединяются в лексемы. Некоторые лексемы состоят только из одного символа (например, скобки и точки с запятой в JSON).
  2. Синтаксический анализ, при котором для потока лексем строятся узлы дерева.

Лексический анализ

Токены описываются с помощью деклараций вида term NAME = <term_content>, где

между обратными знаками (`). Регексы используют синтаксис, аналогичный регулярным выражениям в стиле Perl.

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

term COMMA = ','
term COLON = ':'
term L_BRACE = '{'
term R_BRACE = '}'
term L_BRACKET = '['
term R_BRACKET = ']'
term NULL = 'null'

term BOOL_LIT = `true|false`
term NUMBER_LIT = `-?(0|([1-9][0-9]*))(.[0-9]+)?((e|E)(+|-)?[0-9]+)?`
term STRING_LIT = `"([^"\]|(\[\/bnfrt"])|(\u[a-fA-F0-9]{4}))*"`


ignore term WHITESPACE = `s`
Вход в полноэкранный режим Выход из полноэкранного режима

Правила терминалов для чисел и строк немного усложнены для учета некоторых
особенностей JSON.

Обратите внимание, что объявление термина WHITESPACE (соответствие одному пробельному символу) снабжено ключевым словом ignore. Это означает, что лексемы WHITESPACE не влияют на
структуру документа и могут быть проигнорированы во время синтаксического анализа.

Синтаксический анализ

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

Например, правило, определяющее: «если текущий токен является STRING_LIT, построить узел
узел String» может быть записано следующим образом:

rule string = String { STRING_LIT }
Войти в полноэкранный режим Выйти из полноэкранного режима

Правила могут ссылаться на другие правила для построения вложенных узлов.
Например, вот правило, определяющее, что узел Member (соответствующий члену объекта
в JSON) может быть построен путем создания узла с помощью правила string и последующего сопоставления с маркером COLON, за которым следует любое допустимое значение JSON:

rule member = Member { key@string COLON value@main }
Вход в полноэкранный режим Выход из полноэкранного режима

Вложенные узлы ассоциируются с полем с помощью синтаксиса @.
Правило main является точкой входа для парсера, поэтому в нашем случае оно обозначает любое допустимое значение JSON.

Действительный JSON-документ может состоять из литерала ‘null’, числа, булевого значения,
строки, массива значений JSON или объекта JSON, что отражено в главном правиле:

rule main =
    Null { NULL }
  | Number { NUMBER_LIT }
  | Bool { BOOL_LIT }
  | string
  | Array { L_BRACKET elems@sepBy(COMMA, main) R_BRACKET }
  | Object { L_BRACE members@sepBy(COMMA, member) R_BRACE }
Войти в полноэкранный режим Выйти из полноэкранного режима

Синтаксис sepBy(TOKEN, имя_правила) используется для разбора узлов с помощью правила main,
при этом между каждым разобранным узлом ставится в соответствие токен TOKEN.

Заключение

Теперь у нас есть полная спецификация языка JSON:

node JsonNode { }

node Null: JsonNode { }

node Bool: JsonNode { }

node Number: JsonNode { }

node String: JsonNode { }

node Array: JsonNode { 
    elems: List<JsonNode> 
}

node Object: JsonNode {
    members: List<Member>
}

node Member: JsonNode {
    key: String,
    value: JsonNode
}

term COMMA = ','
term COLON = ':'
term L_BRACE = '{'
term R_BRACE = '}'
term L_BRACKET = '['
term R_BRACKET = ']'
term NULL = 'null'

term BOOL_LIT = `true|false`
term NUMBER_LIT = `-?(0|([1-9][0-9]*))(.[0-9]+)?((e|E)(+|-)?[0-9]+)?`
term STRING_LIT = `"([^"\]|(\[\/bnfrt"])|(\u[a-fA-F0-9]{4}))*"`


ignore term WHITESPACE = `s`

rule string = String { STRING_LIT }

rule member = Member { key@string COLON value@main }

rule main =
    Null { NULL }
  | Number { NUMBER_LIT }
  | Bool { BOOL_LIT }
  | string
  | Array { L_BRACKET elems@sepBy(COMMA, main) R_BRACKET }
  | Object { L_BRACE members@sepBy(COMMA, member) R_BRACE }
Вход в полноэкранный режим Выход из полноэкранного режима

Последний шаг — протестировать его на нашем тестовом файле!
Это делается путем выполнения следующей команды:

В результате получается следующее дерево разбора:

Object {
. ● members: List<Member> {
. . Member {
. . . ● key: String { "variables" }
. . . ● value: Array {
. . . . ● elems: List<JsonNode> {
. . . . . Object {
. . . . . . ● members: List<Member> {
. . . . . . . Member {
. . . . . . . . ● key: String { "name" }
. . . . . . . . ● value: String { "country" }
. . . . . . . }
. . . . . . . Member {
. . . . . . . . ● key: String { "description" }
. . . . . . . . ● value: String { "Customer's country of residence" }
. . . . . . . }
. . . . . . . Member {
. . . . . . . . ● key: String { "values" }
. . . . . . . . ● value: Array {
. . . . . . . . . ● elems: List<JsonNode> {
. . . . . . . . . . String { "us" }
. . . . . . . . . . String { "fr" }
. . . . . . . . . . String { "it" }
. . . . . . . . . }
. . . . . . . . }
. . . . . . . }
. . . . . . }
. . . . . }
. . . . . Object {
. . . . . . ● members: List<Member> {
. . . . . . . Member {
. . . . . . . . ● key: String { "name" }
. . . . . . . . ● value: String { "age" }
. . . . . . . }
. . . . . . . Member {
. . . . . . . . ● key: String { "description" }
. . . . . . . . ● value: String { "Customer's age" }
. . . . . . . }
. . . . . . . Member {
. . . . . . . . ● key: String { "type" }
. . . . . . . . ● value: String { "number" }
. . . . . . . }
. . . . . . }
. . . . . }
. . . . }
. . . }
. . }
. }
}
Войти в полноэкранный режим Выход из полноэкранного режима

В следующей части мы определим бизнес-правила для проверки нашей конфигурации JSON
конфигурации (например, возможные значения для каждой переменной должны быть
одного типа), и мы будем использовать DSL-запрос для определения узлов дерева, которые нарушают эти правила.

Оцените статью
devanswers.ru
Добавить комментарий