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

const reqyire = require("reqyire");
const http = reqyire("htpp");

const server = http.creteServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listem(3000, "127.0.0.1");

Пожалуй, не будет преувеличением сказать, что опечатки — бич и чума программирования. Огромное количество ресурсов уходит на борьбу с ними. Шерстят код хитроумные статические анализаторы, тратя бесценные секунды рабочего времени; автодополнение IDE, нещадно сжирая такты процессора, подсказывает правильные, по его мнению, варианты. Компилятор отказывается компилировать, а интерпретатор — интерпретировать код, где пропущена или заменена какая-нибудь жалкая буква. Даже ежу понятно, что «creteServer» — это «createServer» с пропущенной буквой «a». Но движок V8 — не ёж, и на эту безобидную описку он ругнётся своим беспощадным «undefined is not a function».

И однажды я задумался: возможно, мы что-то делаем не так? Может быть, то, с чем мы боремся как с врагом, способно стать нам союзником и слугой? Результатом этих размышлений стала библиотека reqyire.js, которая, я надеюсь, навсегда изменит мир серверного JS-программирования.

Инструкция по применению


Для начала, разумеется, нужно установить библиотеку:

npm install reqyire

После этого в начале каждого файла, где вы планируете её использовать, напишите:

const reqyire = require("reqyire");

В дальнейшем используйте reqyire вместо require, когда есть опасения, что вы можете сделать опечатку в названии модуля. То есть всегда. Больше вам не придётся видеть бесконечные «Error: cannot find module» - reqyire обязательно найдёт вам модуль. Следующий код:

const reqyire = require("reqyire");
const path = reqyire("pah"),
    fs = reqyire("fds"),
    util = reqyire("./disrt/ytil.js");

— будет работать, и работать так, как надо, а не так, как написано.

Если у вас достаточно свежая версия Node.js (или не совсем свежая, но запускаемая с флагом --harmony-proxies), то чудеса на этом не закончатся. Благодаря магии свежих стандартов JavaScript все объекты, запрошенные через reqyire, также становятся понятливыми и позволяют допускать опечатки в именах своих свойств. Эта понятливость передаётся также дочерним объектам, и даже объектам, возвращаемым функциями:

const path = reqyire("pat"),
    fs = reqyire("fd");
var folder = path.win33.dirmame(".\\Works\\perfectly\\correct\\index.js");
fs.stasSync(folder).isDyrectori(); // true

Если же какому-то объекту не выпало счастье быть вызванным через reqyire, но вы всё же хотите добиться от него понятливости, на помощь приходит reqyire.wrap:

var obj = {prop: "The internet is for"};
var wrappedObj = reqyire.wrap(obj);
console.log(wrappedObj.porn, "porn"); 

Также сгодится reqyire.wrop, reqyire.warp, да и вообще любое имя метода, хотя бы отдалённо похожее на wrap.

Теперь, когда у вас есть reqyire.js, вы сможете тратить меньше сил на исправление опечаток. Это позволит писать больше строк кода в день и, очевидно, сделает вас более эффективным программистом.

Дисклеймер


Вы ведь заметили табличку «Сарказм»? Разумеется, слова про повышение эффективности, как и сама библиотека в принципе — это не всерьёз. Каждый, кто решит использовать это в продакшоне, будет мёрзнуть в ледяном аду Хельхейма.

Как это работает?


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

Нечёткий поиск

Сердце всей библиотеки — это функция fuzzySearch(arrayOfStrings, string). Она находит в массиве строк ту, между которой и вторым аргументом наименьшее расстояние Левенштейна, т.е. для которой требуется наименьшее количество операций добавления, удаления или изменения символа, чтобы превратить одно в другое. Делается это с помощью слегка модифицированного алгоритма Вагнера-Фишера. Вместо того, чтобы честно считать расстояние для каждой пары строк, мы запоминаем наименьшее из уже найденных расстояний и останавливаем алгоритм, если для новой строки расстояние оказывается заведомо больше.

Вообще говоря, расстояние Левенштейна — не самая подходящая метрика. По уму, стоило бы использовать расстояние Дамерау-Левенштейна, которое лучше соответствует характеру опечаток, совершаемых человеком. Но, как писал в своей курсовой герой анекдота:"… А электроды я взял деревянные, потому что всё равно никто это читать не будет".

Поиск модулей

Для того, чтобы использовать нечёткий поиск, нужно сначала откуда-то достать массив вариантов. Первым делом мы смотрим на имя запрашиваемого модуля. Согласно спецификации «родной» функции require, если имя начинается с "/", "./" или "../", то это путь к файлу, иначе — имя установленного модуля. Начнём со второго случая, он проще.

Установленные модули бывают трёх типов: локальные, глобальные и встроенные. Список первых и вторых можно получить с помощью консольных команд:

npm ls -json
npm ls -g -json

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

Поиск файлов

В случае пути к файлу есть две проблемы, очевидная и менее очевидная. Очевидная проблема: в системе может быть (и, скорее всего, будет) астрономическое количество файлов. Собирать массив расположений их всех, а затем делать по нему нечёткий поиск — очень затратная операция. Пораскинув мозгами, я пришёл к следующему решению: разобьём путь на элементарные фрагменты (последовательность имён директорий и имя файла в конце) и будем нечётко искать по каждому из этих фрагментов, постепенно формируя итоговый путь. Таким образом, reqyire("./jd/sctipr.js") найдёт файл "./js/script.js", но не файл "./jdsctipr.js".

Менее очевидная (возможно, только для меня) проблема заключалась в относительных путях. Чтобы резольвить абсолютный путь так, как это сделал бы require, reqyire.js должен знать абсолютный путь к скрипту, из которого вызывали reqyire. Штатными средствами JS его узнать невозможно. Я уже собирался сдаться, но, к счастью, нашёлся грязный хак, связанный с особенностями движка V8 и позволяющий вытащить путь к вызывавшему скрипту из стектрейса.

Лечебные обёртывания

Как нетрудно догадаться, «понятливость» достигается с помощью ES6 Proxy. Обернув объект в Proxy, можно перехватывать обращения к его свойствам, его вызов в качестве функции, а также ещё много других удивительных вещей, которые, однако, нас сейчас меньше интересуют. Обёртка, написанная мной, производит нечёткий поиск по свойствам объекта, если свойства с точным именем не существует. Если возвращаемый результат является объектом, он упаковывается в ту же обёртку. Так же упаковывается результат вызова функции.

Заключение


image

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

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


  1. Envek
    01.09.2017 12:03
    +37

    Кажется, это одна из библиотек серии «Счастливой отладки, суки!» :-D


  1. ihost
    01.09.2017 12:26
    +5

    Очень неплохо! Правда есть и недостатки, например имена лексических переменных с очепятками никак не перехватываются, да и синтактические ошибки по-прежнему актуальны. Так что нужно сделать babel-плагин, который будет выполнять пред-обработку и учитывать опечатки во всех сущностях.


    1. Sirion Автор
      01.09.2017 12:39
      +8

      Когда в следующий раз забуду принять свои таблетки, обязательно напишу такой плагин. Правда, все ошибки перехватить всё равно не удастся — допустим, если функция создаётся через new Function(), а имена переменных и тело функции конструируются в рантайме.

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


      1. ihost
        01.09.2017 12:46
        +4

        почему в JS нельзя получить напрямую доступ к контексту выполнения

        Раньше можно было даже программно доступ получить через parent, потом убрали видимо из соображений чистого кода и безопасности/sandboxing-а. Можно почитать тут подробнее: http://whereswalden.com/2010/05/07/spidermonkey-change-du-jour-the-special-__parent__-property-has-been-removed/


        Сейчас доступ только из отладчика по скрытому свойству [[Scope]]. Подозреваю, что для node.js можно и сейчас получить доступ через V8 natives: https://www.npmjs.com/package/v8-natives, только надо с флажком будет запускать процесс


        Правда, все ошибки перехватить всё равно не удастся

        Если перехватить сами вызовы, которые могут генерировать новый код динамически тем или иным образом, начиная от new Function и заканчивая манипуляциями с DOM Node и создания новых script-секций, то вполне возможно
        Подобная техника, правда не для шутки, а функционального тестирования, применяется в TestCafe: https://github.com/DevExpress/testcafe-hammerhead


        1. Sirion Автор
          01.09.2017 12:47
          +2

          Вы просто кладезь полезных букв, спасибо!


      1. Ntropy
        01.09.2017 19:40
        +1

        Наверное это можно устроить для JavaScript VM доступной для фикса, rhino (Java) или narcissus (javascript). Там можно служебные функции подёргать.


    1. asergrisa
      04.09.2017 12:54
      +1

      Лучше не babel плагина, а eslint. Вообще есть плагина для eslint который проверяет существование модулей и файлов которые подключаются, но вот если бы был плагин который исправляет ошибки при запуске eslint --fix, было бы куда удобнее.


      1. ihost
        04.09.2017 13:04

        Теоретически можно заимплементить плагин для prettier-а, но это решает очень узкую задачу — выявление опечаток в именах подключаемых модулей, заданных в виде строковых констант. А если require(vasya${petya}) ?


        Многие остальные вещи становятся известными только в runtime-фазе, так что ни EsLint, ни Flow тут не помощники. Поэтому если уж заморачиваться, то нужна более тяжелая артиллерия.


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



        Задачи ооочень нетривиальные в общем случае. К примеру, довольно проблемно перехватить сгенерированный script-блок из DOM-манипуляций, чтобы выполнить предварительную обертку.


  1. SergeyRodyushkin
    01.09.2017 12:33
    +10

    Главное теперь — не опечататься вот здесь:

    const reqyire = require("reqyire");


    1. Eldhenn
      01.09.2017 13:25

      Одну-то строчку можно запомнить посимвольно…


      1. alek0585
        02.09.2017 02:34

        Зачем запоминать, когда можно просто копировать и вставить? А страницу в закладки.


  1. alist
    01.09.2017 13:26
    +8

    В Руби есть gem "did_you_mean", который даже идет в стандартной поставке языка. Он тоже детектит опечатки, находит подходящий класс, метод, переменную или модуль, который скорее всего имел в виду автор, и падает с ошибкой, показыая правильное написание. Автор видит, что ошибся, исправляет ошибку, и код остается нормальным. С одной стороны, да, приходится опечатки исправлять, а с другой — все ж лучше получить нормальное сообщение об ошибке, а не "undefined is not a function".


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


    1. arvitaly
      01.09.2017 18:43
      +4

      Те, кому не нравятся опечатки, а нравится, как бонус, автокомплит, пользуются typescript. И вывод и проверка типов там везде, а не только в названии внешних модулей.


    1. SPAHI4
      02.09.2017 04:13

      В graphql тоже такое есть, по крайней мере, в сервере от apollo для nodejs


  1. SirEdvin
    01.09.2017 14:01
    +3

    Ну вот, теперь можно писать нормальный код на javascript)


  1. Frozik
    01.09.2017 15:12
    +1

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


  1. Fen1kz
    01.09.2017 15:32
    +10

    втор сапсибо бoльшео, а бблитека бдет работат с CofeScript?


    1. Sirion Автор
      01.09.2017 15:57

      Не знаю, попробуйте)


  1. atomAltera
    01.09.2017 16:25
    +2

    Ну здорово. Но лучше было бы наверное Babel плагин написать, который конвертил бы код с очепятками в код без опечаток


    1. psvg42
      01.09.2017 20:03
      +8

      По хорошему такой плагин должен находиться между стулом и монитором компьютера на котором пишеться этот самый код.


      1. SPAHI4
        02.09.2017 04:15

        «ться»

        вообще, подобное может делать и IDE. Что, кстати, IDEA и ее производные делают: спорные места выделяются шрифтом.


  1. Strain
    01.09.2017 16:33
    +1

    Есть гораздо более простое и тривиальное решение. Писать на любом языке со статической строгой типизацией.


    1. vlreshet
      01.09.2017 18:11
      +4

      Ээээ… и что это изменит? Не в типизации ведь дело. Я вам и в c++ могу опечаток наделать, и в java, и точно так же упадёт. В чём разница?


      1. Akirus98
        01.09.2017 18:20
        +4

        Разница в том что в одном случае упадет рантайме а во втором при компиляции)


      1. Strain
        01.09.2017 18:20

        Разница в том что оно не упадёт. Оно не скомпилируется. Я не пишу именно на C++, но во многих языках компилятор просто не допустит ошибок типа undefined is not a function / cannot read property of undefined. То же самое касается и не только полей структур / классов но и использования модулей, которые подгружаются не динамически а во время компиляции


        1. ihost
          04.09.2017 13:12

          Одно из самых распространенных заблуждений состоит в том, что статическая типазация, или новомодные обертки для ее эмуляции в виде ES-Flow, могут помочь побороть ошибку undefined is not a function, но на деле это невозможно сделать в статическом анализе.


          Аналогичный примерчик на C#. В общем-то проблема в Nullable объектах, а не то, что часто принято приводить в качестве аргумента


          // One
          MyClass obj = null;
          obj.someMethod();
          
          // Two
          delegate void SomeFunc(int x);
          SomeFunc dlg = cond ? obj.someMethod : null
          dlg(10);


          1. mayorovp
            04.09.2017 13:56

            В TypeScript есть возможность строгой проверки на null, которая такую вот ерунду исключает. Так что я бы осторожнее делал заявления вида "но на деле это невозможно сделать в статическом анализе"


            1. ihost
              04.09.2017 15:03

              Да, наверное можно получше сформулировать. Форма и значения в том или ином объекте могут стать известны только в runtime-фазе, например, когда прочли request body в HTTP-запросе и инстанцировали из него объект посредством JSON.parse — по сути преобразование из произвольного string в произвольный plain object.


              Каждое из полей может быть null, undefined или чем угодно, но эта станет известно только в фазе выполнения. Есть такая штука как tcomb — это да, может помочь.


              Опять-таки самая ближайшая аналогия из строго-типизированного языка, C#, это по типу:


              dynamic excel = Interop.create("Excel.Workbook");
              dynamic book = excel.Workbook[0];
              book.Activate();


  1. firk
    01.09.2017 18:07
    +4

    Хотелось бы в будущем увидеть "истории успеха" о пропихивании этой библиотеки кому-нить в продакшн.


    1. valis
      02.09.2017 10:31

      А вот мне не хотелось бы…


  1. stardust_kid
    01.09.2017 18:48
    -1

    Мне кажется, вы не в том направлении пошли. Про табличку сарказм помню, но намного полезнее был бы плагин для ide для исправления опечаток.


  1. diversenok
    01.09.2017 19:10
    +4

    Когда-то давно слышал о вирусе, который на 1 апреля находит на компе исходники и оставляет там кучу опечаток. Ура, теперь нам и это не страшно!


  1. Ntropy
    01.09.2017 19:42

    был бы хороший вариант с экспортом найденных опечаток.
    что бы легко было после девелопмента в продакшен переводить.


    1. Sirion Автор
      01.09.2017 19:52

      Тут в комментариях уже несколько раз высказывали идеи обратить мою фантазию в полезное русло) Но честно говоря, я не очень вижу, чем экспорт опечаток мог бы помочь. Если опечатка критична, «голый» джаваскрипт в этом месте упадёт с ошибкой, по тексту которой вполне понятно будет, что произошло. А если ошибки нет — так, может, так оно и было задуман? В JS объекты часто используются как хеш-таблицы, обращение к несуществующему свойству в таком случае — штатная ситуация.


      1. Ogra
        02.09.2017 09:27
        -1

        Если опечатка критична, «голый» джаваскрипт в этом месте упадёт с ошибкой

        Вопрос в том, когда он упадет в этом месте. Например, через две недели на продакшене.


        1. Sirion Автор
          02.09.2017 10:43
          +2

          Ну так моя либа не занимается статическим анализом, и сообщение об опечатке может выдать лишь в тот самый момент, когда всё упало бы без неё.


  1. ilya42
    01.09.2017 21:41
    +2

    Под Линукс есть отличный пакет theFuck для консоли — очень удобно )


  1. defusioner
    02.09.2017 10:42
    -3

    Такой модуль делает код не то что двояким, а вообще непонятно каким. Используйте иде и импорты, не занимайтесь анонизмом


    1. mayorovp
      02.09.2017 10:49
      +3

      Обратите внимание на список хабов под заголовком поста.


      1. defusioner
        03.09.2017 18:08
        -1

        прошу прощения, не знал есть фанаты подобного :D как вы живёте?


  1. kurt_live
    03.09.2017 20:45

    Стоп! Товарищ автор, а что мешает тебе сделать линтер или какой нить компилятор, который будет ошибки исправлять прямо в коде в процессе написания? Ведь если ты делаешь это в тихую, просто по сути алиасы добавляя, почему бы не сделать это громко? я б не отказался иногда от такого иструмента. Шорткат какой и перепутанные буквы встали на место или проверка при потере фокуса. И тогда твоя шутка реально станет полезной. Да и думаю многие IDE будут не против такого механизма автоисправления кода


    1. Sirion Автор
      03.09.2017 20:47
      +1

      Уже отвечал выше, но мне, в общем-то, нетрудно повторить)

      Дело в том, что либа работает в рантайме, а для линтера или плагина к IDE требуется статический анализ. Статический анализ JS-кода — это задача не то чтобы совсем уж невозможная, но принципиально отличающаяся от того, что делает reqyire.js.


      1. kurt_live
        04.09.2017 09:56

        Нет, я понимаю, что ты делаешь в этой либе. И прекрасно понимаю, что одним AST тут не обойтись. Но на самом деле я как говорил о том, чтобы использовать твой движок нечеткого поиска дабы именно на основе рантайма генерировать правильный код. К тому же ты можешь, как самый простой вариант, сыпать варнингами в консоль о том что ты заменил, где и на что. ввести глобальный реквайр твоей либы, которая бы обернула обычный, и в девелопмент окружении сыпал матюгами на криворукого и слепого разраба за подмены, а в продакшене — ни подмены, ни варнингов. Упало — сам дурак, поднимай девелопмент и смотри в консоль. В общем идей море, главное время и желание. Тогда твоя разработка перейдет из разряда "сжечь еретика" в полезный инструмент разработчика


        1. Sirion Автор
          04.09.2017 11:27

          То есть вы хотите, чтобы в рантайме вместо «Error: cannot find module X» выдавалось «Error: cannot find module X, did you mean Y?» Это да, это можно организовать. Мне только полезность этого мероприятия представляется сомнительной. Самое сложное в правке опечаток — это найти опечатки. Понять, на что их нужно исправить, обычно тривиально.


          1. kurt_live
            04.09.2017 11:39

            Ну почти. Подменить, сообщить о подмене, указать где исправить. профит следующий:
            1) человек знает где накосячил и может это исправить.
            2) при этом он сразу видит результат своего труда, а не ошибок. допустим он реализует или проверяет некий сложный прототип в действии. так он сможет посмотреть верным ли он путем пошел и исправит опечатки на этапе рефакторинга, например.
            Мне кажется, что в таком случае у программиста будет выше настроение и больше продуктивность. И вам по фану и людям радость


  1. Gennadii_M
    04.09.2017 09:31
    -1

    Ругливые компиляторы и инерпритаторы от того и придумали, чтоб выполнялся тот код, который программист хочет, а не тот, который какой-то другой код, пытается отгадать.
    Мне тоже не нравится «undefined is not a function», но эту ошибку можно увидеть только потому что js и так слишком добрый. Именно от этого его сложно дебажить.
    Строго типизированный язык скажет кто не фанкшин и где не фанкшин. И, если ты очепятался, то он тебе об этом скажет, а не попробует отгадать твои мысли через кривые пальцы.