7 июня 2022 г.

GCC: статическая проверка типов

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

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

Первым делом он, разумеется, проверяет структуру кода и сразу же выдаёт такие ошибки как пропущенная скобка или лишняя запятая.

Но, кроме этого, он умеет проверять типы переменных, используя как свои собственные знания о встроенных javascript-функциях и преобразованиях типов, так и информацию о типах из JSDoc, указываемую javascript-разработчиком.

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

Для вывода предупреждений при проверки типов используется флаг --jscomp_warning checkTypes.

Задание типа при помощи аннотации

Самый очевидный способ задать тип – это использовать аннотацию. Полный список аннотаций вы найдёте в документации.

В следующем примере параметр id функции f1 присваивается переменной boolVar другого типа:

/** @param {number} id */
function f(id) {
  /** @type {boolean} */
  var boolVar;

  boolVar = id; // (!)
}

Компиляция с флагом --jscomp_warning checkTypes выдаст предупреждение:

f.js:6: WARNING - assignment
found   : number
required: boolean
        boolVar = id; // (!)
        ^

Действительно: произошло присвоение значения типа number переменной типа boolean.

Типы отслеживаются по цепочке вызовов.

Ещё пример, на этот раз вызов функции с некорректным параметром:

/** @param {number} id */
function f1(id) {
  f2(id); // (!)
}

/** @param {string} id */
function f2(id) {}

Такой вызов приведёт к предупреждению со стороны минификатора:

f2.js:3: WARNING - actual parameter 1 of f2 does not match formal parameter
found   : number
required: string
  f2(id); // (!)
     ^

Действительно, вызов функции f2 произошёл с числовым типом вместо строки.

Отслеживание приведений и типов идёт при помощи графа взаимодействий и выведению (infer) типов, который строит GCC по коду.

Знания о преобразовании типов

Google Closure Compiler знает, как операторы javascript преобразуют типы. Такой код уже не выдаст ошибку:

/** @param {number} id */
function f1(id) {
  /** @type {boolean} */
  var boolVar;

  boolVar = !!id
}

Действительно – переменная преобразована к типу boolean двойным оператором НЕ. А код boolVar = „test-“+id выдаст ошибку, т.к. конкатенация со строкой даёт тип string.

Знание о типах встроенных функций, объектные типы

Google Closure Compiler содержит описания большинства встроенных объектов и функций javascript вместе с типами параметров и результатов.

Например, объектный тип Node соответствует узлу DOM.

Пример некорректного кода:

/** @param {Node} node */
function removeNode(node) {
  node.parentNode.removeChild(node)
}
document.onclick = function() {
  removeNode("123")
}

Выдаст предупреждение

f3.js:7: WARNING - actual parameter 1 of removeNode does not match formal parameter
found   : string
required: (Node|null)
  removeNode("123")
             ^

Обратите внимание – в этом примере компилятор выдаёт required: Node|null. Это потому, что указание объектного типа (не элементарного) подразумевает, что в функцию может быть передан null.

В следующем примере тип указан жёстко, без возможности обнуления:

/** @param {!Node} node */
function removeNode(node) {
  node.parentNode.removeChild(node)
}

Восклицательный знак означает, что параметр обязателен.

Найти описания встроенных типов и объектов javascript вы можете в файле экстернов: externs.zip находится в корне архива compiler.jar.

Интеграция с проверками типов из Google Closure Library

В Google Closure Library есть функции проверки типов: goog.isArray, goog.isDef, goog.isNumber и т.п.

Google Closure Compiler знает о них и понимает, что внутри следующего if переменная может быть только функцией:

var goog = {
  isFunction: function(f) {
    return typeof f == 'function'
  }
}

if (goog.isFunction(func)) {
  func.apply(1, 2)
}

Сжатие с проверкой выдаст предупреждение:

f.js:6: WARNING - actual parameter 2 of Function.apply does not match formal parameter
found   : number
required: (Object|null|undefined)
  func.apply(1, 2)
                ^      ^

То есть, компилятор увидел, что код, использующий func находится в if (goog.isFunction(func)) и сделал соответствующий вывод, что в этой ветке func является функцией, а значит вызов func.apply(1,2) ошибочен (второй аргумент не может быть числом).

Дело тут именно в интеграции с Google Closure Library. Если поменять goog на g – предупреждения не будет.

Резюме

Из нескольких примеров, которые мы рассмотрели, должна быть понятна общая логика проверки типов.

Соответствующие различным типам и ограничениям на типы аннотации вы можете найти в Документации Google. В частности, возможно указание нескольких возможных типов, типа undefined и т.п.

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

Проверка типов javascript, предоставляемая Google Closure Compiler – пожалуй, самая продвинутая из существующих на сегодняшний день.

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

Очень подробно проверка типов описана в книге Closure: The Definitive Guide, автора Michael Bolin.

Карта учебника