Visão Geral
Nesta seção, você encontra: Referência de um Hook Declarativo do tipo code-transformation.
Sobre o Hook Declarativo code-transformation
O Hook Declarativo code-transformation faz alterações em códigos fonte através de um engine de transformação. As alterações devem ser definidas por meio de padrões de transformação. Esses padrões devem ser escritos pela pessoa desenvolvedora em uma linguagem que estende a linguagem dos códigos fonte originais com operadores que determinam o que deve ser alterado. O engine efetua as alterações sempre que há uma correspondência entre os padrões de transformação e os códigos fonte originais.
O Hook Declarativo code-transformation pode ser muito útil em várias situações onde seja necessária a evolução de código já existente. Por exemplo, quando se deseja atualizar a versão de uma biblioteca ou framework e para isso seja necessário alterar de código em escala; ou quando, para introduzir uma nova funcionalidade aplicando um plugin StackSpot, seja necessário alterar código existente.
A versão atual do engine de transformação de código (0.2.0-beta) dá suporte às seguintes linguagens:
- TypeScript
- HTML
- Java
Como definir o hook de code-transformation
Dentro do campo spec
do seu arquivo de configuração do Plugin (plugin.yaml
), declare o code-transformation
como no exemplo abaixo:
schema-version: v3
kind: plugin
metadata:
name: code-transformation-plugin
display-name: code-transformation-plugin
description: Code transformation example.
version: 1.0.0
spec:
hooks:
- type: code-transformation
engine-version: 0.2.0-beta
trigger: after-render
language: typescript
trans-pattern-path: trans-patterns/pattern.ts
source-path: src/code.ts
type:
O tipo do Hook Declarativo.
engine-version:
Versão da engine de transformação de código utilizada pelo Hook. A versão atual é a 0.2.0-beta
.
trigger:
Suporta apenas a opção after-render
. Executa os comandos do Hook após o Template gerar arquivos no projeto.
language:
Linguagem suportada para a transformação. Atualmente o code-transformation
suporta os padrões de transformação escritos nas linguagens Typescript, HTML e Java.
trans-pattern-path:
Caminho para o arquivo com os padrões de transformação. O caminho é relativo à pasta do Plugin.
Você deve criar a pasta trans-patterns para colocar os arquivos com os padrões de transformação. A extensão do arquivo com os padrões de transformação deve corresponder à linguagem definida no campo language
, ou seja, para Typescript deve ser .ts
, para HTML deve ser .html
e para Java deve ser .java
.
Se você informar o caminho para uma pasta ao invés de um arquivo, o Hook irá executar todos os padrões de transformação presentes nos arquivos (que possuem a extensão da linguagem definida) da pasta trans-patterns, recursivamente.
Exemplos:
Para executar uma transformação com base em um padrão presente em um arquivo específico:
trans-pattern-path: trans-patterns/pattern.ts
Para executar múltiplas transformações com base nos padrões presentes em todos os arquivos de uma pasta, recursivamente:
trans-pattern-path: trans-patterns/
Você também pode informar o nome do arquivo com o padrão de transformação utilizando expressões JINJA:
trans-pattern-path: trans-patterns/{{trans_pattern_file_name}}.ts
source-path:
Caminho da Aplicação onde ficam os códigos fonte preexistentes que sofrerão as transformações. O caminho é relativo à pasta da Aplicação. A extensão do arquivo fonte deve corresponder à linguagem definida no campo language
, ou seja, para Typescript deve ser .ts
, para HTML deve ser .html
e para Java deve ser .java
.
Se você informar o caminho para uma pasta ao invés de um arquivo, todos os arquivos fonte (que possuírem a extensão da linguagem definida) e casarem com os padrões de transformação serão transformados, recursivamente.
Exemplos:
Para transformar o código de um arquivo específico da sua Aplicação:
source-path: app-src/code.ts
Para transformar os códigos de todos os arquivos da pasta da Aplicação, recursivamente:
source-path: app-src/
Você também pode informar o nome de um arquivo da sua Aplicação utilizando expressões JINJA:
source-path: app-src/{{app_file_name}}.ts
Passos para criar e usar o hook de code-transformation
Para criar e usar o Hook de code-transformation
siga os seguintes passos:
- Criar Plugin de app StackSpot;
- Criar a pasta (ex: trans-patterns) que irá conter o(s) arquivo(s) de padrão de transformação;
- Criar o(s) arquivo(s) de padrão de transformação na pasta criada no passo anterior. Cada arquivo pode ter um ou mais padrões de transformação. Os arquivos de padrões de transformação devem ter a mesma extensão dos arquivos fonte que se deseja transformar. Por exemplo, para transformar arquivos fonte TypeScript, os arquivos de padrões de transformação devem ter a extensão
.ts
; - Escrever os padrões de transformações nos arquivos criados no passo anterior. Os padrões devem seguir as regras definidas nas demais seções deste documento;
- Definir o Hook de
code-transformation
no arquivo de configuração do Plugin criado no passo 1, de acordo com a seção Como definir o hook de code-transformation; - Aplicar o Plugin criado no passo 1 em uma Aplicação StackSpot.
Após esses passos, se seus padrões de transformação estiverem corretos e existam ocorrências entre os padrões de transformação e os códigos fonte da Aplicação, a engine irá efetuar as transformações, alterando os arquivos fontes da Aplicação.
Operadores de transformação
Os padrões de transformação possuem cinco operadores para a transformação de código, o operador de inserção, remoção, reticências, match por ID e match por String.
O engine garante que a sintaxe do código fonte transformado se mantenha correta, porém não garante manter a formatação nos trechos que utiliza-se o operador de inserção ou remoção de código. Esse comportamento deve ser melhorado nas próximas versões, em especial para o suporte às linguagens em que a sintaxe depende da formatação do código.
Todos os conceitos da engine de code transformation serão apresentados utilizando exemplos de código com a linguagem Typescript. Para exemplos nas demais linguagens suportadas ver Construções com suporte aos operadores de transformação
1. Operador de Inserção
Operador para inserir trechos de código no código fonte original.
A sintaxe é:
+<TRECHO-CODIGO>+
Você deve substituir TRECHO-CODIGO
pelo trecho de código que deseja inserir no código fonte original.
O exemplo abaixo mostra um padrão de transformação que insere o trecho de código console.log("World!");
após o trecho console.log("Hello");
.
- Código Original
- Padrão de Transformação
- Código Transformado
console.log("Hello");
console.log("Hello");
+<console.log("World!");>+
console.log("Hello");
console.log("World!");
2. Operador de Remoção
Operador remove trechos de código do código fonte original.
A sintaxe é:
-<TRECHO-CODIGO>-
Você deve substituir o TRECHO-CODIGO
pelo trecho de código que deseja remover no código fonte original.
O exemplo abaixo mostra um padrão de transformação que remove o trecho de código console.log("Hello");
. Essa transformação resultará em um arquivo de código fonte vazio.
- Código Original
- Padrão de Transformação
- Código Transformado
console.log("Hello");
-<console.log("Hello");>-
// Empty line
3. Operador de Reticências
Operador para casar com zero ou mais construções no código fonte original. É uma espécie de operador coringa, semelhante ao * em expressões regulares, apesar de não poder ser confundido inteiramente com ele. O operador possui a sintaxe <...>
.
O exemplo a seguir mostra um padrão de transformação que irá casar com qualquer arquivo de código fonte que comece com o trecho console.log("start");
e termine com o trecho console.log("end");
.
console.log("start");
<...>
console.log("end");
Os exemplos a seguir mostram alguns códigos fonte que casam com o padrão de transformação anterior.
- Código Original 1
- Código Original 2
- Código Original 3
- Código Original 4
- Código Original 5
console.log("start");
console.log("end");
console.log("start");
console.log("1");
console.log("end");
console.log("start");
console.log("1");
console.log("2");
console.log("end");
console.log("start");
let myNumber: number = 10;
console.log("end");
console.log("start");
function addNumbers(a: number, b: number): number {
return a + b;
}
let myNumber1: number = 10;
let myNumber2: number = 20;
console.log(addNumbers(myNumber1, myNumber2));
console.log("end");
4. Operador de ID match
O operador de ID match usa uma expressão regular para casar com identificadores no código fonte a ser transformado. Identificadores costumam representar nomes de variáveis, funções, métodos, classes, pacotes, namespaces, dentre outros, a depender da linguagem. Para mais detalhes sobre quais construções são identificadores em cada linguagem, acesse Construções com suporte aos operadores de transformação. O operador possui a sintaxe *<REGEX>*
.
Por exemplo, considere que você precisa inserir o decorator @Transactional
em funções cujo nome começam com trans
. Esse padrão de transformação só pode ser expressado utilizando o operador de ID match, como mostrado abaixo.
- Código Original
- Padrão de Transformação
- Código Transformado
function transAddUser(){
// do some stuff
}
+<@Transactional>+
function *<trans.*>*(){
<...>
}
@Transactional
function transAddUser(){
// do some stuff
}
Como é possível notar no exemplo anterior, o operador de ID match só pode ser utilizado no local que ocorreria um identificador, como é o caso do nome da função na linha 2. A expressão regular do operador é trans.*
que casa com qualquer função cujo nome inicie com trans
, um vez que .*
denota zero ou mais caracteres quaisquer.
5. Operador de String match
O operador de string match é semelhante ao de ID match. Porém, usa uma expressão regular para casar com strings no código fonte a ser transformado. O operador possui a sintaxe *<"REGEX">*
.
O operador de string match deve sempre ser usado com a sintaxe com aspas dupla *<"">*
, mesmo em linguagens que permitam strings com aspas simples.
Por exemplo, considere que você precisa inserir uma chamada à console.log()
antes de outra chamada, independente de qual string é passada como argumento. Esse padrão de transformação só pode ser expressado utilizando o operador de string match, como mostrado abaixo.
- Código Original
- Padrão de Transformação
- Código Transformado
// do some stuff
console.log("any string");
// do other stuff
<...>
+<console.log("pre log");>+
console.log(*<".*">*);
<...>
// do some stuff
console.log("pre log");
console.log("any string");
// do other stuff
Como é possível notar no exemplo anterior, o operador de string match só pode ser utilizado no local que ocorreria uma string, como é o caso do argumento da chamada de função console.log()
. A expressão regular do operador é .*
que casa com qualquer string, um vez que .*
denota zero ou mais caracteres quaisquer.
Aninhamento de operadores
O único operador que permite aninhamento de outros operadores é o de remoção. É possível aninhar os operadores de reticências, ID match e string match para compor padrões poderosos.
A engine de code transformation não suporta aninhar somente um operador de reticências em um operador de remoção, como em -<<...>>-
.
Por exemplo, considere que você precisa remover uma chamada à console.log()
independente de qual string é passada como argumento. Esse padrão de transformação só pode ser expressado utilizando o operador de string match aninhado no operador de remoção, como mostrado abaixo.
- Código Original
- Padrão de Transformação
- Código Transformado
// do some stuff
console.log("any string");
// do other stuff
<...>
-<console.log(*<".*">*);>-
<...>
// do some stuff
// do other stuff
Outro exemplo mostra como é possível utilizar o operador de reticências e de ID match aninhados no operador de remoção para remover uma função cujo nome inicia com create
, independente do corpo e dos parâmetros dela.
- Código Original
- Padrão de Transformação
- Código Transformado
function deleteUser(userId: number) {
const userIndex = users.findIndex(user => user.id === userId);
if (userIndex === -1) {
throw new Error("User not found.");
}
users.splice(userIndex, 1);
return { message: "User delete successfully!" };
}
function createUser(user:User) {
if (!user.firstname || !user.lastname || !user.email || !user.password) {
throw new Error("All fields are mandatory.");
}
repository.registerUser(user);
return {
message: "User created successfully!",
user: newUser,
};
}
<...>
-<function *<create.*>*(<...>) {
<...>
}>-
<...>
function deleteUser(userId: number) {
const userIndex = users.findIndex(user => user.id === userId);
if (userIndex === -1) {
throw new Error("User not found.");
}
users.splice(userIndex, 1);
return { message: "User delete successfully!" };
}
Padrões de transformação
Um padrão de transformação é um arquivo de código fonte comum, exceto pela presença dos cabeçalhos dos padrões e dos operadores de transformação. Todos os trechos de código que não são operadores servem de contexto, para que o engine possa saber precisamente quais partes do código fonte original devem ser alteradas.
Declarando padrão de transformação
Em um arquivo de padrão de transformação é possível declarar um ou mais padrões. Para cada padrão, é necessário um cabeçalho. Os cabeçalhos possuem duas propriedades, o nome do padrão, pattern-name
, e o escopo do padrão, pattern-scope
. A sintaxe do cabeçalho de padrão de transformação é:
<<<< pattern-name: my_pattern_name; pattern-scope: file-scope; >>>>
-
pattern-name: Insira o nome para identificar o seu padrão de transformação. O nome não deve possuir espaços em branco. Exemplo:
my_pattern
,my-pattern
oumy.pattern
. -
pattern-scope: Utilize
file-scope
para padrões de transformação no escopo de arquivo, ousnippet-scope
para utilizar o padrão de transformação no escopo de snippet. Para mais detalhes sobre escopo de padrões de transformação, ver Escopo de transformação.
Até a versão 0.1.0-beta
da engine de code transformation, declarava-se um padrão de transformação por arquivo, sem cabeçalho, todos com escopo de arquivo. A partir da versão 0.2.0-beta
, a declaração de um único padrão sem cabeçalho por arquivo ainda é suportada, mas está depreciada. Encorajamos que todo padrão seja declarado com cabeçalho.
Ao aplicar um arquivo com diversos padrões de transformação, a engine irá transformar o código fonte seguindo uma ordem sequencial dos cabeçalhos que foram definidos. Por exemplo:
<<<< pattern-name: pattern-name; pattern-scope: file-scope; >>>> // first pattern to be applied
// pattern
<<<< pattern-name: pattern-name2; pattern-scope: snippet-scope; >>>> // second pattern to be applied
// pattern
<<<< pattern-name: pattern-name3; pattern-scope: snippet-scope; >>>> // third pattern to be applied
// pattern
Os exemplos a seguir mostram arquivos de padrão de transformação com, respectivamente, apenas um padrão, mais de um padrão e padrões com escopos diferentes.
- Apenas um padrão
- Mais de um padrão
- Padrões com escopos diferentes
<<<< pattern-name: my_only_pattern; pattern-scope: file-scope; >>>>
// esse padrão insere um atributo "id" em uma classe cujo nome termina com "Entity"
<...>
class *<.*Entity>* {
+<id : number;>+
<...>
}
<...>
<<<< pattern-name: my_first_pattern; pattern-scope: file-scope; >>>>
// esse padrão insere um atributo "id" em uma classe cujo nome termina com "Entity"
<...>
class *<.*Entity>* {
+<private _id : number;>+
<...>
}
<...>
<<<< pattern-name: my_second_pattern; pattern-scope: file-scope; >>>>
// esse padrão insere um construtor em uma classe cujo nome termina com "Entity"
<...>
class *<.*Entity>* {
<...>
+<constructor (id:number) {
this._id = id;
}>+
}
<...>
<<<< pattern-name: my_file_scope_pattern; pattern-scope: file-scope; >>>>
// esse padrão (com escopo de arquivo) insere um import no início do arquivo fonte
+<import _ from 'lodash';>+
<...>
<<<< pattern-name: my_snippet_scope_pattern; pattern-scope: snippet-scope; >>>>
// esse padrão (com escopo de snippet) insere uma chamada à console.debug no início de toda função
function *<.*>*(<...>) {
+<console.debug("entering in a function");>+
<...>
}
Exemplo de padrão de transformação
Até o momento mostramos padrões de transformação que utilizam apenas um dos operadores. No entanto, ao utilizar os três operadores é possível criar padrões de transformações ainda mais poderosos.
O exemplo a seguir mostra um padrão de transformação que irá remover a inicialização dos atributos da classe Person
e inserir um construtor. Para remover a inicialização dos atributos da classe utilizamos operadores de remoção seguidos por operadores de inserção. Em seguida, usamos o operador de inserção novamente, dessa vez para inserir um construtor. Por último, utilizamos o operador de reticências para que o padrão de transformação possa casar com qualquer trecho de código que possa existir após a declaração do atributo age
da classe Person
no arquivo fonte original.
O exemplo a seguir mostra um padrão de transformação que irá remover a inicialização dos atributos da classe Person
e inserir um construtor. Para remover a inicialização dos atributos da classe utilizamos operadores de remoção seguidos por operadores de inserção. Em seguida, usamos o operador de inserção novamente, dessa vez para inserir um construtor. Por último, utilizamos o operador de reticências para que o padrão de transformação possa casar com qualquer trecho de código que possa existir após a declaração do atributo age
da classe Person
no arquivo fonte original.
- Código Original
- Padrão de Transformação
- Código Transformado
class Person {
firstname : string = "";
lastName : string = "";
age : number = 0;
getFullName(): string {
return firstName + lastName;
}
}
<<<< pattern-name: pattern_with_many_ops; pattern-scope: file-scope; >>>>
class Person {
-<firstname : string = "";>-
+<firstname : string;>+
-<lastName : string = "";>-
+<lastName : string;>+
-<age : number = 0;>-
+<age : number;>+
+<constructor(firstName: string, lastName: string, age: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}>+
<...>
}
class Person {
firstname : string;
lastName : string;
age : number;
constructor(firstName: string, lastName: string, age: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
getFullName(): string {
return firstName + lastName;
}
}
Criando padrões de transformação sintaticamente válidos
Padrões de transformação são códigos escritos em uma versão estendida da linguagem do código original. Dessa forma, todo o código de contexto presente no padrão de transformação precisa ser sintaticamente correto. O exemplo a seguir mostra um padrão de transformação sintaticamente inválido e que, portanto, não poderá ser executado pelo engine de transformação.
<<<< pattern-name: invalid_pattern; pattern-scope: file-scope; >>>>
function func()
<...>
+<console.log("Finishing func");>+
}
O padrão anterior é sintaticamente inválido pois falta a abertura de chaves {
da função func
. O operador de reticências não atuará casando com esse caractere que falta, pois está no contexto de casar somente com statements dentro do corpo da função.
Outro exemplo de padrão de transformação sintaticamente inválido é o seguinte:
<<<< pattern-name: invalid_pattern; pattern-scope: file-scope; >>>>
console.log(<...>
O padrão anterior é sintaticamente inválido pois falta o fechamento de parênteses )
da chamada ao método log
. O operador de reticências não atuará casando com esse caractere que falta, pois está no contexto de casar somente com argumentos de uma chamada de método.
Padrões de transformação e seus operadores não são expressões regulares (regex). Regex manipulam texto sem qualquer estrutura, já os padrões de transformação são códigos que possuem uma sintaxe definida, assim como uma linguagem de programação qualquer. O engine gera árvores sintáticas dos padrões de transformação e por isso eles precisam respeitar uma estrutura definida.
Ainda sobre os padrões de transformação, além do código de contexto precisar ser sintaticamente válido, os trechos que serão inseridos ou removidos por meio dos operadores de inserção e/ou remoção precisam ser sintaticamente válidos e também manter o código original sintaticamente válido após a transformação. O padrão de transformação a seguir é um exemplo de padrão inválido.
<<<< pattern-name: invalid_pattern; pattern-scope: file-scope; >>>>
class MyClass {
myMethod():void{}
myProperty:string = "";
+<console.log("bla");>+
}
O padrão anterior é sintaticamente inválido pois, apesar do trecho de código a ser inserido (linha 7) ser sintaticamente válido, o código original transformado por ele ficaria sintaticamente inválido após a transformação. Isso corre porque não é possível inserir uma chamada de método console.log("bla");
dentro de uma classe, fora do corpo de um método.
Confira outro exemplo, agora com o trecho de código a ser inserido inválido:
<<<< pattern-name: invalid_pattern; pattern-scope: file-scope; >>>>
class MyClass {
myMethod():void{}
myProperty:string = "";
+<myMethod2():void {>+
+< console.log("Executing myMethod2");>+
+<}>+
}
O padrão anterior é sintaticamente inválido pois, cada um dos três operadores de inserção não possuem trechos de código válidos. Por exemplo, myMethod2():void {
mão é uma estrutura sintaticamente válida em TypeScript. Operadores de inserção e remoção não atuam inserindo ou removendo linhas, mas sim trechos de código completos, sintaticamente válidos. O exemplo a seguir mostra como alterar o padrão de transformação anterior para torná-lo válido.
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>
class MyClass {
myMethod():void{}
myProperty:string = "";
+<myMethod2():void {
console.log("Executing myMethod2");
}>+
}
O padrão de transformação anterior é válido pois utiliza um único operador para inserir o método myMethod2
na classe MyClass
.
Por fim, o próximo exemplo mostra um padrão de transformação sintaticamente inválido pois apresenta a inserção de um trecho de código inválido, o trecho inválido tenta inserir e remover mais de uma construção no mesmo operador de inserção e remoção.
<<<< pattern-name: invalid_pattern; pattern-scope: file-scope; >>>>
+<let myVar1:number = 10; let myVar2:string = "bla";>+
-<console.log("Hello");
console.log("StackSpot");>-
O padrão de transformação anterior é inválido pois tenta inserir dois trechos de código em um único operador de inserção (linha 1) e também é inválido pois tentar remover dois trechos de código em um único operador de remoção (da linha 2 à 3). O exemplo a seguir mostra como alterar o padrão de transformação anterior para torná-lo válido.
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>
+<let myVar1:number = 10;>+ +<let myVar2:string = "bla";>+
-<console.log("Hello");>-
-<console.log("StackSpot");>-
O padrão de transformação anterior é válido pois utiliza um operador para inserir e remover para cada trecho de código.
Uso de operadores em construções separadas por vírgula
Nos padrões de transformação, o uso de operadores de transformação no contexto de construções separadas por vírgula tem um tratamento especial. Observe o exemplo a seguir:
<<<< pattern-name: my_pattern; pattern-scope: file-scope; >>>>
function func(<...>){
<...>
+<console.log("Finishing func");>+
}
O padrão de transformação anterior irá casar com um arquivo que declare uma função com nome func
, independe da quantidade, tipo e nome dos parâmetros e do que hover em seu corpo. O operador de reticências na linha 1 ocorre em uma construção separada por vírgulas, a lista de parâmetros formais na declaração de uma função.
Porém, você poderia querer criar uma padrão de transformação que insira um parâmetro à declaração de uma função, como no exemplo a seguir:
<<<< pattern-name: insert_function_param; pattern-scope: file-scope; >>>>
function func(<...> +<newParam:string>+){
<...>
}
O padrão de transformação anterior irá inserir um parâmetro no final da lista de parâmetros da função func
. No entanto, no código original a função func
pode ter zero ou mais parâmetros. No primeiro caso, não seria necessário acrescentar uma vírgula antes de newParam:string
. Porém, no segundo caso, seria necessário acrescentar a vírgula. Quem cria o padrão não tem como saber quais situações vão demandar a colocação de uma vírgula. Por isso, você não deve colocar a vírgula nem no código de contexto (entre o operador de reticências e o de inserção na linha 1), nem no trecho que será inserido (dentro do operador de inserção da linha 1), o engine irá resolver quando e onde colocar vírgulas quando há operadores em uma construção separada por vírgula.
A seguir há um exemplo do uso de um operador de remoção em uma construção separada por vírgulas:
<<<< pattern-name: remove_array_element; pattern-scope: file-scope; >>>>
<...>
-<import { FormsModule } from '@angular/forms';>-
<...>
@NgModule({
<...>
imports: [<...> -<FormsModule> <...>]
<...>
})
export class AppModule {}
<...>
O padrão de transformação anterior utiliza operadores de transformação em duas construções separadas por vírgula. Os operadores de reticências no início (linha 5) e fim (linha 7) da lista de propriedades de um objeto, e os operadores de reticências e remoção na lista de valores do array da linha 6. No primeiro caso, a engine irá tentar casar no código original, uma propriedade import
, independente de em que ordem apareça no objeto. Já o segundo caso, irá remover o valor FormsModule
, independente de onde apareça no array de imports. Além disso, a engine irá remover qualquer vírgula, caso necessário.
Por fim, caso você queira transformar uma posição específica de uma construção separada por vírgula, pode adotar a estratégia do exemplo a seguir.
<<<< pattern-name: complex_example; pattern-scope: file-scope; >>>>
<...>
function func(<...>,<...>,-<param3:number>- +<newParam3:boolean>+, <...>, -<param5:string>-){
<...>
}
<...>
func(<...>,<...>,-<arg3>- +<newArg3>+, <...>, -<arg5>-)
<...>
Na linha 2 do padrão de transformação anterior:
- Será removido o terceiro parâmetro formal
param3:number
da declaração da funçãofunc
; - Inserido um novo parâmetro no lugar dele, o
newParam3:boolean
; - A remoção do quinto parâmetro formal
param5:number
. O mesmo vale para a passagem de argumentos na chamada da funçãofunc
da linha 6.
Nesse caso, as vírgulas presentes no código de contexto permitem localizar exatamente qual parâmetro ou argumento está sendo inserido ou removido. Mesmo nesse caso, o engine sabe quando deve manter, acrescentar ou remover vírgulas.
Comentários e caracteres em branco
Caracteres em branco e comentários são ignorados para efeito de casamento dos padrões de transformação com arquivos de código fonte originais, exceto em linguagens que a sintaxe depende da formatação do código (e.g. python). Por exemplo:
<<<< pattern-name: pattern_with_comment; pattern-scope: file-scope; >>>>
<...>
let message: string = 'Hello, World!';
console.log(message);
let message2: string = 'Hello, StackSpot!';
+<console.log(message2);>+
<...>
// my comment in transformation pattern
O padrão de transformação anterior casa com o código fonte original a seguir, mesmo o código original tendo comentários e formatação diferentes.
// my comment in original code
let message: string = 'Hello, World!';console.log(message);let message2: string = 'Hello, StackSpot!';
Ancoragem de operadores
Entre os operadores disponíveis atualmente, os de inserção e reticências precisam ser colocados no padrão de transformação de maneira que trechos de código sirvam de contexto para que o engine consiga identificar precisamente quais partes do código fonte original devem ser alteradas. Na StackSpot, esses trechos de código que servem de contexto para os operadores são chamados de Âncora.
Qualquer trecho de código sintaticamente válido que esteja presente no código original pode ser usado como âncora de operadores no padrão de transformação. Além de trechos de código, também pode ser usado como âncora um operador de remoção. Como você verá mais adiante, apenas os operadores de inserção e reticências não são âncoras válidas para outro operador em um padrão de transformação. Até mesmo o início e o fim do arquivo fonte serve como âncoras, desde o padrão tenha escopo de arquivo. Padrões com escopo de snippet não permitem início e o fim do arquivo como âncoras, mais detalhes sobre isso em Escopo de transformação.
As âncoras podem ser de dois tipos:
- Pré-âncora: quando a âncora ocorre antes do operador de transformação;
- Pós-âncora: quando a âncora ocorre depois do operador de transformação.
Entender o conceito de ancoragem de operadores é extremamente importante para que você possa criar padrões de transformações válidos.
Abaixo, confira a relação dos operadores e o uso de âncoras.
1. Regra para operadores de inserção
O operador de inserção +<>+
precisa de ao menos uma âncora para determinar precisamente em que parte do código fonte original irá ocorrer a transformação. Pode ser uma pré-âncora e/ou pós-âncora.
Por exemplo, considere o seguinte padrão de transformação, que insere um trecho de código ao código original:
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>
let message: string = 'Hello, World!';
+<console.log(message);>+
Nesse caso, o trecho da linha 1 é uma pré-âncora e o fim do arquivo é uma pós-âncora do operador de inserção. Confira a seguir um caso em que o operador de inserção tem os dois tipos de âncora. Esse é um padrão de transformação válido.
No entanto, o operador de inserção só exige uma âncora. A seguir, um exemplo dessa situação.
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>
let message: string = 'Hello, World!';
+<console.log(message);>+
<...>
Nesse caso, o trecho da linha 1 é uma pré-âncora, mas o operador de reticências da linha 3 não é uma âncora válida. Assim, o operador de inserção possui apenas uma âncora, a pré-âncora. Esse é um padrão de transformação válido.
Também é possível ocorrer outro cenário com um operador de inserção com apenas uma âncora, mas dessa vez apenas uma pós-âncora. Confira o exemplo a seguir.
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>
let message: string = 'Hello, World!';
<...>
+<console.log(message);>+
Nesse caso, o fim de arquivo serve como pós-âncora, mas o operador de reticências da linha 2 não é uma âncora válida. Assim, o operador de inserção possui apenas uma âncora, a pós-âncora. Esse é um padrão de transformação válido.
Por fim, um padrão de transformação inválido ocorre quando há um operador de inserção sem nenhuma âncora, como no exemplo a seguir.
<<<< pattern-name: invalid_pattern; pattern-scope: file-scope; >>>>
let message: string = 'Hello, World!';
<...>
+<console.log(message);>+
<...>
Nesse caso, o operador de inserção está entre dois operadores de reticências e por isso não possui nenhuma âncora. Esse é um padrão de transformação inválido, pois dessa maneira não é possível para o engine definir em que parte do arquivo fonte original deve ser inserido o trecho de código console.log(message)
.
2. Regra para operadores de reticências
O operador de reticências <...>
precisa de duas âncoras para determinar precisamente a partir de (início) e até (fim) onde o engine irá casar o código fonte original. Ou seja, é necessário tanto uma pré-âncora como uma pós-âncora.
Por exemplo, considere o seguinte padrão de transformação:
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>
let message: string = 'Hello, World!';
<...>
Nesse caso, o trecho da linha 1 é a pré-âncora e o fim do arquivo é a pós-âncora do operador de reticências. Esse padrão irá casar com qualquer código fonte original cujo arquivo inicie com o trecho let message: string = 'Hello, World!';
, independente do que vem depois. Esse é um padrão de transformação válido.
Um padrão de transformação inválido ocorre quando há um operador de reticências sem uma das âncoras, por exemplo, sem a pré-âncora ou pós-âncora. A seguir, um exemplo dessa situação.
<<<< pattern-name: invalid_pattern; pattern-scope: file-scope; >>>>
let message: string = 'Hello, World!';
<...>
<...>
console.log(message);
Nesse caso, o operador de reticências da linha 2 possui uma pré-âncora (o trecho da linha 1), mas não possui pós-âncora, pois é seguido de outro operador de reticências. Já o operador de reticências da linha 3, possui pós-âncora (o trecho da linha 4), mas não possui pré-âncora, pois é antecedido de outro operador de reticências.
Esse é um padrão de transformação inválido. Dessa forma, não é possível para o engine definir em que parte do código fonte original o operador de reticências da linha 2 deve parar de corresponder, e em que parte do código fonte original o operador de reticências da linha 3 vai começar a correspondência dos padrões.
3. Regra para operadores de remoção
O operador de remoção -<>-
não precisa de nenhuma âncora. Isso ocorre poque o trecho de código a ser removido (dentro do operador) precisa, necessariamente, existir no código fonte original e por isso oferece contexto suficiente para o engine definir em que parte do código fonte original a remoção irá ocorrer.
Por exemplo, considere o seguinte padrão de transformação:
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>
<...>
-<let message: string = 'Hello, World!';>-
<...>
Nesse caso, o engine irá remover a primeira ocorrência do trecho let message: string = 'Hello, World!';
, independente do trecho de código que ocorrer antes ou depois no arquivo fonte original. Esse é um padrão de transformação válido, mesmo que o operador de remoção não tenha nenhuma âncora.
Ancoragem na presença de operadores de inserção
Operadores de inserção não são âncoras válidas para outros operadores. No entanto, a presença de um operador de inserção antes e/ou depois de outro operador de transformação não impede que haja uma pré e/ou pós âncora válida para esse outro operador. O engine de transformação atua como se ignorasse operadores de inserção antes e/ou depois do outro operador, em busca de âncoras válidas. Exemplo:
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>
let message: string = 'Hello, World!';
+<console.log(message);>+
<...>
Nesse caso, o operador de inserção da linha 2 possui uma âncora na linha 1 (pré-âncora), tornando-o válido de acordo com a regra de ancoragem para operadores de inserção. Já o operador de reticências da linha 3 aparenta possuir somente uma âncora, a pós-âncora de fim de arquivo, uma vez que o operador de inserção da linha 2 não é uma âncora válida.
Dessa forma, a regra de ancoragem para operadores de reticências que exige duas âncoras, não seria respeitada. No entanto, como o engine ignora os operadores de inserção em busca de âncoras válidas, o trecho de código da linha 1 torna-se uma pré-âncora válida para o operador de reticências da linha 3, cumprindo a exigência de duas âncoras. Esse é um padrão de transformação válido.
A mesma situação pode ocorrer quando há múltiplos operadores de inserção em sequência. Exemplo:
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>
let message: string = 'Hello, World!';
+<console.log(message);>+
+<let message2: string = 'Hello, StackSpot!';>+
+<console.log(message2);>+
<...>
Nesse caso, o operador de reticências da linha 5 possui uma âncora de fim de arquivo (pós-âncora), e uma âncora na linha 1 (pré-âncora), uma vez que o engine ignora os operadores de inserção em busca de uma âncora válida. O mesmo ocorre com os operadores de inserção. Por exemplo, mesmo o operador de inserção da linha 3 estando entre outros operadores de inserção, o engine atua ignorando-os e encontra uma âncora válida na linha 1 (pré-âncora), suficiente para atender à regra de ancoragem de operadores de inserção. Esse é um padrão de transformação válido.
Escopo de transformação
Todo padrão de transformação possui um escopo de transformação, podendo ser escopo de arquivo e escopo de snippet.
Escopo de arquivo
Padrões de transformação com escopo de arquivo só casam com o conteúdo inteiro dos arquivos fonte originais. Para declarar o escopo de arquivo em seu padrão de transformação, adicione o seguinte cabeçalho:
<<<< pattern-name: my-pattern-name; pattern-scope: file-scope; >>>>
O exemplo a seguir mostra um padrão de transformação que irá casar com qualquer arquivo de código fonte que comece com o trecho import axios from 'axios';
, independente dos trechos que ocorrerem depois. Irá também inserir o trecho import { useQuery } from 'react-query';
logo após o import do módulo axios.
<<<< pattern-name: my-pattern-name; pattern-scope: file-scope; >>>>
import axios from 'axios';
+<import { useQuery } from 'react-query';>+
<...>
A seguir um arquivo de código fonte que casa com o padrão de transformação anterior.
import axios from 'axios';
console.log("axios");
No entanto, o exemplo a seguir mostra um arquivo de código fonte que NÃO casa com o padrão de transformação anterior.
import { Dispatch } from 'redux';
import axios from 'axios';
console.log("axios");
Isso ocorre porque padrões com escopo de arquivo precisam corresponder com o arquivo fonte inteiro. Porém, o trecho da linha 1 do padrão de transformação import axios from 'axios';
não corresponde com o trecho da linha 1 do código fonte original import { Dispatch } from 'redux';
.
Uma possível solução seria alterar o padrão de transformação para o exemplo a seguir.
<<<< pattern-name: my-pattern-name; pattern-scope: file-scope; >>>>
<...>
import axios from 'axios';
+<import { useQuery } from 'react-query';>+
<...>
Em padrões de transformação com escopo de arquivo é recomendado iniciar e terminar com o operador de reticências <...>, a menos que se deseje casar o padrão somente com arquivos de código fonte originais que iniciem e terminem exatamente com os mesmos trechos do padrão de transformação.
Ainda sobre os padrões de transformação com escopo de arquivo. Considere o exemplo de padrão de transformação a seguir.
<<<< pattern-name: my-pattern-name; pattern-scope: file-scope; >>>>
<...>
console.log("example");
+<console.log("new-example");>+
<...>
Um possível arquivo de código fonte original que casa com o padrão de transformação anterior é o seguinte:
function func():void{
console.log("example");
}
console.log("example");
console.log("example");
console.log("example");
No entanto, mesmo casando, é importante entender que como o padrão de transformação tem escopo de arquivo, ele irá inserir o trecho console.log("new-example");
somente após a primeira ocorrência "global" (não inclui o trecho de console.log que ocorre dentro do corpo da função) do trecho console.log("example");
no arquivo de código fonte original. Dessa forma, o código original transformado seria o seguinte:
function func():void{
console.log("example");
}
console.log("example");
console.log("new-example");
console.log("example");
console.log("example");
Para inserir o trecho console.log("new-example");
após todas as ocorrências do trecho console.log("example");
no código fonte original, seria necessário um padrão como o exemplo a seguir.
<<<< patter-name: my-pattern-name; pattern-scope: file-scope; >>>>
<...>
function func():void{
<...>
console.log("example");
+<console.log("new-example");>+
<...>
}
<...>
console.log("example");
+<console.log("new-example");>+
<...>
console.log("example");
+<console.log("new-example");>+
<...>
console.log("example");
+<console.log("new-example");>+
<...>
Porém, qualquer outra nova ocorrência do trecho console.log("example");
no código fonte original não seria transformada. Uma forma mais concisa e eficiente de solucionar esse cenário é utilizar padrões de transformação com escopo de snippet.
Escopo de snippet
Diferente do escopo de arquivo que o padrão de transformação deve casar com o arquivo fonte inteiro, o escopo de snippet busca trechos no código fonte original que casem com o padrão e transformam todas as ocorrências. Para declarar o escopo de snippet em seu padrão de transformação, adicione o seguinte cabeçalho:
<<<< pattern-name: my-pattern-name; pattern-scope: snippet-scope; >>>>
Considere o exemplo de escopo de snippet com o padrão de transformação a seguir:
<<<< pattern-name: my-pattern-name; pattern-scope: snippet-scope; >>>>
-<console.info(*<".*">*);>-
O padrão irá remover qualquer construção de código que corresponda a uma chamada ao método console.info()
que receba como argumento uma string com qualquer valor, dado que o operador de String match está aninhado e utiliza a expressão regular ".*"
.
A seguir um exemplo de código fonte original que casa com o padrão de transformação mencionado anteriormente e o código transformado ao aplicar o padrão de transformação:
- Código Original
- Código transformado
class User {
id: number;
name: string;
email: string;
constructor(id: number, name: string, email: string) {
this.id = id;
this.name = name;
this.email = email;
}
}
class UserService {
private users: User[] = [];
createUser(user: User): void {
console.info("createUser");
this.users.push(user);
}
readUser(id: number): User | undefined {
console.info("readUser");
return this.users.find(user => user.id === id);
}
updateUser(id: number, updatedUser: User): void {
console.info("updateUser");
const index = this.users.findIndex(user => user.id === id);
if (index !== -1) {
this.users[index] = updatedUser;
}
}
deleteUser(id: number): void {
console.info("deleteUser");
this.users = this.users.filter(user => user.id !== id);
}
}
class User {
id: number;
name: string;
email: string;
constructor(id: number, name: string, email: string) {
this.id = id;
this.name = name;
this.email = email;
}
}
class UserService {
private users: User[] = [];
createUser(user: User): void {
this.users.push(user);
}
readUser(id: number): User | undefined {
return this.users.find(user => user.id === id);
}
updateUser(id: number, updatedUser: User): void {
const index = this.users.findIndex(user => user.id === id);
if (index !== -1) {
this.users[index] = updatedUser;
}
}
deleteUser(id: number): void {
this.users = this.users.filter(user => user.id !== id);
}
}
É possível notar que todas as chamadas a console.info()
que recebem como argumento uma string foram removidas do código fonte (linhas 17, 22, 27, 35).
Uma vez que o início e fim de arquivo não são âncoras válidas em padrões com escopo de snippet, é inválido o uso de operadores de reticências no início ou no fim desse tipo de padrão, ao contrário do que ocorre com os padrões com escopo de arquivo.
Construções com suporte aos operadores de transformação
O uso dos operadores de transformação são suportados nas principais construções das linguagens suportadas pela engine. Isso significa que, quando você estiver criando um padrão de transformação, em qualquer trecho de código válido da linguagem que ocorrer uma construção que suporta os operadores de transformação, é possível utilizar os operadores. A seguir você encontra uma listagem por linguagem, de construções que suportam os operadores de transformação. Portanto, caso a construção que você deseja utilizar não estiver listada, não será possível utilizar operadores de transformação.