Как начать использовать AST

Иван Стрелков

Как начать использовать AST

Иван Стрелков

Abstract Syntax Tree

Пример кода

      
let sum = 0;

for (let i = 0; i < 10; i++) {
    sum += Math.random();
}

console.log(sum);
      
    
https://goo.gl/whbc2n

Пример кода - AST

  1. VariableDeclaration let sum = 0;
  2. ForStatement for (let i = 0; i < 10; i++) { ... }
  3. ExpressionStatement console.log(sum);

Пример кода - AST

  1. ...
  2. ForStatement for (let i = 0; i < 10; i++) { ... }
    • init VariableDeclaration let i = 0
    • test BinaryExpression i < 10
    • update UpdateExpression i++
    • body BlockStatement { sum += Math.random(); }

Пример кода - AST

  1. BlockStatement { sum += Math.random(); }

Пример кода - AST

  1. CallExpression Math.random()
    • arguments [] ()
    • callee MemberExpression Math.random
      • object Identifier Math
      • property Identifier random

Esprima - получаем AST

        
const esprima = require('esprima');
const sourceCode = 'require("./somefile")';

console.log(esprima.parse(sourceCode));
// { type: 'Program',
//   body:
//    [ { type: 'ExpressionStatement',
//        expression: [Object]
//    } ],
//   sourceType: 'script' }
        
      
http://esprima.org/

План доклада

  1. Нахождение недостижимых файлов
  2. Нахождение неиспользуемых CSS-классов
Демки - https://goo.gl/2ktLN6

Нахождение недостижимых файлов

Пример кода

        
const React = require('react');
const Header = require('./Header');

class App extends React.Component {
  render() {
    return (
      <div className="App">
        <Header />
      </div>
    );
  }
}

module.exports = App;
        
      

Искомая конструкция

      
require('./somefile')
        
      

Искомая конструкция - AST

  1. CallExpression require('./somefile')
    • callee Identifier require
      • name 'require'
    • arguments[0] Literal './somefile'
      • value './somefile'

Находим require

  1. Ищем все CallExpression

Esprima - traverser/walker/iterator

      
esprima.parse(
  'require("./somesthing")',
  {},
  node => console.log(node.type)
)
// Identifier
// Literal
// CallExpression
// ExpressionStatement
// Program
      
    

Реализация - ищем CallExpression

      
esprima.parse(sourceCode, { jsx: true }, node => {
  if (node.type !== 'CallExpression') {
    return;
  }

  // ...
});
      
    

Реализация - фильтруем по callee

      
  // ...
  if (node.callee.type !== 'Identifier') {
    return;
  }

  if (node.callee.name !== 'require') {
    return;
  }
  // ...
      
    

Реализация - вытаскиваем имя файла/пакета

      
esprima.parse(sourceCode, { jsx: true }, node => {
  // ...


  console.log(node.arguments[0].value)
});
      
    

Собираем воедино

  1. Проходимся паттерном Visitor от исходного файла

Почему не RegExp?

      
/require\('\.[^')]+'\)/g
        
      

Преимущества AST vs RegExp

  1. Переиспользуемость
  2. Наличие контекста

Нахождение неиспользуемых CSS-классов

Пример кода - CSS Modules

        
const styles = require('./App.css');

class App extends React.Component {
  render() {
    return (
      <div className={styles.App}>
      	Hello
      </div>
    );
  }
}
        
      

Две задачи работы AST

  1. Найти список объявленных классов из CSS
  2. Найти список использованных класов из JS

Пример кода

        
.App-logo span {
  color: blue;
}
        
      

Находим объявленные CSS-классы

        
const cssAst = csstree.parse(cssSource);

csstree.walk(cssAst, node => {

  if (node.type === 'ClassSelector') {
    console.log(node.name);
  }

});
        
      

Находим использованные CSS-классы из JS

  1. Находим все require('*.css')
  2.             
        {
          styles: './style.css',
          sharedStyles: '../shared.css',
          itemStyles: './item/style.css'
        }
                
              

Находим использованные CSS-классы из JS

  1. Находим все require('*.css')
  2. Запоминаем объявленные импорты в объекте nameToPathMap
  3. Проходимся по всем MemberExpression
    • Только те, у которых left идентификатор из nameToPathMap
    • Помечаем right (идентификатор или литерал) как использованный класс

Собираем воедино

  1. Проходимся по всем JS-файлам
    • Для каждого CSS-импорта в JS-файле
      • Находим список использованных классов и помечаем их, как использованные
  2. Проходимся по всем CSS-файлам
    • Помечаем каждый найденный класс как объявленный
  3. В цикле по всем CSS-файлам
    • Помечаем каждый объявленный, но не использованный, класс

Проблема

      
const styles = require('./App.css');

function pseudo(styles) {
  console.log(styles['pseudo-used']);
}
      
    

Нахождение ссылок на переменные

Escope - инициализация

        
const tree = esprima.parse(sourceCode, { jsx: true });

const scopeManager = escope.analyze(tree, {
  ecmaVersion: 6
});

const moduleScope = scopeManager.acquire(tree);
        
      

Escope - переменные верхнего уровня

        
moduleScope.variables.forEach(v => {
  console.log(`Переменная с именем ${v.name}`);

  v.references.forEach(ref => {
    console.log(ref.identifier);
  });
});
        
      

Находим использованные CSS-классы из JS

  1. Находим все require('*.css')
  2. Запоминаем объявленные импорты в объекте nameToPathMap
  3. Для каждого CSS-импорта находим его references
    • Для каждого reference
      • Проверяем, что они используются только как MemberExpression
      • Помечаем right (идентификатор или литерал) как использованный класс

Валидация

Валидация

      
if (identifierParent.type !== 'MemberExpression' ||
    identifierParent.object !== ref.identifier) {

  console.log(identifierParent);
  console.log(ref.identifier);
  throw new Error('Not expected usage');
}
      
    

Что еще можно с AST

  1. Валидация
  2. Автоматизированный рефакторинг
  3. Генерация кода и артефактов

Спасибо за внимание!

Иван Стрелков, Avito

Презентация: istrel.github.io/ast-getting-started/

Ссылки и демки: https://goo.gl/2ktLN6