Skip to content

Reserved directive keywords collide with property names with no escape syntax #27

Description

@woksin

Describe the bug

Screenplay is line-based, and every block decides what a line is purely from its first word. A property line
is <name> <Type> — matched by:

// Source/DotNET/Screenplay/Parsing/PropertyLineParser.cs:54
[GeneratedRegex(@"^([a-z_]\w*)\s+([\w.]+(?:\[\])?\??)$", RegexOptions.None, 1000)]
private static partial Regex PropertyRegex();

Several enclosing blocks check a line's first word before falling back to this property regex, in order to
recognize their own directives (description, authorize, tag, validate, key, parent, ...). Because
that check happens first and there is no escape syntax, a property whose name collides with a reserved word is
never seen as a property — it is parsed as the directive instead, whether or not that's what the author meant.

This isn't theoretical: generating a .play document from a real Cratis Arc application produced 4 compile
errors purely from command properties named Description — an extremely common domain field name.

Reserved first words

Block Parser Reserved words
command body CommandParser.cs (switch at line 39) authorize, concurrency, description, handler, produces, validate
event body EventParser.cs (line 33) tag
produces mapping block CommandParser.ParseMappingsAndTags (line 232) tag
projection from block ProjectionParser.ParseFrom (lines 174, 183) key, parent
concept body ScreenplayParser.ParseConcept (line 149) validate

Note events is not reserved in a command body — it only exists inside a concurrency block.

Severity varies — two cases are worse than a compile error, because they corrupt silently

  • description, concurrency, validate, handler as a property name in a command body → hard parse errors.
  • authorize / produces as a property name in a command body → the property is swallowed and a misleading
    diagnostic appears, because ScreenplayValidator then checks the (bogus) reference:
    • authorize SomeType → parsed as an authorize declaration referencing policy SomeTypeUnknown policy 'SomeType' - declare it with 'policy SomeType'.
    • produces SomeType → parsed as a production of event SomeTypeUnknown event 'SomeType' - declare it with 'event SomeType'.
    • i.e. the document ends up asserting a policy/event reference the application never made.
  • tag <Type> as a property name in an event body → silently becomes a static tag on the event. No diagnostic
    at all
    , and the property is dropped entirely (TagParser.Parse accepts any PascalCase identifier as a valid
    tag value).
  • validate as an enum value name in a concept body → silently parsed as an empty validate block (because
    ValidateParser.Parse matches on line.Content == "validate" with no further check), swallowing the enum
    value with no diagnostic at all.
  • tag = x in a produces mapping block, and key = x / parent = x in a projection from block → hard
    errors (recognized as the directive before the general <property> = <source> mapping regex is tried).

Steps to reproduce

1. A description property on a command → hard parse error:

module Invoicing
  feature InvoiceManagement
    slice StateChange RegisterInvoice

      command RegisterInvoice
        invoiceId   InvoiceId
        description String

        produces InvoiceRegistered
          invoiceId   = invoiceId
          description = description

      event InvoiceRegistered
        invoiceId   InvoiceId
        description String

Compiling this reports:

Invalid description 'description String' - expected 'description "<text>"'

description is a completely ordinary field name for a command payload — there is no way to declare it as a
property.

2. A tag property on an event → silently absorbed, no diagnostic:

module Invoicing
  feature InvoiceManagement
    slice StateChange RegisterInvoice

      command RegisterInvoice
        invoiceId InvoiceId
        tag       TagType

        produces InvoiceRegistered
          invoiceId = invoiceId
          tag       = tag

      event InvoiceRegistered
        invoiceId InvoiceId
        tag       TagType

This compiles with zero diagnostics, but tag TagType under event InvoiceRegistered is never added as a
property — EventParser recognizes tag as the tag directive first, and TagParser happily accepts
TagType as a literal tag value (it matches the identifier regex ^[A-Za-z_]\w*$). The event silently gets an
extra static tag named "TagType" and loses the tag property the author intended. Nothing in the compiler
output tells you this happened.

Expected behavior

A property line should be recognized as a property whenever it has property shape, regardless of which word the
enclosing block also uses as a directive keyword — and at minimum, the two silent cases above should raise a
diagnostic rather than quietly changing the meaning of the document.

Actual behavior

The first word alone decides directive-vs-property, with no escape syntax, and two of the collisions
(tag in an event body, validate as an enum value) don't even raise a diagnostic — they just silently reinterpret
the input.

Suggested fix

Framed as a suggestion, not a demand — the maintainers know this grammar best:

  • Disambiguate by shape rather than by first word alone. description "some text" (or a fenced block) is
    unambiguously the directive; description SomeType matches the property-line regex and is unambiguously a
    property. The same reasoning applies to tag.
  • Where shape genuinely cannot disambiguate (e.g. validate as a bare enum value vs. the validate block
    header), an explicit escape for property/value names would resolve it.
  • At minimum, the two fully-silent cases (tag swallowing an event property, validate swallowing an enum
    value) should produce a diagnostic instead of quietly changing meaning — even before a full disambiguation fix
    lands.

Environment

  • Screenplay repo: main branch, Source/DotNET/Screenplay/Parsing/
  • Found while building Cratis.Arc.Screenplay (Cratis/Arc PR #2384),
    which generates .play documents from real Cratis Arc application source. It currently works around this by
    omitting the colliding property and reporting its own SP0032 diagnostic instead — which is lossy (the
    property is dropped from the generated document, not just renamed or escaped). The real fix belongs in the
    grammar/parser here.

Additional context

Not a duplicate of #25 — that issue is about ScreenplayPrinter emitting non-round-trippable output (culture-
dependent numeric formatting, unescaped string literals). This issue is about the parser's reserved-word
handling and applies regardless of how the input was produced (hand-written or printed).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingwait

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions