Publicações

Refatoração automatizada com ts-morph

Mariane Soares
Mariane Soares Backend Developer

Introdução

Refatorar código é um processo de reestruturação de código sem influenciar na sua funcionalidade, fazendo pequenas mudanças e transformando-o em um código melhorado, legível e eficiente.

Mas por quê refatorar código? Martin Fowler, autor do livro Refatoração: Aperfeiçoando o Design de Códigos Existentes, identifica quatro principais razões para refatorar:

  • Melhora o design do software

  • Torna o software mais fácil de manter

  • Ajuda a encontrar defeitos

  • Ajuda a programar mais rápido

Além disso, a refatoração ajuda o desenvolvedor a perceber uma nova e simples forma de fazer aquilo que tinha feito antes, examinando a implementação da solução e em seguida visualizando a melhoria, que mesmo sendo mínima é importante.

No entanto, refatorar código pode ser repetitivo e, se em projetos grandes, tedioso. Com ts-morph, uma API que permite a navegação e manipulação de código TypeScript, é possível criar um script para refatorar automaticamente algumas partes do código. Esse é um artigo que irá mostrar como usar essa biblioteca e alguns casos de uso da mesma.

Instalando

Primeiro vamos instalar a lib ts-morph:

npm install --save-dev ts-morph

E também ts-node para rodar nosso script:

npm install --save-dev ts-node typescript

Importando a biblioteca e iniciando o projeto

Vamos criar um arquivo refactor.ts e implementar o nosso script, primeiro vamos importar a biblioteca e iniciar o projeto:

import { Project } from 'ts-morph';

// Inicia o projeto
const project = new Project()

// Adiciona todos os arquivos .ts onde o script irá rodar
project.addSourceFilesAtPaths('/src/**/*.ts')

// Pega todos os arquivos do projeto
const sourceFiles = project.getSourceFiles()

Editando uma interface

Nesse primeiro exemplo iremos editar uma interface, a renomeando, mudando suas propriedades e adicionando novas.

Temos esse arquivo com duas interfaces IFile e ITable:

// IFile interface
interface IFile {
  _id: string
  name: string
}

// ITable interface
interface ITable {
  _id: string
  title: string
  dataIndex: string
  key: string
  width: string
}

Primeiro, iremos refatorar renomeando a interface e removendo o I:

import { Project } from 'ts-morph';

// Inicia o projeto
const project = new Project()

// Adiciona todos os arquivos .ts onde o script irá rodar
project.addSourceFilesAtPaths('/src/**/*.ts')

// Pega todos os arquivos do projeto
const sourceFiles = project.getSourceFiles()

// Percorre todos os arquivos
sourceFiles.forEach((sourceFile) => {
// Pega todas as interfaces em um arquivo
  const interfaces = sourceFile.getInterfaces()

// Percorre as interfaces
  interfaces.forEach(declaration => {
    // Pega o nome de cada interface
    const name = declaration.getName()
    // Aqui removemos o 'I'
    const newName = name.replace(/^I(\[A-Z])/, '$1')
    if (name === newName) {
     return;
    }
  })
// Salva o arquivo com as mudanças
sourceFile.save()
})

Ao rodarmos nosso script com ts-node nome-script.ts, como ficou o arquivo:

// IFile Interface
interface File {
  _id: string
  name: string
}

// ITable interface
interface Table {
  _id: string
  title: string
  dataIndex: string
  key: string
  width: string
}

Removendo referências antigas

E se quisermos remover as referências antigas a essa interface? Existe um método chamado rename() onde as options definem o novo nome e onde mais deve ser renomeado:

declaration.rename(newName, {
  // Qualquer comentário referenciando essa interface será renomeado
  renameInComments: true,
  // Qualquer string referenciando essa interface será renomeada
  renameInStrings: true
})

Adicionando esse trecho, o script fica dessa forma:

import { Project } from 'ts-morph';

// Inicia o projeto
const project = new Project()

// Adiciona todos os arquivos .ts onde o script irá rodar
project.addSourceFilesAtPaths('/src/**/*.ts')

// Pega todos os arquivos do projeto
const sourceFiles = project.getSourceFiles()

// Percorre todos os arquivos
sourceFiles.forEach((sourceFile) => {
// Pega todas as interfaces em um arquivo
  const interfaces = sourceFile.getInterfaces()

// Percorre as interfaces
  interfaces.forEach(declaration => {
    // Pega o nome de cada interface
    const name = declaration.getName()
    // Aqui removemos o 'I'
    const newName = name.replace(/^I(\[A-Z])/, '$1')
    if (name === newName) {
     return;
    }

    declaration.rename(newName, {
      // Qualquer comentário referenciando essa interface será renomeado
      renameInComments: true,
      // Qualquer string referenciando essa interface será renomeada
      renameInStrings: true
    })
  })
// Salva o arquivo com as mudanças
sourceFile.save()
})

Assim, após o método rename() os comentários no arquivo ficam assim:

// File Interface
interface File {
  _id: string
  name: string
}

// Table interface
interface Table {
  _id: string
  title: string
  dataIndex: string
  key: string
  width: string
}

Editando e removendo propriedades

Agora, e se quisermos remover o _ da propriedade _id? É possível, primeiramente utilizando o método getProperty(parâmetro) para pegar a propriedade dentro da interface onde o parâmetro é o nome da propriedade. Após isso, verificamos se a propriedade existe, e se existir a removemos. Então adicionamos uma nova propriedade com o método insertProperty():

import { Project } from 'ts-morph';

// Inicia o projeto
const project = new Project()

// Adiciona todos os arquivos .ts onde o script irá rodar
project.addSourceFilesAtPaths('/src/**/*.ts')

// Pega todos os arquivos do projeto
const sourceFiles = project.getSourceFiles()

sourceFiles.forEach((sourceFile) => {
// Pega todas as interfaces nos arquivos
 const interfaces = sourceFile.getInterfaces()

 interfaces.forEach(declaration => {
    // Pega a propriedade
    const oldProperty = declaration.getProperty('_id')

    if (oldProperty) {
    // Se existir, remove
      oldProperty.remove()
    // Cria uma nova propriedade na posição 0 e adiciona a estrutura que a representa
      declaration.insertProperty(0, {
        name: 'id',
        type: 'string'
      })
    }
  })
  sourceFile.save()
})

É possível, também, mudar o tipo uma propriedade. Utilizando o mesmo fluxo de desenvolvimento anterior, pegamos a propriedade que queremos manipular, verificamos se a mesma existe e se existir a removemos e adicionamos um novo tipo a essa propriedade:

import { Project } from 'ts-morph';

// Inicia o projeto
const project = new Project()

// Adiciona todos os arquivos .ts onde o script irá rodar
project.addSourceFilesAtPaths('/src/**/*.ts')

// Pega todos os arquivos do projeto
const sourceFiles = project.getSourceFiles()

sourceFiles.forEach((sourceFile) => {
// Pega todas as interfaces nos arquivos
 const interfaces = sourceFile.getInterfaces()

 interfaces.forEach(declaration => {
  // Pega a propriedade
   const oldType = declaration.getProperty('key')

   if (oldType) {
   // Se existir, remove
     oldType.remove()

   // Cria uma nova propriedade na posição 3 e adiciona a estrutura que a representa
   declaration.insertProperty(3, {
     name: 'key',
     type: 'number'
   })
 }
})
  sourceFile.save()
})

O resultado ficou assim:

// File Interface
interface File {
    id: string;
  name: string
}

// Table interface
interface Table {
      id: string;
  title: string
  dataIndex: string
  key: number
  width: string
}

No entanto, no resultado acima percebe-se que o código do arquivo não está formatado e a indentação não está feita de forma correta. O ts-morph disponibiliza também de métodos que formatam o arquivo, como o textFormat() que formata o código usando a API interna do TypeScript, onde pode-se configurar a formatação.

Formatando arquivos

O método abaixo formata todos os arquivos do projeto, utilizando a configuração definida de indentações permitidas:

sourceFile.formatText({
    indentSize: 2 // Indica o tamanho das indentações
})

Utilizando esse método antes do sourceFile.save(),

import { Project } from 'ts-morph'; 

// Inicia o projeto
const project = new Project()

// Adiciona todos os arquivos .ts onde o script irá rodar
project.addSourceFilesAtPaths('/src/**/*.ts')

// Pega todos os arquivos do projeto
const sourceFiles = project.getSourceFiles()

sourceFiles.forEach((sourceFile) => {
  sourceFile.formatText({
    indentSize: 2
  })
  sourceFile.save()
})

O arquivo é formatado e retorna esse resultado:

// File Interface
interface File {
  id: string;
  name: string
}

// Table interface
interface Table {
  id: string;
  title: string
  dataIndex: string
  key: number
  width: string
}

Pronto, tudo bem indentado!

Removendo imports desnecessários

Todos esses métodos abaixo aceitam argumentos que especificam como a formatação deve ser feita.

sourceFile.fixMissingImports()
  .organizeImports()
  .fixUnusedIdentifiers()
  .formatText()

O método fixMissingImports() adiciona importações de identificadores que estão referenciados no código mas não estão importadas. O método organizeImports(), como o nome já diz, organiza as importações no arquivo. Já o método fixUnusedIdentifiers() remove declarações que não estão sendo usadas, como de classes, interfaces, funções e variáveis dos arquivos.

Nesse trecho de código temos importações e variáveis que não estão sendo usadas:

import { useBody } from 'h3'
import s3Client from '../../../server/s3'
import prisma from '../../prisma'

export type FileData = {
  folder: string
  fileEncode: string
  fileName: string
}

const unused = 'something'

Ao rodar esse script para formatar o arquivo:

import { Project } from 'ts-morph';

// Inicia o projeto
const project = new Project()

// Adiciona todos os arquivos .ts onde o script irá rodar
project.addSourceFilesAtPaths('/src/**/*.ts')

// Pega todos os arquivos do projeto
const sourceFiles = project.getSourceFiles()

sourceFiles.forEach((sourceFile) => {
  sourceFile.fixMissingImports()
    .organizeImports()
    .fixUnusedIdentifiers()
    .formatText({
      indentSize: 2
    });
  sourceFile.save()
})

O script removerá todas as importações e a variável unused pois não estavam sendo usadas, formatando assim:

export type FileData = {
  folder: string
  fileEncode: string
  fileName: string
}

Mudando declarações de váriaveis

Existe também a possibilidade de mudar declarações de variáveis. No exemplo a seguir, todas as declarações var serão mudadas para let:

function printMatrix (matrix: any) {
 var i
  for (i = 0; i < matrix.length; i++) {
    var line = matrix[i]
    for (i = 0; i < line.length; i++) {
      var element = line[i]
      console.log(element)
    }
}

var matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
]

printMatrix(matrix);

Para que isso aconteça, devemos utilizar o método getDescendantsOfKind(SyntaxKind.VariableStatment) para encontrar as declarações das variáveis com sintaxe var no arquivo, após isso verificar se é do tipo var e mudar a sintaxe para let, como no script a seguir:

import { Project, SyntaxKind, VariableDeclarationKind } from 'ts-morph';
// Inicia projeto
const project = new Project()

// Adiciona todos os arquivos .ts onde o script irá rodar
project.addSourceFilesAtPaths('/src/**/*.ts')

// Pega todos os arquivos 
const sourceFiles = project.getSourceFiles()

sourceFiles.forEach((sourceFile) => {
  // Encontra todas as declarações de variáveis
  const varDeclaration = sourceFile.getDescendantsOfKind(SyntaxKind.VariableStatement)

  // Percorre pelo arquivo e encontra cada declaração var
  varDeclaration.forEach((varStatement) => {
    const declaration = varStatement.getDeclarationKind()

    // Se a declaração for igual a var
    if (declaration === VariableDeclarationKind.Var) {
      // A declaração var se torna let
      varStatement.setDeclarationKind(VariableDeclarationKind.Let)
    }
  })
})

Assim, tendo o seguinte resultado:

function printMatrix (matrix: any) {
 let i
  for (i = 0; i < matrix.length; i++) {
    let line = matrix[i]
    for (i = 0; i < line.length; i++) {
      let element = line[i]
      console.log(element)
    }
  }
}

let matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
]

printMatrix(matrix)

Conclusão

Conforme visto, é possível refatorar de forma automatizada utilizando ts-morph, ajudando a diminuir o tempo do processo de refatorar código. Nesse sentido, o uso dessa lib pode ser eficiente em projetos grandes onde há chance da refatoração ser maçante e demorada.

Referências

Solução de problemas, melhoria de gestão e aumento de produtividade

Com tecnologias renomadas, aplicamos práticas ágeis e capacitamos sua equipe.

Fale com um consultor