Sylver — это платформа, не зависящая от языка, для создания пользовательских анализаторов исходного кода.
анализаторов (представьте себе eslint для любого языка).
Этого может быть много для распаковки, поэтому давайте изучим этот инструмент
на примере решения реальной проблемы: конфигурация нашего приложения хранится в
сложных документах JSON, и мы хотели бы создать инструмент для автоматической проверки
эти документы на соответствие нашим бизнес-правилам.
В этой серии уроков мы пройдем путь от нулевых знаний о Sylver или
статического анализа до создания полноценного линтера для наших конфигурационных файлов.
В качестве примера мы будем использовать JSON, но представленные инструменты и методы
применимы ко многим форматам данных и даже к целым языкам программирования!
Также обратите внимание, что хотя мы будем создавать все с нуля, используя
доменно-специфических языков (DSL) Sylver, каталог встроенных спецификаций для наиболее распространенных языков будет включен в будущие выпуски инструмента.
-
В первой части мы познакомимся с метаязыком Sylver, DSL, используемым для описания формы
языков программирования и форматов данных. После завершения этого руководства у нас будет
полную спецификацию для языка JSON, что позволит нам превращать документы JSON в
деревья разбора Sylver. -
Во второй части мы загрузим деревья разбора в механизм запросов Sylver,
и найдем узлы, которые нарушают наши бизнес-правила, используя SQL-подобный язык запросов.
языка. -
В третьей части мы узнаем, как превратить нашу языковую спецификацию и запросы в набор
правила линтинга, чтобы их можно было удобно запускать и совместно использовать.
Установка
Sylver распространяется в виде одного статического бинарного файла. Установить его очень просто:
- Перейдите по адресу https://sylver.dev, чтобы загрузить бинарный файл для вашей платформы.
- Распакуйте скачанный архив
- Переместите двоичный файл
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, нам нужно указать, как построить дерево разбора из последовательности символов.
дерево разбора из последовательности символов. Этот процесс выполняется в два этапа:
- Лексический анализ: на этом этапе отдельные символы, образующие неделимое целое (например, цифры числа или символы строки), объединяются в лексемы. Некоторые лексемы состоят только из одного символа (например, скобки и точки с запятой в JSON).
- Синтаксический анализ, при котором для потока лексем строятся узлы дерева.
Лексический анализ
Токены описываются с помощью деклараций вида 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-запрос для определения узлов дерева, которые нарушают эти правила.