Статические анализаторы JavaScript и ошибки, от которых они помогут отучиться (Часть 1)

Original author: Zack Grossbart
  • Translation
Далеко не каждая строка моего кода получается идеальной с первого же раза. Ну, в некоторых случаях… Иногда… Ладно – практически никогда. Правда заключается в том, что я трачу значительно больше времени на исправление своих собственных глупых ошибок, чем мне хотелось бы. Именно поэтому я использую статические анализаторы практически в каждом написанном мной файле JavaScript.

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

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


JSLint, JSHint и Closure Compiler

Есть три основных варианта статических анализаторов для JavaScript: JSLint, JSHint и Closure Compiler.

JSLINT

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

var s = 'mystring';
for (var i = 0; i < s.length; i++) {
 console.log(s.charAt(i));
}


JSLint показывает в этом коде две ошибки:

Unexpected '++'.
Move 'var' declarations to the top of the function.


Первая проблема – это определение переменной i в условиях цикла. JSLint также не принимает оператор ++ в конце определения цикла. Он хочет, чтобы код выглядел следующим образом:

var s = 'mystring';
var i;
for (i = 0; i < s.length; i = i + 1) {
 console.log(s.charAt(i));
}


Я ценю создателей JSLint, но как по мне – это перебор. Он оказался жестким и для Антона Ковалева, поэтому он создал JSHint.

JSHINT

JSHint работает так же, как и JSLint, но он написан в дополнение к Node.js, а потому он более гибкий. JSHint включает большое количество опций, что позволяет выполнять пользовательские проверки путем написания своего собственного генератора отчетов.
Запустить JSHint можно с сайта, но в большинстве случаев лучше установить JSHint в качестве локального инструмента командной строки с помощью Node.js. Как только вы установите JSHint, его можно запустить в ваших файлах с помощью такой команды:

jshint test.js


JSHint также включает плагины для популярных текстовых редакторов, поэтому его можно запускать в процессе написания кода.

CLOSURE COMPILER

Closure Compiler от Google – это совсем другая разновидность программы. Как предполагает его название, он представляет собой не только программу для проверки, но и компилятор. Он написан на Java и основан на анализаторе Rhino от Mozilla. Closure Compiler включает простой режим для выполнения базовой проверки кода, и более сложные режимы, позволяющие выполнять дополнительную проверку и обеспечивать соблюдение определений отдельных видов.

Closure Compiler сообщает об ошибках в коде JavaScript, но также создает минимизированные версии JavaScript. Компилятор удаляет пустое пространство, комментарии и неиспользуемые переменные и упрощает длинные выражения, делая скрипт максимально компактным.

Google сделал очень простую версию компилятора, доступную в сети, но скорее всего, вы захотите скачать Closure Compiler и запустить его локально.

Closure Compiler после проверки кода выводит список файлов в один минимизированный файл. Таким образом, вы можете запустить его, скачав файл compiler.jar.

java -jar compiler.jar --js_output_file compress.js --js test1.js --js test2.js


Выбираем правильную программу проверки

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

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

В этой статье для большинства примеров используется JSHint, но Closure Compiler обычно выдает похожие предупреждения.

== или ===?

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

JavaScript предлагает два оператора сравнения для управления такими динамическими типами: == и ===. Давайте рассмотрим это на примере.

var n = 123;
var s = '123';

if (n == s) {
 alert('Переменные равны');
}

if (n === s) {
 alert('Переменные идентичны');
}

Оператор сравнения == — это остатки языка С, в который JavaScript уходит корнями. Его использование практически всегда является ошибкой: сравнивание значений отдельно от типов редко является тем, что разработчик на самом деле хочет сделать. На самом деле, число «сто двадцать три» отличается от строки «один два три». Эти операторы легко неправильно написать и еще легче неправильно прочесть. Проверьте этот код с помощью JSHint и вы получите следующее:

test.js: line 9, col 12, Expected '===' and instead saw '=='.


Неопределенные переменные и поздние определения

Давайте начнем с простого кода:

function test() {
 var myVar = 'Hello, World';
 console.log(myvar);
}


Видите баг? Я совершаю эту ошибку каждый раз. Запустите этот код, и вы получите ошибку:

ReferenceError: myvar is not defined


Давайте сделаем проблему немного более сложной:

function test() {
 myVar = 'Hello, World';
 console.log(myVar);
}


Запустите этот код, и вы получите следующее:

Hello, World

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

test.js: line 3, col 17, 'myvar' is not defined.


Во втором случае он сообщит такое:

test.js: line 2, col 5, 'myVar' is not defined.
test.js: line 3, col 17, 'myVar' is not defined.


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

Проблема второго примера коварно незаметная и сложная. Переменная myVar теперь исчезла из своей области видимости и поднялась в глобальную область. Это означает, что она будет существовать и иметь значение Hello, World даже после запуска функции test. Это называется «загрязнение глобальной области видимости».

Переменная myVar будет существовать для каждой другой функции, которая будет запущена после функции test. Запустите следующий код после того, как выполните функцию test:

console.log('myVar: ' + myVar);


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

PAYSTO
Company
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 48

    +3
    Странно, что не упомянут молодой, довольно богатый и хорошо расширяемый eslint.
      +7
      function test() {
       myVar = 'Hello, World';
       console.log(myVar);
      }
      


      Стоит заметить, что чтобы избегать таких ошибок, можно просто включить строгий режим используя «use strict», и уже сам браузер выдаст ошибку "'myVar' is not defined."

        0
        Мне нравится подход кофискрипта в этом — они просто отключили глобальные переменные. Насовсем. К этому быстро привыкаешь и понимаешь, что глобальные переменные по сути не нужны. А те редкие (крайне) моменты, когда они всё-таки нужны (обычно для пост-отладки (не дебаггером в реал тайме) в инкапсулированном коде) достигаются простым присваиванием какому-нибудь глобальному объекту, например
        window.someVar = 'somVal'
        
        –3
        Сколько ни пытался настроить jsHint под себя, так ни разу и не осилил этот путь до конца. Точнее, не совсем так. Я сталкиваюсь с каким-нибудь предупреждением, которое, на мой взгляд, не уместно. Но не имею рычагов воздействия… Попросту не хватает флагов (хотя их много), либо не все флаги работают так, как я ожидаю от них. В конечном счёте встаю перед задачами: либо полностью подстроиться под jsHint, и писать такой код, который его устроит, что мне кажется, уже очень не правильно, либо вмешаться хирургически. Так и живу с правленными исходниками :)

        Кстати, немалой части проблем можно избежать просто воспользовавшись «use strict»;
          +1
          Во-первых, есть специальные комментарии, которые отключают данный ворнинг для данного куска:
          /*jshint -W106 */
          save_state(id);
          /*jshint +W106 */
          

          (номер ворнинга виден из собственно репорта об ошибке), или для данной функции:
          function helloEval(str) {
              /*jshint evil:true */
              eval(str);
          }
          


          Во-вторых, попробуйте eslint, он гибче.
          Так же посоветую jscs — от отвечает только за форматирование, но консистентное форматирование тоже важно.
            +1
            Про комментарии в курсе. Но не считаю целесообразным их применять. jsHint это инструмент, не более того. Особенно если над проектом работает несколько человек, и только 1 из них использует jsHint. К тому же расхождений между моим представлением о правильном, и мнением jsHint очень много. С самими кодами у меня была какая-то проблема, ибо изначально я ограничивался только кодами в конфиге (точно уже не помню), но это не хватало.

            Во-вторых, попробуйте eslint, он гибче.
            Так же посоветую jscs — от отвечает только за форматирование, но консистентное форматирование тоже важно.
            Спасибо. Посмотрю на досуге.
              +4
              несколько человек, и только 1 из них использует jsHint
              вот в этом проблема, а не в jshint:)
                +2
                jshint-комментарии в коде, кстати, дополнительную полезную инфу несут, не только для jshint.
                Типа «да, здесь на первый взгляд написано не совсем правильно, но конкретно здесь так надо».
            0
            Не понял, где ошибка в:
            function test() {
             var myVar = 'Hello, World';
             console.log(myvar);
            }
            

            Дефайнится локальная переменная и тут же распечатывается, в чём тут проблема?
              +1
              Приглядитесь к букве v в названии переменной.
                +1
                Ух ты ж, теперь понял, посыпаю головку пеплом. В таком ситуации IDE помогает очень хорошо.
              +7
              Исправьте «статистические» анализаторы в заголовке. Пишу специально не в личку (как с обычной опечаткой), чтобы и другие люди понимали, что «статический анализатор» и «статистический анализатор» — это разные вещи.
                0
                Спасибо за замечание. Это таки опечатка)
                0
                JSHint как-то странно отреагировал на классику жанра (определение значения по умолчанию):
                function main(name) {
                  if (name == null) {
                     name = 'World!';
                  }
                  return "Hello, " + name;
                  
                }
                main();
                


                выдал
                One warning
                13 Use '===' to compare with 'null'.

                Хотя это всего «внимание», и вообще в остальном JSHint офигителен и несусветно помогает =)
                  0
                  классика всё-таки
                  name = name || «default»;
                    0
                    Для переданных false и 0 поведение будет неожиданным.
                      0
                      Ещё пустая строка и NaN (да, порой надо понять, что пришёл именно он, а не просто ничего не передали).

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

                      Да, 3 строчки кода вместо одной, зато фундаментальнее. (= А минификатор и так сожмёт =)

                      Слава богу никто не предложил
                      if (varDef == undefined) 
                      
                      или
                      if (varDef === void 0)
                      
                        0
                        Ну, так и NULL с Undefined могут быть допустимыми значениями. Остаётся только в arguments копаться.
                          0
                          Вы правы и не правы. null может, а undefined — отчасти (начиная с ES5) и то, это следует избегать похлеще сравнения с приведением.
                          Да, остаётся копаться или менять кусок логики работы со значениями по умолчанию.
                          Поэтому (не с потолка взято, тот же кофе, тот же типо, тот же дарт) значения задают, сравнивая текущее значение с null

                          coffee
                          Если, кстати, писать на кофе и нужно задать параметр внутри логики, удобно можно писать так

                          param = defaultValue unless param?
                          

                          и вообще проблемы описанные в этой и следующей статье (все кроме цикломатики) кофескрипт отлично решает =)
                            0
                            Как именно кофе задаёт?
                            TypeScript например так:

                            function func(x = 5) {
                            return x * 3;
                            }

                            function func(x) {
                            if (x === void 0) { x = 5; }
                            return x * 3;
                            }

                              0
                              Когда смотрел раньше было у типоскрита как у кофе, не строгое сравнение с null. Удивился.

                              func = (x = 5) -> x * 3
                              


                              var func;
                              
                              func = function(x) {
                                if (x == null) {
                                  x = 5;
                                }
                                return x * 3;
                              };
                              
                      0
                      Добавьте в код /*jshint eqnull:true*/.

                      (А лучше добавьте это в .jshintrc, если в вашем проекте более одного файла)
                        0
                        Эти слова бы да в статью =)
                        Так-то в курсе, что настраиваемо, даже так и поступаем, странно, что об этом ни слова.

                        Просто удивило, т.к. это единственное, с чем можно сравнивать с приведением типов и это корректно, но хинт ругнулся =)
                    • UFO just landed and posted this here
                        0
                        А если, положим, надо различить аргумент, который не передали, от аргумента, вместо котого явно передали null или false? Везде свои камни.
                        • UFO just landed and posted this here
                            +2
                            Я встречал кейс с null. Механизм arguments.length тоже может дать сбой, если, например, два необязательных параметра (util.method('str', undefined, null)).

                            никогда не знаешь, что придёт от бэкенда
                            каков поп, таков и приход бэкенд, таков и фронтенд, видимо.
                            • UFO just landed and posted this here
                          +1
                          Что за «никогда не знаешь, что придёт от бэкенда»? Экстерминейт! Экстерминейт!
                          Ничто не мешает ручками приводить типы и это полезно: таким образом вы покажете другим прогерам, что тут точная типизация невозможна.
                          Да, кода много
                          if ( (var1.toString() === var2.toString()) { /* ... */ }
                          

                          но очевидно, да и можно сократить и ужать =)
                          • UFO just landed and posted this here
                              0
                              Если логика строится на недоверии внешним данным (большинство случаев), то при несоответствии данных представлению ошибка или обработка данного несоответствия обязана произойти (обязаны написать).
                              То, что вам кажется убожеством или нелепостью не менее, чем хороший стиль написания кода.

                              Разумеется, если Вы «ловите» ошибки из-за точного сравнения, и, значит где-то код говно, то не следует держаться хорошего и строгого стиля. Так же, как копать колодец во фраке не стоит. Вопрос применимости.

                              Можете объяснить как == спасает от TypeError при получении метода у неопределённой переменной или null o__O?
                              • UFO just landed and posted this here
                                  0
                                  Вы правда считаете, что тройное равенство уместно, особенно, когда методов (у null и undefined) может и не быть?
                                  Что тогда имелось в виду?

                                  Впервые слышу, чтобы строгий код был говном. о__О Вот если типы хоп и изменились, значит нет тестов, значит что-то в том процессе воняет.
                                  Программист просто не знает возможностей языка и пишет абы не было чего. С обратным результатом, невежество сказывается.
                                  Судя по тому, что пишите Вы, возможностей языка не знаете как раз Вы. Возможно, Вы помните всю таблицу сравнений и приведений типов. Это, возможно, хорошо. Но не все хотят держать это мессиво в голове, куда проще производить строгое сравнение. Если после рефакторинга меняется интерфейс, ему следует измениться во всех местах, иначе… oldNumberNewStringVariable += 11 или наоборот oldStringNewNumberVariables.length простейший артефакт, который из-за отсутствия проверок типов данных создаст баг.

                                  Так что Ваше «проще» выливается в очень странную и неустойчивую конструкцию.
                                  Приводить типы так и так придётся. А === говорит, что тут всё строго, проверено. Это не просто удобно. Это скорость разработки и дисциплина.
                                  • UFO just landed and posted this here
                                      0
                                      Строгое сравнение есть атрибут строгого кода.
                                      Про изобретателя JS не понятно к чему, хотя на данный момент есть сомнения, что нет людей знающих JS лучше его изобретателя (много понавесили уж)

                                      Сложного? Да вот ниже описывался пример с пустым массивом и пустой строкой. И таких плюшек, если память не изменяет, ещё штуки три. + вся таблица того же МДН.

                                      Не увиливаю, коли тема приведения типов и сравнения. И правда, кто мешает? Приведи типы и сравнивай точно.

                                      Он приведён ниже, этого достаточно =) Неустойчивость именно в непредсказуемости в пограничных ситуациях. Пустая строка и ноль (закодировалось так или иначе, мы же не верим данным), void 0 и null (не пришло вообще и пришло, что «здесь пусто»)

                                      Ни капельки не выдумываю типичный кодстайл. Вот установки от первоисточника:
                                      www.w3schools.com/js/js_conventions.asp общие правила
                                      www.w3schools.com/js/js_best_practices.asp рекомендации
                                      • UFO just landed and posted this here
                            0
                            Вот пример реальной баги, которую я исправлял. Функция для удобства принимала на вход либо строку, либо массив строк. В начале была проверка if(param == ""). Всё шло хорошо, пока не передали пустой массив.
                            • UFO just landed and posted this here
                            • UFO just landed and posted this here
                                0
                                Идея в том что от сервера должна приходить или строка или число или массив или объект или что-нибудь еще, но всегда одного и того же типа. Плавающий тип — это всегда спагетти в коде, возможные ошибки и куча лишних проверок, которых можно было бы избежать будь у сервера строгий контракт.
                                • UFO just landed and posted this here
                                    0
                                    > Если раньше, например, парсер просто передавал строку, а потом в новой версии стал распознавать числа, сразу отвергать данные что-ли?

                                    Ну а если вам вместо строки массив или объект прилетит — тоже можно не переживать? Протокол поменялся ведь, в этом случае или версирование используют, или клиента переписывают.
                                    По-поводу операторов — "==" или "===" — это два разных оператора с разной моделью поведения и используемых обычно в строго определенных ситуациях.

                                    Кстати, toFixed для форматирования предназначен, а не для округления. Для округления используют Math.round.
                                    • UFO just landed and posted this here
                              0
                              i++ можно заменить на i += 1

                              JSLint тоже сначала показался слишком жёстким, но потом привык :)

                              У JSHint был плохой парсер. Например:

                              for (k in obj) {
                              if (obj[k] === 5) {...}
                              }

                              Здесь должно быть предупреждение, типа «юзайте if (obj.hasOwnProperty(k))», но JSHint проверял только на то, что после for сразу стоит if.
                              Может исправили, не знаю.
                                0
                                А ещё он не умел
                                switch(variable)
                                {
                                  case 'some': { /* code */ }.
                                

                                Т.е. отдельный блок кода внутри case. Может быть уже умеет. А недавно наткнулся на критичность этого момента в случае использования let. Для let нужен безопасный изолированный блок кода.
                                  +1
                                  JSLint не только жесткий, он еще и заточен под Крокфорда. Крокфорд, конечно, крутой дядька, но не во всем с ним можно согласиться.
                                  +1
                                  Признаюсь, что смотрел я довольно поверхностно, но JSLint мне показался не жестким, а просто дурным.
                                  Он выдает такую кучу бесполезных варнингов, так много ругается на нормальный код, что невольно привыкаешь смотреть на эту кашу по диагонали. И редкие действительно серьезные вещи пропускаешь.
                                    0
                                    Хочу отметить, что JSLint — это один js-файл на 4000 строчек с комментариями. Там очень классный парсер, который описан в отдельной статье (мой перевод). В парсере строится полное синтаксическое дерево с полезными аннотациями (например, списки переменных, доступные в каждой области видимости). Я к тому, что JSLint легко форкнуть, расковырять, убрать ненужные диагностики и добавить новых по своему вкусу.

                                    Only users with full accounts can post comments. Log in, please.