Skip to content

Commit

Permalink
feature: introduce the concept of "Pattern" (#1686)
Browse files Browse the repository at this point in the history
  • Loading branch information
pvojtechovsky authored and monperrus committed Jun 11, 2018
1 parent f1166c2 commit ee1c2ae
Show file tree
Hide file tree
Showing 90 changed files with 10,241 additions and 1,650 deletions.
26 changes: 24 additions & 2 deletions doc/_data/sidebar_doc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,20 @@ entries:
product: all
version: all

- title: Matchers
- title: Matcher
url: /matcher.html
audience: writers, designers
platform: all
product: all
version: all

- title: Pattern
url: /pattern.html
audience: writers, designers
platform: all
product: all
version: all

- title: Transforming source code
audience: writers, designers
platform: all
Expand All @@ -187,13 +194,28 @@ entries:
product: all
version: all

- title: Templates
- title: Generating source code
audience: writers, designers
platform: all
product: all
version: all

items:
- title: Template
url: /template_definition.html
audience: writers, designers
platform: all
product: all
version: all

items:
- title: Generator
url: '/pattern.html#generator'
audience: writers, designers
platform: all
product: all
version: all

- title: Testing
audience: writers, designers
platform: all
Expand Down
188 changes: 188 additions & 0 deletions doc/pattern.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
---
title: Spoon Patterns
---

Spoon patterns enables you to find code elements. A Spoon pattern is based on a one or several AST nodes, which represent the code to match, where some parts of the AST are pattern parameters. When a pattern is matched, one can access to the code matched in each pattern parameter.

The main classes of Spoon patterns are those in package `spoon.pattern`:

* classes: PatternBuilder, Pattern, Match, PatternBuilderHelper, PatternParameterConfigurator, InlinedStatementConfigurator
* eums: ConflictResolutionMode, Quantifier

Example usage:

```java
Factory spoonFactory = ...
//build a Spoon pattern
Pattern pattern = ... build a spoon pattern. For example for an method ...

//search for all occurences of the method in the root package
pattern.forEachMatch(spoonFactory.getRootPackage(), (Match match) -> {
Map<String, Object> parameters = match.getParametersAsMap();
CtMethod<?> matchingMethod = match.getMatchingElement(CtMethod.class);
String aNameOfMatchedMethod = parameters.get("methodName");
...
});
```

## PatternBuilder

To create a Spoon pattern, one must use `PatternBuilder`, which takes AST nodes as input, and **pattern parameters** are defined.


```java
// creates pattern from the body of method "matcher1"
Pattern t = PatternBuilder.create(
new PatternBuilderHelper(fooClass).setBodyOfMethod("matcher1").getPatternElements())
.configurePatternParameters()
.build();
```


This pattern matches all statements of the body of method `statement`, ie. a precondition to check that a list is smaller than a certain size.
This pattern has one single pattern parameter called `_col_`, which is automatically considered as a pattern parameter because it is declared outside of the AST node. This automatic configuration happens when `configurePatternParameters` is called.

## Pattern

The main methods of `Pattern` are `getMatches` and `forEachMatch`.

```
List<Match> matches = pattern.getMatches(ctClass);
```

## Match

A `Match` represent a match of a pattern on a code elements.

The main methods are `getMatchingElement` and `getMatchingElements`.

## PatternBuilderHelper

`PatternBuilderHelper` is used to select AST nodes that would act as pattern. It is mainly used to get the body (method `setBodyOfMethod`) or the return expression of a method (method `setReturnExpressionOfMethod`) .

## PatternParameterConfigurator

To create pattern paramters, one uses a `PatternParameterConfigurator` as a lambda:


```java
//a pattern model
void method(String _x_) {
zeroOneOrMoreStatements();
System.out.println(_x_);
}

//a pattern definition
Pattern t = PatternBuilder.create(...select pattern model...)
.configureParameters(pb ->
// creating a pattern parameter called "firstParamName"
pb.parameter("firstParamName")
//...select which AST nodes are parameters...
//e.g. using parameter selector
.bySimpleName("zeroOneOrMoreStatements")
//...modify behavior of parameters...
//e.g. using parameter modifier
.setMinOccurence(0);

//... you can define as many parameters as you need...

// another parameter (all usages of variable "_x_"
pb.parameter("lastParamName").byVariable("_x_");
)
.build();
```

`ParametersBuilder` has many methods to create the perfect pattern parameters, incl:

* `byType(Class|CtTypeReference|String)` - all the references to the type defined by Class,
CtTypeReference or qualified name are considered as pattern parameter
* `byLocalType(CtType<?> searchScope, String localTypeSimpleName)` - all the types defined in `searchScope`
and having simpleName equal to `localTypeSimpleName` are considered as pattern parameter
* `byVariable(CtVariable|String)` - all read/write variable references to CtVariable
or any variable named with the provided simple name are considered as pattern parameter
* `byInvocation(CtMethod<?> method)` - all invocations of `method` are considered as pattern parameter
* `byVariable(CtVariable|String... variableName)` - each `variableName` is a name of a variable
which references instance of a class with fields. Each such field is considered as pattern parameter.
* `byFilter(Filter)` - any pattern model element, where `Filter.accept(element)` returns true is a pattern parameter.
* `byRole(CtRole role, Filter filter)` - the attribute defined by `role` of all
pattern model elements, where `Filter.accept(element)` returns true is a pattern parameter. It can be used to define a varible on any CtElement attribute. E.g. method modifiers or throwables, ...
* `byString(String name)` - all pattern model string attributes whose value is equal to `name` are considered as pattern parameter.This can be used to define full name of the methods and fields, etc.
* `bySubstring(String stringMarker)` - all pattern model string attributes whose value contains
whole string or a substring equal to `stringMarker`are pattern parameter. Note: only the `stringMarker` substring of the string value is substituted, other parts of string/element name are kept unchanged.
* `byNamedElement(String name)` - any CtNamedElement identified by it's simple name is a pattern parameter.
* `byReferenceName(String name)` - any CtReference identified by it's simple name is a pattern parameter.


Any parameter of a pattern can be configured like this:

* `setMinOccurence(int)` - defines minimal number of occurences of the value of this parameter during **matching**,
which is needed by matcher to accept that value.
* `setMinOccurence(0)` - defines optional parameter
* `setMinOccurence(1)` - defines mandatory parameter
* `setMinOccurence(n)` - defines parameter, whose value must be repeated at least n-times
* `setMaxOccurence(int)` - defines maximal number of occurences of the value of this parameter during **matching**,
which is accepted by matcher to accept that value.
* `setMatchingStrategy(Quantifier)` - defines how to matching engine arehave when two pattern nodes may accept the same value.
* `Quantifier#GREEDY` - Greedy quantifiers are considered "greedy" because they force the matcher to read in, or eat, the entire input prior to attempting the next match.
If the next match attempt (the entire input) fails, the matcher backs off the input by one and tries again,
repeating the process until a match is found or there are no more elements left to back off from.
* `Quantifier#RELUCTANT` - The reluctant quantifier takes the opposite approach: It start at the beginning of the input,
then reluctantly eat one character at a time looking for a match.
The last thing it tries is the entire input.
* `Quantifier#POSSESSIVE` - The possessive quantifier always eats the entire input string,
trying once (and only once) for a match. Unlike the greedy quantifiers, possessive quantifiers never back off,
even if doing so would allow the overall match to succeed.
* `setValueType(Class type)` - defines a required type of the value. If defined the pattern matched, will match only values which are assigneable from the provided `type`
* `matchCondition(Class<T> type, Predicate<T> matchCondition)` - defines a `Predicate`, whose method `boolean test(T)`,
are called by pattern matcher. Template matcher accepts that value only if `test` returns true for the value.
The `setValueType(type)` is called internally too, so match condition assures both a type of value and condition on value.
* `setContainerKind(ContainerKind)` - defines what container are used to store the value.
* `ContainerKind#SINGLE` - only single value is accepted as a parameter value.
It can be e.g. single String or single CtStatement, etc.
* `ContainerKind#LIST` - The values are always stored as `List`.
* `ContainerKind#SET` - The values are always stored as `Set`.
* `ContainerKind#MAP` - The values are always stored as `Map`.


## InlinedStatementConfigurator

It is possible to match inlined code, eg:

```java
System.out.println(1);
System.out.println(2);
System.out.println(3);
```

can be matched by

```java
for (int i=0; i<n; i++) {
System.out.println(n);
}
```

One mark code to be matched inlined using method `configureInlineStatements`, which receives a InlinedStatementConfigurator as follows:
```java
Pattern t = PatternBuilder.create(...select pattern model...)
//...configure parameters...
configureInlineStatements(ls ->
//...select to be inlined statements...
//e.g. by variable name:
ls.byVariableName("intValues")
).build();
```

The inlining methods are:

* `inlineIfOrForeachReferringTo(String varName)` - all CtForEach and CtIf statements
whose expression references variable named `varName` are understood as
inline statements
* `markAsInlined(CtForEach|CtIf)` - provided CtForEach or CtIf statement
is understood as inline statement

## Generator

All patterns can be used for code generation. The idea is that one calls `#generator()` on a pattern object to get a `Generator`. This class contains methods that takes as input a map of string,objects where each string key points to a pattern parameter name and each map value contains the element to be put in place of the pattern parameter.


32 changes: 27 additions & 5 deletions src/main/java/spoon/metamodel/Metamodel.java
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,18 @@ private Metamodel() {
}
}

/**
* @param clazz a {@link Class} of Spoon model
* @return {@link MetamodelConcept} which describes the `clazz`
*/
public MetamodelConcept getConcept(Class<? extends CtElement> clazz) {
MetamodelConcept mc = nameToConcept.get(getConceptName(clazz));
if (mc == null) {
throw new SpoonException("There is no Spoon metamodel concept for class " + clazz.getName());
}
return mc;
}

/**
* @return all {@link MetamodelConcept}s of spoon meta model
*/
Expand All @@ -289,13 +301,23 @@ public List<CtType<? extends CtElement>> getAllInstantiableMetamodelInterfaces()
* @return name of {@link MetamodelConcept}, which represents Spoon model {@link CtType}
*/
public static String getConceptName(CtType<?> type) {
String name = type.getSimpleName();
if (name.endsWith(CLASS_SUFFIX)) {
name = name.substring(0, name.length() - CLASS_SUFFIX.length());
}
return name;
return getConceptName(type.getSimpleName());
}

public static String getConceptName(Class<? extends CtElement> conceptClass) {
return getConceptName(conceptClass.getSimpleName());
}

/**
* @param simpleName a spoon model class or interface, whose concept name has to be returned
* @return name of {@link MetamodelConcept}, which represents Spoon model {@link CtType}
*/
private static String getConceptName(String simpleName) {
if (simpleName.endsWith(CLASS_SUFFIX)) {
simpleName = simpleName.substring(0, simpleName.length() - CLASS_SUFFIX.length());
}
return simpleName;
}

/**
* @param iface the interface of spoon model element
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/spoon/metamodel/MetamodelConcept.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static spoon.metamodel.Metamodel.addUniqueObject;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
Expand Down Expand Up @@ -125,6 +126,23 @@ public Map<CtRole, MetamodelProperty> getRoleToProperty() {
return Collections.unmodifiableMap(role2Property);
}

/**
* @return Collection of all {@link MetamodelProperty} of current {@link MetamodelConcept}
* Note: actually is the order undefined
* TODO: return List in the same order like it is scanned by CtScanner
*/
public Collection<MetamodelProperty> getProperties() {
return Collections.unmodifiableCollection(role2Property.values());
}

/**
* @param role a {@link CtRole}
* @return {@link MetamodelProperty} for `role` of this concept
*/
public MetamodelProperty getProperty(CtRole role) {
return role2Property.get(role);
}

/**
* @return super types
*/
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/spoon/metamodel/MetamodelProperty.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import spoon.reflect.declaration.CtNamedElement;
import spoon.reflect.factory.Factory;
import spoon.reflect.meta.ContainerKind;
import spoon.reflect.meta.RoleHandler;
import spoon.reflect.meta.impl.RoleHandlerHelper;
import spoon.reflect.path.CtRole;
import spoon.reflect.reference.CtTypeParameterReference;
import spoon.reflect.reference.CtTypeReference;
Expand Down Expand Up @@ -72,6 +74,8 @@ public class MetamodelProperty {
*/
private CtTypeReference<?> itemValueType;

private final RoleHandler roleHandler;

private Boolean derived;
private Boolean unsettable;

Expand Down Expand Up @@ -100,6 +104,7 @@ public class MetamodelProperty {
this.name = name;
this.role = role;
this.ownerConcept = ownerConcept;
roleHandler = RoleHandlerHelper.getRoleHandler((Class) ownerConcept.getMetamodelInterface().getActualClass(), role);
}

void addMethod(CtMethod<?> method) {
Expand Down Expand Up @@ -453,6 +458,10 @@ private CtTypeReference<?> getMapValueType(CtTypeReference<?> valueType) {
*/
public boolean isDerived() {
if (derived == null) {
if (getOwner().getKind() == ConceptKind.LEAF && isUnsettable()) {
derived = Boolean.TRUE;
return derived;
}
// by default it's derived
derived = Boolean.FALSE;

Expand Down Expand Up @@ -567,4 +576,26 @@ private static ContainerKind containerKindOf(Class<?> valueClass) {
return ContainerKind.SINGLE;
}

/**
* @return {@link RoleHandler} which can access runtime data of this Property
*/
public RoleHandler getRoleHandler() {
return this.roleHandler;
}

/**
* @param element an instance whose attribute value is read
* @return a value of attribute defined by this {@link MetamodelProperty} from the provided `element`
*/
public <T, U> U getValue(T element) {
return roleHandler.getValue(element);
}

/**
* @param element an instance whose attribute value is set
* @param value to be set value of attribute defined by this {@link MetamodelProperty} on the provided `element`
*/
public <T, U> void setValue(T element, U value) {
roleHandler.setValue(element, value);
}
}
Loading

0 comments on commit ee1c2ae

Please sign in to comment.