The analyzer rules are a set of instructions that are used to analyze source code and detect issues. Rules are fundamental pieces that codify modernization knowledge.
The analyzer parses user provided rules, evaluates them against input source code and generates Violations for matched rules. A collection of one or more rules form a Ruleset. Rulesets provide an opionated way of organizing multiple rules that achieve a common goal.
A Rule is written in YAML. It consists of metadata, conditions and actions. It instructs analyzer to take specified actions when given conditions match.
Rule metadata contains general information about a rule:
ruleID: "unique_id" (1)
labels: (2)
- "label1=val1"
effort: 1 (3)
category: mandatory (4)
- ruleID: This is a unique ID for the rule. It must be unique within the ruleset.
- labels: A list of string labels associated with the rule. (See Labels)
- effort: Effort is an integer value that indicates the level of effort needed to fix this issue.
- category: Category describes severity of the issue for migration. Values can be one of mandatory, potential or optional. (See Categories)
- mandatory
- The issue must be resolved for a successful migration. If the changes are not made, the resulting application will not build or run successfully. Examples include replacement of proprietary APIs that are not supported in the target platform.
- optional
- If the issue is not resolved, the application should work, but the results may not be optimal. If the change is not made at the time of migration, it is recommended to put it on the schedule soon after your migration is completed.
- potential
- The issue should be examined during the migration process, but there is not enough detailed information to determine if the task is mandatory for the migration to succeed.
A rule has two actions - tag
and message
. Either one or two of these actions can be defined on a rule.
A tag action is used to create one or more tags for an application when the rule matches. It takes a list of string tags as its fields:
tag:
# tags can be comma separated
- "tag1,tag2,tag3"
# optionally, tags can be assigned categories
- "Category=tag4,tag5"
When a tag is a key=val pair, the keys are treated as category of that tag. For instance, Backend=Java
is a valid tag with Backend
being the category of tag Java
.
Any rule that has a tag action in it is referred to as a "tagging rule".
A message action is used to create an issue with the specified message when a rule matches:
# when a match is found, analyzer generates incidents each having this message
message: "helpful message about the violation"
Message can also be templated to include information about the match interpolated via custom variables on the rule (See Custom Variables):
- ruleID: lang-ref-004
customVariables:
- pattern: '([A-z]+)\.get\(\)'
name: VariableName
message: "Found generic call - {{ VariableName }}"
when:
<CONDITION>
Hyperlinks can be provided along with a message
or tag
action to provide relevant information about the found issue:
# links point to external hyperlinks
# rule authors are expected to provide
# relevant hyperlinks for quick fixes, docs etc
links:
- url: "konveyor.io"
title: "short title for the link"
Every rule has a when
block that contains exactly one condition. A condition defines a search query to be evaluated against the input source code.
when:
<condition>
There are three types of conditions - and, or and provider. While the provider condition is responsible for performing an actual search in the source code, the and and or conditions are logical constructs provided by the engine to form a complex condition from the results of multiple other conditions.
The analyzer engine enables multi-language source code analysis via something called as “providers”. A "provider" knows how to analyze the source code of a technology. It publishes what it can do with the source code in terms of "capabilities".
A provider condition instructs the analyzer to invoke a specific "provider" and use one of its "capabilities". In general, it is of the form <provider_name>.<capability>
:
For instance, the java
provider provides referenced
capability. To search through Java source code, we can write a java.referenced
condition:
when:
java.referenced:
pattern: org.kubernetes.*
location: IMPORT
Note that depending on the provider, the fields of the condition (for instance, pattern and location above) will change.
Some providers have dependency capability. It means that the provider can generate a list of dependencies for a given application. A dependency condition can be used to query this list and check whether a certain dependency (with a version range) exists for the application. For instance, to check if a Java application has a certain dependency, we can write a java.dependency
condition:
when:
java.dependency:
name: junit.junit
upperbound: 4.12.2
lowerbound: 4.4.0
Analyzer currently supports builtin
, java
, go
and generic
providers. Here is the table that summarizes all the providers and their capabilities:
Provider Name | Capabilities | Description |
---|---|---|
java | referenced | Find references of a pattern with an optional code location for detailed searches |
dependency | Check whether app has a given dependency | |
builtin | xml | Search XML files using xpath queries |
json | Search JSON files using jsonpath queries | |
filecontent | Search content in regular files using regex patterns | |
file | Find files with names matching a given pattern | |
hasTags | Check whether a tag is created for the app via a tagging rule | |
go | referenced | Find references of a pattern |
dependency | Check whether app has a given dependency |
Based on the table above, we should be able to create the first part of the condition that doesn’t contain any of the condition fields. For instance, to create a java
provider condition that uses referenced
capability:
when:
java.referenced:
<fields>
Depending on the provider and the capability, there will be different <fields>
in the condition. Following table summarizes available providers, their capabilities and all of their fields:
Provider | Capability | Fields | Required | Description |
---|---|---|---|---|
java | referenced | pattern | Yes | Regex pattern |
location | No | Source code location (see Java Locations) | ||
annotated | No | Additional query to inspect annotations (see Annotation inspection) | ||
dependency | name | Yes | Name of the dependency | |
nameregex | No | Regex pattern to match the name | ||
upperbound | No | Match versions lower than or equal to | ||
lowerbound | No | Match versions greater than or equal to | ||
builtin | xml | xpath | Yes | Xpath query |
namespaces | No | A map to scope down query to namespaces | ||
filepaths | No | Optional list of files to scope down search | ||
json | xpath | Yes | Xpath query | |
filepaths | No | Optional list of files to scope down search | ||
filecontent | pattern | Yes | Regex pattern to match in content | |
filePattern | No | Only search in files with names matching this pattern | ||
file | pattern | Yes | Find files with names matching this pattern | |
hasTags | This is an inline list of string tags. See Tag Action | |||
go | referenced | pattern | Yes | Regex pattern |
dependency | name | Yes | Name of the dependency | |
nameregex | No | Regex pattern to match the name | ||
upperbound | No | Match versions lower than or equal to | ||
lowerbound | No | Match versions greater than or equal to |
With the information above, we should be able to complete java
condition we created earlier. We will search for references of a package:
when:
java.referenced:
location: PACKAGE
pattern: org.jboss.*
It is possible to add a query to match against specific annotations and their elements. For instance:
when:
java.referenced:
location: METHOD
pattern: org.package.MyApplication.runApplication(java.lang.String)
annotated:
pattern: org.framework.Bean
elements:
- name: url
value: "http://www.example.com"
would match against the runApplication
method in the following java code:
package org.package
import org.framework.Bean;
class MyApplication {
@Bean(url = "http://www.example.com")
public String runApplication(String str) {
// ...
}
}
The structure of the annotated
YAML element is the following:
annotated:
pattern: a Java regex to match the fully qualified name of the annotation (optional)
elements: an array of elements to match within the annotation (optional)
- name: the exact name of the element to match against
value: a Java regex to match the value of the element
It is also possible to match an annotation with specific elements, without having to specify the symbol it annotates.
The following example would also match on the @Bean
annotation in the same code as the last example. Note that
the only element specified with a pattern
is the annotation itself:
when:
java.referenced:
location: ANNOTATION
pattern: org.framework.Bean
annotated:
elements:
- name: url
value: "http://www.example.com"
The java provider allows scoping the search down to certain source code locations. Any one of the following search locations can be used to scope down java searches:
- CONSTRUCTOR_CALL
- TYPE
- INHERITANCE
- METHOD_CALL
- ANNOTATION
- IMPLEMENTS_TYPE
- ENUM_CONSTANT
- RETURN_TYPE
- IMPORT
- VARIABLE_DECLARATION
- FIELD (declaration)
- METHOD (declaration)
Provider conditions can have associated "custom variables". Custom variables are used to capture relevant information from the matched line in the source code. The values of these variables will be interpolated with data matched in the source code. These values can be used to generate detailed templated messages in a rule’s action (See Message action). They can be added to a rule in the customVariables
field:
- ruleID: lang-ref-004
customVariables:
- pattern: '([A-z]+)\.get\(\)' (1)
name: VariableName (2)
message: "Found generic call - {{ VariableName }}" (3)
when:
java.referenced:
location: METHOD_CALL
pattern: com.example.apps.GenericClass.get
- pattern: This is a regex pattern that will be matched on the source code line when a match is found.
- name: This is the name of the variable that can be used in templates.
- message: This is how to template a message using a custom variable.
The And
condition takes an array of conditions and performs a logical
"and" operation on their results:
when:
and:
- <condition1>
- <condition2>
Example:
when:
and:
- java.dependency:
name: junit.junit
upperbound: 4.12.2
lowerbound: 4.4.0
- java.dependency:
name: io.fabric8.kubernetes-client
lowerbound: 5.0.100
Note that the conditions can also be nested within other conditions:
when:
and:
- and:
- go.referenced: "*CustomResourceDefinition*"
- java.referenced:
pattern: "*CustomResourceDefinition*"
- go.referenced: "*CustomResourceDefinition*"
The Or
condition takes an array of other conditions and performs a logical "or" operation on their results:
when:
or:
- <condition1>
- <condition2>
You can also chain the variables from one condition to be used as input in another condition in a or and and block of conditions
What a given condition has in its output, can be seen in the openapi spec, by finding the <provider>.<condition>.out
component. NOTE Every condition, has a list of files where the incidents occurred, and the output for a condition is in the extras section.
Example:
when:
or:
- builtin.xml:
xpath: "//dependencies/dependency"
filepaths: "{{poms.extras.filepaths}}"
from: poms
- builtin.file:
pattern: pom.xml
as: poms
ignore: true
In the above example the output of builtin.file
condition is saved as
poms.
as: poms
The variables of builtin.file
can then be used in the builtin.xml
condition, by saying from
and then using mustache templates in the provider condition` block.
This is how this particular condition, knows to use the variables set to the name poms
.
from: poms
Then you can use the variables by setting them as mustached templates in any of the inputs to the provider condition.
filepaths: "{{poms.filepaths}}"
Note: If you only want to use the values of a condition as a chain, you can set ignore: true
, this will tell the engine to not use this condition to determine if the rule has violated or not. example above:
ignore: true
A set of Rules form a Ruleset. Rulesets are an opinionated way of passing Rules to Rules Engine.
A ruleset is created by placing one or more YAML rules files in a directory and creating a ruleset.yaml
(golden file) file in it.
The golden file stores metadata of the Ruleset.
name: my-ruleset (1)
description: Text description about ruleset (2)
labels: (3)
- key=val
- name: A unique name for the ruleset.
- description: Text description about the ruleset.
- labels: A list of string labels for the ruleset. The labels on a ruleset are automatically inherted by all rules in the ruleset. (See Labels)
The analyzer CLI provides --rules
option to specify a YAML file containing rules or a ruleset directory:
-
It can be a file:
konveyor-analyzer --rules rules-file.yaml ...
It is assumed that the file contains a list of YAML rules. The engine will automatically associate all rules in it with a default Ruleset.
-
It can be a directory:
konveyor-analyzer --rules /ruleset/directory/ ...
It is assumed that the directory contains a Ruleset. (See Ruleset)
-
It can be given more than once with a mix of rules files and rulesets:
konveyor-analyzer --rules /ruleset/directory/ --rules rules-file.yaml ...