CodeFest 2019. Александр Мышов (Яндекс) — Автоматический рефакторинг кода с помощью codemodes

CodeFest 2019. Александр Мышов (Яндекс) — Автоматический рефакторинг кода с помощью codemodes

Иногда бывает так, что изменение сигнатуры одной функции или обновление зависимости может повлечь за собой несколько дней скрупулёзной работы. Для упрощения и автоматизации этого процесса, можно написать свой codemode. Сodemode — это скрипт, работающий с абстрактным синтаксическим деревом (ast) javascript. Цель codemode — автоматизировать рефакторинг кода.

В своём докладе я расскажу про jscodeshift — тулкит для написания codemodes. Покажу и разберу несколько примеров codemodes: начиная простыми и заканчивая теми, которые могут быть использованы в вашем проекте. Вы увидите, что работа с ast на самом деле не такая уж и сложная задача, как может показаться на первый взгляд, и что владение этим инструментом может дать очень сильный прирост вашей эффективности.

16b6c87229eaf58768d25ed7b2bbbf52?s=128

CodeFest

April 06, 2019
Tweet

Transcript

  1. Автоматический рефакторинг кода с помощью codemodes Мышов Александр Разработчик интерфейсов

    Яндекс Маркет
  2. Что будет? >2 › Зачем нужны кодмоды › Немного про

    AST › Описание jscodeshift › Примеры кодмодов
  3. Какие проблемы решаем? Photo by Padden Jhonson (unsplash.com)

  4. Обновление библиотеки >4 // №1 (было/стало) this.createPageObject(pageObject, this.browser); this.createPageObject(pageObject); //

    №2 (было/стало) this.createPageObject(pageObject, someParent); this.createPageObject(pageObject, { parent: 'someParent', }); // №3 (было/стало) this.createPageObject(pageObject, someParent, `some${val}Root`); this.createPageObject(pageObject, { parent: someParent, root: `some${val}Root`, });
  5. Использование нового стандарта (было) >5 var React = require('react'); var

    Clock = React.createClass({ getInitialState: function () { return { date: new Date() }; }, render: function () { return ( <div> Сейчас {this.state.date.toLocaleTimeString()} </div> ); } }); module.exports = Clock;
  6. Использование нового стандарта (стало) >6 import React, {Component} from 'react';

    class Clock extends Component { constructor(props) { super(props); this.state = {date: new Date()}; } render() { return ( <div> Сейчас {this.state.date.toLocaleTimeString()} </div> ); } } export default Clock;
  7. Проблемы >7 › Рефакторинг может быть сложным и долгим ›

    Обычный поиск и замена слишком просты › Регулярные выражения не помогут в сложных случаях › Ограниченные виды рефакторинга с помощью IDE › Нет автоматизации
  8. Codemod – скрипт, модифицирующий программный код с помощью преобразования AST

  9. AST – Abstract Syntax Tree Photo by Fabrice Villard (unsplash.com)

  10. AST – структура данных, являющаяся внутренним представлением исходного кода программы

  11. Пример >11 const two = 1 + 1;

  12. Графическое представление AST >12 VariableDeclarator id: Identifier init: BinaryExpression 1

    1 'two'
  13. { program: { type: "Program", body: [ { type: "VariableDeclaration",

    declarations: [ { type: "VariableDeclarator", id: { type: "Identifier", name: "two" }, init: { type: "BinaryExpression", operator: "+", left: { type: "Literal", value: 1, raw: "1" }, right: { type: "Literal", value: 1, raw: "1" Представление AST в JS >13
  14. AST Explorer >14 https://astexplorer.net

  15. Примеры использования AST >15 › Плагины для babel › Eslint

    правила › Кодмоды › и т.п.
  16. Доклад про AST >16 https://2018.codefest.ru/lecture/1302/

  17. jscodeshift Photo by Chester Alvarez (unsplash.com)

  18. Принцип работы >18 jscodeshift новый js-код изменяемый js-код кодмод

  19. Структура jscodeshift >19 jscodeshift парсеры ast-types recast

  20. Структура jscodeshift >20 › парсеры - babel/parser, flow › ast-types

    - библиотека для создания узлов AST-дерева › recast - библиотека для красивого форматирования кода
  21. Основные понятия >21 › Node - узел ast-дерева › Path

    - обёртка над узлом, позволяющая обходить дерево › Collection - набор объектов типа path с методами трансформации
  22. Блаблафикатор >22 console.log('Hi, Codefest!'); bla.bla('Hi, Codefest!');

  23. Блаблафикатор (план) >23 Поиск старого идентификатора Замена новым идентификатором .find(...)

    .replaceWith(...)
  24. Блаблафикатор (codemod) >24 export default function transformer(file, api) { const

    j = api.jscodeshift; return j(file.source) .find(j.Identifier) .replaceWith( j.identifier('bla') ) .toSource(); }
  25. Блаблафикатор (codemod) >25 export default function transformer(file, api) { const

    j = api.jscodeshift; return j(file.source) .find(j.Identifier) .replaceWith( j.identifier('bla') ) .toSource(); }
  26. Блаблафикатор (codemod) >26 export default function transformer(file, api) { const

    j = api.jscodeshift; return j(file.source) .find(j.Identifier) .replaceWith( j.identifier('bla') ) .toSource(); }
  27. Блаблафикатор (codemod) >27 export default function transformer(file, api) { const

    j = api.jscodeshift; return j(file.source) .find(j.Identifier) .replaceWith( j.identifier('bla') ) .toSource(); } Тип узла (с заглавной буквы)
  28. Блаблафикатор (codemod) >28 export default function transformer(file, api) { const

    j = api.jscodeshift; return j(file.source) .find(j.Identifier) .replaceWith( j.identifier('bla') ) .toSource(); }
  29. export default function transformer(file, api) { const j = api.jscodeshift;

    return j(file.source) .find(j.Identifier) .replaceWith( j.identifier('bla') ) .toSource(); } Блаблафикатор (codemod) >29 Builder (со строчной буквы)
  30. export default function transformer(file, api) { const j = api.jscodeshift;

    return j(file.source) .find(j.Identifier) .replaceWith( j.identifier('bla') ) .toSource(); } Блаблафикатор (codemod) >30 Builder (со строчной буквы)
  31. export default function transformer(file, api) { const j = api.jscodeshift;

    return j(file.source) .find(j.Identifier) .replaceWith( j.identifier('bla') ) .toSource(); } Блаблафикатор (bla-transform.js) >31
  32. export default function transformer(file, api) { const j = api.jscodeshift;

    return j(file.source) .find(j.Identifier) .replaceWith( j.identifier('bla') ) .toSource(); } export const parser = 'flow'; Блаблафикатор (с нестандартным паресером) >32
  33. Блаблафикатор (трансформация в cli) >33 $ npm i -g jscodeshift

    $ jscodeshift -t bla-transform.js file.js
  34. Блаблафикатор (результат) >34 bla.bla('Hi, Codefest!');

  35. Примеры кодмодов Photo by Ahmad Ardity (pixabay.com)

  36. Удаление console.log() #1 Удаление узлов в AST

  37. Удаление console.log() >37 function sum(a, b) { const result =

    a + b; console.log('Hello'); return result; }
  38. Удаление console.log() (план) >38 Поиск console.log() Проход по коллекции Удаление

    .find(..., filter) .forEach(...) .remove()
  39. Анатомия console.log() >39 console.log('Hello') CallExpression callee: MemberExpression arguments: [Literal]

  40. Анатомия console.log() - часть 2 >40 console.log MemberExpression object
 Identifier

    property
 Identifier
  41. Анатомия console.log() - часть 3 >41 console Identifier name
 "console"

  42. { type: "CallExpression", callee: { type: "MemberExpression", object: { type:

    "Identifier", name: "console" }, property: { type: "Identifier", name: "log" } } } Анатомия console.log() - часть 4 >42
  43. { type: "CallExpression", callee: { type: "MemberExpression", object: { type:

    "Identifier", name: "console" }, property: { type: "Identifier", name: "log" } } } Анатомия console.log() - часть 4 >43
  44. Анатомия console.log() - часть 4 >44 { type: "CallExpression", callee:

    { type: "MemberExpression", object: { type: "Identifier", name: "console" }, property: { type: "Identifier", name: "log" } } }
  45. .find(j.CallExpression, { type: "CallExpression", callee: { type: "MemberExpression", object: {

    type: "Identifier", name: "console" }, property: { type: "Identifier", name: "log" } } }) Поиск console.log() >45
  46. .find(j.CallExpression, { type: "CallExpression", callee: { type: "MemberExpression", object: {

    type: "Identifier", name: "console" }, property: { type: "Identifier", name: "log" } } }) Поиск console.log() – filter >46
  47. .find(j.CallExpression, { callee: { object: { type: "Identifier", name: "console"

    }, property: { type: "Identifier", name: "log" } } }) Поиск console.log() – filter >47
  48. Поиск console.log() >48 .find(j.CallExpression, { callee: { object: { name:

    'console' }, property: { name: 'log' } } })
  49. Удаление всех console.log() >49 export default function transformer(file, api) {

    const j = api.jscodeshift; return j(file.source) .find(j.CallExpression, { callee: { object: { name: 'console' }, property: { name: 'log' } } }) .forEach(path => { j(path).remove(); }) .toSource(); }
  50. Удаление всех console.log() >50 return j(file.source) .find(j.CallExpression, { callee: {

    object: { name: 'console' }, property: { name: 'log' } } }) .forEach(path => { j(path).remove(); }) .toSource();
  51. Удаление всех console.log() >51 return j(file.source) .find(j.CallExpression, { callee: {

    object: { name: 'console' }, property: { name: 'log' } } }) .forEach(path => { j(path).remove(); }) .toSource();
  52. Удаление всех console.log() >52 return j(file.source) .find(j.CallExpression, { callee: {

    object: { name: 'console' }, property: { name: 'log' } } }) .forEach(path => { j(path).remove(); }) .toSource();
  53. Удаление всех console.log() >53 return j(file.source) .find(j.CallExpression, { callee: {

    object: { name: 'console' }, property: { name: 'log' } } }) .forEach(path => { j(path).remove(); }) .toSource();
  54. Удаление всех console.log() >54 return j(file.source) .find(j.CallExpression, { callee: {

    object: { name: 'console' }, property: { name: 'log' } } }) .remove() .toSource();
  55. Удаление всех console.log() >55 return j(file.source) .find(j.CallExpression, { callee: {

    object: { name: 'console' }, property: { name: 'log' } } }) .remove() .toSource();
  56. Удаление console.log() – исходный код >56 function sum(a, b) {

    const result = a + b; console.log('Hello'); return result; }
  57. Удаление console.log() – результат >57 function sum(a, b) { const

    result = a + b; return result; }
  58. Замена аргументов функции #2 Создание новых узлов в AST

  59. Замена аргументов функции >59 increment(i); increment({value: i});

  60. Замена аргументов функции (план) >60 Поиск increment() Создание нового AST-узла

    .find() .replaceWith() Замена arguments buildNode()
  61. Builders – формат аргументов >61 j.identifier('bla'); j.objectExpression(???);

  62. Definition files - описание AST-узлов >62 https://github.com/benjamn/ast-types/blob/master/def

  63. ObjectExpression - definition >63 def("ObjectExpression") .bases("Expression") .build("properties") .field("properties", [def("Property")]);

  64. ObjectExpression - аргументы builder'а >64 def("ObjectExpression") .bases("Expression") .build("properties") .field("properties", [def("Property")]);

  65. ObjectExpression - типы аргументов >65 def("ObjectExpression") .bases("Expression") .build("properties") .field("properties", [def("Property")]);

  66. Property - definition >66 def("Property") .bases("Node") .build("kind", "key", "value") .field("kind",

    or("init", "get", "set")) .field("key", or(def("Literal"), def("Identifier"))) .field("value", def("Expression"));
  67. Property - аргументы builder'а >67 def("Property") .bases("Node") .build("kind", "key", "value")

    .field("kind", or("init", "get", "set")) .field("key", or(def("Literal"), def("Identifier"))) .field("value", def("Expression"));
  68. Property - типы аргументов >68 def("Property") .bases("Node") .build("kind", "key", "value")

    .field("kind", or("init", "get", "set")) .field("key", or(def("Literal"), def("Identifier"))) .field("value", def("Expression"));
  69. Поиск функции >69 .find(j.CallExpression, { callee: { type: 'Identifier', name:

    'increment' } })
  70. Замена аргументов функции >70 .find(...) .replaceWith(nodePath => { const {node}

    = nodePath; const args = node.arguments; node.arguments = [ j.objectExpression([ buildProperty('value', args[0]) ]) ]; return node; })
  71. Замена аргументов функции >71 .find(...) .replaceWith(nodePath => { const {node}

    = nodePath; const args = node.arguments; node.arguments = [ j.objectExpression([ buildProperty('value', args[0]) ]) ]; return node;
  72. Замена аргументов функции >72 function buildProperty(name, value) { return j.property(

    'init', j.identifier(name), value, ) }
  73. Замена аргументов функции >73 .replaceWith(nodePath => { const {node} =

    nodePath; const args = node.arguments; node.arguments = [ j.objectExpression([ buildProperty('value', args[0]) ]) ]; return node; })
  74. Замена аргументов функции >74 .replaceWith(nodePath => { const {node} =

    nodePath; const args = node.arguments; node.arguments = [ j.objectExpression([ buildProperty('value', args[0]) ]) ]; return node; })
  75. Замена аргументов функции >75 .replaceWith(nodePath => { const {node} =

    nodePath; const args = node.arguments; node.arguments = [ j.objectExpression([ buildProperty('value', args[0]) ]) ]; return node; })
  76. Замена аргументов функции (кодмод) >76 function buildProperty(name, value) { return

    j.property( 'init', j.identifier(name), value, ) } return root .find(j.CallExpression, { callee: { type: 'Identifier', name: 'increment' } }) .replaceWith(nodePath => { const {node} = nodePath; const args = node.arguments; node.arguments = [ j.objectExpression([ buildProperty('value', args[0]) ]) ]; return node; }) .toSource();
  77. Результат работы кодмода >77 increment(increment(increment(1))); increment({ value: increment({ value: increment({

    value: 1 }) }) });
  78. Замена устаревшего кода #3 Замена узлов в AST

  79. Замена устаревшего кода >79 _.orderBy(); _.sortBy();

  80. Возможный import lodash >80 import _ from lodash; import l

    from lodash; import lodash from lodash;
  81. Замена устаревшего кода (план) >81 Поиск имени идентифик атора .find()

    .replaceWith() Замена имени метода .find() Поиск вызовов метода
  82. Поиск имени идентификатора >82 const importDeclarations = root.find(j.ImportDeclaration, { source:

    { type: 'Literal', value: 'lodash', }, }); const localNameSpace = importDeclarations .find(j.Identifier) .get(0) .node.name;
  83. Поиск имени идентификатора >83 const importDeclarations = root.find(j.ImportDeclaration, { source:

    { type: 'Literal', value: 'lodash', }, }); const localNameSpace = importDeclarations .find(j.Identifier) .get(0) .node.name;
  84. Поиск и замена вызова метода >84 .find(j.MemberExpression, { object: {

    name: localNameSpace, }, property: { name: 'sortBy', }, }) .replaceWith(nodePath => { const {node} = nodePath; node.property.name = 'orderBy'; return node; }) .toSource()
  85. Поиск и замена вызова метода >85 .find(j.MemberExpression, { object: {

    name: localNameSpace, }, property: { name: 'sortBy', }, }) .replaceWith(nodePath => { const {node} = nodePath; node.property.name = 'orderBy'; return node; }) .toSource()
  86. Поиск и замена вызова метода >86 .find(j.MemberExpression, { object: {

    name: localNameSpace, }, property: { name: 'sortBy', }, }) .replaceWith(nodePath => { const {node} = nodePath; node.property.name = 'orderBy'; return node; }) .toSource()
  87. Поиск и замена вызова метода >87 .find(j.MemberExpression, { object: {

    name: localNameSpace, }, property: { name: 'sortBy', }, }) .replaceWith(nodePath => { const {node} = nodePath; node.property.name = 'orderBy'; return node; }) .toSource()
  88. Результат первой трансформации >88 _.orderBy(); _.sortBy();

  89. Проблемный код >89 _.chain([]).uniq().sortBy() _.chain([]).uniq().sortBy()

  90. Замена устаревшего кода (план Б) >90 Поиск имени идентифик атора

    .find() isLodashChain() Проверка чейнинга .find() Поиск вызовов метода .replaceWith() Замена имени метода
  91. Поиск и замена вызова метода >91 .find(j.MemberExpression, { property: {

    name: 'sortBy', }, }) .replaceWith(nodePath => { const {node} = nodePath; if (isLodashChain(node)) { node.property.name = 'orderBy'; return node; } return node; }) .toSource()
  92. Поиск и замена вызова метода >92 .find(j.MemberExpression, { property: {

    name: 'sortBy', }, }) .replaceWith(nodePath => { const {node} = nodePath; if (isLodashChain(node)) { node.property.name = 'orderBy'; return node; } return node; }) .toSource()
  93. Поиск и замена вызова метода >93 .find(j.MemberExpression, { property: {

    name: 'sortBy', }, }) .replaceWith(nodePath => { const {node} = nodePath; if (isLodashChain(node)) { node.property.name = 'orderBy'; return node; } return node; }) .toSource()
  94. Проверка чейнинга >94 function isLodashChain(node) { while ( node.type !==

    'Identifier' && node.object.type === 'CallExpression' ) { node = node.object.callee; } let name = node.name || node.object.name; return name === localNameSpace; }
  95. Поиск и замена вызова метода >95 .find(j.MemberExpression, { property: {

    name: 'sortBy', }, }) .replaceWith(nodePath => { const {node} = nodePath; if (isLodashChain(node)) { node.property.name = 'orderBy'; return node; } return node; }) .toSource()
  96. Замена устаревшего кода >96 export default (fileInfo, api) => {

    const j = api.jscodeshift; const root = j(fileInfo.source); const importDeclarations = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'lodash', }, }); const oldMethodName = 'sortBy'; const newMethodName = 'orderBy'; if (importDeclarations.size() === 0) { return null; }; const localNameSpace = importDeclarations .find(j.Identifier) .get(0) .node.name; function isLodashChain(node) { while ( node.type !== 'Identifier' && node.object.type === 'CallExpression' ) { node = node.object.callee; } let name = node.name || node.object.name; return name === localNameSpace; } return root .find(j.MemberExpression, { property: { name: oldMethodName, }, }) .replaceWith(nodePath => { const {node} = nodePath; if (isLodashChain(node)) { node.property.name = newMethodName; } return node; }) .toSource(); }; 50 строк
  97. Итоги

  98. Известные проблемы >98 › Существует ненулевой порог входа › С

    написанием кодмодов могут быть неочевидные проблемы › Сталкивался со странным форматированием
  99. Плюсы >99 › Простой переход на новые версии библиотек ›

    Упрощение поддержки кода в здоровом состоянии › Упрощение рефакторинга большой кодовой базы › Кодмод - это обычный скрипт › Автоматизация
  100. Полезные ссылки >100 › https://github.com/facebook/jscodeshift - jscodeshift › https://github.com/benjamn/ast-types/blob/master/def -

    определения AST-узлов › https://www.toptal.com/javascript/write-code-to-rewrite-your-code - статья с хорошими примерами › https://astexplorer.net - инспектор ast › https://github.com/sejoker/awesome-jscodeshift - полезная подборка › https://www.youtube.com/watch?v=d0pOgY8__JM - видео от создателя › https://2018.codefest.ru/lecture/1302/ - доклад про AST › https://github.com/reactjs/react-codemod - кодмоды для React
  101. Мышов Александр Яндекс Маркет myshov@yandex-team.ru @myshov github.com/myshov https://clck.ru/FQjCn