Skip to content

Add support for enumString, literal, and enumValues in Ack.object()#70

Open
leoafarias wants to merge 10 commits intomainfrom
claude/install-dart-melos-UQthl
Open

Add support for enumString, literal, and enumValues in Ack.object()#70
leoafarias wants to merge 10 commits intomainfrom
claude/install-dart-melos-UQthl

Conversation

@leoafarias
Copy link
Collaborator

Summary

This PR adds proper type resolution support for Ack.enumString(), Ack.literal(), and Ack.enumValues<T>() when used as fields inside Ack.object() schemas. Previously, these schema methods were not properly handled by the schema analyzer, resulting in incorrect or missing type information.

Key Changes

  • Enhanced SchemaAstAnalyzer._resolveSchemaType(): Added cases for enumString, literal, and enumValues to properly resolve their Dart types

    • enumString and literal resolve to String type
    • enumValues<T> resolves to the generic enum type T by extracting it from type arguments or inferring from the argument pattern
  • New _resolveEnumValuesType() method: Implements two-strategy type resolution for Ack.enumValues<T>():

    • Strategy 1: Extracts type name from explicit type argument (Ack.enumValues<UserRole>(...))
    • Strategy 2: Infers type from argument pattern (Ack.enumValues(UserRole.values))
    • Looks up the resolved type in the library's declared enums and classes
  • Updated _mapSchemaMethodToType() and _extractEnumValuesTypeName(): Added support for string representation of enum types in list element type resolution

  • Test coverage: Added comprehensive test suite covering:

    • Basic enumString() field handling
    • enumString() with optional/nullable modifiers
    • literal() field handling
    • enumValues<T>() with explicit type arguments
    • enumValues<T>() with optional modifier
    • Nested Ack.list(Ack.enumString()) patterns
  • Test utilities: Added mock implementations of literal(), enumString(), and enumValues() methods to the test Ack class, plus an EnumSchema<T> class for proper schema representation

Implementation Details

The solution handles both explicit type arguments and inferred types, making it flexible for different coding styles. The enum type resolution gracefully falls back to String type when the enum cannot be resolved, ensuring safe defaults while maintaining type safety when possible.

https://claude.ai/code/session_01VF6gNrszmCJwYe1AcWPzez

… in Ack.object()

These schema types were handled correctly as top-level @AckType schemas
but threw "Unsupported schema method" when used as field values inside
Ack.object({...}). The bug was in _mapSchemaTypeToDartType (field-level
type resolution) and _mapSchemaMethodToType (list element type strings),
which were missing cases for these three schema types.

Fixes:
- _mapSchemaTypeToDartType: add enumString/literal → String, enumValues → resolved enum DartType
- _mapSchemaMethodToType: add enumString/literal → 'String'
- _extractListElementTypeString: handle enumValues with type extraction from invocation
- Add _resolveEnumValuesType helper to resolve enum DartType from library elements
- Add _extractEnumValuesTypeName helper for string-based type resolution in list contexts
- Update test mock Ack class with enumString(), enumValues(), literal() static methods
- Add 6 regression tests covering all field-level enum/literal scenarios

https://claude.ai/code/session_01VF6gNrszmCJwYe1AcWPzez
@docs-page
Copy link

docs-page bot commented Feb 16, 2026

To view this pull requests documentation preview, visit the following URL:

docs.page/btwld/ack~70

Documentation is deployed and generated using docs.page.

…alues/literal fields

- Add 9 golden tests verifying generated extension type getters for enumString,
  literal, and enumValues fields inside Ack.object() schemas
- Add 3 edge case tests for Ack.list(Ack.enumValues<T>()), Ack.list(Ack.literal()),
  and Ack.enumValues<T>().nullable() without optional()
- Refactor duplicate enum type name extraction into shared
  _extractEnumTypeNameFromInvocation() helper

https://claude.ai/code/session_01VF6gNrszmCJwYe1AcWPzez
- Remove pointless _extractEnumValuesTypeName wrapper; call shared
  helper directly
- Strip redundant inline comments that narrate the obvious
- Trim over-documented docstrings to match surrounding code style
- Remove verbose reason strings from test assertions where the
  framework output is already clear

https://claude.ai/code/session_01VF6gNrszmCJwYe1AcWPzez
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds missing schema-analyzer support so Ack.enumString(), Ack.literal(), and Ack.enumValues<T>() fields inside Ack.object({...}) produce correct Dart field types in generated @AckType extension types.

Changes:

  • Extend SchemaAstAnalyzer to resolve enumString, literal, and enumValues invocations to Dart types (including list element type string extraction).
  • Expand test utilities with mock Ack.literal/enumString/enumValues and a minimal EnumSchema<T> for test compilation.
  • Add new integration tests and regression tests covering these schema patterns (including optional/nullable and nested lists).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
packages/ack_generator/lib/src/analyzer/schema_ast_analyzer.dart Adds enum/literal/enumValues handling in type resolution + list element type string logic
packages/ack_generator/test/test_utils/test_assets.dart Adds mock Ack methods and EnumSchema<T> used by generator tests
packages/ack_generator/test/integration/ack_type_enum_literal_fields_test.dart New integration tests validating generated getters/types
packages/ack_generator/test/bugs/schema_variable_bugs_test.dart New regression tests validating AST analyzer field typing

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +634 to +652
DartType? _resolveEnumValuesType(
MethodInvocation invocation,
LibraryElement2 library,
) {
final enumTypeName = _extractEnumTypeNameFromInvocation(invocation);
if (enumTypeName == null) return null;

for (final enumElement in library.enums) {
if (enumElement.name3 == enumTypeName) {
return enumElement.thisType;
}
}

// Also check classes (for class-based enums)
for (final classElement in library.classes) {
if (classElement.name3 == enumTypeName) {
return classElement.thisType;
}
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_resolveEnumValuesType only checks library.enums/library.classes, so enums imported from other libraries (or referenced via prefixed imports / re-exports) won’t resolve and will trigger the fallback path. Since the AST is resolved in these builders, prefer extracting T directly from the invocation’s resolved static type (EnumSchema) or from the type argument’s resolved element/type instead of searching only the current library.

Copilot uses AI. Check for mistakes.
return 'List<$nestedType>';
}

if (methodName == 'enumValues') {
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In list element type string extraction, using _extractEnumTypeNameFromInvocation will miss common shapes like alias.UserRole.values (AST is typically a PropertyAccess, not a PrefixedIdentifier) and can again drop prefixes, leading to incorrect List<...> representation strings. Consider reusing the same resolved-type-based approach as enumValues field resolution so list element typing works with prefixed/imported enums too.

Suggested change
if (methodName == 'enumValues') {
if (methodName == 'enumValues') {
// Prefer resolved static types so we correctly handle shapes like
// `alias.UserRole.values` and preserve import prefixes.
final enumArgs = ref.ackBase!.argumentList.arguments;
if (enumArgs.isNotEmpty) {
final argType = enumArgs.first.staticType;
if (argType is InterfaceType) {
InterfaceType? enumType;
// Typical case: `MyEnum.values` has static type `List<MyEnum>`.
if (argType.isDartCoreList && argType.typeArguments.isNotEmpty) {
final elementType = argType.typeArguments.first;
if (elementType is InterfaceType) {
enumType = elementType;
}
} else {
// Fallback: treat the interface type itself as the enum type.
enumType = argType;
}
final enumElement = enumType?.element2;
if (enumElement is EnumElement2) {
// Use the analyzer's display string to keep prefixes and drop nullability.
return enumType!.getDisplayString(withNullability: false);
}
}
}
// Fall back to the existing AST-based extraction to avoid regressions.

Copilot uses AI. Check for mistakes.
Generated examples verified — build_runner produces identical output,
all toJson tests pass, and dart analyze is clean.
…Values

Source-based enum type extraction was prioritized over resolved static
types in _extractEnumTypeNameFromInvocation, which could produce
incorrect generated types when a non-enum class exposes a .values getter
(e.g., Ack.enumValues(holder.values) would generate 'holder' instead of
the actual enum type).
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +629 to +642
/// Prefers resolved static types so imported/re-exported enums and prefixed
/// references (e.g., `alias.UserRole`) are preserved in generated code.
/// Falls back to source-based extraction if static typing is unavailable.
String? _extractEnumTypeNameFromInvocation(MethodInvocation invocation) {
final resolvedType = _resolveEnumValuesType(invocation);
if (resolvedType != null) {
return resolvedType.getDisplayString(withNullability: false);
}

final sourceTypeName = _extractEnumTypeNameFromSource(invocation);
if (sourceTypeName != null) {
return sourceTypeName;
}

Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_extractEnumTypeNameFromInvocation() returns the resolved DartType display string when available. DartType.getDisplayString() does not reliably preserve import prefixes (e.g., models.UserRole), but generated code in a part file must use the same qualified name as the source library when the enum is only in scope via a prefixed import. Consider preferring the source-based extraction (toSource() / .values target) when present (or at least when it contains a prefix), and only falling back to resolved types when source extraction is unavailable.

Suggested change
/// Prefers resolved static types so imported/re-exported enums and prefixed
/// references (e.g., `alias.UserRole`) are preserved in generated code.
/// Falls back to source-based extraction if static typing is unavailable.
String? _extractEnumTypeNameFromInvocation(MethodInvocation invocation) {
final resolvedType = _resolveEnumValuesType(invocation);
if (resolvedType != null) {
return resolvedType.getDisplayString(withNullability: false);
}
final sourceTypeName = _extractEnumTypeNameFromSource(invocation);
if (sourceTypeName != null) {
return sourceTypeName;
}
/// Prefers source-based extraction (`toSource()` / `.values` target) so
/// imported/re-exported enums and prefixed references (e.g., `alias.UserRole`)
/// are preserved in generated code. Falls back to resolved static types when
/// source-based extraction is unavailable.
String? _extractEnumTypeNameFromInvocation(MethodInvocation invocation) {
final sourceTypeName = _extractEnumTypeNameFromSource(invocation);
if (sourceTypeName != null) {
return sourceTypeName;
}
final resolvedType = _resolveEnumValuesType(invocation);
if (resolvedType != null) {
return resolvedType.getDisplayString(withNullability: false);
}

Copilot uses AI. Check for mistakes.
Comment on lines +744 to +766
final scopeResult = library.firstFragment.scope.lookup(normalizedTypeName);
final scopeType = _resolveTypeFromElement(scopeResult.getter);
if (scopeType != null) {
return scopeType;
}

// Try import namespaces directly as a fallback for prefixed names.
for (final import in library.firstFragment.libraryImports) {
final importedElement = import.namespace.get2(normalizedTypeName);
final importedType = _resolveTypeFromElement(importedElement);
if (importedType != null) {
return importedType;
}
}

// Last-resort local lookup.
for (final enumElement in library.enums) {
if (enumElement.name3 == normalizedTypeName) {
return enumElement.thisType;
}
}
for (final classElement in library.classes) {
if (classElement.name3 == normalizedTypeName) {
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_resolveTypeByName() claims to be a fallback for prefixed names, but scope.lookup(normalizedTypeName) / import.namespace.get2(normalizedTypeName) won't resolve a qualified identifier like models.UserRole because lookups are by simple name. If you intend to support prefixed type names here, split on . and resolve via the matching import prefix namespace (or resolve the prefix element first, then the type).

Suggested change
final scopeResult = library.firstFragment.scope.lookup(normalizedTypeName);
final scopeType = _resolveTypeFromElement(scopeResult.getter);
if (scopeType != null) {
return scopeType;
}
// Try import namespaces directly as a fallback for prefixed names.
for (final import in library.firstFragment.libraryImports) {
final importedElement = import.namespace.get2(normalizedTypeName);
final importedType = _resolveTypeFromElement(importedElement);
if (importedType != null) {
return importedType;
}
}
// Last-resort local lookup.
for (final enumElement in library.enums) {
if (enumElement.name3 == normalizedTypeName) {
return enumElement.thisType;
}
}
for (final classElement in library.classes) {
if (classElement.name3 == normalizedTypeName) {
// Support both simple and prefixed names, e.g. `UserRole` and `models.UserRole`.
final dotIndex = normalizedTypeName.indexOf('.');
final String? prefix =
dotIndex == -1 ? null : normalizedTypeName.substring(0, dotIndex);
final String simpleName = dotIndex == -1
? normalizedTypeName
: normalizedTypeName.substring(dotIndex + 1);
// For simple names, try resolving in the library scope first.
if (prefix == null) {
final scopeResult = library.firstFragment.scope.lookup(simpleName);
final scopeType = _resolveTypeFromElement(scopeResult.getter);
if (scopeType != null) {
return scopeType;
}
}
// Try import namespaces directly. For prefixed names, only consider
// imports whose prefix matches; for simple names, consider all imports.
for (final import in library.firstFragment.libraryImports) {
if (prefix != null) {
final prefixElement = import.prefix?.element;
if (prefixElement == null || prefixElement.name3 != prefix) {
continue;
}
}
final importedElement = import.namespace.get2(simpleName);
final importedType = _resolveTypeFromElement(importedElement);
if (importedType != null) {
return importedType;
}
}
// Last-resort local lookup by simple name (local types are not prefixed).
for (final enumElement in library.enums) {
if (enumElement.name3 == simpleName) {
return enumElement.thisType;
}
}
for (final classElement in library.classes) {
if (classElement.name3 == simpleName) {

Copilot uses AI. Check for mistakes.
Comment on lines +615 to +618
_log.warning(
'Could not resolve enum type for Ack.enumValues(); falling back to dynamic.',
);
return (typeProvider.dynamicType, null);
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says enumValues resolution “falls back to String type when the enum cannot be resolved”, but the implementation falls back to dynamic here. Please either update the PR description to match this behavior or change the fallback type to String (if that’s the intended contract).

Copilot uses AI. Check for mistakes.
- `Ack.anyOf(List<AckSchema> schemas)`: Creates an `AnyOfSchema` for union types.
- `Ack.any()`: Creates an `AnySchema` that accepts any value.
- `Ack.discriminated({required String discriminatorKey, required Map<String, AckSchema> schemas})`: Creates a discriminated union schema.
- `Ack.discriminated({required String discriminatorKey, required Map<String, AckSchema<MapValue>> schemas})`: Creates a discriminated union schema.
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs now mention AckSchema<MapValue>, but MapValue isn’t introduced/explained on this page and (from the public exports) may not be importable by consumers without reaching into src/. Consider documenting it (and how users should reference it) or keep the signature in docs as Map<String, AckSchema<Map<String, Object?>>> / Map<String, AckSchema<Map<String, Object?>>> to avoid exposing an internal alias.

Suggested change
- `Ack.discriminated({required String discriminatorKey, required Map<String, AckSchema<MapValue>> schemas})`: Creates a discriminated union schema.
- `Ack.discriminated({required String discriminatorKey, required Map<String, AckSchema<Map<String, Object?>>> schemas})`: Creates a discriminated union schema.

Copilot uses AI. Check for mistakes.
Schema for polymorphic validation based on a discriminator field.

- Created using `Ack.discriminated({required String discriminatorKey, required Map<String, AckSchema> schemas})`
- Created using `Ack.discriminated({required String discriminatorKey, required Map<String, AckSchema<MapValue>> schemas})`
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reference uses AckSchema<MapValue> but MapValue isn’t explained here and may not be part of the stable public surface (it’s not exported from package:ack/ack.dart). Consider replacing with an explicit Map<String, Object?> type in the docs, or add a short note defining MapValue for readers.

Suggested change
- Created using `Ack.discriminated({required String discriminatorKey, required Map<String, AckSchema<MapValue>> schemas})`
- Created using `Ack.discriminated({required String discriminatorKey, required Map<String, AckSchema<Map<String, Object?>>> schemas})`

Copilot uses AI. Check for mistakes.
Convert traditional switch/case statements to concise switch expressions
in field_analyzer and schema_ast_analyzer for improved readability.
_log.warning(
'Could not resolve enum type for Ack.enumValues(); falling back to dynamic.',
);
return (typeProvider.dynamicType, null);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack.enumValues(...) inside Ack.object({...}) can lose enum typing for variable/property inputs without an explicit type arg, and this branch falls back to dynamic.

Why this is a bug:

  • The fallback path is explicit at schema_ast_analyzer.dart:618, which then drives generated field typing to dynamic/Object? instead of enum type.
  • This can happen when source extraction cannot derive an enum name from non-.values forms (schema_ast_analyzer.dart:657).

Code references:

// Enums
if (field.isEnum) {
return field.type.getDisplayString(withNullability: false);
return field.displayTypeOverride ??
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

displayTypeOverride preserves prefixed enum names for getters, but copyWith still types parameters from raw field.type, which drops the import prefix.

Impact:

  • For Ack.enumValues(models.UserRole.values), generated code can become inconsistent:
    • getter: models.UserRole get role (qualified)
    • copyWith: copyWith({UserRole? role}) (unqualified)
  • If UserRole is only available via prefixed import, the unqualified copyWith parameter type is unresolved.

Code references:

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.

3 participants