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

CodeFest 2019. Александр Мышов (Яндекс) — Автом...

Avatar for CodeFest CodeFest
April 06, 2019

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

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

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

Avatar for CodeFest

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