Skip to main content

Overview

In this section, you will find the reference of a Declarative Hook of the type code-transformation.


About the Declarative Hook code-transformation

The code-transformation Declarative Hook changes source code through a transformation engine. Changes must be defined using transformation patterns. Developers must write these patterns in a language that extends the language of the source code with operators that determine what should be changed. The engine applies the changes whenever there is a match between the transformation patterns and the source code.

The code-transformation Declarative Hook can be very helpful in several situations where you need to evolve existing code. For example, when you want to update a library or framework version and must change code at scale, or when you need to modify existing code to introduce a new feature by applying a StackSpot Plugin.

Supported languages

The current version of the code transformation engine (0.2.0-beta) supports the following languages:

  • TypeScript
  • HTML
  • Java

How to define the code-transformation hook

Inside the spec field of your Plugin configuration file (plugin.yaml), declare the code-transformation as in the example below:

schema-version: v4
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:

The Declarative Hook type.

engine-version:

The version of the code transformation engine used by the Hook. The current version is 0.2.0-beta.

trigger:

Supports only the after-render option. It executes Hook commands after the Template generates files in the project.

language:

Supported language for the transformation. Currently, code-transformation supports transformation patterns written in TypeScript, HTML and Java.

trans-pattern-path:

The path where the files with the transformation patterns are located. The path is relative to the Plugin folder.

You must create the trans-patterns folder to place the files with the transformation patterns. The pattern file extension must match the language defined in the language field. For example, for TypeScript it must be .ts, for HTML .html, and for Java .java.

warning

If you provide only the folder path instead of a file name, the Hook will execute all transformation patterns present in the trans-patterns folder (that have the defined language extension), recursively.

Examples:

To perform a transformation based on a pattern present in a specific file:

trans-pattern-path: trans-patterns/pattern.ts

To perform multiple transformations based on patterns present in all files in a folder, recursively:

trans-pattern-path: trans-patterns/

You can also enter the name of the file with the transformation pattern using JINJA expressions:

trans-pattern-path: trans-patterns/{{trans_pattern_file_name}}.ts

source-path:

Application path where the pre-existing source code to be transformed is located. The path is relative to the application folder. The source file extension must match the language defined in the language field. For example, for TypeScript it must be .ts, for HTML .html, and for Java .java.

warning

If you provide only the application folder path instead of a file name, all source files (that have the defined language extension) that match the transformation patterns will be transformed, recursively.

Examples:

To transform the code of a specific file in your application:

source-path: app-src/code.ts

To transform the code of all files in the application folder, recursively:

source-path: app-src/

You can also enter the name of a file in your application using JINJA expressions:

source-path: app-src/{{app_file_name}}.ts

Steps to create and use the code-transformation hook

To create and use the code-transformation Hook, follow these steps:

  1. Create a StackSpot app Plugin;
  2. Create a folder (for example, trans-patterns) that will contain the transformation pattern file(s);
  3. Create transformation pattern file(s) in the folder created in the previous step. Each file can contain one or more transformation patterns. Transformation pattern files must have the same extension as the source files you want to transform. For example, to transform TypeScript source files, the transformation pattern files must have the .ts extension;
  4. Write the transformation patterns in the files created in the previous step. Patterns must follow the rules defined in the other sections of this document;
  5. Define the code-transformation Hook in the Plugin configuration file created in Step 1, according to the section How to define the code-transformation hook;
  6. Apply the Plugin created in Step 1 to a StackSpot app.

After these steps, if your transformation patterns are correct and there are matches between them and the application source code, the engine will perform the transformations, changing the application source files.

Transformation operators

Transformation patterns have five operators for code transformation: the insertion, removal, ellipsis, ID match, and string match operators.

info

The engine guarantees that the syntax of the transformed source code remains correct. However, it does not guarantee maintaining the formatting in sections that use the code insertion or removal operator. This behavior should be improved in future versions, especially for languages where the syntax depends on code formatting.

info

All concepts of the code transformation engine are presented using TypeScript examples. For examples in the other supported languages, see Constructs Supported by Transformation Operators.

1. Insertion operator

Operator for inserting code snippets into the original source code.
The syntax is: +<CODE-SNIPPET>+

Replace the CODE-SNIPPET with the code snippet you want to insert into the original source code.

The example below shows a transformation pattern that inserts the code snippet console.log("World!"); after the snippet console.log("Hello");.

console.log("Hello");
+<console.log("World!");>+

2. Removal operator

The operator removes code snippets from the source code.
The syntax is:

-<CODE-SNIPPET>-

Replace the CODE-SNIPPET with the code snippet you want to remove from the source code.

The example below shows a transformation pattern that removes the code snippet console.log("Hello");. This transformation will result in an empty source code file.

-<console.log("Hello");>-

3. Ellipsis operator

The operator matches zero or more constructs in the source code. It is a wildcard operator, similar to * in regular expressions, although it cannot be entirely compared to it. The operator has the syntax <...>.

The following example shows a transformation pattern that will match any source code file that begins with the snippet console.log("start"); and ends with the snippet console.log("end");.

console.log("start");
<...>
console.log("end");

The following examples show some source codes that match the previous transformation pattern.

console.log("start");
console.log("1");
console.log("end");

4. ID match operator

The ID match operator uses a regular expression to match identifiers in the source code to be transformed. Identifiers usually represent names of variables, functions, methods, classes, packages, namespaces, and others, depending on the language. For more details on which constructs are identifiers in each language, see Constructs Supported by Transformation Operators. The operator has the syntax *<REGEX>*.

For example, if you need to insert the @Transactional decorator into functions whose names start with trans, you can only express this transformation pattern using the ID match operator, as shown below.

+<@Transactional>+
function *<trans.*>*(){
<...>
}

As you can see in the previous example, you can only use the ID match operator where an identifier would occur, such as the function name on line 2. The operator's regular expression is trans.*, which matches any function whose name starts with trans, since .* denotes zero or more characters.

Other examples with identifiers

1. ID match to match functions that start with a specific prefix:

  • Regex: get.*

The following example adds a log before any function whose name starts with get.

example1.ts with ID match '*<get.*>*'
function *<get.*>* (<...>) {
+<console.log("Function called");>+
<...>
}

2. ID match to identify variables that end with a specific suffix:

  • Regex: .*Service

The following example adds a comment above any variable whose name ends with Service:

example2.ts with ID match '*<.*Service>*'
+<// This is a service variable >+
let *<.*Service>*: <...>;

3. ID match to identify classes that contain a specific word:

  • Regex: .*Controller.*

The following example adds a @Controller decorator to classes that contain Controller in the name:

example3.ts with ID match '*<.*Controller.*>*'
+<@Controller>+
class *<.*Controller.*>* {
<...>
}

5. String match operator

The string match operator is similar to the ID match operator, but it uses a regular expression to match strings in the source code that need to be transformed. While the ID match operator uses a regular expression to match identifiers within languages supported by the Declarative Hook code-transformation, the string match operator can match any string in the source code, regardless of the constructs allowed by the transformation operators. This means that the string match operator can target strings found within method calls, class declarations, or variable values, which is not possible with the ID match operator. The syntax for the string match operator is *<"REGEX">*.

warning

The string match operator should always be used with the double-quoted syntax *<"">*, even in languages that allow single-quoted strings.

For example, if you need to insert a console.log() call before another call, regardless of which string is passed as an argument, you can only express this transformation pattern using the string match operator, as shown below.

<...>
+<console.log("pre log");>+
console.log(*<".*">*);
<...>

As you can see in the previous example, you can only use the string match operator where a string would occur, such as the argument of the console.log() function call. The operator's regular expression is .*, which matches any string, since .* denotes zero or more characters.

Other examples with strings

1. String match to identify specific strings in function calls:

  • Regex: "error.*"

The following example adds a log before any call to console.log that contains a string starting with error:

example1.ts with string match '*<
<...>
+<console.log("Logging an error");>+
console.log(*<"error.*">*);
<...>

2. String match to match exact strings:

  • Regex: "Hello World"

The following example removes any call to console.log that contains the exact string Hello World:

example2.ts with string match '*<
<...>
-<console.log(*<"Hello World">*);>-
<...>

3. String match to match strings that contain numbers:

example3.ts with string match '*<
<...>
+<console.log("String contains numbers");>+
console.log(*<".*\\d+.*">*);
<...>

Operator nesting

The only operator that allows nesting of other operators is the removal operator. You can nest the ellipsis, ID match, and string match operators to create powerful patterns.

warning

The code transformation engine does not support nesting only an ellipsis operator within a removal operator, such as -<<...>>-.

For example, if you need to remove a console.log() call regardless of which string is passed as an argument, you can only express this transformation pattern using the string match operator nested within the removal operator, as shown below.

<...>
-<console.log(*<".*">*);>-
<...>

Another example shows how you can use the ellipsis and ID match operators nested within the removal operator to remove a function whose name starts with create, regardless of its body and parameters.

<...>

-<function *<create.*>*(<...>) {
<...>
}>-

<...>

Transformation patterns

A transformation pattern is a regular source code file with the addition of pattern headers and transformation operators. All code snippets that are not operators serve as context so that the engine can determine which parts of the original source code should be changed.

Declaring transformation patterns

In a transformation pattern file, you can declare one or more patterns. Each pattern requires a header. The headers have two properties: the pattern name, pattern-name, and the pattern scope, pattern-scope. The syntax for the transformation pattern header is:

<<<< pattern-name: my_pattern_name; pattern-scope: file-scope; >>>>
  • pattern-name: Enter a name to identify your transformation pattern. The name should not contain spaces. Example: my_pattern, my-pattern, or my.pattern.

  • pattern-scope: Use file-scope for transformation patterns with file scope, or snippet-scope to use the transformation pattern with snippet scope. For more details on transformation pattern scope, see Transformation scope.

warning

Until version 0.1.0-beta of the code transformation engine, you declared one transformation pattern per file, without a header, all with file scope. Starting from version 0.2.0-beta, declaring a single pattern without a header per file is still supported but deprecated. We encourage declaring all patterns with a header.

warning

When applying a file with multiple transformation patterns, the engine will transform the source code following a sequential order of the headers defined. For example:

Transformation file with multiple patterns
<<<< 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

The examples below show transformation pattern files with, respectively, only one pattern, more than one pattern, and patterns with different scopes.

<<<< pattern-name: my_only_pattern; pattern-scope: file-scope; >>>>
// This pattern inserts an "id" attribute in a class whose name ends with "Entity"

<...>

class *<.*Entity>* {
+<id : number;>+
<...>
}

<...>

Example of transformation pattern

So far, you have seen transformation patterns that use only one operator. However, creating more powerful transformation patterns is possible using all operators.

The following example shows a transformation pattern that removes the initialization of attributes from the Person class and inserts a constructor. To remove the attribute initialization, we use removal operators followed by insertion operators. Next, we use the insertion operator again to insert a constructor. Finally, we use the ellipsis operator so that the transformation pattern can match any code snippet that might exist after declaring the age attribute of the Person class in the original source file.

<<<< 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;
}>+

<...>
}

Creating syntactically valid transformation patterns

Transformation patterns are code written in an extended version of the original code language. Therefore, all context code in the transformation pattern must be syntactically correct. The following example shows a syntactically invalid transformation pattern. As a result, the transformation engine will not be able to execute it.

INVALID transformation pattern
<<<< pattern-name: invalid_pattern; pattern-scope: file-scope; >>>>

function func()
<...>
+<console.log("Finishing func");>+
}

The previous pattern is syntactically invalid because it lacks the opening brace { of the func function. The ellipsis operator will not match this missing character, as it is in the context of matching only statements within the function body.

The following is another example of a syntactically invalid transformation pattern:

INVALID transformation pattern
<<<< pattern-name: invalid_pattern; pattern-scope: file-scope; >>>>

console.log(<...>

The previous code pattern is invalid due to the absence of the closing parenthesis ) in the log method call. The ellipsis operator will not match this missing character since it only matches arguments from a method call.

info

Transformation patterns and their operators are not regular expressions (regex). Regex manipulates text without structure, whereas transformation patterns are code with a defined syntax, just like any programming language. The engine generates syntactic trees of transformation patterns, so they must follow a specific structure.

Still on transformation patterns, in addition to the context code needing to be syntactically valid, the sections to be inserted or removed through the insertion and removal operators must be syntactically valid and also keep the original code syntactically valid after the transformation. The following transformation pattern is an example of an invalid pattern.

INVALID transformation pattern
<<<< pattern-name: invalid_pattern; pattern-scope: file-scope; >>>>

class MyClass {

myMethod():void{}

myProperty:string = "";

+<console.log("bla");>+
}

The previous pattern is syntactically invalid because, although the code snippet to be inserted (line 8) is syntactically valid, the original code transformed by it would be syntactically invalid after the transformation. This happens because it is impossible to insert a console.log("bla"); call directly inside a class, outside the body of a method.

See another example, now with the code snippet to be inserted invalid:

INVALID transformation pattern
<<<< pattern-name: invalid_pattern; pattern-scope: file-scope; >>>>

class MyClass {

myMethod():void{}

myProperty:string = "";

+<myMethod2():void {>+
+< console.log("Executing myMethod2");>+
+<}>+
}

The previous pattern is syntactically invalid because each insertion operator does not contain a complete valid code snippet. For example, myMethod2():void { is not a syntactically valid structure in TypeScript. Insertion and removal operators do not act by inserting or removing lines but by inserting or removing complete, syntactically valid code snippets. The following example shows how to change the previous transformation pattern to make it valid.

Valid transformation pattern
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>

class MyClass {

myMethod():void{}

myProperty:string = "";

+<myMethod2():void {
console.log("Executing myMethod2");
}>+
}

The previous transformation pattern is valid because it uses a single operator to insert the myMethod2 method into the MyClass class.

Finally, the following example shows a syntactically invalid transformation pattern because it presents the insertion of an invalid code snippet. The invalid snippet tries to insert and remove more than one construct using the same insertion and removal operator.

INVALID transformation pattern
<<<< pattern-name: invalid_pattern; pattern-scope: file-scope; >>>>

+<let myVar1:number = 10; let myVar2:string = "bla";>+
-<console.log("Hello");
console.log("StackSpot");>-

The previous transformation pattern is invalid because it tries to insert two pieces of code in a single insertion operator (line 3). It is also invalid because it tries to remove two pieces of code in a single removal operator (lines 4 and 5). The following example shows how to change the previous transformation pattern to make it valid.

Valid transformation pattern
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>

+<let myVar1:number = 10;>+ +<let myVar2:string = "bla";>+
-<console.log("Hello");>-
-<console.log("StackSpot");>-

The previous transformation pattern is valid because it uses one insertion operator and one removal operator for each code snippet.

Use of operators in comma-separated constructions

In transformation patterns, the use of transformation operators in comma-separated constructions receives special treatment. See the following example:

Transformation pattern
<<<< pattern-name: my_pattern; pattern-scope: file-scope; >>>>

function func(<...>){
<...>
+<console.log("Finishing func");>+
}

The previous transformation pattern will match a file that contains a function named func, regardless of the number, type, and name of its parameters and the content of its body. The ellipsis operator on line 5 is part of a comma-separated list of formal parameters in a function declaration.

However, if you want to add a new parameter to a function declaration, you can create a transformation pattern as in the following example:

Transformation pattern
<<<< pattern-name: insert_function_param; pattern-scope: file-scope; >>>>

function func(<...> +<newParam:string>+){
<...>
}

The previous transformation pattern will insert a parameter at the end of the parameter list of the func function. However, the func function can have zero or more parameters in the original code. In the first case, it would not be necessary to add a comma before newParam:string. In the second case, adding the comma would be required. Whoever creates the pattern cannot know in advance which situations will require the comma. Therefore, you should not place the comma in the context code (between the ellipsis operator and the insertion operator on line 4) or in the section that will be inserted (within the insertion operator on line 4). The engine will decide when and where to place commas when operators appear in a comma-separated construction.

The following is an example of using a removal operator in a comma-separated construction:

Transformation pattern
<<<< pattern-name: remove_array_element; pattern-scope: file-scope; >>>>

<...>
-<import { FormsModule } from '@angular/forms';>-
<...>
@NgModule({
<...>
imports: [<...> -<FormsModule> <...>]
<...>
})
export class AppModule {}
<...>

The previous transformation pattern uses transformation operators in two comma-separated constructs. The ellipsis operators at the beginning (line 7) and end (line 9) of the object property list, and the ellipsis and removal operators in the values of the imports array (line 8). In the first case, the engine will try to match an imports property in the original code, regardless of its position in the object. In the second case, it will remove the FormsModule value, regardless of its position in the imports array. Additionally, the engine will remove any commas if necessary.

Finally, if you want to transform a specific position from a comma-separated construction, you can adopt the strategy in the following example.

Transformation pattern
<<<< pattern-name: complex_example; pattern-scope: file-scope; >>>>

<...>
function func(<...>,<...>,-<param3:number>- +<newParam3:boolean>+, <...>, -<param5:string>-){
<...>
}

<...>

func(<...>,<...>,-<arg3>- +<newArg3>+, <...>, -<arg5>-)
<...>

In line 5 of the previous transformation pattern:

  1. The third formal parameter param3:number is removed from the func function declaration;
  2. A new parameter newParam3:boolean is inserted in its place;
  3. The fifth formal parameter param5:string is removed.

The same applies to passing arguments when calling the func function on line 9.

In this case, the commas present in the context code allow you to locate precisely which parameter or argument is being inserted or removed. Even in this case, the engine knows when to keep, add, or remove commas.

Comments and blank characters

warning

Blank characters and comments are ignored when matching transformation patterns with original source code files, except in languages where syntax depends on code formatting (for example, Python). For example:

Transformation pattern
<<<< 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 on the transformation pattern

The previous transformation pattern matches the source code below, even though the original code has different comments and formatting.

Original code with different formatting and comments
// my comment in original code
let message: string = 'Hello, World!';console.log(message);let message2: string = 'Hello, StackSpot!';

Operator anchoring

Among the currently available operators, insertion and ellipsis must be placed in the transformation pattern so that code snippets serve as context and the engine can identify precisely which parts of the source code should be changed. At StackSpot, these code snippets that serve as the context for operators are called anchors.

You can use any syntactically valid code snippet present in the original code as an anchor for operators in the transformation pattern. In addition to code snippets, the beginning and end of the source file and even a removal operator can also be used as anchors. However, insertion and ellipsis operators alone cannot be valid anchors for another operator in a transformation pattern. For patterns with file scope, the beginning and end of the file are valid anchors. For snippet-scope patterns, the beginning and end of the file are not valid anchors. For more details, see Transformation scope.

Anchors can be of two types:

  1. Pre-anchor: when the anchor occurs before the transformation operator;
  2. Post-anchor: when the anchor occurs after the transformation operator.
warning

Understanding the concept of operator anchoring is extremely important so that you can create valid transformation patterns.

Below, check the list of operators and how they use anchors.

1. Rule for insertion operators

The insertion operator +<>+ needs at least one anchor to determine precisely where in the original source code the transformation will occur. It can be a pre-anchor and/or a post-anchor.

For example, consider the following transformation pattern, which inserts a snippet of code into the original code:

Valid transformation pattern with insertion operator with two anchors
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>

let message: string = 'Hello, World!';
+<console.log(message);>+

In this case, the snippet on line 3 is a pre-anchor, and the end of the file is a post-anchor of the insertion operator. This is a valid transformation pattern.

However, the insertion operator only requires one anchor. See the following example of this situation.

Valid transformation pattern with insertion operator with only one anchor (pre-anchor)
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>

let message: string = 'Hello, World!';
+<console.log(message);>+
<...>

In this case, the snippet on line 3 is a pre-anchor, but the ellipsis operator on line 4 is not a valid anchor. Thus, the insertion operator has only one anchor, the pre-anchor. This is a valid transformation pattern.

Another scenario is the insertion operator with only a post-anchor. See the example below.

Valid transformation pattern with insertion operator with only one anchor (post-anchor)
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>

let message: string = 'Hello, World!';
<...>
+<console.log(message);>+

In this case, the end of file is the post-anchor, but the ellipsis operator on line 4 is not a valid anchor. Thus, the insertion operator has only one anchor, the post-anchor. This is a valid transformation pattern.

Finally, an invalid transformation pattern occurs when an insertion operator has no anchors, as in the following example.

INVALID transformation pattern with insertion operator without any anchor
<<<< pattern-name: invalid_pattern; pattern-scope: file-scope; >>>>

let message: string = 'Hello, World!';
<...>
+<console.log(message);>+
<...>

In this case, the insertion operator is between two ellipsis operators and, therefore, has no anchor. This is an invalid transformation pattern, as the engine cannot define where in the source file the console.log(message) snippet should be inserted.

2. Rule for ellipsis operators

The ellipsis operator <...> needs two anchors to determine from (start) and to (end) where the engine will match the source code. In other words, both a pre-anchor and a post-anchor are needed.

For example, consider the following transformation pattern:

Valid transformation pattern with ellipsis operator with two anchors
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>

let message: string = 'Hello, World!';
<...>

In this case, the snippet on line 3 is the pre-anchor, and the end of the file is the post-anchor of the ellipsis operator. This pattern will match any source code whose file begins with let message: string = 'Hello, World!';, regardless of what comes after. This is a valid transformation pattern.

An invalid transformation pattern occurs when there is an ellipsis operator without one of the anchors, for example, without a pre-anchor or a post-anchor. See the following example of this situation.

INVALID transformation pattern with an ellipsis operator without a pre-anchor and another without a post-anchor
<<<< pattern-name: invalid_pattern; pattern-scope: file-scope; >>>>

let message: string = 'Hello, World!';
<...>
<...>
console.log(message);

The ellipsis operator on line 4 has a pre-anchor, which refers to the snippet on line 3. However, it has no post-anchor because another ellipsis operator follows it. On the other hand, the ellipsis operator on line 5 has a post-anchor, which refers to the snippet on line 6, but it does not have a pre-anchor, as another ellipsis operator precedes it.

This is an invalid transformation pattern. The engine cannot define where in the source code the ellipsis operator on line 4 should stop matching and where the ellipsis operator on line 5 should begin matching.

3. Rule for removal operators

The -<>- removal operator does not need anchors. This occurs because the piece of code to be removed (within the operator) must necessarily exist in the source code and, therefore, offers sufficient context for the engine to define where the removal will occur.

For example, consider the following transformation pattern:

Valid transformation pattern with removal operator
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>

<...>
-<let message: string = 'Hello, World!';>-
<...>

In this case, the engine will remove the first occurrence of let message: string = 'Hello, World!';, regardless of the code snippet that occurs before or after in the source file. This is a valid transformation pattern even if the removal operator has no anchor.

Anchoring in the presence of insertion operators

Insertion operators are not valid anchors for other operators. However, the presence of an insertion operator before and/or after another transformation operator does not prevent there from being a valid pre and post-anchor for that other operator. The transformation engine acts as if it ignores insertion operators before and after the other operator in search of valid anchors. Example:

Transformation pattern valid even in the presence of an insertion operator
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>

let message: string = 'Hello, World!';
+<console.log(message);>+
<...>

In this case, the insertion operator on line 4 has an anchor on line 3 (pre-anchor), making it valid according to the anchoring rule for insertion operators. The ellipsis operator on line 5 appears to have only one anchor, the end-of-file post-anchor, since the insertion operator on line 4 is not valid as an anchor.

Therefore, the anchoring rule for ellipsis operators, which requires two anchors, would not be respected. However, as the engine ignores insertion operators when searching for valid anchors, the code snippet on line 3 becomes a valid pre-anchor for the ellipsis operator on line 5, fulfilling the requirement for two anchors. This is a valid transformation pattern.

The same situation can occur when there are multiple insertion operators in sequence. Example:

Transformation pattern valid even in the presence of multiple insertion operators in sequence
<<<< pattern-name: valid_pattern; pattern-scope: file-scope; >>>>

let message: string = 'Hello, World!';
+<console.log(message);>+
+<let message2: string = 'Hello, StackSpot!';>+
+<console.log(message2);>+
<...>

On line 7, the ellipsis operator has an end-of-file anchor (post-anchor) and an anchor on line 3 (pre-anchor), because the engine ignores the insertion operators and searches for a valid anchor. The same applies to the insertion operators. For instance, although the insertion operator on line 5 is among other insertion operators, the engine ignores them and finds a valid anchor on line 3 (pre-anchor), satisfying the anchoring rule for insertion operators. This is a valid transformation pattern.

Transformation scope

Every transformation pattern has a transformation scope, which can be file scope or snippet scope.

File scope

File-scope transformation patterns only match the entire content of the original source files. To declare the file scope in your transformation pattern, add the following header:

<<<< pattern-name: my-pattern-name; pattern-scope: file-scope; >>>>

The following example shows a transformation pattern that will match any source code file that begins with the import axios from 'axios'; snippet, regardless of the snippets that occur later. It will also insert the import { useQuery } from 'react-query'; snippet right after the import of the axios module.

Transformation pattern
<<<< pattern-name: my-pattern-name; pattern-scope: file-scope; >>>>

import axios from 'axios';
+<import { useQuery } from 'react-query';>+
<...>

Below is a source code file that matches the previous transformation pattern.

Original source code file that matches the transformation pattern
import axios from 'axios';
console.log("axios");

However, the following example shows a source code file that DOES NOT match the previous transformation pattern.

Original source code file that does NOT match the transformation pattern
import { Dispatch } from 'redux';
import axios from 'axios';
console.log("axios");

This is because file-scope patterns must match the entire source file. However, the snippet on line 4 of the transformation pattern import axios from 'axios'; does not match the snippet on line 4 of the source code import { Dispatch } from 'redux';.

A possible solution would be to change the transformation pattern as in the following example.

Transformation pattern changed
<<<< pattern-name: my-pattern-name; pattern-scope: file-scope; >>>>

<...>
import axios from 'axios';
+<import { useQuery } from 'react-query';>+
<...>
tip

In file-scope transformation patterns, it is recommended to start and end with the ellipsis operator <...>, unless you want to match the pattern only with source code files that start and end exactly with the same snippets in the transformation pattern.

Still on file-scope transformation patterns. Consider the following transformation pattern example.

Transformation pattern
<<<< pattern-name: my-pattern-name; pattern-scope: file-scope; >>>>

<...>
console.log("example");
+<console.log("new-example");>+
<...>

A possible original source code file that matches the previous transformation pattern is the following:

Original source code file that matches the transformation pattern
function func():void{
console.log("example");
}

console.log("example");
console.log("example");
console.log("example");

However, even when matching, it is important to understand that, as the transformation pattern has file scope, it will insert the console.log("new-example"); snippet only after the first "global" occurrence (it does not include the console.log snippet that occurs within the function body) of console.log("example"); in the source code file. Thus, the original transformed code would be the following:

Transformed original source code file
function func():void{
console.log("example");
}

console.log("example");
console.log("new-example");
console.log("example");
console.log("example");

To insert the console.log("new-example"); snippet after all occurrences of console.log("example"); in the source code, you would need a pattern like the following example.

Transformation pattern changed
<<<< pattern-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");>+
<...>

However, any other new occurrence of the console.log("example"); snippet in the source code would not be transformed. A more concise and efficient way to resolve this scenario is to use snippet-scope transformation patterns.

Snippet scope

Unlike the file scope, where the transformation pattern must match the entire source file, the snippet scope looks for code snippets in the original source that match the pattern and transforms all occurrences. To declare the snippet scope in your transformation pattern, add the following header:

<<<< pattern-name: my-pattern-name; pattern-scope: snippet-scope; >>>>

Consider the following example of a snippet-scope transformation pattern:

Transformation pattern
<<<< pattern-name: my-pattern-name; pattern-scope: snippet-scope; >>>>

-<console.info(*<".*">*);>-

The pattern will remove any construct that matches a call to the console.info() method with a string argument, because the string match operator is nested and uses the regular expression ".*".

Below is an example of original source code that matches the transformation pattern mentioned earlier and the transformed code after applying the pattern:

Original source code
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);
}
}

You can see that all calls to console.info() with a string argument were removed from the source code (lines 17, 22, 27, 35).

warning

Since the beginning and end of the file are not valid anchors in snippet-scope patterns, using ellipsis operators at the beginning or end of this type of pattern is invalid, unlike file-scope patterns.

Constructs Supported by Transformation Operators

Transformation operators are supported in the main constructs of the languages supported by the engine. This means that when you create a transformation pattern, you can use the operators in any valid code snippet of the language where a construct that supports transformation operators occurs. Below, you will find a list by language of constructs that support transformation operators. Therefore, if the construct you want to use is not listed, it will not be possible to use transformation operators.