Static Type Checking

Static type checking (STC) provides information about whether your script is correctly written.

Groovy is a dynamic language, which means that method and property names are looked up when your code is run, not when it’s compiled (like Java).

Let’s look at the following simple, but complete, script:

foo.bar()

We call the method bar() on the object foo. This script compiles without errors, but you get a MissingPropertyException when you run the script because foo hasn’t been defined. This behaviour is useful because there are circumstances that could make this code execute successfully, like an object called foo or a closure getFoo() being passed to the script’s binding.

Although Groovy is a dynamic language, we can compile scripts in a manner that checks method and property references at compilation. The STC feature shows you problems in your scripts when you are writing them, as opposed to when they execute.

When your scripts are executed, they are always compiled dynamically. When they are compiled for the STC, the resulting generated bytecode is thrown away.

Limitations

There are limitations to the type checker. It is possible to write code that shows errors, but it is valid and executes fine. Some of these situations are:

  • Using certain builders

  • Using closures where the parameter types can’t be inferred

However, if you write code like this, you probably use an IDE, which does not work with the STC.

Additionally, your code could have runtime errors that won’t be found until the code executes.

Let us know if you find code that should compile but doesn’t.

Examples

The code for a condition to check that the remaining estimate on the issue was zero might look like the following:

type checked condition

The STC shows that we are trying to set the estimate, rather than retrieve it. We should have used the equality operator:

type checked condition ok

Note the green dot, which tells us that the code is syntactically correct after checking the methods and properties.

Let’s continue this example with code for Additional Issues Actions. This is commonly used to manipulate the resulting issue when creating a subtask or cloing and linking an issue.

issue.estimate = 0L

When code is not valid as a condition, it can be used as an additional issue action because the type checker is aware of what type the 'issue' object is when the code executes.

Deprecations

The STC also shows methods and properties that are deprecated. These are parts of the API that Atlassian would prefer you not to use.

In the following image, there are four deprecated methods:

type checked condition depr

It is advisable to change your code to the suggested alternative because Atlassian usually removes deprecated code on major version releases. If you switch to the non-deprecated code, there is a better chance of your script continuing to work on your next upgrade.

A fixed version of the above script might look like:

type checked condition depr fixed

Tips

Fields

To write classes, give information about the field types to the STC.

If you do not, you’ll receive the following STC errors:

type checked condition class depr

Instead, declare your field types like this instead:

import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.applinks.api.ApplicationLinkService

class Foo {
    ApplicationLinkService applicationLinkService = ComponentLocator.getComponent(ApplicationLinkService)

    void doSomething() {
        applicationLinkService.getApplicationLinks()
    }
}

Closures

When writing closures, you may need to provide additional type information.

For example:

import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.applinks.api.ApplicationLinkService

def applicationLinkService = ComponentLocator.getComponent(ApplicationLinkService)

applicationLinkService.getApplicationLinks().findAll {
    it.name == "Confluence"
}

It is inferred from the context that the type of it is an ApplicationLink object.

While this code will execute without issues, the STC produces an error: No such property: name for class: java.lang.Object.

Using the following code, you can fix this error by explicitly informing the STC that the type of it is an ApplicatonLink object.

import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.applinks.api.ApplicationLinkService
import com.atlassian.applinks.api.ApplicationLink

def applicationLinkService = ComponentLocator.getComponent(ApplicationLinkService)

applicationLinkService.getApplicationLinks().findAll { ApplicationLink it ->
    it.name == "Confluence"
}
Either version of the code is fine. If you want to ignore the STC, you can. The version of the bytecode that is actually executed is always compiled in "dynamic" mode.

If you wanted to take the closure in the previous example and reuse it, you should write code like this:

import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.applinks.api.ApplicationLinkService
import com.atlassian.applinks.api.ApplicationLink

def applicationLinkService = ComponentLocator.getComponent(ApplicationLinkService)

def findApplicationByName = { ApplicationLink it -> it.name == "Confluence" }

applicationLinkService.getApplicationLinks().findAll(findApplicationByName)

Type Information

Certain parts of Atlassian’s API are not strongly typed.

For example, the methods for getting and setting custom field values are defined to receive and return a java.lang.Object. The type checker only has access to this information, and it is not aware of the types of your custom fields. ??? NOTE: A special case is when using cfValues['My Field'], where a special effort is made to attempt to introspect the type of field named My Field.

The following script, which sets a text field to the display name of a user read from a user custom field, is flagged as having errors: ???

type checked customfields
Remember, despite the STC error, the script will still run successfully.

The only information that the Jira API gives the STC is that user has the type of java.lang.Object. Fix this by casting the value from the custom field to an ApplicationUser: ???

type checked customfields fixed

Script Roots

Previously when entering a file to be run (console, workflow function, script field etc) you were required to give the full script path, or the path relative to the catalina.base directory.

This version introduces the concept of script roots - these are directories in whose files and subdirectories you can keep your scripts. The advantages of this is that changes to dependent classes will be detected automatically, and get automatically recompiled. Note - the one exception is when you first change a dependent class without having changed the class/script that’s actually called (a JQL function, workflow function etc). In this case, make some irrelevant change like adding a space to a comment to the calling script. After having done this, changes to the base or dependent class will trigger recompilation of both.

When the plugin is first installed it will create a directory called "scripts" under your JIRA home directory, and register it as one of its script roots. This should be sufficient for most users and no other configuration need be made. This is a logical place to store your scripts, as it’s preserved during JIRA upgrades, and by definition will be accessible by all nodes in a clustered JIRA.

You can create subdirectories for your scripts, perhaps dividing them up into the business processes they support. You can also create supporting or utility classes to be used by them, but ensure they have the correct package, otherwise you will get a compilation error.

For instance, a script and a class:

<jira-home>/scripts/foo.groovy
import util.Bollo

log.debug ("Hello from the script")
Bollo.sayHello()
<jira-home>/scripts/util/Bollo.groovy
package util

public class Bollo {
    public static String sayHello() {
        "hello sailor!!!"
    }
}

Absolute paths outside of script roots will continue to work, although changes to dependent classes may not get picked up.

Upgrading from Previous Versions

In previous versions of ScriptRunner, relative paths were resolved to the container working directory, ie $catalina.base on Tomcat.

This is no longer the case…​ relative paths will be resolved relative to each of the script roots until a file is found. If you don’t wish to change all your paths, you can add a new script root, pointing to either the working directory, or better, to where you had your scripts.

For instance, let’s say your jira instance is in /usr/opt/jira, and you had your scripts in /usr/opt/scripts. Therefore you would have referred to them as ../scripts/foo.groovy.

Now you will add a new property pointing to your scripts dir:

set JAVA_OPTS=%JAVA_OPTS% -Dplugin.script.roots=/usr/opt/scripts

Resolving ../scripts/foo.groovy relative to this script path will have the same result.

If you have multiple roots then use a comma to delimit them.

If you are working on a script locally before deploying to production, you can set breakpoints in scripts or classes and attach the debugger.

If you are working on the plugin it makes sense to add the src and test directories from the checkout, so you can work on the scripts without having to recompile.

set JAVA_OPTS=%JAVA_OPTS% -Dplugin.script.roots=checkout-directory\src\main\resources,checkout-directory\src\test\resources

ScriptRunner has the ability to search for scripts contained within your configured script roots. Wherever you were able to enter the path of a script to run, you can now search for the script directly in the script file input. You can search by script name, or traverse the sub-directory structure to find the script you’re looking for!

To help you easily find the scripts you’re looking for, here are a few tips:

Find a Script by Name

If you know the name of the script root you can simply start typing the name of the script you want in the normal file entry:

  1. Start typing the name of the script you want to find

  2. After typing the first character, the search will return results that you can select from

    FindScriptFirstChar
    If there are too many results, simply keep typing characters to refine the search
  3. When your script is found, either:

    • Highlight the script you want by moving down to it with the keyboard (down key) then press "Enter"

      OR

    • Use the mouse to left-click the script you wish to select

      FindScriptSelection
  4. The script will now be loaded for use and the static-type checking will begin

Browse the Script Roots to Find a Script

If you don’t know the name of the file, but suspect that exists somewhere in one of your script roots, you can browse the directory structure under those roots until you find it:

  1. Start typing the name of the sub directory you want to browse in your script root

  2. After typing the first character, the search will return results that you can select from

    If there are lots of directories returned, you can simply keep typing to refine the search. As you add characters to your search phrase, the results will be refined.
  3. Once you find the directory you wish to check inside:

    • Left-click the desired directory

      OR

    • Press the "Down" key on the keyboard to highlight the entry, then press the "Tab" key

  4. Now you will see a list of scripts and subdirectories in that directory!

  5. Repeat that process until you find the script you are looking for

When you navigate into a directory, the results will only show the scripts in that directory. If you want to search ALL scripts, just remove all characters from the input and start again.
You can use tab completion to navigate the directory structure. If you go too far, simply remove the characters to the previous slash and then the search should come back with the correct results.

Result Types

This table demonstrates the different types of result that the search could return.

Table 1. Result Types
Icon Description
IconScript

An executable groovy script

IconDirectory

A sub-directory within one of your script roots

Logging and Profiling

Each execution of any of your scripts are recorded. We record:

  • any parameters passed to it (the script binding), which is known as the payload

  • the log output including any exception message, if present

  • timing information, which is the total elapsed time, and the CPU time used

The last 15 executions are displayed where relevant, eg when viewing workflows, script fields, script listeners, and REST endpoints administration.

A summary of recent history is displayed, eg

diags failure message

Clicking through on any of these will give you further information about that particular invocation.

diags failure display
diags failure dialog

This is most useful for viewing why your own scripts failed, particularly if it’s an intermittent failure, which may only happen because of certain issue attributes - for instance a field value being unexpectedly null.

Only uncaught exceptions are shown as failures

On JIRA shutdown the last 15 invocations of each function are written to the database so that they persist a restart.

Known Issues

  • Uncaught exceptions in conditions or additional code are not displayed as errors, but the error will appear in the logs

  • When using JIRA Data Center, only invocations that executed on that node are shown. If using Data Center, and you suspect issues on just one node, you will need to open the corresponding URL on that DC instance

  • Certain categories of scripts are currently excluded from log captures, namely JQL functions and administration scripts (built-in scripts)

  • On JIRA 6.x the persistence functionality may not always work

Have questions? Visit the Atlassian Community to connect, share, and learn with other Atlassian users and experts, including Adaptavist staff.

Ask a question about ScriptRunner for JIRA, Bitbucket Server, or Confluence.

Want to learn more? Check out courses on Adaptavist Learn, an online platform to onboard and train new users for Atlassian solutions.