Повышение уровня понимания RxJS путем написания преобразователя TypeScript

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

Преобразователи TypeScript могут быть решением этой и других проблем. С помощью преобразователей TypeScript можно расширить поведение определенных программ в автоматическом режиме. В этой статье я опишу как написать такой преобразователь TypeScript.

Предпосылки

Введение

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

В этой статье рассматриваются следующие темы:

  • Обход AST
  • Классификация узлов
  • Создание метаданных
  • Замена узлов
  • Функции замены

Обход AST

Входными данными для преобразователя TypeScript является программа, состоящая из нескольких исходных файлов. Каждый исходный файл имеет форму AST, заполненного узлами.

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

Первым шагом в этом процессе является обход каждого узла и передача его диспетчерской функции:

Внутри функции диспетчеризации сначала классифицируется каждый узел. В зависимости от классификации узел преобразуется соответствующей функцией или возвращается без изменений.

Классификация узлов

Чтобы классифицировать узел и убедиться, что это определенная часть оператора RxJS, используется набор функций классификатора. Каждый классификатор будет смотреть как на структуру, так и на содержимое узла.

В приведенном ниже фрагменте кода показана функция isObjectOrSubjectConstructor. Эта функция классифицирует операторы построения объекта и субъекта RxJS; например

new Observable<number>();

Если не смотреть на структуру узла, будут сделаны неверные классификации. Например, следующий комментарий не преобразовывать.

//new Observable<number>();

Отличный инструмент для помощи в написании функций классификатора - AST Explorer. Этот инструмент помогает, создавая представление AST для данного кода TypeScript.

Модульное тестирование

Еще один отличный инструмент помощи при разработке преобразователя TypeScript - это модульные тесты. Использование модульных тестов избавляет от необходимости компилировать полный проект с использованием преобразователя, экономя много времени в долгосрочной перспективе.

Чтобы упростить модульное тестирование функций, действующих на узлы TypeScript, я создал несколько вспомогательных функций. Функция createNode создает AST из заданного фрагмента кода в строковом формате, а функция printNode преобразует узел обратно в строку.

Использование функции createNode позволяет упростить модульное тестирование, как показано ниже, что значительно облегчает разработку.

Создание метаданных

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

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

Для таких вещей, как получение идентификатора, принадлежащего данному наблюдаемому объекту, важно еще раз взглянуть на инструмент AST Explorer. На основе структуры AST для данного оператора RxJS может быть построен рекурсивный алгоритм для перехода к узлу идентификатора, начиная с наблюдаемого узла.

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

Чтобы достичь этого желания, метаданные хранятся в AST. Поскольку обычный литерал объекта не может быть сохранен в AST TypeScript, для него должен быть создан литерал объекта TypeScript.

Замена узлов

Теперь узлы классифицированы и соответствующие метаданные созданы, узлы необходимо заменить. Цель этой части преобразователя - заменить классифицированные узлы, содержащие части операторов RxJS, таким образом, чтобы исходное поведение, задуманное программистом, не изменилось, но сгенерированные метаданные будут собираться и отправляться в приложение с графическим интерфейсом пользователя.

Это будет сделано путем замены операторов RxJS обернутой версией, например:

new Observable<number>();

будет заменен на:

wrap(metadata)(new Observable<number>());

После выполнения функция curryable обертывания отправит метаданные и вернет наблюдаемую конструкцию в программу. Таким образом, нормальное выполнение программы будет продолжаться, как если бы преобразователь ничего не изменил.

Следующий фрагмент кода создает вложенный узел выражения вызова, что приводит к вызову каррированного вызова функции, как показано в предыдущем фрагменте кода. Исходный узел вместе с метаданными и специальной функцией идентификатора sendMessage передается в качестве аргументов.

Как видно из приведенного выше примера, функция touch вызывается для исходного узла. Это необходимо из-за того, как проходит AST во время преобразования. Исходный узел заменяется узлом wrap, содержащим исходный узел в качестве дочернего узла. Без функции касания этот исходный узел будет заменяться узлом переносом при каждом повторении, запускающим бесконечный цикл. Помечая узел как затронутый, диспетчер будет знать, что этот узел уже преобразован, что предотвращает его отправку для повторного преобразования.

Импорт функций

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

Как вы могли видеть ранее во фрагменте кода AST Traversal, Set из RxJSParts создается во время обхода исходного файла.

const imports = new Set<RxJSPart>();

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

Функции замены

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

Конечный результат

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

Приложение, использующее все эти метаданные, будет рассмотрено в следующей статье. Я надеюсь, что эта статья дала некоторое представление о трудностях, связанных с написанием TypeScript Transformer.