ESLint для Ember — еще чуток правил (1.7.0)

ESLint + Ember

Я продолжаю работать над плагином ember-cleanup для ESLint. Вот уже вышла версия 1.7.0, в которой были добавлены 4 правила.

Установка как обычно через npm (глобально, если eslint установлен глобально):

npm i eslint-plugin-ember-cleanup

`square-brackets` (#)

В одной из предыдущих версий (1.3.0) я написал правило one-level-each, которое проверяло использование @each внутри зависимых ключей. В нем была проверка на наличие @each в конце ключа (предлагалось замена на []). Прошло немного времени и я понял, что для [] внутри ключей тоже есть ряд ограничений. Так появилось правило square-brackets. В нем осуществляется 3 проверки:

  • Зависимый ключ должен заканчиваться на [], а не на @each (перекочевало из one-level-each).
  • [] должно быть только в конце зависимого ключа.
  • Перед [] должна быть точка (бывают описки).

Само правило довольно простое:

module.exports = function(context) {
  var m1 = "Dependent key should not end with `@each`, use `[]` instead.";
  var m2 = "`[]` should be at the end of the dependent key.";
  var m3 = "Dot should be before `[]`.";
 
  function onlyUnique(value, index, self) {
    return self.indexOf(value) === index;
  }
 
  return {
    "CallExpression": function (node) {
      if (ember.isEmberField(node)) {
        var dependentKeys = ember.getDependentKeys(node);
        dependentKeys = ember.expandDependentKeys(dependentKeys.filter(onlyUnique));
        var report1 = false, report2 = false, report3 = false;
        for (var i = 0; i < dependentKeys.length; i++) {
          var key = dependentKeys[i];
          var subKeys = key.split(".");
          if (subKeys[subKeys.length - 1] === "@each") {
            report1 = true;
            continue;
          }
          var pos = key.indexOf("[]");
          if (pos === -1) {
            continue;
          }
          if (key.length !== pos + 2) {
            report2 = true;
            continue;
          }
          if (key[pos - 1] !== ".") {
            report3 = true;
          }
        }
        if (report1) {context.report(node, m1);}
        if (report2) {context.report(node, m2);}
        if (report3) {context.report(node, m3);}
      }
    }
  };
 
};

Для каждого CallExpression (если оно обсервер или вычислимое свойство) берем список зависимых ключей и раскрываем в них фигурные скобки. Далее, каждый из них проверяем на наличие описанных выше дефектов. При обнаружении одного из них, проверка ключа прекращается. То есть, если ключ содержит несколько ошибок (например, a[].b), то пользователь получит уведомление только по первой.

`no-empty-declaration` (#)

Генераторы в ember-cli позволяют быстро создавать «скелеты» таких единиц, как роуты, компоненты, контроллеры и т.д (пример, ember g <type> instance). В то же время часто бывают ситуации, когда разработчик что-то сгенерировал и так и оставил «пустым» (такое часто бывает с роутами). Никаких ошибок в работе приложения при этом не будет, но зачем нам такие пустышки в коде? Правило no-empty-declaration находит конструкции вида Route|Component|Controller.extend({/* пусто */});.

С точки зрения «сложности», правило довольно простое:

var message = "Empty `extend` is redundant.";
 
module.exports = function(context) {
 
  var options = context.options[0] || {};
  var allowedFor = options.allowedFor || [];
  var allowedForL = allowedFor.length;
 
  return {
 
    "CallExpression": function (node) {
      var caller = n.getCaller(node);
      if (n.getFunctionName(node) !== "extend") {
        return;
      }
      for (var i = 0; i < allowedForL; i++) {
        if (caller.indexOf(allowedFor[i]) !== -1) {
          return;
        }
      }
      var args = node.arguments;
      if (args.length) {
        if (args[0].type !== "ObjectExpression") {
          return;
        }
        if(!args[0].properties.length) {
          context.report(node, message);
        }
      }
      else {
        context.report(node, message);
      }
    }
 
  };
 
};

В правило можно задать массив сущностей, для которых можно оставлять «пустышки». Пример: при настройках правила {allowedFor: ["Model"]}, оно не будет реагировать на код вида Model.extend({}).

`no-define-property` (#)

В документации Ember’а для метода defineProperty сказано следующее:

This is a low-level method used by other parts of the API. You almost never want to call this method directly. Instead you should use Ember.mixin() to define new properties.

Опять-таки, приложение не развалится, если вы будете использовать этот метод. Но, не зря авторы Ember рекомендуют не использовать его. Причина простая — читаемость кода. Сущности, описание которых находится в одном месте, читаются/поддерживаются/расширяются куда проще, чем те, у которых объявление свойств разбросано по коду. Тут есть шансы получить и дублирование кода, и повышение его сложности:

// don't do this
 
var obj = Ember.Object.create({p1: [1, 2, 3]});
/*
    ... some lines of code ...
*/
Ember.defineProperty(obj, 'p2', Ember.computed.sum('p1'));
/*
    ... more code ...
*/
Ember.defineProperty(obj, 'p3', Ember.computed.bool('p2'));

А ведь может быть так:

// a little bit better
 
var O = Ember.Object.extend({
    p1: [],
    p2: Ember.computed.sum('p1'),
    p3: Ember.computed.bool('p2')
});
 
var obj = O.create({p1: [1, 2, 3]});

Код правила доступен ниже:

 
var n = require("../utils/node.js");
var ember = require("../utils/ember.js");
 
module.exports = function (context) {
 
  var m = "`Ember.defineProperty` should not be used. Use `Ember.mixin()` to define new properties.";
 
  var definePropertyFromEmber = false;
 
  return {
 
    "VariableDeclarator": function (node) {
      var id = node.id;
      if (!id) {
        return;
      }
      var init = node.init;
      if (id.type === "Identifier" && id.name === "defineProperty" && init) {
        if (init.type === "MemberExpression") {
          var caller = n.getCaller(init);
          if (caller === "Ember.defineProperty" || caller === "Em.defineProperty") {
            definePropertyFromEmber = true;
          }
        }
      }
      if (id.type === "ObjectPattern" && ember.isEmber(init.name)) {
        var properties = id.properties;
        var pLength = properties.length;
        for (var i = 0; i < pLength; i++) {
          var prop = properties[i];
          if (prop.key.name === "defineProperty" && prop.value.name === "defineProperty") {
            definePropertyFromEmber = true;
            break;
          }
        }
      }
    },
 
    "CallExpression": function (node) {
      var caller = n.cleanCaller(n.getCaller(node));
      if (caller === "Ember.defineProperty" || caller === "Em.defineProperty") {
        context.report(node, m);
      }
      else {
        if (caller === "defineProperty" && definePropertyFromEmber) {
          context.report(node, m);
        }
      }
    }
 
  }
 
};

А почему так много? Тяжело что-ли проверить CallExpression на равенство Ember|Em.defineProperty? Когда defineProperty вызван явно из Эмбера, то проблемы нет. Проблема возникает, когда defineProperty вызван «as is». Почему? Потому что в JS у Object тоже есть метод defineProperty и его вызовы вполне «законны»:

const {defineProperty} = Object;
defineProperty(obj, 'p1', {/**/}); // valid
Ember.defineProperty(obj, 'p1', {/**/}); // error
 
// some other place
const {defineProperty} = Ember;
defineProperty(obj, 'p1', {/**/}); // error

Функция-обработчик для VariableDeclarator пытается найти определение defineProperty и понять, взято оно из Эмбера или нет. Внутри обработчика для CallExpression проверяется вызов Ember|Em.defineProperty или же просто defineProperty (если он Эмберовский).

`cp-macro-alias` (#)

Часто ли попадается кода вида:

Ember.computed('a', function() {
    return this.get('a');
});
 
function () {
    return this.get('a');
}.property('a')
 
Ember.computed('a', function() {
    return this.get('a.b');
});
 
function () {
    return this.get('a.b');
}.property('a')

Мне не часто, но попадается. Вышеописанные фрагменты соответствуют коду Ember.computed.alias('...'). При чем, в двух последних функциях допущена ошибка — вычислимое свойство возвращает значение a.b, а вот зависимый ключ для него просто a.

Что является отличительными особенностями такого кода:

  • Наличие одной инструкции в теле функции — ReturnStatement.
  • Эта инструкция являет собой вызов функции this.get с одним строчным аргументом или же вызов Em|Ember.get с двумя аргументами, первый из которых является ThisExpression, а второй — строка.

Код правила:

var m = "May be simplified to `computed.alias`";
 
module.exports = function (context) {
 
  return {
 
    "CallExpression": function (node) {
      if (ember.isComputedProperty(node)) {
        var cpBody = ember.getCpBody(node);
        if (cpBody && cpBody.type === "FunctionExpression") {
          var body = cpBody.body.body;
          if (body.length === 1) {
            var statement = body[0];
            if (statement.type === "ReturnStatement") {
              var ret = statement.argument;
              if (ret.type === "CallExpression") {
                var callee = n.getCaller(ret);
                var args = ret.arguments;
                if ("this.get" === callee && args.length === 1 && args[0].type === "Literal") {
                  return context.report(node, m);
                }
                if (["Em.get", "Ember.get", "get"].indexOf(callee) !== -1 && args.length === 2 && args[0].type === "ThisExpression" && args[1].type === "Literal") {
                  return context.report(node, m);
                }
              }
            }
          }
        }
      }
    }
 
  };
 
};

P.S.

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

, ,

Оставить комментарий

Top ↑ | Main page | Back