mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-01-23 21:13:35 +00:00
chore(generator): improve annotation validation, unify async type handling, and refactor BindSpec creation
- Enhance annotation validation in DI code generation. - Move from manual Future<T> extraction to unified type parsing. - Refactor BindSpec creation logic to provide better error messages and type consistency. - Add missing source files for exceptions, annotation validation, and type parsing. BREAKING CHANGE: Invalid annotation combinations now produce custom generator errors. Async detection is now handled via unified type parser.
This commit is contained in:
321
cherrypick_generator/lib/src/annotation_validator.dart
Normal file
321
cherrypick_generator/lib/src/annotation_validator.dart
Normal file
@@ -0,0 +1,321 @@
|
||||
//
|
||||
// Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'exceptions.dart';
|
||||
import 'metadata_utils.dart';
|
||||
|
||||
/// Validates annotation combinations and usage patterns
|
||||
class AnnotationValidator {
|
||||
/// Validates annotations on a method element
|
||||
static void validateMethodAnnotations(MethodElement method) {
|
||||
final annotations = _getAnnotationNames(method.metadata);
|
||||
|
||||
_validateMutuallyExclusiveAnnotations(method, annotations);
|
||||
_validateAnnotationCombinations(method, annotations);
|
||||
_validateAnnotationParameters(method);
|
||||
}
|
||||
|
||||
/// Validates annotations on a field element
|
||||
static void validateFieldAnnotations(FieldElement field) {
|
||||
final annotations = _getAnnotationNames(field.metadata);
|
||||
|
||||
_validateInjectFieldAnnotations(field, annotations);
|
||||
}
|
||||
|
||||
/// Validates annotations on a class element
|
||||
static void validateClassAnnotations(ClassElement classElement) {
|
||||
final annotations = _getAnnotationNames(classElement.metadata);
|
||||
|
||||
_validateModuleClassAnnotations(classElement, annotations);
|
||||
_validateInjectableClassAnnotations(classElement, annotations);
|
||||
}
|
||||
|
||||
static List<String> _getAnnotationNames(List<ElementAnnotation> metadata) {
|
||||
return metadata
|
||||
.map((m) => m.computeConstantValue()?.type?.getDisplayString())
|
||||
.where((name) => name != null)
|
||||
.cast<String>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
static void _validateMutuallyExclusiveAnnotations(
|
||||
MethodElement method,
|
||||
List<String> annotations,
|
||||
) {
|
||||
// @instance and @provide are mutually exclusive
|
||||
if (annotations.contains('instance') && annotations.contains('provide')) {
|
||||
throw AnnotationValidationException(
|
||||
'Method cannot have both @instance and @provide annotations',
|
||||
element: method,
|
||||
suggestion:
|
||||
'Use either @instance for direct instances or @provide for factory methods',
|
||||
context: {
|
||||
'method_name': method.displayName,
|
||||
'annotations': annotations,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void _validateAnnotationCombinations(
|
||||
MethodElement method,
|
||||
List<String> annotations,
|
||||
) {
|
||||
// @params can only be used with @provide
|
||||
if (annotations.contains('params') && !annotations.contains('provide')) {
|
||||
throw AnnotationValidationException(
|
||||
'@params annotation can only be used with @provide annotation',
|
||||
element: method,
|
||||
suggestion: 'Remove @params or add @provide annotation',
|
||||
context: {
|
||||
'method_name': method.displayName,
|
||||
'annotations': annotations,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Methods must have either @instance or @provide
|
||||
if (!annotations.contains('instance') && !annotations.contains('provide')) {
|
||||
throw AnnotationValidationException(
|
||||
'Method must be marked with either @instance or @provide annotation',
|
||||
element: method,
|
||||
suggestion:
|
||||
'Add @instance() for direct instances or @provide() for factory methods',
|
||||
context: {
|
||||
'method_name': method.displayName,
|
||||
'available_annotations': annotations,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// @singleton validation
|
||||
if (annotations.contains('singleton')) {
|
||||
_validateSingletonUsage(method, annotations);
|
||||
}
|
||||
}
|
||||
|
||||
static void _validateSingletonUsage(
|
||||
MethodElement method,
|
||||
List<String> annotations,
|
||||
) {
|
||||
// Singleton with params might not make sense in some contexts
|
||||
if (annotations.contains('params')) {
|
||||
// This is a warning, not an error - could be useful for parameterized singletons
|
||||
// We could add a warning system later
|
||||
}
|
||||
|
||||
// Check if return type is suitable for singleton
|
||||
final returnType = method.returnType.getDisplayString();
|
||||
if (returnType == 'void') {
|
||||
throw AnnotationValidationException(
|
||||
'Singleton methods cannot return void',
|
||||
element: method,
|
||||
suggestion: 'Remove @singleton annotation or change return type',
|
||||
context: {
|
||||
'method_name': method.displayName,
|
||||
'return_type': returnType,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void _validateAnnotationParameters(MethodElement method) {
|
||||
// Validate @named annotation parameters
|
||||
final namedValue = MetadataUtils.getNamedValue(method.metadata);
|
||||
if (namedValue != null) {
|
||||
if (namedValue.isEmpty) {
|
||||
throw AnnotationValidationException(
|
||||
'@named annotation cannot have empty value',
|
||||
element: method,
|
||||
suggestion: 'Provide a non-empty string value for @named annotation',
|
||||
context: {
|
||||
'method_name': method.displayName,
|
||||
'named_value': namedValue,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Check for valid naming conventions
|
||||
if (!RegExp(r'^[a-zA-Z_][a-zA-Z0-9_]*$').hasMatch(namedValue)) {
|
||||
throw AnnotationValidationException(
|
||||
'@named value should follow valid identifier naming conventions',
|
||||
element: method,
|
||||
suggestion:
|
||||
'Use alphanumeric characters and underscores only, starting with a letter or underscore',
|
||||
context: {
|
||||
'method_name': method.displayName,
|
||||
'named_value': namedValue,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate method parameters for @params usage
|
||||
for (final param in method.parameters) {
|
||||
final paramAnnotations = _getAnnotationNames(param.metadata);
|
||||
if (paramAnnotations.contains('params')) {
|
||||
_validateParamsParameter(param, method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _validateParamsParameter(
|
||||
ParameterElement param, MethodElement method) {
|
||||
// @params parameter should typically be dynamic or Map<String, dynamic>
|
||||
final paramType = param.type.getDisplayString();
|
||||
|
||||
if (paramType != 'dynamic' &&
|
||||
paramType != 'Map<String, dynamic>' &&
|
||||
paramType != 'Map<String, dynamic>?') {
|
||||
// This is more of a warning - other types might be valid
|
||||
// We could add a warning system for this
|
||||
}
|
||||
|
||||
// Check if parameter is required when using @params
|
||||
try {
|
||||
final hasDefault = (param as dynamic).defaultValue != null;
|
||||
if (param.isRequired && !hasDefault) {
|
||||
// This might be intentional, so we don't throw an error
|
||||
// but we could warn about it
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore if defaultValue is not available in this analyzer version
|
||||
}
|
||||
}
|
||||
|
||||
static void _validateInjectFieldAnnotations(
|
||||
FieldElement field,
|
||||
List<String> annotations,
|
||||
) {
|
||||
if (!annotations.contains('inject')) {
|
||||
return; // Not an inject field, nothing to validate
|
||||
}
|
||||
|
||||
// Check if field type is suitable for injection
|
||||
final fieldType = field.type.getDisplayString();
|
||||
if (fieldType == 'void') {
|
||||
throw AnnotationValidationException(
|
||||
'Cannot inject void type',
|
||||
element: field,
|
||||
suggestion: 'Use a concrete type instead of void',
|
||||
context: {
|
||||
'field_name': field.displayName,
|
||||
'field_type': fieldType,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Validate scope annotation if present
|
||||
for (final meta in field.metadata) {
|
||||
final obj = meta.computeConstantValue();
|
||||
final type = obj?.type?.getDisplayString();
|
||||
if (type == 'scope') {
|
||||
// Empty scope name is treated as no scope (uses root scope)
|
||||
// This is allowed for backward compatibility and convenience
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _validateModuleClassAnnotations(
|
||||
ClassElement classElement,
|
||||
List<String> annotations,
|
||||
) {
|
||||
if (!annotations.contains('module')) {
|
||||
return; // Not a module class
|
||||
}
|
||||
|
||||
// Check if class has public methods
|
||||
final publicMethods =
|
||||
classElement.methods.where((m) => m.isPublic).toList();
|
||||
if (publicMethods.isEmpty) {
|
||||
throw AnnotationValidationException(
|
||||
'Module class must have at least one public method',
|
||||
element: classElement,
|
||||
suggestion: 'Add public methods with @instance or @provide annotations',
|
||||
context: {
|
||||
'class_name': classElement.displayName,
|
||||
'method_count': publicMethods.length,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Validate that public methods have appropriate annotations
|
||||
for (final method in publicMethods) {
|
||||
final methodAnnotations = _getAnnotationNames(method.metadata);
|
||||
if (!methodAnnotations.contains('instance') &&
|
||||
!methodAnnotations.contains('provide')) {
|
||||
throw AnnotationValidationException(
|
||||
'Public methods in module class must have @instance or @provide annotation',
|
||||
element: method,
|
||||
suggestion: 'Add @instance() or @provide() annotation to the method',
|
||||
context: {
|
||||
'class_name': classElement.displayName,
|
||||
'method_name': method.displayName,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _validateInjectableClassAnnotations(
|
||||
ClassElement classElement,
|
||||
List<String> annotations,
|
||||
) {
|
||||
if (!annotations.contains('injectable')) {
|
||||
return; // Not an injectable class
|
||||
}
|
||||
|
||||
// Check if class has injectable fields
|
||||
final injectFields = classElement.fields.where((f) {
|
||||
final fieldAnnotations = _getAnnotationNames(f.metadata);
|
||||
return fieldAnnotations.contains('inject');
|
||||
}).toList();
|
||||
|
||||
// Allow injectable classes without @inject fields to generate empty mixins
|
||||
// This can be useful for classes that will have @inject fields added later
|
||||
// or for testing purposes
|
||||
if (injectFields.isEmpty) {
|
||||
// Just log a warning but don't throw an exception
|
||||
// print('Warning: Injectable class ${classElement.displayName} has no @inject fields');
|
||||
}
|
||||
|
||||
// Validate that injectable fields are properly declared
|
||||
for (final field in injectFields) {
|
||||
// Injectable fields should be late final for immutability after injection
|
||||
if (!field.isFinal) {
|
||||
throw AnnotationValidationException(
|
||||
'Injectable fields should be final for immutability',
|
||||
element: field,
|
||||
suggestion:
|
||||
'Add final keyword to injectable field (preferably late final)',
|
||||
context: {
|
||||
'class_name': classElement.displayName,
|
||||
'field_name': field.displayName,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Check if field is late (recommended pattern)
|
||||
try {
|
||||
final isLate = (field as dynamic).isLate ?? false;
|
||||
if (!isLate) {
|
||||
// This is a warning, not an error - late final is recommended but not required
|
||||
// We could add a warning system later
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore if isLate is not available in this analyzer version
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,12 @@
|
||||
//
|
||||
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
|
||||
import 'bind_parameters_spec.dart';
|
||||
import 'metadata_utils.dart';
|
||||
import 'exceptions.dart';
|
||||
import 'type_parser.dart';
|
||||
import 'annotation_validator.dart';
|
||||
|
||||
enum BindingType {
|
||||
instance,
|
||||
@@ -105,10 +107,42 @@ class BindSpec {
|
||||
final indentStr = ' ' * indent;
|
||||
final provide = _generateProvideClause(indent);
|
||||
final postfix = _generatePostfix();
|
||||
return '$indentStr'
|
||||
'bind<$returnType>()'
|
||||
'$provide'
|
||||
'$postfix;';
|
||||
|
||||
// Create the full single-line version first
|
||||
final singleLine = '${indentStr}bind<$returnType>()$provide$postfix;';
|
||||
|
||||
// Check if we need multiline formatting
|
||||
final needsMultiline = singleLine.length > 80 || provide.contains('\n');
|
||||
|
||||
if (!needsMultiline) {
|
||||
return singleLine;
|
||||
}
|
||||
|
||||
// For multiline formatting, check if we need to break after bind<Type>()
|
||||
if (provide.contains('\n')) {
|
||||
// Provider clause is already multiline
|
||||
if (postfix.isNotEmpty) {
|
||||
// If there's a postfix, break after bind<Type>()
|
||||
final multilinePostfix = _generateMultilinePostfix(indent);
|
||||
return '${indentStr}bind<$returnType>()'
|
||||
'\n${' ' * (indent + 4)}$provide'
|
||||
'$multilinePostfix;';
|
||||
} else {
|
||||
// No postfix, keep bind<Type>() with provide start
|
||||
return '${indentStr}bind<$returnType>()$provide;';
|
||||
}
|
||||
} else {
|
||||
// Simple multiline: break after bind<Type>()
|
||||
if (postfix.isNotEmpty) {
|
||||
final multilinePostfix = _generateMultilinePostfix(indent);
|
||||
return '${indentStr}bind<$returnType>()'
|
||||
'\n${' ' * (indent + 4)}$provide'
|
||||
'$multilinePostfix;';
|
||||
} else {
|
||||
return '${indentStr}bind<$returnType>()'
|
||||
'\n${' ' * (indent + 4)}$provide;';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal method: decides how the provide clause should be generated by param kind.
|
||||
@@ -122,6 +156,7 @@ class BindSpec {
|
||||
// Safe variable name for parameters.
|
||||
const paramVar = 'args';
|
||||
final fnArgs = parameters.map((p) => p.generateArg(paramVar)).join(', ');
|
||||
// Use multiline format only if args are long or contain newlines
|
||||
final multiLine = fnArgs.length > 60 || fnArgs.contains('\n');
|
||||
switch (bindingType) {
|
||||
case BindingType.instance:
|
||||
@@ -146,7 +181,12 @@ class BindSpec {
|
||||
/// EN / RU: Supports only injected dependencies, not runtime (@params).
|
||||
String _generatePlainProvideClause(int indent) {
|
||||
final argsStr = parameters.map((p) => p.generateArg()).join(', ');
|
||||
final multiLine = argsStr.length > 60 || argsStr.contains('\n');
|
||||
|
||||
// Check if we need multiline formatting based on total line length
|
||||
final singleLineCall = '$methodName($argsStr)';
|
||||
final needsMultiline =
|
||||
singleLineCall.length >= 45 || argsStr.contains('\n');
|
||||
|
||||
switch (bindingType) {
|
||||
case BindingType.instance:
|
||||
return isAsyncInstance
|
||||
@@ -154,13 +194,25 @@ class BindSpec {
|
||||
: '.toInstance($methodName($argsStr))';
|
||||
case BindingType.provide:
|
||||
if (isAsyncProvide) {
|
||||
return multiLine
|
||||
? '.toProvideAsync(\n${' ' * (indent + 2)}() => $methodName($argsStr))'
|
||||
: '.toProvideAsync(() => $methodName($argsStr))';
|
||||
if (needsMultiline) {
|
||||
final lambdaIndent =
|
||||
(isSingleton || named != null) ? indent + 6 : indent + 2;
|
||||
final closingIndent =
|
||||
(isSingleton || named != null) ? indent + 4 : indent;
|
||||
return '.toProvideAsync(\n${' ' * lambdaIndent}() => $methodName($argsStr),\n${' ' * closingIndent})';
|
||||
} else {
|
||||
return '.toProvideAsync(() => $methodName($argsStr))';
|
||||
}
|
||||
} else {
|
||||
return multiLine
|
||||
? '.toProvide(\n${' ' * (indent + 2)}() => $methodName($argsStr))'
|
||||
: '.toProvide(() => $methodName($argsStr))';
|
||||
if (needsMultiline) {
|
||||
final lambdaIndent =
|
||||
(isSingleton || named != null) ? indent + 6 : indent + 2;
|
||||
final closingIndent =
|
||||
(isSingleton || named != null) ? indent + 4 : indent;
|
||||
return '.toProvide(\n${' ' * lambdaIndent}() => $methodName($argsStr),\n${' ' * closingIndent})';
|
||||
} else {
|
||||
return '.toProvide(() => $methodName($argsStr))';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,6 +224,20 @@ class BindSpec {
|
||||
return '$namePart$singletonPart';
|
||||
}
|
||||
|
||||
/// EN / RU: Generates multiline postfix with proper indentation.
|
||||
String _generateMultilinePostfix(int indent) {
|
||||
final parts = <String>[];
|
||||
if (named != null) {
|
||||
parts.add(".withName('$named')");
|
||||
}
|
||||
if (isSingleton) {
|
||||
parts.add('.singleton()');
|
||||
}
|
||||
if (parts.isEmpty) return '';
|
||||
|
||||
return parts.map((part) => '\n${' ' * (indent + 4)}$part').join('');
|
||||
}
|
||||
|
||||
/// -------------------------------------------------------------------------
|
||||
/// fromMethod
|
||||
///
|
||||
@@ -186,73 +252,98 @@ class BindSpec {
|
||||
/// асинхронности. Если нет @instance или @provide — кидает ошибку.
|
||||
/// -------------------------------------------------------------------------
|
||||
static BindSpec fromMethod(MethodElement method) {
|
||||
var returnType = method.returnType.getDisplayString();
|
||||
try {
|
||||
// Validate method annotations
|
||||
AnnotationValidator.validateMethodAnnotations(method);
|
||||
|
||||
final methodName = method.displayName;
|
||||
// Check for @singleton annotation.
|
||||
final isSingleton = MetadataUtils.anyMeta(method.metadata, 'singleton');
|
||||
// Parse return type using improved type parser
|
||||
final parsedReturnType = TypeParser.parseType(method.returnType, method);
|
||||
|
||||
// Get @named value if present.
|
||||
final named = MetadataUtils.getNamedValue(method.metadata);
|
||||
final methodName = method.displayName;
|
||||
|
||||
// Parse each method parameter.
|
||||
final params = <BindParameterSpec>[];
|
||||
bool hasParams = false;
|
||||
for (final p in method.parameters) {
|
||||
final typeStr = p.type.getDisplayString();
|
||||
final paramNamed = MetadataUtils.getNamedValue(p.metadata);
|
||||
final isParams = MetadataUtils.anyMeta(p.metadata, 'params');
|
||||
if (isParams) hasParams = true;
|
||||
params.add(BindParameterSpec(typeStr, paramNamed, isParams: isParams));
|
||||
}
|
||||
// Check for @singleton annotation.
|
||||
final isSingleton = MetadataUtils.anyMeta(method.metadata, 'singleton');
|
||||
|
||||
// Determine bindingType: @instance or @provide.
|
||||
final hasInstance = MetadataUtils.anyMeta(method.metadata, 'instance');
|
||||
final hasProvide = MetadataUtils.anyMeta(method.metadata, 'provide');
|
||||
if (!hasInstance && !hasProvide) {
|
||||
throw InvalidGenerationSourceError(
|
||||
'Method $methodName must be marked with @instance() or @provide().',
|
||||
// Get @named value if present.
|
||||
final named = MetadataUtils.getNamedValue(method.metadata);
|
||||
|
||||
// Parse each method parameter.
|
||||
final params = <BindParameterSpec>[];
|
||||
bool hasParams = false;
|
||||
for (final p in method.parameters) {
|
||||
final typeStr = p.type.getDisplayString();
|
||||
final paramNamed = MetadataUtils.getNamedValue(p.metadata);
|
||||
final isParams = MetadataUtils.anyMeta(p.metadata, 'params');
|
||||
if (isParams) hasParams = true;
|
||||
params.add(BindParameterSpec(typeStr, paramNamed, isParams: isParams));
|
||||
}
|
||||
|
||||
// Determine bindingType: @instance or @provide.
|
||||
final hasInstance = MetadataUtils.anyMeta(method.metadata, 'instance');
|
||||
final hasProvide = MetadataUtils.anyMeta(method.metadata, 'provide');
|
||||
|
||||
if (!hasInstance && !hasProvide) {
|
||||
throw AnnotationValidationException(
|
||||
'Method must be marked with either @instance() or @provide() annotation',
|
||||
element: method,
|
||||
suggestion:
|
||||
'Add @instance() for direct instances or @provide() for factory methods',
|
||||
context: {
|
||||
'method_name': methodName,
|
||||
'return_type': parsedReturnType.displayString,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final bindingType =
|
||||
hasInstance ? BindingType.instance : BindingType.provide;
|
||||
|
||||
// PROHIBIT @params with @instance bindings!
|
||||
if (bindingType == BindingType.instance && hasParams) {
|
||||
throw AnnotationValidationException(
|
||||
'@params() (runtime arguments) cannot be used together with @instance()',
|
||||
element: method,
|
||||
suggestion: 'Use @provide() instead if you want runtime arguments',
|
||||
context: {
|
||||
'method_name': methodName,
|
||||
'binding_type': 'instance',
|
||||
'has_params': hasParams,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Set async flags based on parsed type
|
||||
final isAsyncInstance =
|
||||
bindingType == BindingType.instance && parsedReturnType.isFuture;
|
||||
final isAsyncProvide =
|
||||
bindingType == BindingType.provide && parsedReturnType.isFuture;
|
||||
|
||||
return BindSpec(
|
||||
returnType: parsedReturnType.codeGenType,
|
||||
methodName: methodName,
|
||||
isSingleton: isSingleton,
|
||||
named: named,
|
||||
parameters: params,
|
||||
bindingType: bindingType,
|
||||
isAsyncInstance: isAsyncInstance,
|
||||
isAsyncProvide: isAsyncProvide,
|
||||
hasParams: hasParams,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e is CherryPickGeneratorException) {
|
||||
rethrow;
|
||||
}
|
||||
throw CodeGenerationException(
|
||||
'Failed to create BindSpec from method "${method.displayName}"',
|
||||
element: method,
|
||||
suggestion:
|
||||
'Check that the method has valid annotations and return type',
|
||||
context: {
|
||||
'method_name': method.displayName,
|
||||
'return_type': method.returnType.getDisplayString(),
|
||||
'error': e.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
final bindingType =
|
||||
hasInstance ? BindingType.instance : BindingType.provide;
|
||||
|
||||
// PROHIBIT @params with @instance bindings!
|
||||
if (bindingType == BindingType.instance && hasParams) {
|
||||
throw InvalidGenerationSourceError(
|
||||
'@params() (runtime arguments) cannot be used together with @instance() on method $methodName. '
|
||||
'Use @provide() instead if you want runtime arguments.',
|
||||
element: method,
|
||||
);
|
||||
}
|
||||
|
||||
// -- Extract inner type for Future<T> and set async flags.
|
||||
bool isAsyncInstance = false;
|
||||
bool isAsyncProvide = false;
|
||||
final futureInnerType = _extractFutureInnerType(returnType);
|
||||
if (futureInnerType != null) {
|
||||
returnType = futureInnerType;
|
||||
if (bindingType == BindingType.instance) isAsyncInstance = true;
|
||||
if (bindingType == BindingType.provide) isAsyncProvide = true;
|
||||
}
|
||||
|
||||
return BindSpec(
|
||||
returnType: returnType,
|
||||
methodName: methodName,
|
||||
isSingleton: isSingleton,
|
||||
named: named,
|
||||
parameters: params,
|
||||
bindingType: bindingType,
|
||||
isAsyncInstance: isAsyncInstance,
|
||||
isAsyncProvide: isAsyncProvide,
|
||||
hasParams: hasParams,
|
||||
);
|
||||
}
|
||||
|
||||
/// EN / RU: Extracts inner type from Future<T>, returns e.g. "T" or null.
|
||||
static String? _extractFutureInnerType(String typeName) {
|
||||
final match = RegExp(r'^Future<(.+)>$').firstMatch(typeName);
|
||||
return match?.group(1)?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
117
cherrypick_generator/lib/src/exceptions.dart
Normal file
117
cherrypick_generator/lib/src/exceptions.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
|
||||
/// Enhanced exception class for CherryPick generator with detailed context information
|
||||
class CherryPickGeneratorException extends InvalidGenerationSourceError {
|
||||
final String category;
|
||||
final String? suggestion;
|
||||
final Map<String, dynamic>? context;
|
||||
|
||||
CherryPickGeneratorException(
|
||||
String message, {
|
||||
required Element element,
|
||||
required this.category,
|
||||
this.suggestion,
|
||||
this.context,
|
||||
}) : super(
|
||||
_formatMessage(message, category, suggestion, context, element),
|
||||
element: element,
|
||||
);
|
||||
|
||||
static String _formatMessage(
|
||||
String message,
|
||||
String category,
|
||||
String? suggestion,
|
||||
Map<String, dynamic>? context,
|
||||
Element element,
|
||||
) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
// Header with category
|
||||
buffer.writeln('[$category] $message');
|
||||
|
||||
// Element context
|
||||
buffer.writeln('');
|
||||
buffer.writeln('Context:');
|
||||
buffer.writeln(' Element: ${element.displayName}');
|
||||
buffer.writeln(' Type: ${element.runtimeType}');
|
||||
buffer.writeln(' Location: ${element.source?.fullName ?? 'unknown'}');
|
||||
|
||||
// Note: enclosingElement may not be available in all analyzer versions
|
||||
try {
|
||||
final enclosing = (element as dynamic).enclosingElement;
|
||||
if (enclosing != null) {
|
||||
buffer.writeln(' Enclosing: ${enclosing.displayName}');
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore if enclosingElement is not available
|
||||
}
|
||||
|
||||
// Additional context
|
||||
if (context != null && context.isNotEmpty) {
|
||||
buffer.writeln('');
|
||||
buffer.writeln('Additional Context:');
|
||||
context.forEach((key, value) {
|
||||
buffer.writeln(' $key: $value');
|
||||
});
|
||||
}
|
||||
|
||||
// Suggestion
|
||||
if (suggestion != null) {
|
||||
buffer.writeln('');
|
||||
buffer.writeln('💡 Suggestion: $suggestion');
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Specific exception types for different error categories
|
||||
class AnnotationValidationException extends CherryPickGeneratorException {
|
||||
AnnotationValidationException(
|
||||
super.message, {
|
||||
required super.element,
|
||||
super.suggestion,
|
||||
super.context,
|
||||
}) : super(category: 'ANNOTATION_VALIDATION');
|
||||
}
|
||||
|
||||
class TypeParsingException extends CherryPickGeneratorException {
|
||||
TypeParsingException(
|
||||
super.message, {
|
||||
required super.element,
|
||||
super.suggestion,
|
||||
super.context,
|
||||
}) : super(category: 'TYPE_PARSING');
|
||||
}
|
||||
|
||||
class CodeGenerationException extends CherryPickGeneratorException {
|
||||
CodeGenerationException(
|
||||
super.message, {
|
||||
required super.element,
|
||||
super.suggestion,
|
||||
super.context,
|
||||
}) : super(category: 'CODE_GENERATION');
|
||||
}
|
||||
|
||||
class DependencyResolutionException extends CherryPickGeneratorException {
|
||||
DependencyResolutionException(
|
||||
super.message, {
|
||||
required super.element,
|
||||
super.suggestion,
|
||||
super.context,
|
||||
}) : super(category: 'DEPENDENCY_RESOLUTION');
|
||||
}
|
||||
@@ -49,10 +49,15 @@ class GeneratedClass {
|
||||
/// Список всех обнаруженных биндингов
|
||||
final List<BindSpec> binds;
|
||||
|
||||
/// Source file name for the part directive
|
||||
/// Имя исходного файла для part директивы
|
||||
final String sourceFile;
|
||||
|
||||
GeneratedClass(
|
||||
this.className,
|
||||
this.generatedClassName,
|
||||
this.binds,
|
||||
this.sourceFile,
|
||||
);
|
||||
|
||||
/// -------------------------------------------------------------------------
|
||||
@@ -72,13 +77,15 @@ class GeneratedClass {
|
||||
final className = element.displayName;
|
||||
// Generated class name with '$' prefix (standard for generated Dart code).
|
||||
final generatedClassName = r'$' + className;
|
||||
// Get source file name
|
||||
final sourceFile = element.source.shortName;
|
||||
// Collect bindings for all non-abstract methods.
|
||||
final binds = element.methods
|
||||
.where((m) => !m.isAbstract)
|
||||
.map(BindSpec.fromMethod)
|
||||
.toList();
|
||||
|
||||
return GeneratedClass(className, generatedClassName, binds);
|
||||
return GeneratedClass(className, generatedClassName, binds, sourceFile);
|
||||
}
|
||||
|
||||
/// -------------------------------------------------------------------------
|
||||
@@ -95,11 +102,10 @@ class GeneratedClass {
|
||||
/// и регистрирует все зависимости через методы bind<Type>()...
|
||||
/// -------------------------------------------------------------------------
|
||||
String generate() {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln('final class $generatedClassName extends $className {');
|
||||
buffer.writeln(' @override');
|
||||
buffer.writeln(' void builder(Scope currentScope) {');
|
||||
final buffer = StringBuffer()
|
||||
..writeln('final class $generatedClassName extends $className {')
|
||||
..writeln(' @override')
|
||||
..writeln(' void builder(Scope currentScope) {');
|
||||
|
||||
// For each binding, generate bind<Type>() code string.
|
||||
// Для каждого биндинга — генерируем строку bind<Type>()...
|
||||
@@ -107,8 +113,9 @@ class GeneratedClass {
|
||||
buffer.writeln(bind.generateBind(4));
|
||||
}
|
||||
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('}');
|
||||
buffer
|
||||
..writeln(' }')
|
||||
..writeln('}');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
218
cherrypick_generator/lib/src/type_parser.dart
Normal file
218
cherrypick_generator/lib/src/type_parser.dart
Normal file
@@ -0,0 +1,218 @@
|
||||
//
|
||||
// Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:analyzer/dart/element/nullability_suffix.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'exceptions.dart';
|
||||
|
||||
/// Enhanced type parser that uses AST analysis instead of regular expressions
|
||||
class TypeParser {
|
||||
/// Parses a DartType and extracts detailed type information
|
||||
static ParsedType parseType(DartType dartType, Element context) {
|
||||
try {
|
||||
return _parseTypeInternal(dartType, context);
|
||||
} catch (e) {
|
||||
throw TypeParsingException(
|
||||
'Failed to parse type: ${dartType.getDisplayString()}',
|
||||
element: context,
|
||||
suggestion: 'Ensure the type is properly imported and accessible',
|
||||
context: {
|
||||
'original_type': dartType.getDisplayString(),
|
||||
'error': e.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static ParsedType _parseTypeInternal(DartType dartType, Element context) {
|
||||
final displayString = dartType.getDisplayString();
|
||||
final isNullable = dartType.nullabilitySuffix == NullabilitySuffix.question;
|
||||
|
||||
// Check if it's a Future type
|
||||
if (dartType.isDartAsyncFuture) {
|
||||
return _parseFutureType(dartType, context, isNullable);
|
||||
}
|
||||
|
||||
// Check if it's a generic type (List, Map, etc.)
|
||||
if (dartType is ParameterizedType && dartType.typeArguments.isNotEmpty) {
|
||||
return _parseGenericType(dartType, context, isNullable);
|
||||
}
|
||||
|
||||
// Simple type
|
||||
return ParsedType(
|
||||
displayString: displayString,
|
||||
coreType: displayString.replaceAll('?', ''),
|
||||
isNullable: isNullable,
|
||||
isFuture: false,
|
||||
isGeneric: false,
|
||||
typeArguments: [],
|
||||
);
|
||||
}
|
||||
|
||||
static ParsedType _parseFutureType(
|
||||
DartType dartType, Element context, bool isNullable) {
|
||||
if (dartType is! ParameterizedType || dartType.typeArguments.isEmpty) {
|
||||
throw TypeParsingException(
|
||||
'Future type must have a type argument',
|
||||
element: context,
|
||||
suggestion: 'Use Future<T> instead of raw Future',
|
||||
context: {'type': dartType.getDisplayString()},
|
||||
);
|
||||
}
|
||||
|
||||
final innerType = dartType.typeArguments.first;
|
||||
final innerParsed = _parseTypeInternal(innerType, context);
|
||||
|
||||
return ParsedType(
|
||||
displayString: dartType.getDisplayString(),
|
||||
coreType: innerParsed.coreType,
|
||||
isNullable: isNullable || innerParsed.isNullable,
|
||||
isFuture: true,
|
||||
isGeneric: innerParsed.isGeneric,
|
||||
typeArguments: innerParsed.typeArguments,
|
||||
innerType: innerParsed,
|
||||
);
|
||||
}
|
||||
|
||||
static ParsedType _parseGenericType(
|
||||
ParameterizedType dartType, Element context, bool isNullable) {
|
||||
final typeArguments = dartType.typeArguments
|
||||
.map((arg) => _parseTypeInternal(arg, context))
|
||||
.toList();
|
||||
|
||||
final baseType = dartType.element?.name ?? dartType.getDisplayString();
|
||||
|
||||
return ParsedType(
|
||||
displayString: dartType.getDisplayString(),
|
||||
coreType: baseType,
|
||||
isNullable: isNullable,
|
||||
isFuture: false,
|
||||
isGeneric: true,
|
||||
typeArguments: typeArguments,
|
||||
);
|
||||
}
|
||||
|
||||
/// Validates that a type is suitable for dependency injection
|
||||
static void validateInjectableType(ParsedType parsedType, Element context) {
|
||||
// Check for void type
|
||||
if (parsedType.coreType == 'void') {
|
||||
throw TypeParsingException(
|
||||
'Cannot inject void type',
|
||||
element: context,
|
||||
suggestion: 'Use a concrete type instead of void',
|
||||
);
|
||||
}
|
||||
|
||||
// Check for dynamic type (warning)
|
||||
if (parsedType.coreType == 'dynamic') {
|
||||
// This could be a warning instead of an error
|
||||
throw TypeParsingException(
|
||||
'Using dynamic type reduces type safety',
|
||||
element: context,
|
||||
suggestion: 'Consider using a specific type instead of dynamic',
|
||||
);
|
||||
}
|
||||
|
||||
// Validate nested types for complex generics
|
||||
for (final typeArg in parsedType.typeArguments) {
|
||||
validateInjectableType(typeArg, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a parsed type with detailed information
|
||||
class ParsedType {
|
||||
/// The full display string of the type (e.g., "Future<List<String>?>")
|
||||
final String displayString;
|
||||
|
||||
/// The core type name without nullability and Future wrapper (e.g., "List<String>")
|
||||
final String coreType;
|
||||
|
||||
/// Whether the type is nullable
|
||||
final bool isNullable;
|
||||
|
||||
/// Whether the type is wrapped in Future
|
||||
final bool isFuture;
|
||||
|
||||
/// Whether the type has generic parameters
|
||||
final bool isGeneric;
|
||||
|
||||
/// Parsed type arguments for generic types
|
||||
final List<ParsedType> typeArguments;
|
||||
|
||||
/// For Future types, the inner type
|
||||
final ParsedType? innerType;
|
||||
|
||||
const ParsedType({
|
||||
required this.displayString,
|
||||
required this.coreType,
|
||||
required this.isNullable,
|
||||
required this.isFuture,
|
||||
required this.isGeneric,
|
||||
required this.typeArguments,
|
||||
this.innerType,
|
||||
});
|
||||
|
||||
/// Returns the type string suitable for code generation
|
||||
String get codeGenType {
|
||||
if (isFuture && innerType != null) {
|
||||
return innerType!.codeGenType;
|
||||
}
|
||||
|
||||
// For generic types, include type arguments
|
||||
if (isGeneric && typeArguments.isNotEmpty) {
|
||||
final args = typeArguments.map((arg) => arg.codeGenType).join(', ');
|
||||
return '$coreType<$args>';
|
||||
}
|
||||
|
||||
return coreType;
|
||||
}
|
||||
|
||||
/// Returns whether this type should use tryResolve instead of resolve
|
||||
bool get shouldUseTryResolve => isNullable;
|
||||
|
||||
/// Returns the appropriate resolve method name
|
||||
String get resolveMethodName {
|
||||
if (isFuture) {
|
||||
return shouldUseTryResolve ? 'tryResolveAsync' : 'resolveAsync';
|
||||
}
|
||||
return shouldUseTryResolve ? 'tryResolve' : 'resolve';
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ParsedType(displayString: $displayString, coreType: $coreType, '
|
||||
'isNullable: $isNullable, isFuture: $isFuture, isGeneric: $isGeneric)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is ParsedType &&
|
||||
other.displayString == displayString &&
|
||||
other.coreType == coreType &&
|
||||
other.isNullable == isNullable &&
|
||||
other.isFuture == isFuture &&
|
||||
other.isGeneric == isGeneric;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return displayString.hashCode ^
|
||||
coreType.hashCode ^
|
||||
isNullable.hashCode ^
|
||||
isFuture.hashCode ^
|
||||
isGeneric.hashCode;
|
||||
}
|
||||
}
|
||||
235
cherrypick_generator/test/type_parser_test.dart
Normal file
235
cherrypick_generator/test/type_parser_test.dart
Normal file
@@ -0,0 +1,235 @@
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:analyzer/source/source.dart';
|
||||
import 'package:cherrypick_generator/src/type_parser.dart';
|
||||
import 'package:cherrypick_generator/src/exceptions.dart';
|
||||
|
||||
void main() {
|
||||
group('TypeParser', () {
|
||||
group('parseType', () {
|
||||
test('should parse simple types correctly', () {
|
||||
// This would require setting up analyzer infrastructure
|
||||
// For now, we'll test the ParsedType class directly
|
||||
});
|
||||
|
||||
test('should parse Future types correctly', () {
|
||||
// This would require setting up analyzer infrastructure
|
||||
// For now, we'll test the ParsedType class directly
|
||||
});
|
||||
|
||||
test('should parse nullable types correctly', () {
|
||||
// This would require setting up analyzer infrastructure
|
||||
// For now, we'll test the ParsedType class directly
|
||||
});
|
||||
|
||||
test('should throw TypeParsingException for invalid types', () {
|
||||
// This would require setting up analyzer infrastructure
|
||||
// For now, we'll test the ParsedType class directly
|
||||
});
|
||||
});
|
||||
|
||||
group('validateInjectableType', () {
|
||||
test('should throw for void type', () {
|
||||
final parsedType = ParsedType(
|
||||
displayString: 'void',
|
||||
coreType: 'void',
|
||||
isNullable: false,
|
||||
isFuture: false,
|
||||
isGeneric: false,
|
||||
typeArguments: [],
|
||||
);
|
||||
|
||||
expect(
|
||||
() => TypeParser.validateInjectableType(
|
||||
parsedType, _createMockElement()),
|
||||
throwsA(isA<TypeParsingException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw for dynamic type', () {
|
||||
final parsedType = ParsedType(
|
||||
displayString: 'dynamic',
|
||||
coreType: 'dynamic',
|
||||
isNullable: false,
|
||||
isFuture: false,
|
||||
isGeneric: false,
|
||||
typeArguments: [],
|
||||
);
|
||||
|
||||
expect(
|
||||
() => TypeParser.validateInjectableType(
|
||||
parsedType, _createMockElement()),
|
||||
throwsA(isA<TypeParsingException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('should pass for valid types', () {
|
||||
final parsedType = ParsedType(
|
||||
displayString: 'String',
|
||||
coreType: 'String',
|
||||
isNullable: false,
|
||||
isFuture: false,
|
||||
isGeneric: false,
|
||||
typeArguments: [],
|
||||
);
|
||||
|
||||
expect(
|
||||
() => TypeParser.validateInjectableType(
|
||||
parsedType, _createMockElement()),
|
||||
returnsNormally,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('ParsedType', () {
|
||||
test('should return correct codeGenType for simple types', () {
|
||||
final parsedType = ParsedType(
|
||||
displayString: 'String',
|
||||
coreType: 'String',
|
||||
isNullable: false,
|
||||
isFuture: false,
|
||||
isGeneric: false,
|
||||
typeArguments: [],
|
||||
);
|
||||
|
||||
expect(parsedType.codeGenType, equals('String'));
|
||||
});
|
||||
|
||||
test('should return correct codeGenType for Future types', () {
|
||||
final innerType = ParsedType(
|
||||
displayString: 'String',
|
||||
coreType: 'String',
|
||||
isNullable: false,
|
||||
isFuture: false,
|
||||
isGeneric: false,
|
||||
typeArguments: [],
|
||||
);
|
||||
|
||||
final parsedType = ParsedType(
|
||||
displayString: 'Future<String>',
|
||||
coreType: 'Future<String>',
|
||||
isNullable: false,
|
||||
isFuture: true,
|
||||
isGeneric: false,
|
||||
typeArguments: [],
|
||||
innerType: innerType,
|
||||
);
|
||||
|
||||
expect(parsedType.codeGenType, equals('String'));
|
||||
});
|
||||
|
||||
test('should return correct resolveMethodName for sync types', () {
|
||||
final parsedType = ParsedType(
|
||||
displayString: 'String',
|
||||
coreType: 'String',
|
||||
isNullable: false,
|
||||
isFuture: false,
|
||||
isGeneric: false,
|
||||
typeArguments: [],
|
||||
);
|
||||
|
||||
expect(parsedType.resolveMethodName, equals('resolve'));
|
||||
});
|
||||
|
||||
test('should return correct resolveMethodName for nullable sync types', () {
|
||||
final parsedType = ParsedType(
|
||||
displayString: 'String?',
|
||||
coreType: 'String',
|
||||
isNullable: true,
|
||||
isFuture: false,
|
||||
isGeneric: false,
|
||||
typeArguments: [],
|
||||
);
|
||||
|
||||
expect(parsedType.resolveMethodName, equals('tryResolve'));
|
||||
});
|
||||
|
||||
test('should return correct resolveMethodName for async types', () {
|
||||
final parsedType = ParsedType(
|
||||
displayString: 'Future<String>',
|
||||
coreType: 'String',
|
||||
isNullable: false,
|
||||
isFuture: true,
|
||||
isGeneric: false,
|
||||
typeArguments: [],
|
||||
);
|
||||
|
||||
expect(parsedType.resolveMethodName, equals('resolveAsync'));
|
||||
});
|
||||
|
||||
test('should return correct resolveMethodName for nullable async types',
|
||||
() {
|
||||
final parsedType = ParsedType(
|
||||
displayString: 'Future<String?>',
|
||||
coreType: 'String',
|
||||
isNullable: true,
|
||||
isFuture: true,
|
||||
isGeneric: false,
|
||||
typeArguments: [],
|
||||
);
|
||||
|
||||
expect(parsedType.resolveMethodName, equals('tryResolveAsync'));
|
||||
});
|
||||
|
||||
test('should implement equality correctly', () {
|
||||
final parsedType1 = ParsedType(
|
||||
displayString: 'String',
|
||||
coreType: 'String',
|
||||
isNullable: false,
|
||||
isFuture: false,
|
||||
isGeneric: false,
|
||||
typeArguments: [],
|
||||
);
|
||||
|
||||
final parsedType2 = ParsedType(
|
||||
displayString: 'String',
|
||||
coreType: 'String',
|
||||
isNullable: false,
|
||||
isFuture: false,
|
||||
isGeneric: false,
|
||||
typeArguments: [],
|
||||
);
|
||||
|
||||
expect(parsedType1, equals(parsedType2));
|
||||
expect(parsedType1.hashCode, equals(parsedType2.hashCode));
|
||||
});
|
||||
|
||||
test('should implement toString correctly', () {
|
||||
final parsedType = ParsedType(
|
||||
displayString: 'String',
|
||||
coreType: 'String',
|
||||
isNullable: false,
|
||||
isFuture: false,
|
||||
isGeneric: false,
|
||||
typeArguments: [],
|
||||
);
|
||||
|
||||
final result = parsedType.toString();
|
||||
expect(result, contains('ParsedType'));
|
||||
expect(result, contains('String'));
|
||||
expect(result, contains('isNullable: false'));
|
||||
expect(result, contains('isFuture: false'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Mock element for testing
|
||||
Element _createMockElement() {
|
||||
return _MockElement();
|
||||
}
|
||||
|
||||
class _MockElement implements Element {
|
||||
@override
|
||||
String get displayName => 'MockElement';
|
||||
|
||||
@override
|
||||
String get name => 'MockElement';
|
||||
|
||||
@override
|
||||
Source? get source => null;
|
||||
|
||||
@override
|
||||
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
Reference in New Issue
Block a user