Skip to content

feat: allow attributes on event declarations - #52

Merged
R-unic merged 2 commits into
masterfrom
feat/event-attributes
Jul 24, 2026
Merged

feat: allow attributes on event declarations#52
R-unic merged 2 commits into
masterfrom
feat/event-attributes

Conversation

@R-unic

@R-unic R-unic commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • event declarations can now carry attributes (e.g. [luau_name("OnConsume")] event consumer(x: string);) in both interface-member and top-level/global positions — previously only PropertyDeclaration implemented IWithAttributes, and the parser had no path to attach a bracketed attribute list to an event in either context.
  • Interface-member events get the same luau_name renaming behavior properties already have: the generated Luau table field and every .name access site are renamed. This is intentional and mirrors the existing convention for properties, since interface members often model pre-existing Roblox API shapes.
  • Global events resolve/type-check attributes cleanly (no crash, no false diagnostics) but attributes have no effect on generated code for them, by design — a global event compiles to an internal Loom.Event.new() local with no external Luau identity worth renaming.
  • Adds a LooksLikeAttributesBeforeEvent parser lookahead so top-level [attr] event ...; parses without breaking ordinary array-literal statements ([1, 2, 3];).

Stacked on #51 (fix/interface-member-event-codegen) — this PR's diff will shrink to just the attribute-support changes once that one merges.

Test plan

  • dotnet build / dotnet build -c Release
  • dotnet test / dotnet test -c Release — full suite passes (1773/1773)
  • New ParserTest coverage: attributes parse on events in both contexts, plain array-literal statements are unaffected
  • New ResolverTest/TypeCheckerTest coverage: attribute symbols populate for interface events, global event attributes resolve with no diagnostics and stay a plain Symbol
  • New snapshot pair Loom.Testing/Snapshots/Luau/event_luau_name_attribute.loom/.luau: interface event renames table field + access sites; global event with the same attribute emits unchanged

R-unic and others added 2 commits July 23, 2026 22:05
The LuauGenerator's event-assignment gate only recognized bare
identifier events tracked via a Resolver Symbol, so `eo.consumer +=
handler` (an interface member event accessed through a variable) fell
through to generic binary-operator codegen and emitted invalid Luau
(`eo.consumer += ...`) instead of a `:Connect()` call. The TypeChecker
already resolved these structurally via the interface's ObjectType, so
this was purely a codegen gap.

- SemanticModel: extract the structural expression -> PropertySymbol
  walk out of TryGetIntrinsicAttribute into a new public
  GetPropertySymbol, so codegen can resolve "what interface member does
  this expression refer to" without needing a resolver Symbol.
- EventConnectionTracker: widen the tracked-connection key from a bare
  Symbol to an EventTarget (event + owning instance), so two variables
  of the same interface type don't collide on the same tracked
  connection.
- LuauGenerator: resolve the event target for both global events
  (Symbol-based, unchanged) and interface member events (structural,
  via GetPropertySymbol) before generating Connect/Disconnect calls.
  Also emit a `Loom.Event<...>` field for `event` members in generated
  interface types, which were previously dropped entirely.
- TypeChecker: extend the event-invocation arity/optional-argument
  check to also look up property-access-invoked events structurally.

Adds interface-member-event coverage to LuauGeneratorTest and
TypeCheckerTest, and a new interface_events Luau snapshot pair.
Adds attribute support to EventDeclaration (both interface-member and
global/top-level forms, which share one AST node), so `luau_name` and
other attributes parse and resolve/type-check cleanly in both contexts.

- EventDeclaration implements IWithAttributes and gains
  TryGetIntrinsicAttribute, mirroring PropertyDeclaration. Because
  interface-member events end up with two symbols registered against
  the same declaration node (a plain Symbol from the Resolver's generic
  traversal, and the real PropertySymbol with attributes from
  ResolveInterfaceBody), the lookup searches all symbols for the node
  rather than assuming a single match, so it correctly finds the
  PropertySymbol for interface events and correctly finds nothing for
  global events.
- Parser: interface bodies now check for `event` before falling back to
  property parsing after a bracketed attribute list, and a new
  bracket-depth lookahead lets top-level `[attr] event ...;` parse
  without disturbing plain array-literal statements.
- Resolver builds real AttributeSymbols for interface events, the same
  way it already does for properties.
- TypeChecker visits event attribute expressions in both the global
  event path and interface event resolution, so `luau_name` must
  resolve to something function-typed, same as for properties.
- LuauGenerator's interface event codegen honors `luau_name` to rename
  the table field, matching property behavior; existing renamed-access
  codegen already worked generically once the Resolver populated
  attributes. Global event codegen is untouched by design: attributes
  have no effect on generated code for global events, since they
  compile to an internal Loom.Event.new() local with no external Luau
  identity to rename.

Tests: parser (both contexts, plus a regression guard that plain array
literals aren't misparsed as attribute lists), resolver (attribute
population for interface events, no-op for global events), type
checker (clean type-check plus NonFunctionAttribute diagnostic in both
contexts), and a new Luau snapshot pair plus targeted codegen
assertions locking in the field/access-site rename for interface events
and the "no effect" behavior for global events.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Qodana for .NET

253 new problems were found

Inspection name Severity Problems
Auto-property accessor is never used (private accessibility) 🔶 Warning 6
Auto-property accessor is never used (non-private accessibility) 🔶 Warning 5
Non-accessed positional property (non-private accessibility) 🔶 Warning 4
Redundant argument with default value 🔶 Warning 2
Redundant using directive 🔶 Warning 2
'??' condition is never null according to nullable reference types' annotations 🔶 Warning 1
Redundant name qualifier 🔶 Warning 1
Redundant nullable warning suppression expression 🔶 Warning 1
Type member is never used (private accessibility) 🔶 Warning 1
Unused parameter (private accessibility) 🔶 Warning 1
Unused local variable 🔶 Warning 1
Type member is never used (non-private accessibility) ◽️ Notice 93
Use preferred style of 'new' expression when created type is not evident ◽️ Notice 83
Member can be made private (non-private accessibility) ◽️ Notice 18
Class is never instantiated (non-private accessibility) ◽️ Notice 4
Some values of the enum are not processed inside 'switch' statement ◽️ Notice 4
Auto-property can be made get-only (non-private accessibility) ◽️ Notice 3
RoslynAnalyzers Do not use Enumerable methods on indexable collections ◽️ Notice 3
Invert 'if' statement to reduce nesting ◽️ Notice 3
RoslynAnalyzers The member referenced by the MemberData attribute returns untyped data rows ◽️ Notice 3
Use preferred style for trailing comma before new line in multiline lists ◽️ Notice 2
Remove redundant parentheses ◽️ Notice 1
Auto-property can be made get-only (private accessibility) ◽️ Notice 1
RoslynAnalyzers Use concrete types when possible for improved performance ◽️ Notice 1
Foreach loop can be converted into LINQ-expression but another 'GetEnumerator' method will be used ◽️ Notice 1
Merge null/pattern checks into complex pattern ◽️ Notice 1
Redundant string interpolation ◽️ Notice 1
Replace with 'field' keyword ◽️ Notice 1
Some values of the enum are not processed inside 'switch' statement and are handled via default section ◽️ Notice 1
Tail recursive call can be replaced with loop ◽️ Notice 1
Type member is never accessed via base type (non-private accessibility) ◽️ Notice 1
Method return value is never used (non-private accessibility) ◽️ Notice 1
Literal length can be reduced by using verbatim string ◽️ Notice 1

☁️ View the detailed Qodana report

Detected 1 dependency

Third-party software list

This page lists the third-party software dependencies used in Loom

Dependency Version Licenses
Tomlyn 2.6.0 BSD-2-Clause
Contact Qodana team

Contact us at qodana-support@jetbrains.com

@R-unic
R-unic merged commit b5ca703 into master Jul 24, 2026
3 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Loom Release 1.0.0 Jul 24, 2026
@R-unic
R-unic deleted the feat/event-attributes branch July 24, 2026 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant