Writing a recipe, part 1¶
We believe in the method of learning by example, so let's experiment with Sensei by going through a quick example as 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:
Using the menu item Tools | Sensei | Cookbook Manager and clicking Manage recipes after selecting a Cookbook entry.
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 new recipes.
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 ⌘↵)
and choose . Next, you will be presented with two options:
and .
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.
Click on + and choose a 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 four tabs: Metadata
, Search
, Fix
and Documentation
.
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 Fix
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 quick fix 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.
Write the expected result for this current example. Afterwards, we will make it more generic.
private Logger logger = Logger.getLogger(Example.class.getName())
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())
Lastly, don't assume that the class
Logger
has already been imported. Tell Sensei to automatically import this class by fully qualifying theLogger
tojava.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
quick fix 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