Creating custom Statements
Abstract
In which the author outlines the method used in Calidus to parse a set of tokens into a new type of statement.
Concrete
Creating a new statement in Calidus is simple. A lot of utilities, methods and classes are available to make creating a statement a breeze.
Creating the basic classes
The first thing to do is to make sure that all parts are in place. The statement class itself must be declared. This class is derived from the AccessModifierStatement because an indexer can have an access modifier, and the base AccessModifierStatement class provides a few methods to work with these access modifiers.
/// This class represents an indexer statement
/// </summary>
public class IndexerStatement : AccessModifierStatement
{
/// <summary>
/// Create a new instance of this class
/// </summary>
/// <param name="tokens">The list of tokens in the statement</param>
public IndexerStatement(IEnumerable<TokenBase> tokens)
: base(tokens)
{
}
}
The basic code for the statement factory class is also needed. In order to make this parsing as easy as possible the factory is derived from the FluentStatementFactory class to gain access to statement expressions. These statement expressions provide a fluent interface to declare the requirements for a statement.
/// This class creates indexer statements
/// </summary>
public class IndexerStatementFactory : FluentStatementFactory<IndexerStatement>
{
protected override IndexerStatement BuildStatement(IEnumerable<TokenBase> input)
{
return new IndexerStatement(input);
}
protected override bool IsValidContext(IStatementContext context)
{
return false;
}
protected override IStatementExpression Expression
{
get
{
StatementExpression expression = new StatementExpression();
return expression;
}
}
}
Since Calidus is test-driven, a set of tests are needed to indicate that the statement is parsed correctly. For a factory, define a base test class that derives from CalidusTestBase. This class provides a set of utility methods and classes such as a TokenCreator and a StatementCreator that can be used by Calidus tests.
The TDD aficionado’s will notice that this is technically not a valid test: an additional unit test should be created to validate the context checking separately. However, this test already checks that the context is called through the mock repository in the VerifyAll-method. An extra test provides no additional advantage here.
public class IndexerStatementFactoryTest : CalidusTestBase
{
[SetUp]
public override void SetUp()
{
base.SetUp();
}
}
Next, define a series of tests to indicate what is needed, and in this case a single test will suffice. Some additional information is needed to make this test work, so the basic frame of the test class must be expanded to suit the requirements for mocking.
public class IndexerStatementFactoryTest : CalidusTestBase
{
private IndexerStatementFactory _factory;
private IStatementContext _context;
private MockRepository _mocker;
[SetUp]
public override void SetUp()
{
base.SetUp();
_factory = new IndexerStatementFactory();
_mocker = new MockRepository();
_context = _mocker.DynamicMock<IStatementContext>();
}
[Test]
public void FactoryShouldCreateStatementFromThisFollowedBySquareBracketInClass()
{
Expect.Call(_context.Parents).Return(new[] { new StatementParent(StatementCreator.CreateClassStatement(), StatementCreator.CreateOpenBlockStatement()) }).Repeat.Once();
IList<TokenBase> input = new List<TokenBase>();
input.Add(TokenCreator.Create<PublicModifierToken>());
input.Add(TokenCreator.Create<SpaceToken>());
input.Add(TokenCreator.Create<IdentifierToken>("String"));
input.Add(TokenCreator.Create<SpaceToken>());
input.Add(TokenCreator.Create<ThisToken>());
input.Add(TokenCreator.Create<OpenSquareBracketToken>());
input.Add(TokenCreator.Create<IntToken>());
input.Add(TokenCreator.Create<SpaceToken>());
input.Add(TokenCreator.Create<IdentifierToken>("index"));
input.Add(TokenCreator.Create<CloseSquareBracketToken>());
_mocker.ReplayAll();
Assert.IsTrue(_factory.CanCreateStatementFrom(input, _context));
_mocker.VerifyAll();
}
}
This test code defines a mock statement context that indicates that the list of tokens to be parsed by the factory was found inside a class. The token list to parse is also created here.
Making it work
The following code is fairly simple as there are only two requirements for an indexer statement. First of all, it is part of a class: it can only be defined where the parent of the statement is the scope of a class. Second, an indexer statement consists of the This-keyword followed by a square bracket open. Both requirements can be expressed in code.
{
return context.Parents.FirstParentIsOfType<ClassStatement>();
}
protected override IStatementExpression Expression
{
get
{
StatementExpression expression = new StatementExpression();
expression.Contains<ThisToken>()
.FollowedBy<OpenSquareBracketToken>();
return expression;
}
}
Combining these two pieces of codes validates both the context and the tokens, and running the unit tests with this code now yields a nice green bar.