Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

CodeFest
April 06, 2019

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

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

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

CodeFest

April 06, 2019
Tweet

More Decks by CodeFest

Other Decks in Programming

Transcript

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

    AST › Описание jscodeshift › Примеры кодмодов
  2. Обновление библиотеки >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`, });
  3. Использование нового стандарта (было) >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;
  4. Использование нового стандарта (стало) >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;
  5. Проблемы >7 › Рефакторинг может быть сложным и долгим ›

    Обычный поиск и замена слишком просты › Регулярные выражения не помогут в сложных случаях › Ограниченные виды рефакторинга с помощью IDE › Нет автоматизации
  6. { 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
  7. Структура jscodeshift >20 › парсеры - babel/parser, flow › ast-types

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

    - обёртка над узлом, позволяющая обходить дерево › Collection - набор объектов типа path с методами трансформации
  9. Блаблафикатор (codemod) >24 export default function transformer(file, api) { const

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

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

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

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

    j = api.jscodeshift; return j(file.source) .find(j.Identifier) .replaceWith( j.identifier('bla') ) .toSource(); }
  14. 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 (со строчной буквы)
  15. 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 (со строчной буквы)
  16. 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
  17. 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
  18. Удаление console.log() >37 function sum(a, b) { const result =

    a + b; console.log('Hello'); return result; }
  19. { type: "CallExpression", callee: { type: "MemberExpression", object: { type:

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

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

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

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

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

    }, property: { type: "Identifier", name: "log" } } }) Поиск console.log() – filter >47
  25. Удаление всех 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(); }
  26. Удаление всех console.log() >50 return j(file.source) .find(j.CallExpression, { callee: {

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

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

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

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

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

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

    const result = a + b; console.log('Hello'); return result; }
  33. 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"));
  34. 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"));
  35. 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"));
  36. Замена аргументов функции >70 .find(...) .replaceWith(nodePath => { const {node}

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

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

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

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

    nodePath; const args = node.arguments; node.arguments = [ j.objectExpression([ buildProperty('value', args[0]) ]) ]; return node; })
  41. Замена аргументов функции (кодмод) >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();
  42. Замена устаревшего кода (план) >81 Поиск имени идентифик атора .find()

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

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

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

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

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

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

    name: localNameSpace, }, property: { name: 'sortBy', }, }) .replaceWith(nodePath => { const {node} = nodePath; node.property.name = 'orderBy'; return node; }) .toSource()
  49. Замена устаревшего кода (план Б) >90 Поиск имени идентифик атора

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

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

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

    name: 'sortBy', }, }) .replaceWith(nodePath => { const {node} = nodePath; if (isLodashChain(node)) { node.property.name = 'orderBy'; return node; } return node; }) .toSource()
  53. Проверка чейнинга >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; }
  54. Поиск и замена вызова метода >95 .find(j.MemberExpression, { property: {

    name: 'sortBy', }, }) .replaceWith(nodePath => { const {node} = nodePath; if (isLodashChain(node)) { node.property.name = 'orderBy'; return node; } return node; }) .toSource()
  55. Замена устаревшего кода >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 строк
  56. Известные проблемы >98 › Существует ненулевой порог входа › С

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

    Упрощение поддержки кода в здоровом состоянии › Упрощение рефакторинга большой кодовой базы › Кодмод - это обычный скрипт › Автоматизация
  58. Полезные ссылки >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