Query Syntax

SOUL queries are highly declarative. They describe the characteristics of the code a developer is looking for (e.g., two instructions x.close() and y.read() that are executed consecutively and operate on the same file), rather than an operational search for these characteristics. In SOUL, source code characteristics can be expressed through a combination of the following:

Logic conditions over the program's code:

The following SOUL query quantifies over all abstract methods ?method named ?name in an Eclipse project.

if ?method methodDeclarationHasName: ?name,
   ?method isAbstractMethod 

Soul queries start with the keyword 'if'. Logic variables are preceded by a question mark. The syntax for a predicate in SOUL closely resembles the one of Smalltalk for a message sent to the first argument of the predicate. The first condition quantifies over all method declarations and their names. The second condition only succeeds for methods that are declared abstract. The solutions to this query therefore consist of abstract methods and their names. 

Although SOUL adorns predicates with a Smalltalk-like notation, compound terms retain their traditional notation (i.e., a functor followed by its arguments) and can be used as arguments to predicates. This is illustrated by the following query:

if ?return isStatement,
   ?return returnStatementHasExpression: ?exp,
   ?exp equals: castExpression(?type, ?casted)

The query identifies return statements ?return that return a cast expression ?exp which is casting ?casted to ?type. Unifying cast expression ?exp with the compound term castExpression(?type, ?casted) on line 3 reveals the ?type it is casting ?casted to.  

The predicates used in the above queries (e.g., returnStatementHasExpression:/2 and isMethodDeclaration/1) stem from the CAVA library for SOUL. This library defines several predicates for querying an Eclipse project. Additional libraries are available for querying Smalltalk, C and Cobol programs.

Template conditions over the program's code:

In addition to logic conditions, SOUL queries can contain template conditions:

if jtMethodDeclaration(?method){
     abstract ?type ?name(?paramList); 
   } 

Template conditions consist of a functor (e.g., jtMethodDeclaration), a single argument (e.g., ?method) and a code snippet containing logic variables delimited by braces. The functor of the template identifies the grammar rule adhered to by the snippet. The argument of the template will be bound to code from the program that matches the snippet. In other words, ?method will be bound to an abstract method named ?name with return type ?type and formal parameter list ?paramList. 

The query involving return statements and cast expressions can also be revisited:

if jtStatement(?return){ return (?type) ?casted; }

The following query illustrates a larger template condition. Its code snippet corresponds to the prototypical implementation of a getter method in Java:

if jtClassDeclaration(?class){
     class ?className {
       private ?fieldDeclarationType ?fieldName;
       ?modList ?returnType ?methodName(?paramList) {
         return ?fieldName;
       }
     }
   }

Although the template condition only exemplifies the prototypical implementation of a getter method, solutions to the above query will even include the following getter method for the field Person.birthday: 

class Person {
  private Object birthday;
  public Date getBirthday() {
    Logger.log("Birthday retrieved");
    return (Date) this.birthday();
  }
}

This is because SOUL does not match code snippets in a syntactic manner with the program's code. Instead, it lends code snippets an example-based interpretation. A match has to exhibit all characteristics exemplified by the code snippet, but is allowed to exhibit additional ones. The matching process recognizes implementation variants of the exemplified characteristics. To this end, it consults the results of whole-program analyses. As a result, developers no longer need to enumerate all implementation variants of the code they are looking for.

Imperative expressions involving the program's code:

Thanks to linguistic symbiosis, SOUL variables can be bound to logic as well as Smalltalk or Java values. Moreover, SOUL supports embedding Smalltalk and Java expressions in its queries. Such imperative expressions have to be delimited by square brackets. Any SOUL variables within an imperative expression will be substituted by their value before the imperative expression is evaluated. This is illustrated by the following query:

?m isMethodDeclaration,
[?m parentTypeDeclaration] equals: ?typeDeclaration,
[?typeDeclaration isKindOf: JavaWorld.org.eclipse.jdt.core.dom.TypeDeclaration],
[?typeDeclaration bodyDeclarations lastIndexOf_Object: ?m] equals: ?index
if ?m isMethodDeclaration,
   [?m getParent] equals: ?t,
   [?t bodyDeclarations lastIndexOf_Object: ?m] equals: ?index

On line 2, the imperative expression on the left-hand side of the equals:/2 predicate sends message 'getParent' to ?m. The result of this message send, the type declaration in which the method is declared, is bound to variable ?t. 

Sending a message to ?m is possible because ?m is bound to an actual MethodDeclaration object rather than a logic fact. SOUL foregoes a transcription to logic facts of the queried program. Instead, SOUL queries the actual AST nodes used by your IDE (i.e. subclasses of org.eclipse.jdt.core.dom.ASTNode for Java and Refactory.Browser.RBProgramNode for Smalltalk). Among others, this facilitates integration with other software engineering tools.

Line 3 subsequently binds ?index to the index at which ?m can be found in the list of body declarations of ?t. Note that 'bodyDeclarations' and 'lastIndexOf_Object:' are Java methods (in a Smalltalk-like notation) declared in org.eclipse.jdt.core.dom.TypeDeclaration and java.util.AbstractList respectively.