Query Examples
With our query language we can build either simple or more complex, compound quries. In the followings there are some useful and often used queries with different complexity.
Returns a list of expressions which call the pointed function:
@fun.refs
Returns all function calls from current module group by the module's own functions:
@file.funs.calls
Returns all functions which have 3 arguments:
@file.funs[arity==3]
Returns all functions which have a variable named "Expl" (it is useful when we want to know which functions use variables with same name):
@file.funs.vars[name=="Expl"]
Returns all io:format calls (this query is very useful when you have finished your software, and you want to find all debug messages):
mods[name=="io"].funs[name==format].refs
If the current expression is a variable, with running this query, we can find, where the variable gets its value from. This functionality uses data-flow analysis?.
@expr.origin
Returns information about the function gets its return value from where and how its calculated:
@fun.refs.origin
Checking coding convensions
In RefactorErl, metrics can be applied to modules or to functions. Modules are equivalent to file entities in the semantic query language, and functions are equivalent to function entities. We can say that a metric is a kind of property belongs to a file or function entity, so we can simply add the proper metrics to the properties of entities.
Usually we have some coding conventions applied to our modules or functions. With our extended semantic query language we can check these conventions, and filter improper modules or functions. Hereinafter we present some design rules and some metrics to check these rules.
Rule1. A module should not contain more then 400 lines.
When we would like to filter modules containing more than 400 effective lines of code, we have to load our modules to RefactorErl system, and enter the following query:
mods[line_of_code > 400]
In the result we will find our too long modules.
Rule2. A function should not contain more then 15 to 20 lines.
When we would like to check, which functions do not fulfil this convention in our modules loaded into the RefactorErl database, we use the following query:
mods.funs[line_of_code > 20]
Rule3. Use at most two level of nesting, do not write deeply nested code. It is achieved by dividing the code into shorter functions.
With one of our metrics we can count the nesting level of case expressions, so we can filter functions with more than two maximum depth of cases. In this example, we would like to get the result just from our actual module.
@file.funs[max_depth_of_cases > 2]
If we just would like to know, whether all of the functions fulfil this convention or not, we can simply query the maximum nesting level of cases in the whole module. If this value is more than two, there is at least one function containing deeply nested cases.
@file.max_depth_of_cases
At least, let's filter modules containing functions with too deeply nested cases.
mods[max_depth_of_cases > 2]
Rule4. Use no more than 80 characters on a line.
We can filter all of the functions, which contains lines with more than 80 characters with the following query:
mods.funs[max_length_of_line > 80]
Rule5. Use space after commas.
We have a metric which returns with the number of cases when we do not fulfil this convention. When a modul or a function breaks this rule, the result of the metric will be more, then 0.
Filter functions containing at least one case when whitespace misses after a comma:
mods.funs[no_space_after_comma > 0]
Rule6. Every recursive function should tail recursive.
Tail recursion means that we have no recursive call (either direct or indirect) in our function, just in the last expression. Filter functions that recursive, but not tail recursive:
mods.funs[is_tail_recursive == non_tail_rec]
Practical example for using "-type", "-spec" related selectors and properties:
Let's say we are new to a project with a large code-base and our task is to implement a new interface in which we have to do some operations on a very complex data structure. The source of this complex structure is remote, but we know there are multiple compatible data structures already implemented, although not sure about the specifics, so we would like to investigate and do this without manually going through several thousand lines of code.
We might start by running the following query:
"files.typerefs"
This gives us all used types in every file. After scrolling to the end of the hundred and something pages of results, we at least know types properly specified, but not much else. Maybe we should only look at types defined in "general" modules:
"mods[name ~ \"gen\"].types"
Unfortunately these seem to be either irrelevant or too general for us to use. We decide that we should look at types from other modules by filtering results using the "-spec" attributes (and let's assume, we are looking for a structure that has an "append" function implemented). But first we would like to know if "-spec" is used in our modules.
"mods.funs[not (.name in files.specs.name)][loc > 10]"
The query results are promising, it looks like most of the non-trivial functions have a specification. We can take a look at returned types of functions named like "append":
mods.funs[name ~ \"append\"].returntype
This query also gives us too many possible results, we are only interested in exported ones:
mods.funs[name ~ \"append\"].returntype[exported]
Turns out most of them were exported. However when we have looked at types from "gen" modules, we may have noticed a type("person") for a structure which is a part of what we have to append to the large structure.
mods.funs[name ~ \"append\"][.params.type.(subtype.(params)+)+[name = 'person']].returntype[exported]
This query only yields return-types of ~"append" functions that have a parameter whose type uses the "person" type. If we would like to see in which files these types have been used we could run the following:
mods.funs[name ~ \"append\"][.params.type.(subtype.(params)+)+[name = 'person']].returntype[exported].references.file