Пользовательские правила ESLint и анализ исходного кода

внешний интерфейс ESLint

Связанный фон:

Эта статья и предыдущая статья «Внешние инструменты обнаружения кода от 0 до 1» изначально были полной статьей, но поскольку длина слишком велика, а сама связь не имеет большого отношения, она разделена на две статьи: первая статья в основном о конфигурации, связанной с ESLint, исследует процесс интеграции плагинов и реализации проекта, который смещен в сторону конфигурации; вторая часть в основном посвящена обработке третьего параметра _.get() в пользовательских правилах ESLint, который смещен в сторону принципа исходного кода.

Статус проблемы:

Используется в проекте Loadash.js в методе _.get (), это часто будет какая-то довольно странная проблема (ошибка), такая как доступ к свойству, используйте _.get в неопределенной переменную () Получает следующее значение, вызывает значениеnull:

var obj = { name: null }
var name = _.get(obj, 'name', 'zly')

a 的值: null

Такой способ записи не играет никакой роли в защите данных, легко выдавать ошибки при возврате данных о местоположении в фоновом режиме из-за метода _.get().第三个参数失效, так что этой проблемы можно избежать, только изменив способ записи в будущем:

var obj = { name: null }
var name = _.get(obj, 'name') || 'zly'

a 的值: 'zly'

1. АСТ

1.1 Концепция АСТ:

ast, абстрактный синтаксический код,源代码的抽象语法结构的树状表示Каждый узел на дереве представляет структуру в исходном коде. Так называемая абстракция означает, что код JS был структурирован и преобразован в структуру данных. Эта структура данных на самом деле является большим объектом JSON. Мы все знакомы с JSON. Это похоже на листовое дерево: есть корни, стволы, ветви и листья. Независимо от того, насколько маленький или большой, это полное дерево.

Передний конец JS от компиляции процесса запуска:

image.pngЛексический анализ (токены) -> Синтаксический анализ -> AST

1.2 Структура АСТ

Онлайн астэксплорер:blogz.gitee.io/ast/(выбрать особенное)

var name = _.get(obj, 'name', 'zly')Сгенерированный АСТ:

{
  "type": "Program",
  "start": 0,
  "end": 36,
  "range": [
    0,
    36
  ],
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 36,
      "range": [
        0,
        36
      ],
      "declarations": [
        {
          "type": "VariableDeclarator", //声明
          "start": 4,
          "end": 36,
          "range": [
            4,
            36
          ],
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 8,
            "range": [
              4,
              8
            ],
            "name": "name"
          },
          "init": {
            "type": "CallExpression", //函数调用
            "start": 11,
            "end": 36,
            "range": [
              11,
              36
            ],
            "callee": {
              "type": "MemberExpression", //函数调用的成员表达式(解析) == _.get
              "start": 11,
              "end": 16,
              "range": [
                11,
                16
              ],
              "object": {
                "type": "Identifier", //标识符
                "start": 11,
                "end": 12,
                "range": [
                  11,
                  12
                ],
                "name": "_"
              },
              "property": {
                "type": "Identifier", //标识符
                "start": 13,
                "end": 16,
                "range": [
                  13,
                  16
                ],
                "name": "get"
              },
              "computed": false
            },
            "arguments": [ //参数
              {
                "type": "Identifier", //标识符
                "start": 17,
                "end": 20,
                "range": [
                  17,
                  20
                ],
                "name": "obj"
              },
              {
                "type": "Literal", //文本
                "start": 22,
                "end": 28,
                "range": [
                  22,
                  28
                ],
                "value": "name",
                "raw": "'name'"
              },
              {
                "type": "Literal",
                "start": 30,
                "end": 35,
                "range": [
                  30,
                  35
                ],
                "value": "zly",
                "raw": "'zly'"
              }
            ]
          }
        }
      ],
      "kind": "var" //关键字
    }
  ],
  "sourceType": "module"
}

Упрощенная древовидная структура выглядит следующим образом:

image.png

1.3 Процесс компиляции AST

2. Блок-схема правил запуска ESLint

Каждое правило eslint на самом деле является модулем node.После того, как пользователь настроит соответствующее правило, eslint загружает соответствующее правило, выполняет соответствующий модуль, а затем проверяет в соответствии с параметрами, предоставленными пользователем (например, чтобы увидеть, есть ли автоматическое исправление). требуется)

По задумке общая схема разборки эслинта выглядит следующим образом:

image.png

3. Пользовательские правила

Документация по пользовательскому правилу:ESL INT.BOOT CSS.com/docs/ VELO ...

3.1 Отладка локального файла правил

использовать--rulesdirПараметры конфигурации могут настроить локальные файлы правил, которые необходимо отладить:

"script": {
  "lint": "eslint --rulesdir ./scripts/lodash/rules"
}

3.2 Структура файла правил

module.exports = {
    meta: {
        type: "xxx",
        docs: {
          // 提示相关的文档信息
        },
        fixable: "code", // 是否可修复
        messages: { // 在单元测试可以使用
            unexpectedThirParameter:'Lodash.get() 第三个参数不建议使用',
            unexpectedParameterLength: 'Lodash.get() 参数数量错误',
        }
    },
    create: function(context) { // 分析时,会调用这个函数
        return {
            CallExpression(node) { // 函数调用的节点,也就是这个类型的节点
              
            }
        };
    }
};

Правило — это модуль узла, который в основном состоит из двух частей: мета и создания:

3.2.1 meta

meta содержит метаданные правила, где параметры:

  • ИСПРАВЛЕНО: Поддерживает ли плагин автоматическое восстановление
  • сообщения: в нем может быть установлен соответствующий messageId для использования подключаемым модулем (подсказка информации для внешних отчетов об ошибках)

3.2.2 create

Если мета выражает то, что мы хотим сделать, то создайте выражения这条 rule 具体会怎么分析代码:

  • Контекст параметра: объект содержит: метод, используемый ESLint для доступа к узлам при обходе абстрактного синтаксического дерева кода JavaScript.
  • Возвращаемое значение: необходимо вернуть объект, который может предоставить соответствующийast 节点类型的函数.
  • Выражение вызова функции:抽象语法树对应的节点类型,每种节点类型在遍历的时候,解析器都会检测外部是否提供了对应的函数,如果有就调用、并且传递出当前节点.

3.3 Пользовательские правила работают по приблизительной логике

3.4 Правила проверки нелегального кода

Когда подключаемый модуль запускает это правило, он проверяет наличие кода, не соответствующего правилу.Если вы хотите указать, что код является недопустимым, вы можете вызвать его внутри правила.context.report()Передайте объект, параметры, которые могут быть переданы:对应的节点а также错误的提示消息:

create: function(context) {
  return {
    CallExpression(node) {
      if (node.arguments.length === 3) {  // var a = _.get(obj, 'a', 'zly')
       	context.report({
           node,
           message: 'unexpectedThirParameter'
         })
       }
     }
   };
}

3.5 Функция исправления в ESLint

Когда обнаруживается, что код не соответствует спецификации, подключаемый модуль оценивает некоторые исходные данные пользовательских правил, такие какfixable(Можно ли настроить исправление). Если предоставлены эти метаданные и предоставлен отчетfix 函数, плагин вызовет функцию fix в правиле и передаст объект, который содержит несколько методов, которые могут работать ast, что нам нужно:replaceText(заменяет текст внутри данного узла или токена):

create: function(context) {
  return {
    CallExpression(node) {
       context.report({
         node,
         message: 'unexpectedParameterLength',
         fix(fixer) {
           // fix 的逻辑  
           // 最终输出 newCode
           return fixer.replaceText(node, newCode)
         }
       })
     }
   };
}

Основными из них являются функция исправления и параметр исправления, который она предоставляет.Плагин будет вызывать функцию исправления, а функция исправления должна возвращатьfixing 对象.

3.6 Исходный код

После получения необходимой информации, такой как объекты fix() и fixer, следует рассмотреть вопрос о том, как получить соответствующий исходный код, поскольку все, что здесь можно получить, — это узлы (ast), поэтому библиотеку синтаксического анализа ast необходимо введен для преобразования ast в исходный код:

const escodegen = require('escodegen')

create(context) {
  return {
    CallExpression(node) {
      const code = escodegen.generate(node) // 获取到源代码
  }
  }
}

В контексте предоставляется метод getSourceCode для получения исходного кода текущего узла:

create: function(context) {
  const source = context.getSourceCode() //获取源代码: _.get(xx,xx,xx)
  
   return {
      CallExpression(node) {
         if (node.arguments.length === 3) {
             context.report({
               node,
               message: '_.get()不建议使用第三个参数当默认值'
             })
         }
      }
   }
 }

В процессе получения исходников я тоже наступил на яму.Далее сравнение схем.

Цель мое решение Предоставляемая документация
получить исходный код Внедрение espree, ручного антианализа context.getSourceCode()
fixable установить на истину Официально: "код"

3.7 аналитические параметры

3.7.1 Код раскроя по специальному логотипу

Хотя можно использовать схему нарезки кода по специальному идентификатору, код выглядит странно и стоимость чтения становится высокой:

const codeArr = escodegen.generate(node).split(',')  
// 错误示范,反编译,切代码: _.get(a, "b", 0)  会变成: ['_.get(a', '"b"', '0']

const defaultValue = codeArr[codeArr.length - 1].trim().split(')')[0]  
// 利用括号去切代码,得到默认值

const code = `${codeArr[0]}, ${codeArr[1]}) || ${defaultValue}`

3.7.2 Регулярное сопоставление

Хотя схема использования обычного сопоставления выглядит простой, она дороже в чтении и неудобна в обслуживании:

const paramsReg = /(_\.get\([^,]+,[^,]+),([^,]+)([^)]+)?(\))/

3.7.3 Интервал AST

Используйте интервал, предоставленный ast, чтобы вычислить соответствующий интервал параметра, и точно сегментируйте код в соответствии с интервалом, чтобы получить код, который нам нужен:

create(context) {
   CallExpression(node) {
      const [ object, path, defaultValue ] = getArgumentsByNode(context, node)  
   }
}

function getArgumentsByNode(context, node) {
  if (!context || !node) {
    throw new Error('参数错误')
  }

  const nodeArguments = node.arguments

  if (!nodeArguments || nodeArguments.length === 0) {
    throw new Error('传入的节点,没有参数; 或者不是一个函数调用')
  }

  const sourceCode = context.getSourceCode() // 资源的一个实例,上面提供很多方法,比如获取给定节点的源码
  const originCode = sourceCode.getText(node) // 给定节点的源码

  return nodeArguments.map((item) => {
    const argumentLength = item.end - item.start
    const sliceStartPosition = item.start - node.start
    const sliceEndPosition = sliceStartPosition + argumentLength
    
    return originCode.slice(sliceStartPosition, sliceEndPosition)
  })
}

Все три вышеперечисленных решения имеют свои плюсы и минусы, но最可靠 && 最易扩展Он по-прежнему основан на интервале сегментации кода.

image.png

3.9 Автоматическое исправление правил

Мы получили важную информацию, которая нам нужна (например, исходный код, новый код и т. д.) раньше, и затем мы можем собрать правила.核心逻辑фиксированный:

create: function(context) {
  return {
    CallExpression(node) {
       context.report({
         node,
         message: 'unexpectedParameterLength',
         fix(fixer) {
            const [ object, path, defaultValue ] = getArgumentsByNode(context, node)
            const newCode = `_.get(${object}, ${path}) || ${defaultValue}`
            
            return fixer.replaceText(node, newCode)
         }
       })
     }
   }
}

дляgetArgumentsByNodeЭтот метод: просто перейдите в上下文а также对应的节点, Сможет получить узел对应的参数. Последующие попытки для некоторых пользовательских правил могут быть расширены на основе этой функции, чтобы инкапсулировать некоторые дополнительные аналитические методы, подходящие для самого бизнеса.

3.10 Совместимость с некоторыми особыми случаями

На данный момент основная функция правила выполнена, но поскольку это правило является не только функцией исправления, есть еще и основная функция校验逻辑,Например参数不提供,参数超过3个以上Проверьте ситуацию:

var a = _.get() //不合法

var a = _.get(object) //不合法

var a = _.get(object, 'a', 0, 0) //不合法

var a = _.get(object, 'a', 0) //不合法,但是会自动fix

Для исправления также совместим особый случай, например, когда есть двоичное выражение или есть проблема с приоритетом оператора и т. д.:

var a = _.get(object, 'a', 0) + 1

// 如果 auto fix 后,代码如下
// 由于 + 优先级比 || 高, fix 后就有问题
var a = _.get(object, 'a') || 0 + 1 

Полная картина совместимого кода выглядит следующим образом:

create(context) {
  CallExpression(node) {
    // ——————————————————————————参数兼容——————————————————————————————
    // 没有参数或者一个参数: _.get() / _.get(object)
    if (node.arguments.length === 0 || node.arguments.length === 1) {
      context.report({
         node,
         messageId: 'unexpectedParameterLength',  // 错误Id
      })
      return
    }
    // 参数大于超过3个: _.get(object, 'key', 0, 0)   不推荐的写法
    if (node.arguments.length > 3) { 
      context.report({ 
        node,
        messageId: 'unexpectedParameterLength', // 错误Id
      })
      return
    }

    const [ object, path, defaultValue ] = getArgumentsByNode(context, node)
    const parentNode = node.parent
    let newCode = `_.get(${object}, ${path}) || ${defaultValue}`
    
    // ————————————————————————特殊运算符的各种兼容———————————————————————
    //二元表达式  var key = _.get(object, "key", 0) + 1
    if (parentNode.type === 'BinaryExpression') {
      newCode = `(${newCode})`
    }
  }
}

4. Модульное тестирование

4.1 Шуточные модульные тесты

ООО "О ООО". Смущая технология est to / docs / день Фитнес ...

4.1.1 Правила шутливого теста

Относительно просто использовать jest для тестирования — просто введите этот фреймворк для тестирования и напишите соответствующие тестовые примеры:

const Linter = require('eslint').Linter
const rules = require('../../lib/rules/lodash-get')

describe("关于lodash第三个参数的单元测试",()=>{
  const linter = new Linter()
  const config = {
    rules: {
      "lodash-get": "error"
    }
  }
  
  linter.defineRule(key, rules['lodash-get'])
 
  it("var key = _.get(object, 'key', '')", () => {
    const code = `var key = _.get(object, 'key', '')`
    const output = `var key = _.get(object, 'key') || ''`
    expect(linter.verifyAndFix(code, config).output).toBe(output);
  }) 
  .....
}

4.1.2 Проблемы при шутливом тестировании

При тестировании с помощью шутки будут странные тест-кейсы, например, когда параметр теста пуст, параметр в правиле пуст.只做了抛错, и нет автоматического исправления:

  it("Lodash.get() 参数为空", () => {
    const code = `var get = _.get()`
    const message = linter.verifyAndFix(code, config).messages[0].message
    expect(message).toBe('Lodash.get() 参数为空');
  })

Сосредоточение внимания на проблемах, возникающих во время обучения, можно резюмировать следующим образом:

  • Когда параметр пуст, его не следует использоватьlinter.verifyAndFix, потому что правила не фиксированы
  • Сравнение выброшенных ошибок вручную в значительной степени зависит от сообщений об ошибках, что может привести к测试用例不健壮(потому что есть волшебные строки)
  • даже использоватьlinter.verfify()Для тестирования особых случаев использования также необходимо автоматически получать информацию об ошибках для сравнения (что приведет к测试用例不健壮)
  • Плохое различие参数个数нелегально, до сих пор参数незаконный
  • Читабельность плохая, код избыточен, трудно различить参数个数нелегально, до сих пор参数незаконный

4.2 Справочник сообщества

Не только потому, что нашли проблемы во время тестирования, но и потому, что окончательные результаты не соответствовали нашим ожиданиям, поэтому нам нужно обратиться к тестовым случаям, используемыми проектами, связанными со сообществом. Основная ссылка здесьeslint-plugin-vueТестовые случаи в этом плагине.

Следующее настроено в eslint-plugin-vueblock-spacing(块里面前后要有空格)Правила, даже такой масштабный проект имеет непрофессиональные аспекты, такие как использование魔法字符 brace-style:

image.png

4.3 Тесты, предоставленные ESLint

Здесь messageId — это метаинформация в определении плагина. Когда ruleTester вызывает метод run, плагин проверяет код. Плагин выдает соответствующее сообщение об ошибке, а затем сравнивает тестовый пример и messageId, выданный внутри плагина, с посмотрите, непротиворечивы ли они. Если они непротиворечивы, тест пройден:

const RuleTester = require('eslint').RuleTester
const ruleTester = new RuleTester();
const rules = require('../../lib/rules/lodash-get')
ruleTester.run('lodash-get', rules, {
    valid: [
      {
        code: 'var key = _.get(object, "key") || {}',
      }
    ],
    invalid: [
      {
        code: 'var get = _.get()',
        errors: [{ 
          messageId: 'unexpectedParameterLength'
        }]
      },
      {
        code: 'var key = _.get(object)',
        errors: [{ 
          messageId: 'unexpectedParameterLength'
        }]
      },
      {
        code: 'var key = _.get(object, "key", {}, {})',
        errors: [{ 
          messageId: 'unexpectedParameterLength'
        }]
      },
      {
        code: 'var key = _.get(object, "key", 0) - 1',
        output: 'var key = (_.get(object, "key") || 0) - 1',
        errors: [{
          messageId: 'unexpectedThirParameter',
        }]
      },
    ]
)}

4.4 правила последующего обслуживания

В настоящее время некоторые вите, совместимые на вите несколько краев, тестовое покрытие не завершено, потому что нет бизнеса, прежде чем крещение трудно проверить целостность этого правила, в течение некоторого времени, они постепенно нашли некоторые условия границы:

逻辑运算符 
var key = _.get(object, "key", {}) && {}


for in循环
for (var key in _.get(object, "key", {})) {}


typeOf 操作符
typeof _.get(object, "key", {})


链式调用
// fix后变成有问题的代码 _.get(object, 'key') || [].map()
_.get(object, 'key', []).map() 

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

if (
   parentNode.type === 'BinaryExpression' ||   //二元表达式    
   parentNode.type === 'LogicalExpression' ||  // 逻辑运算符     
   parentNode.type === 'ForInStatement' ||     // for in       
   parentNode.operator === 'typeof' ||         // typeOf 操作符      
   parentNode.property //_.get(object, 'key', []).map()
 ) {
   newCode = `(${newCode})`
}

После того, как код для этих границ станет совместимым, эффект исправления будет следующим:

逻辑运算符 
var key = _.get(object, "key", {}) && {}

fix后的代码
var key = (_.get(object, "key") || []) && {}

V. Резюме

слишком далеко上篇 + 下篇Полный объем книги — это почти полное понимание применения ESLint. Благодаря бывшей команде и руководителю команды были закодированы названия некоторых внутренних проектов с участием бывшей компании.