The content of this section will cover the part of new entities/relations creation specific to MooseQuery. It will not cover how to create new Moose entities. For this, please refer to Moose documentation.
MooseQuery is the result of the Inria research team RMoD. This page documents the internals of MooseQuery. As prerequisites, it supposes the reader has a minimum of knowledge of Fame and Famix. If not, please read the Moose documentation, if you wish to understand everything.
This documentation covers:
The first section will be useful if you wish to extend/create a meta-model. The three next sections only describe how the query system works. It does not requires any change from the developers of a meta-model.
The content of this section will cover the part of new entities/relations creation specific to MooseQuery. It will not cover how to create new Moose entities. For this, please refer to Moose documentation.
First, the developer of the model has to make his new entities/relations queryable with MooseQuery.
As said in the user documentation, MooseQuery is based on two concepts:
Here, for example, Entity1 is the container of Entity2 and Entity1 is the source of an association whose Entity3 is the target.
Moose query needs to know those relations in order to work. To declare them you just need to use three pragma: <container>, <source> and <target>
The container pragma should be put in the contained entity in the method defined to access the container.
At the meta-model level, an entity may have several parents. For example, the container of a class can be a package or a method in the case of inner class. In that case, both methods #parentPackage and #container must have the pragma.
FAMIXAttribute>>parentType
<MSEProperty: #parentType type: #FAMIXType opposite: #attributes>
<MSEComment: 'Type declaring the attribute. belongsTo implementation'>
<container> "<=== Here is the containment relation definition"
^ parentType
The source and target pragmas must be in the methods relevant of the association.
FAMIXAccess>>accessor
<MSEProperty: #accessor type: #FAMIXBehaviouralEntity opposite: #accesses>
<MSEComment: 'Behavioural entity making the access to the variable. from-side of the association'>
<source> "<=== Here is the association's source definition"
^ accessor
FAMIXAccess>>variable
<MSEProperty: #variable type: #FAMIXStructuralEntity opposite: #incomingAccesses>
<MSEComment: 'Variable accessed. to-side of the association'>
<target> "<=== Here is the association's target definition"
^ variable
Relations definitions (via the pragmas) give to MooseQuery all the requirements for querying the meta-model. Now, the new entities of the model need to have the behaviour, i.e., the methods, to execute the queries. To do so, MooseQuery comes with a set of Traits to use in the new entities.
Trait | Default users | Description |
---|---|---|
TAssociationMetaLevelDependency | FAMIXAssociation | This trait includes the information about associations needed for the navigation queries. It needs to be used by every association in a meta-model. |
TEntityMetaLevelDependency | FAMIXNamedEntity | This trait includes the information about entities and adds the behaviour to explore the containment tree and execute scope queries. It needs to be used in every entity's hierarchy in a meta-model. |
TDependencyQueries | FAMIXNamedEntity | This trait includes the information about entities and adds the behaviour to execute navigation queries. It needs to be used in every entity's hierarchy in a meta-model. |
TOODependencyQueries | FAMIXContainerEntity | This trait includes the information about object-oriented entities and adds the behaviour to execute navigation queries. It needs to be used in every entity's hierarchy in a meta-model instead of TDependencyQueries. |
All those traits need to be used when implementing a new meta-model. For performance reasons, some more work is needed when using those traits. Some methods exist in two versions. One private with the real behaviour, and the other public that access a class instance variable or, if nil, set it with the result of the private method. Due to the fact that traits are currently stateless in Pharo, all the public methods have to be implemented for your new metaclass using directly the traits and not by inheritance. For example, FAMIXAnnotationInstance does not inherit from FAMIXNamedEntity but can be queried similarly. The same occur for new links using TAssociationMetaLevelDependency without inheriting from FAMIXAssociation. Here is the list of caches to implement:
"Already defined in MooseQuery:"
TEntityMetaLevelDependency class>>allChildrenTypes
^ self explicitRequirement
TEntityMetaLevelDependency class>>privateAllChildrenTypes
^ (self childrenTypes withDeepCollect: #childrenTypes as: Set) asOrderedCollection
"Method to implement:"
FAMIXNamedEntity class>>allChildrenTypes
^ allChildrenTypes ifNil: [ allChildrenTypes := self privateAllChildrenTypes ]
The model builds caches on classes to speed up queries. It means that during the development of the meta-model it is often required to reset those caches.
In order to reset the caches you can execute:
MooseModel resetMeta
This method will call #resetMooseQueryCaches on all subclasses of FAMIXEntity. If you add new caches (in a class that directly uses the trait and not through inheritance like FAMIXAnnotationInstance), you need to override in the class side of your entity #resetMooseQueryCaches to reset the added caches.
FAMIXNamedEntity class>>resetMooseQueryCaches
super resetMooseQueryCaches.
childrenSelectors := parentSelectors := allChildrenTypes := allParentTypes := outgoingMSEProperties := incomingMSEProperties := incomingAssociationTypes := outgoingAssociationTypes := nil
This section and the next ones explains how works MooseQuery's queries. From this point, no more action is required from the developer to have working queries on its meta-model.
Now that we saw how to make entities queryable, this documentation will focus on explaining the internal mechanisms of MooseQuery. The easiest queries are the ones related to the exploration of the containment tree.
MooseQuery allows querying the children/parents of an entity. This behaviour is based on the meta-model of the application. Each Moose entity possesses a meta-description that lists all the properties of the entity. These properties can be obtained with:
MooseEntity class>>allDeclaredProperties
"All properties described in the meta-model"
^self mooseDescription allAttributes
FAMIXType allDeclaredProperties. "==> An array of property descriptions (See screenshot bellow)"
Each property knows:
So, based on the meta-model and the moose pragma, it is possible for a Famix class to get the selectors to access its parents or children entity using the #parentSelectors and #childrenSelectors methods respectively. Here again a cache mechanism is used.
"Implementation details:"
TEntityMetaLevelDependency class>>privateChildrenSelectors
^ self allDeclaredProperties select: #isChildrenProperty thenCollect: #implementingSelector
FAMIXNamedEntity class>>childrenSelectors
^ childrenSelectors ifNil: [ childrenSelectors := self privateChildrenSelectors ]
TEntityMetaLevelDependency class>privateParentSelectors
^ self allDeclaredProperties select: #isContainer thenCollect: #implementingSelector
FAMIXNamedEntity class>>parentSelectors
^ parentSelectors ifNil: [ parentSelectors := self privateParentSelectors ]
"Example of use:"
FAMIXType parentSelectors. "=> #(#container #parentPackage)"
FAMIXType childrenSelectors. "=> #(#annotationInstances #types #definedAnnotationTypes #attributes #functions #methods)"
So, whatever the selector used in the meta-model to access the contained elements of a given entity, it is possible to access them through the children method that dynamically call each selector returned by the #childrenSelectors methods. The same is possible to get the parents of a given entity. Once we retrieved all parents/children selectors it is possible to perform them on an entity to get their results and gather the children/parents of this entity.
"Implementation details:"
TEntityMetaLevelDependency>>children
| res |
res := OrderedCollection new.
"Since some selector can return nil, an entity or a collection of entities, we need to do a nil check and to ensure everything is a collection via #asCollection"
self childrenSelectors do: [ :accessor | (self perform: accessor) ifNotNil: [ :r | res addAll: r asCollection ] ].
^ res asSet
TEntityMetaLevelDependency>>parents
| res |
res := OrderedCollection new.
"Since some selector can return nil, an entity or a collection of entities, we need to do a nil check and to ensure everything is a collection via #asCollection"
self parentSelectors do: [ :accessor | (self perform: accessor) ifNotNil: [ :r | res addAll: r asCollection ] ].
^ res asSet
"Example from the user documentation:"
package1 children. "=> { package2 . class1 }"
class3 children. "=> { attribute1 . attribute2 }"
package1 parents. "=> { }"
class3 parents. "=> { package2 }"
class4 parents. "=> { package3 . namespace1 }"
No risk to forget a selector, it is dynamically computed from the meta-model.
Similarly, it is possible to get children/parents recursively. Since containment trees can be pretty big, we use an accumulator to avoid creating a lot of collections.
"Implementation details:"
TEntityMetaLevelDependency>>allChildren
"Returns all the children and sub-children of an entity, i.e my children and those of my children, and those of the children of my children, etc"
^ self addAllChildrenIn: OrderedCollection new
TEntityMetaLevelDependency>>addAllChildrenIn: aCollection
aCollection addAll: self children.
self children do: [ :each | each addAllChildrenIn: aCollection ].
^ aCollection
"Examples from the user documentation:"
package1 allChildren. "=> { package2 . class1 . class2 . class3 . attribute1 . attribute2 }"
class3 allChildren. "=> { attribute1 . attribute2 }"
class3 allParents. "=> { package2 . package1 }"
attribute1 allParents. "=> { class3 . package2 . package1 }"
This section explains how the scope queries works.
A scope query is parametrized by:
A scope query can be applied on:
Queries going up or down in the containment tree for the scope search are usable respectively via the methods TEntityMetaLevelDependency>>#atScope: and #toScope:. For example we can want all the incoming invocations of package P2 (cf. user documentation examples) at class scope. Meaning that we want to compute all the invocations that reach a method recursively contained in P2. But, as a result, we don't want neither the invocations nor the target methods of these invocations. We want the classes containing these methods.
These methods begin to create a collection to store the results and call #atScope:in:/#toScope:in: with the newly created collection as a parameter. This is done for performance reasons. Instead of creating multiple collections and use concatenation on them, we create one at the beginning and pass it as a parameter of the query methods.
The query then checks if the receiver is of the searched kind. In case it is, the query end and returns the receiver as result. If it is not the case then the query is repeated on the parents/children of the entity depending on the direction in the containment tree of the query.
TEntityMetaLevelDependency>>atScope: aClassFAMIX in: aCollection
(self isKindOf: aClassFAMIX)
ifTrue: [ aCollection add: self ]
ifFalse: [ "The content of this block could be much more readable with #do: but we do this solution for performances... We need this method to be really really performant."
| selectors |
1 to: (selectors := self parentSelectors) size do: [ :ind | (self perform: (selectors at: ind)) atScope: aClassFAMIX in: aCollection ] ].
^ aCollection
As in the navigation queries, this code could be more readable but it is the way it is for performance reasons. Find the more readable code below.
TEntityMetaLevelDependency>>atScope: aClassFAMIX in: aCollection
(self isKindOf: aClassFAMIX)
ifTrue: [ aCollection add: self ]
ifFalse: [ self parentSelectors do: [ :selector | (self perform: selector) atScope: aClassFAMIX in: aCollection ] ].
^ aCollection
In the case of the up and down queries, we will just check if the entity can do up/down scopes and apply them if needed.
TEntityMetaLevelDependency>>withScope: aClassFAMIX in: aCollection
self allParentTypes detect: [ :class | aClassFAMIX = class or: [ aClassFAMIX inheritsFrom: class ] ] ifFound: [ self atScope: aClassFAMIX in: aCollection ].
self allChildrenTypes detect: [ :class | aClassFAMIX = class or: [ aClassFAMIX inheritsFrom: class ] ] ifFound: [ self toScope: aClassFAMIX in: aCollection ].
^ aCollection
The recursive version (#allAtScope:, #allToScope: and #allWithScope:) is approximately the same, except it will not stop if an entity of the right kind is found.
TEntityMetaLevelDependency>>allAtScope: aClassFAMIX in: aCollection
| selectors |
(self isKindOf: aClassFAMIX) ifTrue: [ aCollection add: self ].
"The content of this block could be much more readable with #do: but we do this solution for performances... We need this method to be really really performant."
1 to: (selectors := self parentSelectors) size do: [ :ind | (self perform: (selectors at: ind)) allAtScope: aClassFAMIX in: aCollection ].
^ aCollection
Once we understand the scope queries applied to entities, it is easy to understand the scope queries on MooseQueryResult.
MooseQueryResults store the associations gathered via a navigation query. Applying a scope query on it gathers the opposites of the first query receiver and apply the scope query on each of them.
TDependencyQueryResult>>atScope: aClassFamix
^ self newObjectResultWith: (self storage inject: OrderedCollection new into: [ :res :dep | (self opposite: dep) atScope: aClassFamix in: res ]) asSet