From d728fcb9dd9b2d5bf520db52e396cfb0bab336c7 Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Wed, 25 Dec 2024 17:19:57 -0700 Subject: [PATCH] update: package analyzer pass 10 fail 1 --- packages/mirrors/lib/mirrors.dart | 5 + .../lib/src/discovery/package_analyzer.dart | 654 ++++++++++++++++++ .../mirrors/lib/src/discovery/proxy_type.dart | 20 + .../discovery/runtime_type_discoverer.dart | 2 +- packages/mirrors/pubspec.yaml | 6 +- .../test/discovery/package_analyzer_test.dart | 279 ++++++++ 6 files changed, 963 insertions(+), 3 deletions(-) create mode 100644 packages/mirrors/lib/src/discovery/package_analyzer.dart create mode 100644 packages/mirrors/lib/src/discovery/proxy_type.dart create mode 100644 packages/mirrors/test/discovery/package_analyzer_test.dart diff --git a/packages/mirrors/lib/mirrors.dart b/packages/mirrors/lib/mirrors.dart index aed6981..a22632f 100644 --- a/packages/mirrors/lib/mirrors.dart +++ b/packages/mirrors/lib/mirrors.dart @@ -4,6 +4,7 @@ import 'package:platform_contracts/contracts.dart'; import 'src/core/mirror_system.dart'; import 'src/mirrors/instance_mirror.dart'; +import 'src/reflector/runtime_reflector.dart'; /// Annotations export 'src/annotations/reflectable.dart'; @@ -16,6 +17,7 @@ export 'src/discovery/type_analyzer.dart'; export 'src/discovery/library_analyzer.dart'; export 'src/discovery/runtime_library_discoverer.dart'; export 'src/discovery/runtime_type_discoverer.dart'; +export 'src/discovery/package_analyzer.dart'; /// Discovery Models export 'src/discovery/models/models.dart'; @@ -58,6 +60,9 @@ export 'src/registry/reflection_registry.dart'; /// Types export 'src/types/special_types.dart'; +// Export the runtime reflector instance globally +final reflector = RuntimeReflector.instance; + /// Reflects an instance. InstanceMirrorContract reflect(dynamic reflectee) { return InstanceMirror( diff --git a/packages/mirrors/lib/src/discovery/package_analyzer.dart b/packages/mirrors/lib/src/discovery/package_analyzer.dart new file mode 100644 index 0000000..2bf2ac2 --- /dev/null +++ b/packages/mirrors/lib/src/discovery/package_analyzer.dart @@ -0,0 +1,654 @@ +import 'dart:io'; +import 'package:path/path.dart' as path; +import '../metadata/type_metadata.dart'; +import '../metadata/constructor_metadata.dart'; +import '../metadata/method_metadata.dart'; +import '../metadata/parameter_metadata.dart'; +import '../metadata/property_metadata.dart'; +import '../registry/reflection_registry.dart'; +import '../reflector/runtime_reflector.dart'; +import 'proxy_type.dart'; + +/// Discovers and analyzes types in a package automatically. +class PackageAnalyzer { + // Private constructor to prevent instantiation + PackageAnalyzer._(); + + // Cache of discovered types and mappings + static final Map> _packageTypes = {}; + static final Map _typeCache = {}; + static final Map _typeNameCache = {}; + + // The runtime reflector instance + static final _reflector = RuntimeReflector.instance; + + /// Gets or creates a Type instance for a type name. + static Type _getTypeForName(String name) { + // Remove any generic type parameters and whitespace + name = name.split('<')[0].trim(); + + return _typeCache.putIfAbsent(name, () { + // For built-in types, return the actual type + switch (name) { + case 'String': + return String; + case 'int': + return int; + case 'double': + return double; + case 'bool': + return bool; + case 'List': + return List; + case 'Map': + return Map; + case 'Set': + return Set; + case 'Object': + return Object; + case 'dynamic': + return Object; // Use Object as fallback for dynamic + case 'void': + return Object; // Use Object as fallback for void + case 'Null': + return Object; // Use Object as fallback for Null + default: + // For user-defined types, create a proxy type + final type = ProxyType(name); + _typeNameCache[type] = name; + ReflectionRegistry.register(type); + return type; + } + }); + } + + /// Gets the name of a Type. + static String? getTypeName(Type type) { + if (type == String) return 'String'; + if (type == int) return 'int'; + if (type == double) return 'double'; + if (type == bool) return 'bool'; + if (type == List) return 'List'; + if (type == Map) return 'Map'; + if (type == Set) return 'Set'; + if (type == Object) return _typeNameCache[type] ?? 'Object'; + return _typeNameCache[type]; + } + + /// Discovers all types in a package. + static Set discoverTypes(String packagePath) { + if (_packageTypes.containsKey(packagePath)) { + return _packageTypes[packagePath]!; + } + + final types = _scanPackage(packagePath); + _packageTypes[packagePath] = types; + return types; + } + + /// Scans a package directory for Dart files and extracts type information. + static Set _scanPackage(String packagePath) { + final types = {}; + final libDir = Directory(path.join(packagePath, 'lib')); + + if (!libDir.existsSync()) { + return types; + } + + // Get all .dart files recursively + final dartFiles = libDir + .listSync(recursive: true) + .where((f) => f.path.endsWith('.dart')) + .cast(); + + for (final file in dartFiles) { + try { + // Parse file and extract type information + final fileTypes = _analyzeFile(file.path); + types.addAll(fileTypes); + } catch (e) { + print('Warning: Failed to analyze ${file.path}: $e'); + } + } + + return types; + } + + /// Analyzes a Dart file and extracts type information. + static Set _analyzeFile(String filePath) { + final types = {}; + final source = File(filePath).readAsStringSync(); + + // Extract class declarations using regex + final classRegex = RegExp( + r'(?:abstract\s+)?class\s+(\w+)(?:<[^>]+>)?(?:\s+extends\s+(\w+)(?:<[^>]+>)?)?(?:\s+implements\s+([^{]+))?\s*{', + multiLine: true); + final matches = classRegex.allMatches(source); + + for (final match in matches) { + final className = match.group(1)!; + final superclass = match.group(2); + final interfaces = match.group(3); + + // Extract class content for further analysis + final classContent = _extractClassContent(source, className); + + // Extract properties + final properties = _extractProperties(classContent, className); + + // Extract methods + final methods = _extractMethods(classContent, className); + + // Extract constructors + final constructors = _extractConstructors(classContent, className); + + // Register with reflection system + _registerType( + className, + properties, + methods, + constructors, + superclass, + interfaces, + source.contains('abstract class $className'), + ); + + // Add to discovered types + types.add(_getTypeForName(className)); + } + + return types; + } + + /// Registers a type with the reflection system. + static void _registerType( + String className, + Map properties, + Map methods, + List constructors, + String? superclass, + String? interfaces, + bool isAbstract, + ) { + final type = _getTypeForName(className); + + // Register properties + properties.forEach((name, metadata) { + ReflectionRegistry.registerProperty( + type, + name, + metadata.type, + isWritable: metadata.isWritable, + ); + }); + + // Register methods + methods.forEach((name, metadata) { + // Get parameter names for named parameters only + final parameterNames = metadata.parameters + .where((p) => p.isNamed) + .map((p) => p.name) + .toList(); + + // Get required flags for named parameters only + final isRequired = metadata.parameters + .where((p) => p.isNamed) + .map((p) => p.isRequired) + .toList(); + + // Get isNamed flags for all parameters + final isNamed = metadata.parameters.map((p) => p.isNamed).toList(); + + ReflectionRegistry.registerMethod( + type, + name, + metadata.parameterTypes, + metadata.returnsVoid, + returnType: metadata.returnType, + parameterNames: parameterNames.isNotEmpty ? parameterNames : null, + isRequired: isRequired.isNotEmpty ? isRequired : null, + isNamed: isNamed.isNotEmpty ? isNamed : null, + isStatic: metadata.isStatic, + ); + }); + + // Register constructors + for (final constructor in constructors) { + // Get parameter names for named parameters + final parameterNames = constructor.parameters + .where((p) => p.isNamed) + .map((p) => p.name) + .toList(); + + ReflectionRegistry.registerConstructor( + type, + constructor.name, + parameterTypes: constructor.parameterTypes, + parameterNames: parameterNames.isNotEmpty ? parameterNames : null, + ); + } + + // Create type metadata + final metadata = TypeMetadata( + type: type, + name: className, + properties: properties, + methods: methods, + constructors: constructors, + supertype: superclass != null + ? TypeMetadata( + type: _getTypeForName(superclass), + name: superclass, + properties: const {}, + methods: const {}, + constructors: const [], + ) + : null, + interfaces: interfaces + ?.split(',') + .map((i) => i.trim()) + .where((i) => i.isNotEmpty) + .map((i) => TypeMetadata( + type: _getTypeForName(i), + name: i, + properties: const {}, + methods: const {}, + constructors: const [], + )) + .toList() ?? + const [], + ); + + // Register metadata with reflection system + ReflectionRegistry.registerTypeMetadata(type, metadata); + } + + /// Extracts property information from a class. + static Map _extractProperties( + String classContent, String className) { + final properties = {}; + + // Extract field declarations using regex + final fieldRegex = RegExp( + r'(?:final|const)?\s*(\w+(?:<[^>]+>)?)\s+(\w+)(?:\s*\?)?(?:\s*=\s*[^;]+)?;', + multiLine: true, + ); + + // Extract nullable field declarations + final nullableFieldRegex = RegExp( + r'(?:final|const)?\s*(\w+(?:<[^>]+>)?)\s*\?\s*(\w+)(?:\s*=\s*[^;]+)?;', + multiLine: true, + ); + + // Extract field declarations from initializer lists + final initializerRegex = RegExp( + r':\s*([^{]+)', + multiLine: true, + ); + + // Extract constructor declarations + final constructorRegex = RegExp( + r'$className\s*\(((?:[^)]|\n)*)\)', + multiLine: true, + ); + + // Process regular field declarations + final fieldMatches = fieldRegex.allMatches(classContent); + for (final match in fieldMatches) { + final type = match.group(1)!; + final name = match.group(2)!; + final fullDecl = classContent.substring(match.start, match.end); + + properties[name] = PropertyMetadata( + name: name, + type: _getTypeForName(type), + isReadable: true, + isWritable: !fullDecl.contains('final $type') && + !fullDecl.contains('const $type'), + ); + } + + // Process nullable field declarations + final nullableMatches = nullableFieldRegex.allMatches(classContent); + for (final match in nullableMatches) { + final type = match.group(1)!; + final name = match.group(2)!; + final fullDecl = classContent.substring(match.start, match.end); + + properties[name] = PropertyMetadata( + name: name, + type: _getTypeForName(type), + isReadable: true, + isWritable: !fullDecl.contains('final $type') && + !fullDecl.contains('const $type'), + ); + } + + // Process initializer list assignments + final initializerMatch = initializerRegex.firstMatch(classContent); + if (initializerMatch != null) { + final initializers = initializerMatch.group(1)!; + + // Extract assignments in initializer list + final assignmentRegex = RegExp( + r'(\w+)\s*=\s*([^,{]+)(?:,|\s*{)', + multiLine: true, + ); + + final assignmentMatches = assignmentRegex.allMatches(initializers); + for (final match in assignmentMatches) { + final name = match.group(1)!; + if (!properties.containsKey(name)) { + // Find type from field declaration or parameter + final typeMatch = RegExp( + r'(?:final|const)?\s*(\w+(?:<[^>]+>)?)\s+' + + name + + r'(?:\s*\?)?[;=]', + ).firstMatch(classContent); + + if (typeMatch != null) { + final type = typeMatch.group(1)!; + properties[name] = PropertyMetadata( + name: name, + type: _getTypeForName(type), + isReadable: true, + isWritable: !classContent.contains('final $type $name') && + !classContent.contains('const $type $name'), + ); + } + } + } + } + + // Process constructor parameters + final constructorMatch = constructorRegex.firstMatch(classContent); + if (constructorMatch != null) { + final paramList = constructorMatch.group(1)!; + + // Extract positional parameters + final positionalRegex = RegExp( + r'(?:required\s+)?(\w+(?:<[^>]+>)?)\s+(?:this\.)?(\w+)(?:\s*\?)?(?=\s*,|\s*\{|\s*\))', + multiLine: true, + ); + + final positionalMatches = positionalRegex.allMatches(paramList); + for (final match in positionalMatches) { + final type = match.group(1)!; + final name = match.group(2)!; + final fullParam = paramList.substring(match.start, match.end); + final isNullable = fullParam.contains('$type?') || + fullParam.contains('$type ?') || + fullParam.contains('this.$name?'); + + if (!properties.containsKey(name)) { + properties[name] = PropertyMetadata( + name: name, + type: _getTypeForName(type), + isReadable: true, + isWritable: !classContent.contains('final $type $name') && + !classContent.contains('const $type $name'), + ); + } + } + + // Extract named parameters section + final namedParamsMatch = RegExp(r'{([^}]*)}').firstMatch(paramList); + if (namedParamsMatch != null) { + final namedParams = namedParamsMatch.group(1)!; + + // Extract named parameters + final namedRegex = RegExp( + r'(?:required\s+)?(\w+(?:<[^>]+>)?)\s+(?:this\.)?(\w+)(?:\s*\?)?(?:\s*=\s*[^,}]+)?(?:\s*,|\s*$)', + multiLine: true, + ); + + final namedMatches = namedRegex.allMatches(namedParams); + for (final match in namedMatches) { + final type = match.group(1)!; + final name = match.group(2)!; + final fullParam = namedParams.substring(match.start, match.end); + final isNullable = fullParam.contains('$type?') || + fullParam.contains('$type ?') || + fullParam.contains('this.$name?'); + + if (!properties.containsKey(name)) { + properties[name] = PropertyMetadata( + name: name, + type: _getTypeForName(type), + isReadable: true, + isWritable: !classContent.contains('final $type $name') && + !classContent.contains('const $type $name'), + ); + } + } + } + } + + // Extract getter declarations + final getterRegex = RegExp( + r'(?:get|set)\s+(\w+)(?:\s*=>|\s*{)', + multiLine: true, + ); + + final getterMatches = getterRegex.allMatches(classContent); + for (final match in getterMatches) { + final name = match.group(1)!; + if (!properties.containsKey(name)) { + // Try to find the return type from the getter implementation + final getterImpl = RegExp( + r'get\s+' + name + r'\s*(?:=>|\{)\s*(?:return\s+)?(\w+)', + ).firstMatch(classContent); + final type = getterImpl?.group(1) ?? 'dynamic'; + + properties[name] = PropertyMetadata( + name: name, + type: _getTypeForName(type), + isReadable: true, + isWritable: classContent.contains('set $name'), + ); + } + } + + return properties; + } + + /// Extracts method information from a class. + static Map _extractMethods( + String classContent, String className) { + final methods = {}; + + // Extract method declarations using regex + final methodRegex = RegExp( + r'(?:static\s+)?(\w+(?:<[^>]+>)?)\s+(\w+)\s*\((.*?)\)', + multiLine: true, + ); + + final matches = methodRegex.allMatches(classContent); + + for (final match in matches) { + final returnType = match.group(1)!; + final name = match.group(2)!; + final params = match.group(3)!; + + if (name != className) { + // Skip constructors + methods[name] = MethodMetadata( + name: name, + parameterTypes: _extractParameterTypes(params), + parameters: _extractParameters(params), + returnsVoid: returnType == 'void', + returnType: _getTypeForName(returnType), + isStatic: classContent.contains('static $returnType $name'), + ); + } + } + + return methods; + } + + /// Extracts constructor information from a class. + static List _extractConstructors( + String classContent, String className) { + final constructors = []; + + // Find the class declaration + final classRegex = RegExp( + '(?:abstract\\s+)?class\\s+' + RegExp.escape(className) + '[^{]*\\{', + ); + final classMatch = classRegex.firstMatch(classContent); + if (classMatch == null) return constructors; + + print('Class content for $className:'); + print(classContent); + + // Extract all constructor declarations using regex + final constructorPattern = '(?:const\\s+)?(?:factory\\s+)?' + + RegExp.escape(className) + + '(?:\\.([\\w.]+))?\\s*\\(([^)]*?)\\)(?:\\s*:[^{;]*?)?(?:\\s*(?:=>|{|;))'; + print('Constructor pattern: $constructorPattern'); + + final constructorRegex = + RegExp(constructorPattern, multiLine: true, dotAll: true); + final matches = constructorRegex.allMatches(classContent); + + print('Found ${matches.length} constructor matches:'); + for (final match in matches) { + final fullMatch = classContent.substring(match.start, match.end); + print('Full match: $fullMatch'); + print('Group 1 (name): ${match.group(1)}'); + print('Group 2 (params): ${match.group(2)}'); + } + + // Use a map to deduplicate constructors by name + final constructorMap = {}; + + // Process constructors + for (final match in matches) { + final fullMatch = classContent.substring(match.start, match.end); + final name = match.group(1) ?? ''; + final params = match.group(2) ?? ''; + final isFactory = fullMatch.trim().startsWith('factory'); + + // For factory constructors without a name, use 'create' + final constructorName = isFactory && name.isEmpty ? 'create' : name; + + // Only add if we haven't seen this constructor name before + if (!constructorMap.containsKey(constructorName)) { + constructorMap[constructorName] = ConstructorMetadata( + name: constructorName, + parameterTypes: _extractParameterTypes(params), + parameters: _extractParameters(params), + ); + } + } + + final result = constructorMap.values.toList(); + print('Returning ${result.length} constructors'); + return result; + } + + /// Extracts parameter types from a parameter list string. + static List _extractParameterTypes(String params) { + final types = []; + + final paramRegex = RegExp( + r'(?:required\s+)?(\w+(?:<[^>]+>)?)\s+(?:this\.)?(\w+)(?:\s*\?)?', + multiLine: true, + ); + final matches = paramRegex.allMatches(params); + + for (final match in matches) { + final type = match.group(1)!; + types.add(_getTypeForName(type)); + } + + return types; + } + + /// Extracts parameters from a parameter list string. + static List _extractParameters(String params) { + final parameters = []; + + final paramRegex = RegExp( + r'(?:required\s+)?(\w+(?:<[^>]+>)?)\s+(?:this\.)?(\w+)(?:\s*\?)?(?:\s*=\s*([^,}]+))?(?:,|\s*$|\s*\}|\s*\)|$)', + multiLine: true, + ); + final matches = paramRegex.allMatches(params); + + for (final match in matches) { + final type = match.group(1)!; + final name = match.group(2)!; + final defaultValue = match.group(3); + + parameters.add(ParameterMetadata( + name: name, + type: _getTypeForName(type), + isRequired: params.contains('required $type $name'), + isNamed: params.contains('{') && params.contains('}'), + defaultValue: + defaultValue != null ? _parseDefaultValue(defaultValue) : null, + )); + } + + return parameters; + } + + /// Extracts the content of a class from source code. + static String _extractClassContent(String source, String className) { + // Find the class declaration + final classRegex = RegExp( + '(?:abstract\\s+)?class\\s+' + RegExp.escape(className) + '[^{]*{', + ); + final match = classRegex.firstMatch(source); + if (match == null) return ''; + + final startIndex = match.start; + var bracketCount = 1; + var inString = false; + var stringChar = ''; + var content = source.substring(startIndex, match.end); + + // Extract everything between the opening and closing braces + for (var i = match.end; i < source.length; i++) { + final char = source[i]; + + // Handle string literals to avoid counting braces inside strings + if ((char == '"' || char == "'") && source[i - 1] != '\\') { + if (!inString) { + inString = true; + stringChar = char; + } else if (stringChar == char) { + inString = false; + } + } + + if (!inString) { + if (char == '{') { + bracketCount++; + } else if (char == '}') { + bracketCount--; + if (bracketCount == 0) { + content += source.substring(match.end, i + 1); + break; + } + } + } + } + + return content; + } + + /// Parses a default value string into an actual value. + static dynamic _parseDefaultValue(String value) { + // Basic parsing of common default values + if (value == 'null') return null; + if (value == 'true') return true; + if (value == 'false') return false; + if (value.startsWith("'") && value.endsWith("'")) { + return value.substring(1, value.length - 1); + } + if (int.tryParse(value) != null) return int.parse(value); + if (double.tryParse(value) != null) return double.parse(value); + return value; + } +} diff --git a/packages/mirrors/lib/src/discovery/proxy_type.dart b/packages/mirrors/lib/src/discovery/proxy_type.dart new file mode 100644 index 0000000..8f379b4 --- /dev/null +++ b/packages/mirrors/lib/src/discovery/proxy_type.dart @@ -0,0 +1,20 @@ +/// A proxy type used for representing user-defined types during reflection. +class ProxyType implements Type { + /// The name of the type. + final String name; + + /// Creates a new proxy type with the given name. + const ProxyType(this.name); + + @override + String toString() => name; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is ProxyType && other.name == name; + } + + @override + int get hashCode => name.hashCode; +} diff --git a/packages/mirrors/lib/src/discovery/runtime_type_discoverer.dart b/packages/mirrors/lib/src/discovery/runtime_type_discoverer.dart index b38f3df..177e241 100644 --- a/packages/mirrors/lib/src/discovery/runtime_type_discoverer.dart +++ b/packages/mirrors/lib/src/discovery/runtime_type_discoverer.dart @@ -17,7 +17,7 @@ class RuntimeTypeDiscoverer { ReflectionRegistry.register(type); // Get mirror system and analyze type - //final mirrorSystem = MirrorSystem.current(); + final mirrorSystem = MirrorSystem.current(); final typeInfo = TypeAnalyzer.analyze(type); // Convert properties, methods, and constructors to metadata diff --git a/packages/mirrors/pubspec.yaml b/packages/mirrors/pubspec.yaml index bc8d53e..334222e 100644 --- a/packages/mirrors/pubspec.yaml +++ b/packages/mirrors/pubspec.yaml @@ -7,9 +7,11 @@ environment: sdk: '>=3.0.0 <4.0.0' dependencies: - platform_contracts: ^0.1.0 - meta: ^1.9.0 collection: ^1.17.0 + meta: ^1.9.0 + path: ^1.9.1 + platform_contracts: ^0.1.0 + dev_dependencies: test: ^1.24.0 diff --git a/packages/mirrors/test/discovery/package_analyzer_test.dart b/packages/mirrors/test/discovery/package_analyzer_test.dart new file mode 100644 index 0000000..fb967ae --- /dev/null +++ b/packages/mirrors/test/discovery/package_analyzer_test.dart @@ -0,0 +1,279 @@ +import 'dart:io'; +import 'package:path/path.dart' as path; +import 'package:platform_mirrors/mirrors.dart'; +import 'package:test/test.dart'; + +// Test classes +@reflectable +class TestUser { + final String id; + String name; + int? age; + List tags; + + TestUser(this.id, this.name, {this.age, List? tags}) + : tags = tags ?? []; + + static TestUser create(String id, String name) => TestUser(id, name); + + void addTag(String tag) { + tags.add(tag); + } + + String greet() => 'Hello, $name!'; +} + +@reflectable +abstract class BaseEntity { + String get id; + Map toJson(); +} + +@reflectable +class TestProduct implements BaseEntity { + @override + final String id; + final String name; + final double price; + + const TestProduct(this.id, this.name, this.price); + + factory TestProduct.create(String name, double price) { + return TestProduct(DateTime.now().toString(), name, price); + } + + @override + Map toJson() => { + 'id': id, + 'name': name, + 'price': price, + }; +} + +void main() { + group('PackageAnalyzer Tests', () { + late Directory tempDir; + late String libPath; + + setUp(() async { + // Create temporary directory structure + tempDir = await Directory.systemTemp.createTemp('package_analyzer_test_'); + libPath = path.join(tempDir.path, 'lib'); + await Directory(libPath).create(); + + // Create test files + await File(path.join(libPath, 'test_user.dart')).writeAsString(''' +import 'package:platform_mirrors/mirrors.dart'; + +@reflectable +class TestUser { + final String id; + String name; + int? age; + List tags; + + TestUser(this.id, this.name, {this.age, List? tags}) + : tags = tags ?? []; + + static TestUser create(String id, String name) => TestUser(id, name); + + void addTag(String tag) { + tags.add(tag); + } + + String greet() => 'Hello, \$name!'; +} +'''); + + await File(path.join(libPath, 'test_product.dart')).writeAsString(''' +import 'package:platform_mirrors/mirrors.dart'; + +@reflectable +abstract class BaseEntity { + String get id; + Map toJson(); +} + +@reflectable +class TestProduct implements BaseEntity { + @override + final String id; + final String name; + final double price; + + const TestProduct(this.id, this.name, this.price); + + factory TestProduct.create(String name, double price) { + return TestProduct(DateTime.now().toString(), name, price); + } + + @override + Map toJson() => { + 'id': id, + 'name': name, + 'price': price, + }; +} +'''); + }); + + tearDown(() async { + // Clean up temporary directory + await tempDir.delete(recursive: true); + // Reset reflection registry + ReflectionRegistry.reset(); + }); + + test('discovers all types in package', () { + final types = PackageAnalyzer.discoverTypes(tempDir.path); + expect(types, isNotEmpty); + + // Verify type names are registered + final typeNames = + types.map((t) => PackageAnalyzer.getTypeName(t)).toSet(); + expect(typeNames, containsAll(['TestUser', 'BaseEntity', 'TestProduct'])); + }); + + test('analyzes class properties correctly', () { + final types = PackageAnalyzer.discoverTypes(tempDir.path); + final userType = + types.firstWhere((t) => PackageAnalyzer.getTypeName(t) == 'TestUser'); + + final metadata = ReflectionRegistry.getTypeMetadata(userType); + expect(metadata, isNotNull); + expect(metadata!.properties, hasLength(4)); + + final idProperty = metadata.properties['id']!; + expect(PackageAnalyzer.getTypeName(idProperty.type), equals('String')); + expect(idProperty.isWritable, isFalse); + + final nameProperty = metadata.properties['name']!; + expect(PackageAnalyzer.getTypeName(nameProperty.type), equals('String')); + expect(nameProperty.isWritable, isTrue); + }); + + test('analyzes class methods correctly', () { + final types = PackageAnalyzer.discoverTypes(tempDir.path); + final userType = + types.firstWhere((t) => PackageAnalyzer.getTypeName(t) == 'TestUser'); + + final metadata = ReflectionRegistry.getTypeMetadata(userType); + expect(metadata, isNotNull); + expect(metadata!.methods, hasLength(3)); // addTag, greet, create + + final addTagMethod = metadata.methods['addTag']!; + expect(PackageAnalyzer.getTypeName(addTagMethod.parameterTypes.first), + equals('String')); + expect(addTagMethod.returnsVoid, isTrue); + + final greetMethod = metadata.methods['greet']!; + expect(greetMethod.parameterTypes, isEmpty); + expect(PackageAnalyzer.getTypeName(greetMethod.returnType), + equals('String')); + }); + + test('analyzes constructors correctly', () { + final types = PackageAnalyzer.discoverTypes(tempDir.path); + final userType = + types.firstWhere((t) => PackageAnalyzer.getTypeName(t) == 'TestUser'); + + final metadata = ReflectionRegistry.getTypeMetadata(userType); + expect(metadata, isNotNull); + expect(metadata!.constructors, hasLength(1)); + + final constructor = metadata.constructors.first; + expect(constructor.parameterTypes, hasLength(4)); + expect(constructor.parameters.where((p) => p.isNamed), hasLength(2)); + }); + + test('analyzes inheritance correctly', () { + final types = PackageAnalyzer.discoverTypes(tempDir.path); + final productType = types + .firstWhere((t) => PackageAnalyzer.getTypeName(t) == 'TestProduct'); + + final metadata = ReflectionRegistry.getTypeMetadata(productType); + expect(metadata, isNotNull); + expect(metadata!.interfaces, hasLength(1)); + expect(metadata.interfaces.first.name, equals('BaseEntity')); + }); + + test('handles abstract classes correctly', () { + final types = PackageAnalyzer.discoverTypes(tempDir.path); + final baseEntityType = types + .firstWhere((t) => PackageAnalyzer.getTypeName(t) == 'BaseEntity'); + + final metadata = ReflectionRegistry.getTypeMetadata(baseEntityType); + expect(metadata, isNotNull); + expect(metadata!.methods['toJson'], isNotNull); + expect(metadata.properties['id'], isNotNull); + }); + + test('handles factory constructors correctly', () { + final types = PackageAnalyzer.discoverTypes(tempDir.path); + final productType = types + .firstWhere((t) => PackageAnalyzer.getTypeName(t) == 'TestProduct'); + + final metadata = ReflectionRegistry.getTypeMetadata(productType); + expect(metadata, isNotNull); + + final factoryConstructor = + metadata!.constructors.firstWhere((c) => c.name == 'create'); + expect(factoryConstructor, isNotNull); + expect( + factoryConstructor.parameterTypes + .map((t) => PackageAnalyzer.getTypeName(t)), + equals(['String', 'double'])); + }); + + test('handles static methods correctly', () { + final types = PackageAnalyzer.discoverTypes(tempDir.path); + final userType = + types.firstWhere((t) => PackageAnalyzer.getTypeName(t) == 'TestUser'); + + final metadata = ReflectionRegistry.getTypeMetadata(userType); + expect(metadata, isNotNull); + + final createMethod = metadata!.methods['create']!; + expect(createMethod.isStatic, isTrue); + expect( + createMethod.parameterTypes + .map((t) => PackageAnalyzer.getTypeName(t)), + equals(['String', 'String'])); + }); + + test('handles nullable types correctly', () { + final types = PackageAnalyzer.discoverTypes(tempDir.path); + final userType = + types.firstWhere((t) => PackageAnalyzer.getTypeName(t) == 'TestUser'); + + final metadata = ReflectionRegistry.getTypeMetadata(userType); + expect(metadata, isNotNull); + + final ageProperty = metadata!.properties['age']!; + expect(PackageAnalyzer.getTypeName(ageProperty.type), equals('int')); + // Note: Currently we don't track nullability information + // This could be enhanced in future versions + }); + + test('caches discovered types', () { + final firstRun = PackageAnalyzer.discoverTypes(tempDir.path); + final secondRun = PackageAnalyzer.discoverTypes(tempDir.path); + + expect(identical(firstRun, secondRun), isTrue); + }); + + test('handles generic types correctly', () { + final types = PackageAnalyzer.discoverTypes(tempDir.path); + final userType = + types.firstWhere((t) => PackageAnalyzer.getTypeName(t) == 'TestUser'); + + final metadata = ReflectionRegistry.getTypeMetadata(userType); + expect(metadata, isNotNull); + + final tagsProperty = metadata!.properties['tags']!; + expect(PackageAnalyzer.getTypeName(tagsProperty.type), equals('List')); + // Note: Currently we don't track generic type arguments + // This could be enhanced in future versions + }); + }); +}