I. Предыстория


Я много лет использую UltraEdit как редактор на самые разные случаи жизни. Одна из основных причин — быстрая работа с гигабайтными файлами без загрузки их в память. Для программирования на JavaScript он тоже достаточно удобен, вот только с одним существенным недостатком: автодополнение в нём основывается на достаточно бедном, жёстко заданном списке ключевых слов и глобальных переменных, вдобавок отстающем от развития языка. Как-то я задался вопросом, можно ли пополнить этот список полным перечнем всех готовых свойств и методов, какие только можно ввести в контексте Node.js и Web API (браузера). Где бы такой список можно раздобыть? Мне приходили в голову такие варианты:


  1. Готовый перечень, кем-то составляемый и обновляемый для всеобщего пользования, вроде библиотеки globals, но полнее.


  2. Парсинг документации (спецификация ECMAScript, сайты MDN и Node.js и т.п.), вручную или программно.


  3. Получение списка метапрограммированием.

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


Поскольку готовых списков я не нашёл, а парсить документацию — путь долгий и ненадёжный, я решил попробовать третий способ.


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 является основным двигателем процесса.


Сперва мы проверяем, с корневым ли объектом мы имеем дело. Если да, мы сразу заносим его имя в номенклатуру. Если объект расположен в дочернем свойстве объекта, это занесение уже совершилось на предыдущем этапе рекурсии, поэтому мы его пропускаем.


Затем мы заносим объект в список обработанных объектов, чтобы не попасть в дурную бесконечность.


После этого начинаем обходить все свойства объекта. Каждое из них мы заносим в nomenclatureTerms (тип Set автоматически отбрасывает повторения), затем формируем цепочку из имени родительского объекта и текущего свойства и заносим её в nomenclatureChains; эта же цепочка станет именем объекта для следующего рекурсивного вызова, поэтому она будет постоянно расти с продвижением вглубь (я выбрал нотацию с квадратными скобками на все случаи для унификации сортировки: если использовать точку для обычных идентификаторов и скобки для сложных строковых, это ломает порядок при выводе списка; JSON.stringify употребляется для перестраховки — для экранирования возможных кавычек в составе имён свойств).


На следующем этапе мы проверяем, что хранится в свойстве: если это объект, мы делаем новый рекурсивный вызов, если этого объекта ещё нет в списке обработанных. Проверка на объектность двойная, поскольку instanceof Object возвращает false для Object.prototype и для объектов, созданных при помощи Object.create(null).


Такое повсеместное прохождение по свойствам часто вызывает ошибки, поэтому нам придётся добавить обработчик, чтобы процесс не прерывался (вывод сообщений об ошибках оставлен ради любопытства). Также в консоль, помимо нашего желания, будет выведено несколько предупреждений о попытках запросить свойства, получившие статус deprecated.


5. Функция output отвечает за вывод результатов в зависимости от контекста выполнения. Сначала она формирует список, отсортированный в более привычном словарном порядке (правда, параметр caseFirst в Firefox не работает). Затем проверяет контекст выполнения: в браузере списки выводятся в два текстовых поля, встраиваемые в текущую страницу (вверх списка добавляется для удобства имя файла, с которым список можно сохранить при помощи редактора); в Node.js создаются два файла в текущем каталоге.


Следует учитывать, что к браузерному списку добавляются имена функций нашего скрипта, а к списку Node.js — разные переменные окружения; также в перечень включаются разные недокументированные свойства внутреннего употребления, индексы массивов и т.п. С другой стороны, в наш список не попадают многие строковые элементы номенклатуры (например, названия событий или стандартные строковые параметры функций).


III. Результаты


После прогона скрипта на последней стабильной версии Node.js и на ночных сборках двух браузеров я получил следующие списки:


Node 6.6.0
» Terms: 1 813
» Chains: 7 282


Google Chrome Canary 55.0.2867.0
» Terms: 3 339
» Chains: 14 435


Firefox Nightly 52.0a1 (2016-09-21)
» Terms: 5 040
» Chains: 14 417


Возможно, у результатов программы могут быть разные применения. Например, сравнение номенклатуры разных браузеров или разных версий одного браузера (во время тестирования я замечал, что ночные сборки соседних дней могут давать результаты, различающиеся десятками позиций — что-то вводится, что-то уходит в историю). Если автоматизировать процесс, можно, например, создать историю API Node.js на протяжении многих версий. А можно собрать разнообразную языковую статистику: глубина вложения свойств, длина идентификаторов, принципы их создания и т.д.


Наверняка код можно оптимизировать по скорости, по удобству использования, по полноте результатов или их читабельности. Также я мог допустить какие-то глупые ошибки из-за незнания тонкостей языка или контекстов использования. Буду благодарен за поправки и добавления. Спасибо за внимание.


P.S. Хороший пример: http://electron.atom.io/blog/2016/09/27/api-docs-json-schema

Поделиться с друзьями
-->

Комментарии (17)


  1. bromzh
    22.09.2016 07:23
    +2

    Как-то я задался вопросом, можно ли пополнить этот список полным перечнем всех готовых свойств и методов, какие только можно ввести в контексте Node.js и Web API (браузера). Где бы такой список можно раздобыть?

    Есть же для Typescript .d.ts-файлы, где описывают сигнатуры для всяких библиотек, в том числе — стандартных. Многие редакторы/IDE их и используют.


    Вот тут список файлов с сигнатурами стандартных библиотек ES и работы с DOM: https://github.com/Microsoft/TypeScript/tree/master/lib


    Для ноды можно взять эти файлы из репозитория с кучей сигнатур для кучи библиотек: https://github.com/DefinitelyTyped/DefinitelyTyped


    Вот папка с сигнатурами ноды: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/node


    В общем, странно что вы ничего не слышали про это, раз пишите на JS.


    1. vmb
      22.09.2016 11:29

      Спасибо. Я пишу на непрофессиональном уровне и в Typescript никогда не углублялся. Постараюсь разобраться, как извлечь из этих файлов чистые списки.


      1. bromzh
        22.09.2016 12:57
        +1

        можете посмотреть на проект Atom Typescript, посмотреть, как там реализован выбор списка для автодополнений.


        1. vmb
          22.09.2016 14:12

          Попробовал понять с наскока. Понял, что для использования этих файлов нужно сперва выучить Typescript, а потом опять-таки или самому парсить эти описания, или искать для этого инструменты, поскольку просто так любой редактор не заставишь с этим работать без плагинов, которых может и не быть. Для меня это довольно сложный путь, я как-то для своих нужд всегда обходился без этого направления в языке — строгая типизация, классы, все прелести ООП. Но всё равно спасибо, это, по крайней мере, надёжная стратегия для тех, кто уже в этом хорошо разбирается.


          1. bromzh
            22.09.2016 15:28
            +1

            TS — это свежий JS + типы, но можно писать и без них.
            Чтобы распарсить файл, можно использовать API компилятора Typescript, ведь он сам написан на Typescript. Ещё можно посмотреть [тут]( https://github.com/Microsoft/TypeScript/wiki/Using-the-Language-Service-API).


            Готовых реализаций я не нашёл, но есть вот такая статья: http://blog.scottlogic.com/2015/01/20/typescript-compiler-api.html
            И вот этот модуль (но он древний): https://www.npmjs.com/package/dts-parser


            1. vmb
              22.09.2016 16:28

              Спасибо ещё раз. Если всё-таки пойду в этом направлении, будет намного легче.


  1. lorus
    22.09.2016 07:26
    +1

    Есть готовый перечень вообще-то.


    1. vmb
      22.09.2016 11:26

      Спасибо.


  1. bustEXZ
    22.09.2016 15:07
    +1

    Есть ещё способ. Например воспользовавшись webstorm. Пишем в коде например `document`. И с щелчка переходим на его описание (в DHTML.js). Там описана структура почти всех элементов js'a.
    Это лишь пример :)


    1. vmb
      22.09.2016 16:22

      Спасибо.


  1. jt3k
    22.09.2016 16:46

    Не читал но осуждаю.
    Чем SublimeText Вам не угодил? Он же для людей писан. А ултраедит думаю уже давно не то что нужно. Как и Нотепад++


    1. vmb
      22.09.2016 16:48

      А в SublimeText можно открыть текстовый файл в 2 гигабайта и провести в нём замены по регулярным выражениям? Он не будет читать его в память, не будет подвисать? Я просто регулярно работаю с исходниками больших цифровых словарей, мне это важно. Чаще всего мне для их обработки и нужен JavaScript.


      1. jt3k
        22.09.2016 17:38
        +1

        Эх нет :(. На текстовом файле в 2гб он наедается и умирает. vim и nano тоже умирают.
        Atom даже не пытался запускать :)


        1. vmb
          22.09.2016 17:54

          Жаль. Я периодически тестировал популярные редакторы общего профиля, это распространённая проблема. Можно, конечно, простые скрипты писать для таких файлов, чтобы потоками обрабатывали, но это не всегда удобно.


  1. jt3k
    22.09.2016 17:45
    +2

    Возьмите готовый перечень у тех кто уже решил схожую проблему. например у Ternjs
    Так у вас отпадёт нужда в поддержке(правда есть риск, что они могут изменить формат. но это не проблема, всегда можно узнать это заранее через ишьюсы)


    1. vmb
      22.09.2016 17:57

      Спасибо.