I. Предыстория
Я много лет использую UltraEdit как редактор на самые разные случаи жизни. Одна из основных причин — быстрая работа с гигабайтными файлами без загрузки их в память. Для программирования на JavaScript он тоже достаточно удобен, вот только с одним существенным недостатком: автодополнение в нём основывается на достаточно бедном, жёстко заданном списке ключевых слов и глобальных переменных, вдобавок отстающем от развития языка. Как-то я задался вопросом, можно ли пополнить этот список полным перечнем всех готовых свойств и методов, какие только можно ввести в контексте Node.js и Web API (браузера). Где бы такой список можно раздобыть? Мне приходили в голову такие варианты:
Готовый перечень, кем-то составляемый и обновляемый для всеобщего пользования, вроде библиотеки globals, но полнее.
Парсинг документации (спецификация ECMAScript, сайты MDN и Node.js и т.п.), вручную или программно.
- Получение списка метапрограммированием.
Основным ответом на мои вопросы было предложение сменить редактор и не мучиться. Но так как удобных редакторов для больших файлов не так уж и много, а мой минимализм затруднял использование нескольких под разные нужды, да и программирование не было моим основным занятием, я не сдавался. В конце концов, это стало для меня не только практической задачкой, но и принципиальным интересом: как же так, вроде бы такая простая нужда, а лёгких решений нет.
Поскольку готовых списков я не нашёл, а парсить документацию — путь долгий и ненадёжный, я решил попробовать третий способ.
II. Код
Воспользовавшись несколькими советами, я написал небольшой скрипт, который выводит основную часть языковой номенклатуры в разных контекстах несколькими способами.
Скрипт можно запустить в Node.js или в браузере (через консоль или вставку в страницу). В первом случае результат будет выведен в файлы, во втором — в прибавленные к текущему документу текстовые поля (можно открыть about:blank
вместе с консолью).
Постараюсь прокомментировать код.
1. Сперва мы создаём основные переменные-контейнеры. В первых двух мы будем накапливать нашу номенклатуру: в nomenclatureTerms
будет храниться простой список всех лексем, в nomenclatureChains
— те же лексемы, но с полными цепочками, начиная от корневых объектов. В globs
мы будем хранить наши отправные точки для разматывания клубка и построения дерева — глобальные (корневые) объекты. Чтобы избежать бесконечной рекурсии из-за циклических ссылок, все обработанные объекты мы будем складывать в processedObjects
для последующей проверки.
2. На втором этапе мы заполняем globs
.
Сначала скрипт пытается определить, в каком контексте он выполняется. Если это браузер, нам достаточно объекта window
.
Если это Node.js, всё немного сложнее. Сначала мы добавляем два основных глобальных объекта, а также require
, поскольку иначе на эту функцию мы не выйдем. Потом мы добавляем объекты всех стандартных библиотек: основную часть — отталкиваясь от недокументированного списка require('repl')._builtinLibs
, посоветованного одним из разработчиков Node.js, а затем несколько недостающих модулей. В завершение, поскольку несколько внутримодульных переменных (__dirname
и __filename
) не привязаны ни какому глобальному объекту, мы сразу же добавим их в наши номенклатурные контейнеры.
3. Далее следует основная работа: при помощи рекурсивной функции processKeys
мы обходим все глобальные объекты и все объекты, хранящиеся в их свойствах, до последней возможной глубины. Затем выводим результаты в зависимости от контекста и завершаем их итоговым выводом в консоль размеров наших номенклатур (скрипт работает ощутимое время, так что этот вывод может служить сигналом завершения работы — хотя в Chrome может потребоваться дополнительное время на обновление страницы даже после этого сигнала).
4. Функция processKeys
является основным двигателем процесса.
Сперва мы проверяем, с корневым ли объектом мы имеем дело. Если да, мы сразу заносим его имя в номенклатуру. Если объект расположен в дочернем свойстве объекта, это занесение уже совершилось на предыдущем этапе рекурсии, поэтому мы его пропускаем.
Затем мы заносим объект в список обработанных объектов, чтобы не попасть в дурную бесконечность.
После этого начинаем обходить все свойства объекта. Для этого мы используем метод Reflect.ownKeys()
, поскольку только он перечисляет и обычные строковые ключи объекта, и ключи типа Symbol. Каждое из свойств мы заносим в nomenclatureTerms
(тип Set
автоматически отбрасывает повторения), затем формируем цепочку из имени родительского объекта и текущего свойства и заносим её в nomenclatureChains
; эта же цепочка станет именем объекта для следующего рекурсивного вызова, поэтому она будет постоянно расти с продвижением вглубь (я выбрал нотацию с квадратными скобками на все случаи для унификации сортировки: если использовать точку для обычных идентификаторов и скобки для сложных строковых, это ломает порядок при выводе списка; JSON.stringify
употребляется для перестраховки — для экранирования возможных кавычек в составе имён свойств). Ключи типа Symbol перед занесением в базу приводятся к строкам (к сожалению, это делает элементы базы с такими ключами в цепочках свойств непригодными для непосредственной интерпретации, например, в REPL Node.js или в консолях браузеров — перед этим нужно опять приводить такие ключи к типу Symbol, убирая лишнее из строкового представления).
На следующем этапе мы проверяем, что хранится в свойстве: если это объект, мы делаем новый рекурсивный вызов, если этого объекта ещё нет в списке обработанных. Проверка на объектность двойная, поскольку instanceof Object
возвращает false
для Object.prototype
и для объектов, созданных при помощи Object.create(null)
.
Такое повсеместное прохождение по свойствам часто вызывает ошибки, поэтому нам придётся добавить обработчик, чтобы процесс не прерывался (вывод сообщений об ошибках оставлен ради любопытства). Также в консоль, помимо нашего желания, будет выведено несколько предупреждений о попытках запросить свойства, получившие статус deprecated
.
5. Функция output
отвечает за вывод результатов в зависимости от контекста выполнения. Сначала она формирует список, отсортированный в более привычном словарном порядке (правда, параметр caseFirst
в Firefox не работает). Затем проверяет контекст выполнения: в браузере списки выводятся в два текстовых поля, встраиваемые в текущую страницу (вверх списка добавляется для удобства имя файла, с которым список можно сохранить при помощи редактора); в Node.js создаются два файла в текущем каталоге.
Следует учитывать, что к браузерному списку добавляются имена функций нашего скрипта, а к списку Node.js — разные переменные окружения; также в перечень включаются разные недокументированные свойства внутреннего употребления, индексы массивов и т.п. С другой стороны, в наш список не попадают многие строковые элементы номенклатуры (например, названия событий или стандартные строковые параметры функций).
III. Результаты
После прогона скрипта на последней бета-версии Node.js и на ночных сборках двух браузеров я получил следующие списки (данные обновлены по состоянию на 18.10.2016):
Node.js 7.0.0-test201610107f7d1d385d
Terms: 1 822
Chains: 7 394
Google Chrome Canary 56.0.2891.0
Terms: 3 352
Chains: 15 091
Mozilla Firefox Nightly 52.0a1 (2016-10-17)
Terms: 5 082
Chains: 16 125
Возможно, у результатов программы могут быть разные применения. Например, сравнение номенклатуры разных браузеров или разных версий одного браузера (во время тестирования я замечал, что ночные сборки соседних дней могут давать результаты, различающиеся десятками позиций — что-то вводится, что-то уходит в историю). Если автоматизировать процесс, можно, например, создать историю API Node.js на протяжении многих версий. А можно собрать разнообразную языковую статистику: глубина вложения свойств, длина идентификаторов, принципы их создания и т.д.
Наверняка код можно оптимизировать по скорости, по удобству использования, по полноте результатов или их читабельности. Также я мог допустить какие-то глупые ошибки из-за незнания тонкостей языка или контекстов использования. Буду благодарен за поправки и добавления. Спасибо за внимание.
P.S. Хороший пример: http://electron.atom.io/blog/2016/09/27/api-docs-json-schema