Writing a recipe, part 1

Since we prefer the method of learning by example, we've chosen to let you experiment with Sensei by following a quick tutorial.

Creating the example project.

Start by creating a regular Java project and include the following code.

1
2
3
4
5
class Example {
    public void main() {
        System.out.println("test");
    }
}

Currently the code System.out.println prints the message 'test' to the console. Next, we will rewrite this statement to use a logger instead.

Opening Sensei to start recipe writing

There are two ways to open the recipe editor:

  1. By using the menu item Tools | Sensei | Cookbook Manager and clicking Manage recipes after selecting a Cookbook entry.

  2. Or by using the intentions menu. Modern IDEs will suggest possible actions to take while coding. These range from fixing typos to performing a code change. These actions are listed in the intentions menu. Since the plugin is seamlessly integrated into the coding environment, an intention has been added to this list in order facilitate creating a new recipe.

On line 4, put the cursor on the word println. Now press the Show intention actions and quick-fixes shortcut (by default: Alt + Enter or Cmd + Enter) and choose Create a new recipe. Next, you will be presented with two options: start from scratch and search for similar methodcalls. The latter option significantly reduces the time it takes to create a recipe. However, this guide will walk you through both options, step by step.

Firstly, choose the option to start from scratch. Now sensei will immediately ask where to store the recipe. Since this is the first time that you're using Sensei, click on + and choose a Cookbook ID to your liking. We will pick project-recipes. Next, specify in the text box where Sensei should store these recipes. Please choose project://recipes. Sensei will now store the recipes inside a subfolder in the project.

The cookbook has been created, so now we can specify a name for our new recipe and a short description such as Logging: Enforce java.util.logging and Do not use System.out.println to write log statements. After filling in the details, click on Create and edit recipe.

Writing the recipe

The recipe editor window contains 3 tabs: General Settings, Recipe Settings and QuickFix Settings. During the first part of this tutorial, we'll focus on the second tab. This section is split up into two parts. The left side contains our search definition where we can specify what Sensei should look for. The right side provides a preview panel. This feature comes in handy during writing to verify if the recipe behaves as expected.

Let's start by specifying the target. We are searching for method calls with the name println. There are 2 ways to configure this: through the UI view and through the Code view. Which one you choose is purely preference but the recommended way to start is to use the UI view since this will show all available options when clicking the + buttons. However, this guide will show the YAML code view syntax as it facilitates copying and pasting the examples.

The recipe should currently look like this:

search:
  methodcall:
    name: "println"

After writing this, we should see our preview panel updating and highlighting the correct piece of code.

Lastly, it is possible to only highlight println if it has been performed on System.out. We can use the on option for that purpose

search:
  methodcall:
    name: "println"
    on:
      field:
        name: "out"
        in:
          class:
            name: "java.lang.System"

Creating the suggestions

Once the search part of the recipe is done, the next step is to create the suggestion to use the built-in logging framework. First, go to the QuickFix Settings tab. By default, a common structure will be shown. This includes a name and a single action that rewrites the highlighted statement.

Choose a descriptive name such as Use logger. Next, rewrite the current statement into logger.log(Level.INFO, {{{ arguments.0 }}}).

As you can see, we've used the variable {{{ arguments.0 }}}. All possible variables will be shown inside a tree-like structure, below the rewrite text box. You can choose to write these variables yourself in the text box or simply double-click on one of the entries to add them to the text box automatically.

The quickfix should be looking like this:

availableFixes:
- name: "Use logger"
  actions:
  - rewrite:
      to: "logger.log(Level.INFO, {{{ arguments.0 }}})"

The only missing bit here is that we still need to declare the field logger at class level. This can be done by clicking the + button at the actions level and choosing addField. Once done, we can immediately see that Sensei has specified to run this action at the parentClass. It is important to know that almost any action can be performed on different elements, including the rewrite action that we've used before. Next step is to build the statement that will create the logger field.

  1. Write the expected result for this current example. Afterwards, we will make it more generic.

private Logger logger = Logger.getLogger(Example.class.getName())
  1. Of course, this would only work if we perform the suggestion inside a class named Example. To make this applicable to different classes, use a variable to extract the current class name. The variable needed, is {{{ name }}} (variables depend on the target of the action).

private Logger logger = Logger.getLogger({{{ name }}}.class.getName())
  1. Lastly, don't assume that the class Logger has already been imported. Tell Sensei to automatically import this class by fully qualifying the Logger to java.util.logging.Logger.

private java.util.logging.Logger logger = Logger.getLogger({{{ name }}}.class.getName())

The final recipe should look like this:

availableFixes:
- name: "Use logger"
  actions:
  - rewrite:
      to: "logger.log(Level.INFO, {{{ arguments.0 }}})"
  - addField:
      field: "private java.util.logging.Logger logger = Logger.getLogger({{{ name }}}.class.getName())"
      target: "parentClass"

How to know what element to search for?

As stated at the beginning of the example, we started searching for a methodcall. It would also be possible to start searching for a class that contains a method call to System.out.println. So why didn't we choose the latter?

Sensei will mark the element that has been searched for. This means that if you want to highlight the method call, you have to search for it. Next, it is also important to note that the context in the quickfix actions are based on the element that has been searched for. If we would have searched for a class, there is no way to extract that first argument as we did in the example. In the beginning, it can be a bit overwhelming to consider what variables we will need in the actions. So start by thinking of which element you want to be highlighted, and that should give you a good start.

Continue with: Writing a recipe, part 2