Create a new Meta-model

Create a new Meta-model Moose 7 Moose 8 Moose 9

To analyze a system in a given programming language, Moose must have a meta-model for that language. For example, for Java, the meta-model defines that Java programs have classes, containing methods, invoking other methods, etc. The meta-model describes the entities that compose a program in the given language and how they are related.

In the following, we describe how to create a new meta-model or extend an existing one. Moose being more specifically dedicated to source code analysis, there are a number of pre-set entities/traits that should help one define new meta-models for a given programming language. These are described in another page Unfinished.

Set up

First of all, we need to download Moose version 7 or higher.

The first step is to create a FamixMetamodelGenerator. It will describe our meta-model.

FamixMetamodelGenerator subclass: #DemoMetamodelGenerator
    slots: { }
    classVariables: { }
    package: 'Demo-Model-Generator'

Then we need to configure the generator. We have to specify the package in which the meta-model will be generated.

DemoMetamodelGenerator class >> #packageName

    ^ #'Demo-Model-generated'

By default the package name will be used as a prefix for the generated classes. But we can specify a custom prefix by defining the method #prefix.

DemoMetamodelGenerator class >> #prefix

    ^ #'Demo'

Basic meta-model

In this section, we will see how to create a simple meta-model.

To design a meta-model, we need to specify its entities, their relations, and their properties.

You may also consult a presentation of Famix generator from Julien Delplanque.

Define entities

A meta-model is composed of entities. These entities represent the elements of the model we will manipulate. To define entities in the generator, we extend the method #defineClasses. Each entity is defined using the generator builder (provided by FamixMetamodelGenerator) to which we send the the message #newClassNamed:.

DemoMetamodelGenerator>>#defineClasses

    super defineClasses.
    entity := builder newClassNamed: #Entity.
    package := builder newClassNamed: #Package.
    class := builder newClassNamed: #Class.
    method := builder newClassNamed: #Method.
    variable := builder newClassNamed: #Variable.
    localVariable := builder newClassNamed: #LocalVariable.
    attribute := builder newClassNamed: #Attribute.

It is important to comment the entities to help other developers understand our meta-model. This can be done with the #newClassNamed:comment: method.

class := builder newClassNamed: #Class comment: 'I represent a Smalltalk class'.

It is also possible to use entities that are already defined in a library of predefined entities or in another meta-model (see submetamodels).

Define hierarchy

Once the entities are defined, the next step is to specify their hierarchy.

binary definition
--|> Left entity extends (inherits from) the right one
<|-- Right entity extends (inherits from) the left one

Note that these symbols are actually Pharo binary methods. One can also use a Pharo keyword method: #generalization: defining that the receiver extends the parameter (i.e., similar to --|>).

The hierarchy is defined in the generator with the method #defineHierarchy.

DemoMetamodelGenerator>>#defineHierarchy

    super defineHierarchy.
    package --|> entity.
    class --|> entity.
    method --|> entity.

    variable <|-- localVariable.
    variable <|-- attribute.

Define relations

Then we will define the relations between the entities. Multiple relations are available in Famix. In the following, we present the relations and the keywords to define them.

method binary
#oneToOne: -
#oneToMany: -*
#manyToOne: *-
#manyToMany: *-*
#containsOne: <>-
#containsMany: <>-*
#oneBelongsTo: -<>
#manyBelongsTo: *-<>

We can now define the relations between the entities of our meta-model in the method #defineRelations.

DemoMetamodelGenerator>>#defineRelations
    super defineRelations.
    package <>-* class.
    class <>-* attribute.
    method <>-* localVariable

As for the definition of the entities, it is possible to define a comment for each side of the relation and to use a custom name for the accessors.

DemoMetamodelGenerator>>#defineRelations
    super defineRelations.
    ((package property: #classes) comment: 'The classes inside the package')
        <>-*
    ((class property: #package) comment: 'The package that contains this class').

Finally, it is possible to set several other properties. Some may be applied on one side of the relation:

  • container (the side “contains” the other one, see the different keyword)
  • derived (the relation is not part of the model but is computed)
  • source (source of an association)
  • target (source of an association)

Others can be applied on the relation itself:

  • withoutPrimaryContainer (Since an element can only have one container, it allows one to create another containment relation. This relation will not set the method #belongsTo)
  • withNavigation (force the relation to appear in the Moose Playground)

Define properties

The last step before generating the model is the definition of the properties of the entities. A property can be of any type. It is also possible to define a comment for each property. Let’s create the method #defineProperties in our generator:

DemoMetamodelGenerator>>#defineProperties

    super defineProperties.

   (entity property: #name type: #String)
       comment: 'The name of the entity'.

Moose defines the following types for properties:

  • Character
  • Number
  • Fraction
  • String
  • Symbol
  • Boolean
  • Object

Using the Object, you will not be able to export the property in .mse and .json (see: import and export model).

Generate

We have described our meta-model. The last step is to actually generate it with: DemoMetamodelGenerator generate.

Later, if the description of the meta-model is modified, the generation will only regenerate the modified elements and remove the old ones. It is possible to force a full (clean) generation of the model with: DemoMetamodelGenerator generateWithCleaning.

Introducing traits

Using traits in a meta-model

In addition to the entities, we can use traits to add information to the meta-model. Traits are a flexible tool to avoid problems with multiple inheritances. Traits are defined in the same way as entities. In our previous example, classes are inside a package. However, we forgot that a package can also contain another package, and we need to model that.

First of all, we need to define the traits to create the containment relation of a package. We declare the trait in the method #defineTraits.

defineTraits

    super defineTraits.

    tPackageable := builder newTraitNamed: #TPackageable comment: 'I can be inside a Package'.
    tWithPackages := builder newTraitNamed: #TWithPackages comment: 'I can contains packageable elements'.

Then, we have to change the hierarchy of our entity to add the traits.

DemoMetamodelGenerator>>#defineHierarchy

    super defineHierarchy.

    package --|> entity.
    package --|> tWithPackages.
    package --|> tPackageable.

    class --|> entity.
    class --|> tPackageable.

    method --|> entity.

    variable <|-- localVariable.
    variable <|-- attribute.

Finally, we define the relations between the traits.

DemoMetamodelGenerator>>#defineRelations
    super defineRelations.
    package <>-* class.
    package <>-* package.
    tWithPackages <>-* tPackageable.
    class <>-* attribute.
    class <>-* method.
    method <>-* localVariable

It is also possible to use traits that are already defined in another meta-model (see submetamodels).

Dealing with trait conflict

When a class inherits from two different traits that both defined a method with the same name, there is a conflict between the trait. This is a classic problem when allowing multiple inheritances.

PlantUML Image

In this example, the class MyClass uses the traits TraitA and TraitB. Both traits define the method methodXY. In such a situation, there is a trait conflict.

The Famix Generator proposes two ways to deal with such a problem:

  • withPrecedenceOf
  • inheritsFromTrait:without:

The first one, withPrecedenceOf, specifies which trait should be used in favor of the other one. In our case, considering we want to use the methodXY of TraitA, the code will look like this:

defineHierarchy
    super defineHierarchy.
    myClass --|> traitA.
    myClass --|> traitB.
    myClass withPrecedenceOf: traitA.

The second option, inheritsFromTrait:without:, is about using a trait without some methods. This time, we replace the common --|> with the new selector, and we specify the method we do not want to use. The final code will look like this:

defineHierarchy
    super defineHierarchy.
    myClass --|> traitA.
    myClass inheritsFromTrait: traitB without: #methodXY.

Additional features

Generating test method

When using a model, it is often required to select entities that are of a specified kind. It is the case when needed all the FamixJavaClass of a group.

The good practice in Pharo is to use a isXXX method that is defined at the top of the hierarchy and returns false. Then, one overrides this method for the entity that returns true.

To automatically generate the isXXX methods, the generator comes with the withTesting method.

In our FamixJavaClass example, one can modify the FamixJavaGenerator as follow:

FamixJavaGenerator>>defineClasses
    super defineClasses.
    "..."

    class := builder newClassNamed: #Class.
    class withTesting.

    "..."

This creates the following methods:

FamixJavaClass>>#isClass

    <generated>
    ^ true
FamixJavaEntity>>#isClass

    <generated>
    ^ false

Generating Famix Group

Another nice feature of Moose is the Group. It allows one to create query and inspector extensions based on the type of a group. A typed group is a group that contains only elements of a type. For example, a FamixJavaClassGroup only contains FamixJavaClass entities. These kinds of groups are subclasses of MooseSpecializedGroup and can be created using asMooseSpecializedGroup on any collections or moose entities.

To generate the group classes, the generator comes with the withGroup method. One only needs to use it on the concept defined in the generator.

In our FamixJavaClass example, one can modify the FamixJavaGenerator as follow:

FamixJavaGenerator>>defineClasses
    super defineClasses.
    "..."

    class := builder newClassNamed: #Class.
    class withGroup.

    "..."

This will generates the following class:

MooseSpecializedGroup subclass: #FamixJavaClassGroup
    instanceVariableNames: ''
    classVariableNames: ''
    package: 'Famix-Java-Entities-Entities'

Introducing submetamodels

One powerful feature of Famix is the possibility to use submetamodels. It allows one to extend or compose several meta-models.

There are two ways of extending of meta-model:

  1. Create a generator that extends the first one.
  2. Create a separate generator that declares another as submetamodel.

Although the first one can be used, when the meta-model is generated, the entities that come from the extended generator will be created two times, in the new generator and in the old one. So, the best way to extend a meta-model is to use the submetamodels. In the following, we present how to configure a generator for submetamodels.

Set up submetamodels

In this example, we will create a new generator that will add the interface entity that will be use to represent an interface. The interface is packageable and contains methods.

Note that it is done in an example. The best way, in this case, would be to modify the previous generator.

First of all, we create a new generator:

FamixMetamodelGenerator subclass: #DemoInterfaceMetamodelGenerator
    slots: { }
    classVariables: { }
    package: 'Demo-InterfaceModel-Generator'

Then we declare our previous generator as submetamodel:

DemoInterfaceMetamodelGenerator class >> #submetamodels

    ^ { DemoMetamodelGenerator }

We also have to define the #prefix and the #packageName.

Define remote entities and traits

Once the generator is configured we can define the entities of the AST meta-model and the entities that come from the Demo meta-model. To define an entity of another meta-model we used the method #remoteEntity:withPrefix:. The prefix is then the prefix defined for the submetamodel.

DemoInterfaceMetamodelGenerator>>#defineEntities

    super defineEntities.
    interface := builder newClassNamed: #Interface.

    method := self remoteEntity: #Method withPrefix: #Demo.

In some cases, it may be necessary to define remote traits. Use the method #remoteTrait:withPrefix: on the generator.

Define remote hierarchy

To represent the relation of containment of a package on an interface, we use the trait #TPackageable (see Introducing traits). Because there is only one trait with this name in the submetamodels, we can use the notation with the symbol instead of defining it in a variable in the “defineEntities” section:

DemoInterfaceMetamodelGenerator>>#defineHierarchy

    super defineHierarchy.
    "use the trait of the other metamodel"
    interface --|> #TPackageable.

Define remote relations

Finally, we create the relations between the interface and the methods:

DemoInterfaceMetamodelGenerator>>#defineRelations
    super defineRelations.

    ((interface property: #methods) comment: 'The methods of the interface')
        <>-*
    ((method property: #interface) comment: 'The interface that own me').

Complementary information

In our example, an interface contains methods and a class contains methods, too. However, it is not possible to have two main containers for an entity (see Define relations). In this case, we can either declare the relation “interface <>-* method” without a primary container, or define two Traits in the first meta-model. One would be #TMethod and the other #TWithMethods.

Thanks

This wiki page is inspired by the FamixNG booklet.