-
Notifications
You must be signed in to change notification settings - Fork 56
Implement topological sort for const initializers #246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Wow, cool. I thought this might need a graph setup and this is cleaner than I thought it'd be :) First off, there's nothing about this particular change that should apply only to Second, and probably most important, it doesn't appear that this will work if one static references a static from another file, and may in fact be a regression in this case. Using topLevelDeclarationsMap for your main loop seems inappropriate - visibleDeclarationsByIndex is more likely what you're looking for. Please add a test for this case. Finally, could you switch this to use the directed_graph package for its implementation? dart_eval already depends on that package for library graphs and it appears to have toposort built-in. Those are all I need to see to accept this specific PR, but I do want to get a bit more in the weeds about const and your The "most correct" way to evaluate const is to do as much as possible of it at compile time. It's not really clear to me where these JSON structures with the A second option would be to just use the existing pipeline to compile const values into bytecode that gets specially marked to be automatically executed when the Runtime is initialized. Class instantiation and expression evaluation would be substantially simpler with this approach, though it's less correct. Fundamentally, top-level and class consts should be 100% exactly the same, so there's certainly no intentional reason why you'd need a method like this for top-level consts but not class consts. I'll have to take a deeper look at #195 when I get a chance. |
Thanks for the quick reply. Here's my initial thoughts, I may edit this post with more details later.
I should have clarified. tl;dr they are not getting synthesized into instances at all. In my case, I have a file outside of dart eval that rebuilds all classes that are a subtype of Widget + some utility classes into a single file via walking the AST. (not a flutter widget, but my own layout engine based largely on Flutter) Then I feed this rebuilt version to dart_eval and use the same AST walk to automatically generate my own bridge/glue functions. I have similar code that outputs JS instead of dart and creates JS glue as well. In order to make forward declared constants work with dart_eval, I added this function to inline all those const references. So it would take a dart class with forward declared references and then output another almost identical class in another file with all the constants resolved that dart_eval would use. As for how it should be done in dart_eval, for primitive const values we could just emit the bytecode representing the evaluated constant value so instead of pushing value 2, value 2,(or pushing some op that gets this value from the const field) add, you could just emit value 4. For classes I'm also not sure. It seems dart stores these values in an "object pool" see https://github.com/dart-lang/sdk/blob/a8879f91334c47aa77ff9bbc414169c824632d42/runtime/vm/object.h#L5615 via https://mrale.ph/dartvm/glossary.html#object-pool-literal-pool which is called a constant pool in java. I see you have a constant pool file already, can we use that here? However, I don't actually see why we need an object pool, it seems like we could just inline all the field accesses of a const class. eg. if you have this class: final class NameHolder {
final String name;
const NameHolder({required this.name});
}
it seems we can always inline the any usage of NameHolder.name to just the actual string. So we never need to know about the class itslef. But I'm probably missing some other obvious cases where we do need to know the actual class.
Good point. Still, I will need to make sure to only look at other consts when trying to resolve a const member's dependencies. eg this code: class A {
const A(this.a);
static const A c = A(2 * b);
static double b = 1.0; //<-- non const
final double a;
}
gives this error:
Thanks, will try these.
It's very possible I simply messed up the implementation somewhere. |
That's really cool :) Would love to see this if it was ever open-sourced
Very ambitious, and probably doable as an optimization within the confines of a single method, but any method that gets an instance from somewhere else can't guarantee if it was instantiated in a const context regardless of whether the class has a const constructor.
Ideally yes it should emit a representation of the evaluated constant value. I don't think it needs to be a bytecode op necessarily.
Sure, you could use that as the underlying store for data the runtime needs to initialize the constants. But it can only hold serializable data so it couldn't contain fully materialized |
I also realized I oversimplified this. Fundamentally, your idea of grouping the declarations into 'constDeclarationsByContainer' isn't going to work - there's no non-overlapping 'containers' that can be guaranteed to be defined purely by code in Dart (naively using visibileDeclarationsByIndex would result in compiling the same statics multiple times). I think all the statics from the entire codebase will have to be put into a single graph, and from there you can find the connected component "containers" using graph algorithms if needed. It's possible there's another solution I'm not thinking of though. |
I plan to release the app soon, it will be open source. Here's the main file that rebuilds classes: import 'dart:io';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element2.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/file_system/file_system.dart' as file_system;
import 'package:code_builder/code_builder.dart' as cb;
import 'package:collection/collection.dart';
import 'package:dart_style/dart_style.dart';
import 'package:path/path.dart' as p;
import 'generate_js_reconstructor.dart';
import 'generate_proxies_js.dart';
import 'generate_reconstructor.dart';
const String _inputLibraryPath = 'lib\\typesetting_prototype.dart';
const String _outputLibraryPath = 'lib/eval/script_stdlib.dart';
const Set<String> _methodsToSkip = {
//Standard Object methods
'toString',
'hashCode',
'runtimeType',
'noSuchMethod',
//project-specific methods to ignore
'createRenderNode',
'resolve',
'save',
};
const Map<String, Set<String>> _membersToSkip = {
'PageContext': {'settings', 'metadata', 'getMetadata'},
};
const Set<String> _typesToPrecalculate = {'PageFormat'};
const Map<String, String> _additionalRootClasses = {
'Document': 'lib\\document\\document.dart',
'FootnoteLayoutInfo': 'lib\\typesetting_prototype.dart',
'TtfFont': 'lib\\text\\font.dart',
};
final Set<String> requiredImports = {};
void main() async {
final stopwatch = Stopwatch()..start();
print('Starting proxy generation...');
final projectPath = Directory.current.path;
final absoluteInputPath = p.join(projectPath, _inputLibraryPath);
final collection = AnalysisContextCollection(includedPaths: [projectPath]);
final session = collection.contextFor(absoluteInputPath).currentSession;
final allProjectClasses = await _findAllProjectClasses(session);
final initialClasses = <ClassElement2>[];
final widgetClasses = await _findAllWidgetSubclasses(session);
initialClasses.addAll(widgetClasses);
print('Found ${widgetClasses.length} Widget classes.');
for (final entry in _additionalRootClasses.entries) {
final className = entry.key;
final classPath = entry.value;
final absoluteClassPath = p.join(projectPath, classPath);
final classLibraryResult = await session.getResolvedLibrary(absoluteClassPath);
if (classLibraryResult is! ResolvedLibraryResult) {
print('WARNING: Could not resolve library for file "$classPath"');
continue;
}
final classLibrary = classLibraryResult.element2;
final classElement = classLibrary.getClass2(className);
if (classElement != null) {
print('Found additional root class: $className in $classPath');
initialClasses.add(classElement);
} else {
print("WARNING: Could not find class '$className' in file '$classPath'");
}
}
final allFoundElements = await _findDependentTypes(initialClasses, allProjectClasses, session);
print('Resolving any name ambiguities...');
final allElementsToProxy = _resolveAmbiguities(allFoundElements, session);
final dataClasses = allElementsToProxy.whereType<ClassElement2>().toList()
..sort((a, b) => (a.name3 ?? '').compareTo(b.name3 ?? ''));
final enumClasses = allElementsToProxy.whereType<EnumElement2>().toList()
..sort((a, b) => (a.name3 ?? '').compareTo(b.name3 ?? ''));
print('Found ${dataClasses.length} total dependent data classes and ${enumClasses.length} enums.');
final allProxyNames = <String?>{
...dataClasses.map((e) => e.name3),
...enumClasses.map((e) => e.name3),
}.where((name) => name != null).cast<String>().toSet();
final libraryBuilder = cb.LibraryBuilder();
for (final enumElement in enumClasses) {
libraryBuilder.body.add(_buildEnumProxy(enumElement));
}
final allProxyClasses = {
...initialClasses,
...dataClasses,
}.toList().sorted((a, b) => (a.name3 ?? '').compareTo(b.name3 ?? ''));
final generatedClasses = <String>{};
for (final classElement in allProxyClasses) {
final name = classElement.name3;
if (name != null && !generatedClasses.contains(name)) {
libraryBuilder.body.add(await _buildClassProxy(classElement, allProxyNames, session));
generatedClasses.add(name);
}
}
for (final importUri in requiredImports) {
libraryBuilder.directives.add(cb.Directive.import(importUri));
}
final librarySpec = libraryBuilder.build();
final emitter = cb.DartEmitter(useNullSafetySyntax: true, orderDirectives: true);
final code = librarySpec.accept(emitter);
final formatter = DartFormatter(languageVersion: DartFormatter.latestLanguageVersion);
final formattedCode = formatter.format(code.toString());
final header = _generateHeader();
final finalContent = '$header\n\n$formattedCode';
final outputFile = File(_outputLibraryPath);
await outputFile.writeAsString(finalContent, flush: true);
print('Generating reconstructor logic...');
final reconstructorContent = generateReconstructorFile(dataClasses, enumClasses);
final reconstructorFile = File('lib/eval/reconstructor_generated.dart');
await reconstructorFile.writeAsString(reconstructorContent, flush: true);
print('✅ Successfully generated proxy library at $_outputLibraryPath');
print('✅ Successfully generated reconstructor library at ${reconstructorFile.path}');
print('Generating JavaScript API library...');
final jsApiContent = await generateJsApiFile(allProxyClasses, enumClasses, session, _membersToSkip);
final jsApiFile = File('web/lib.js');
await jsApiFile.writeAsString(jsApiContent, flush: true);
print('✅ Successfully generated JavaScript API library at ${jsApiFile.path}');
print('Generating JavaScript reconstructor library...');
final jsReconstructorContent = generateJsReconstructorFile(dataClasses, enumClasses);
final jsReconstructorFile = File('lib/eval/js_reconstructor_generated.dart');
await jsReconstructorFile.writeAsString(jsReconstructorContent, flush: true);
print('✅ Successfully generated JavaScript reconstructor library at ${jsReconstructorFile.path}');
stopwatch.stop();
print('Task completed in ${stopwatch.elapsed.inSeconds} seconds.');
}
Future<Set<InterfaceElement2>> _findDependentTypes(
List<ClassElement2> initialClasses,
List<ClassElement2> allProjectClasses,
AnalysisSession session,
) async {
final Set<InterfaceElement2> foundElements = {};
final List<InterfaceElement2> queue = List.from(initialClasses);
final Set<InterfaceElement2> processedElements = {};
final projectPath = session.analysisContext.contextRoot.root.path;
while (queue.isNotEmpty) {
final currentElement = queue.removeAt(0);
if (processedElements.contains(currentElement)) {
continue;
}
final path = session.uriConverter.uriToPath(currentElement.library2.uri);
if (path == null || !path.startsWith(projectPath)) {
continue;
}
processedElements.add(currentElement);
foundElements.add(currentElement);
if (currentElement is! ClassElement2) continue;
final classLibraryResult = await session.getResolvedLibraryByElement2(currentElement.library2);
if (classLibraryResult is! ResolvedLibraryResult) continue;
final visitor = _TypeFinderVisitor(queue, processedElements, session, projectPath);
if (currentElement.isAbstract || currentElement.isSealed) {
for (final potentialSubtype in allProjectClasses) {
if (potentialSubtype.supertype?.element3 == currentElement) {
if (!processedElements.contains(potentialSubtype)) {
queue.add(potentialSubtype);
}
}
}
}
final supertype = currentElement.supertype;
if (supertype != null) {
_collectTypes(supertype, queue, processedElements, session, projectPath);
}
for (final constructor in currentElement.constructors2) {
for (final param in constructor.formalParameters) {
_collectTypes(param.type, queue, processedElements, session, projectPath);
}
final declaration = classLibraryResult.getFragmentDeclaration(constructor.firstFragment);
declaration?.node.accept(visitor);
}
for (final field in currentElement.fields2) {
_collectTypes(field.type, queue, processedElements, session, projectPath);
final declaration = classLibraryResult.getFragmentDeclaration(field.firstFragment);
final initializer = (declaration?.node as VariableDeclaration?)?.initializer;
initializer?.accept(visitor);
}
final executables = [...currentElement.methods2, ...currentElement.getters2, ...currentElement.setters2];
for (final executable in executables) {
if (executable.isPrivate || _methodsToSkip.contains(executable.name3)) {
continue;
}
_collectTypes(executable.returnType, queue, processedElements, session, projectPath);
for (final param in executable.formalParameters) {
_collectTypes(param.type, queue, processedElements, session, projectPath);
}
final declaration = classLibraryResult.getFragmentDeclaration(executable.firstFragment);
declaration?.node.accept(visitor);
}
}
return foundElements;
}
void _collectTypes(
DartType type,
List<InterfaceElement2> queue,
Set<InterfaceElement2> processedElements,
AnalysisSession session,
String projectPath,
) {
if (type is FunctionType) {
_collectTypes(type.returnType, queue, processedElements, session, projectPath);
for (final parameter in type.formalParameters) {
_collectTypes(parameter.type, queue, processedElements, session, projectPath);
}
return;
}
if (type is InterfaceType) {
final element = type.element3;
final uri = element.library2.uri;
if (!uri.isScheme('dart')) {
final path = session.uriConverter.uriToPath(uri);
if (path != null && path.startsWith(projectPath)) {
if (!processedElements.contains(element)) {
queue.add(element);
}
}
}
for (final generic in type.typeArguments) {
_collectTypes(generic, queue, processedElements, session, projectPath);
}
}
}
cb.Enum _buildEnumProxy(EnumElement2 enumElement) {
final nativeName = enumElement.name3;
if (nativeName == null || nativeName.startsWith('_')) return cb.Enum((b) {});
return cb.Enum((b) {
b.name = 'Script$nativeName';
for (final field in enumElement.fields2) {
if (field.isEnumConstant && field.name3 != null) {
b.values.add(cb.EnumValue((v) => v.name = field.name3!));
}
}
});
}
Future<cb.Class> _buildClassProxy(
ClassElement2 classElement,
Set<String> allProxyNames,
AnalysisSession session,
) async {
final nativeName = classElement.name3;
if (nativeName == null) return cb.Class((b) {});
final membersToSkipForThisClass = _membersToSkip[nativeName] ?? const <String>{};
String proxyName;
if (classElement.isPrivate) {
if (!nativeName.startsWith('_')) return cb.Class((b) {});
proxyName = 'Script${nativeName.substring(1)}';
} else {
proxyName = 'Script$nativeName';
}
final classLibraryResult = await session.getResolvedLibraryByElement2(classElement.library2);
if (classLibraryResult is! ResolvedLibraryResult) {
return cb.Class((b) => b.name = proxyName);
}
final classBuilder = cb.ClassBuilder()
..name = proxyName
..sealed = classElement.isSealed
..abstract = classElement.isAbstract && !classElement.isSealed;
final supertype = classElement.supertype;
if (supertype != null && !supertype.element3.library2.uri.isScheme('dart')) {
var supertypeName = supertype.element3.name3 ?? '';
if (supertypeName.startsWith('_')) {
supertypeName = 'Script${supertypeName.substring(1)}';
} else {
supertypeName = 'Script$supertypeName';
}
classBuilder.extend = cb.refer(supertypeName);
}
final Map<String, dynamic> resolvedStaticValues = {};
final List<VariableDeclaration> unresolvedStaticFields = [];
final classDeclaration = classLibraryResult.getFragmentDeclaration(classElement.firstFragment)?.node;
if (classDeclaration is ClassDeclaration) {
for (final member in classDeclaration.members) {
if (member is FieldDeclaration && member.isStatic && !member.fields.variables.first.name.lexeme.startsWith('_')) {
for (final variable in member.fields.variables) {
final fieldName = variable.name.lexeme;
if (!membersToSkipForThisClass.contains(fieldName)) {
unresolvedStaticFields.add(variable);
}
}
}
}
int lastCount = -1;
while (unresolvedStaticFields.isNotEmpty && unresolvedStaticFields.length != lastCount) {
lastCount = unresolvedStaticFields.length;
unresolvedStaticFields.removeWhere((variable) {
final fieldName = variable.name.lexeme;
final initializer = variable.initializer;
final evaluatedValue = _evaluateConstantExpression(initializer, resolvedStaticValues, _typesToPrecalculate);
if (evaluatedValue != null) {
resolvedStaticValues[fieldName] = evaluatedValue;
final fieldElement = classElement.getField2(fieldName);
if (fieldElement == null) return true;
final fieldType = fieldElement.type;
final fieldTypeName = fieldType.element3?.name3;
final isProxyableObject = fieldTypeName != null && allProxyNames.contains(fieldTypeName);
final isPrimitive =
fieldType.isDartCoreDouble ||
fieldType.isDartCoreInt ||
fieldType.isDartCoreBool ||
fieldType.isDartCoreString;
if (isProxyableObject || isPrimitive) {
cb.Code assignmentCode;
if (initializer is SimpleIdentifier) {
assignmentCode = cb.Code(initializer.name);
} else if (evaluatedValue is Map && evaluatedValue['__constructor__'] == true) {
final String className = evaluatedValue['className'];
final List<dynamic> posArgs = evaluatedValue['positionalArgs'];
final posArgsStr = posArgs.map((v) => cb.literal(v).code).join(', ');
assignmentCode = cb.Code('Script$className($posArgsStr)');
} else {
assignmentCode = cb.literal(evaluatedValue).code;
}
classBuilder.fields.add(
cb.Field(
(f) => f
..name = fieldName
..static = true
..modifier = cb.FieldModifier.constant
..type = _mapTypeToCodeBuilderType(fieldType, allProxyNames)
..assignment = assignmentCode,
),
);
}
return true;
}
return false;
});
}
}
for (final variable in unresolvedStaticFields) {
final fieldName = variable.name.lexeme;
final fieldElement = classElement.getField2(fieldName);
final initializerSource = variable.initializer?.toSource();
if (fieldElement != null && initializerSource != null) {
classBuilder.fields.add(
cb.Field(
(f) => f
..name = fieldName
..static = true
..modifier = cb.FieldModifier.constant
..type = _mapTypeToCodeBuilderType(fieldElement.type, allProxyNames)
..assignment = cb.Code(_transformDefaultValue(initializerSource, allProxyNames)),
),
);
}
}
for (final field in classElement.fields2) {
if (field.isStatic || field.isSynthetic) continue;
final fieldName = field.name3;
if (fieldName == null || membersToSkipForThisClass.contains(fieldName)) {
continue;
}
final fieldBuilder = cb.FieldBuilder()
..name = fieldName
..type = _mapTypeToCodeBuilderType(field.type, allProxyNames, forceNullable: field.type.isNullable);
if (field.isFinal) {
fieldBuilder.modifier = cb.FieldModifier.final$;
}
final fieldLibraryResult = await session.getResolvedLibraryByElement2(field.library2);
if (fieldLibraryResult is ResolvedLibraryResult) {
final declaration = fieldLibraryResult.getFragmentDeclaration(field.firstFragment);
AstNode? nodeWithInitializer;
if (declaration?.node is VariableDeclaration) {
nodeWithInitializer = declaration!.node;
} else if (declaration?.node is FieldDeclaration) {
final fieldDeclNode = declaration!.node as FieldDeclaration;
nodeWithInitializer = fieldDeclNode.fields.variables.firstWhereOrNull((v) => v.name.lexeme == fieldName);
}
final initializer = (nodeWithInitializer as VariableDeclaration?)?.initializer;
if (initializer != null) {
final initializerCode = initializer.toSource();
if (initializerCode != 'null') {
fieldBuilder.assignment = cb.Code(_transformDefaultValue(initializerCode, allProxyNames));
}
}
}
classBuilder.fields.add(fieldBuilder.build());
}
final executables = [...classElement.methods2, ...classElement.getters2, ...classElement.setters2];
for (final executable in executables) {
final memberName = executable.name3;
if (executable.isPrivate || executable.isSynthetic || memberName == null) continue;
if (_methodsToSkip.contains(memberName) || membersToSkipForThisClass.contains(memberName)) {
continue;
}
classBuilder.methods.add(_buildMethodProxy(executable, allProxyNames, classLibraryResult));
}
final constructors = classElement.constructors2.toList();
if (constructors.isNotEmpty) {
for (final constructor in constructors) {
if (constructor.isFactory) {
classBuilder.constructors.add(
_buildFactoryConstructor(constructor, allProxyNames, classLibraryResult, nativeName),
);
} else {
classBuilder.constructors.add(_buildConstructor(constructor, allProxyNames, classLibraryResult, nativeName));
}
}
} else if (classElement.isAbstract) {
classBuilder.constructors.add(cb.Constructor((c) => c..constant = true));
}
return classBuilder.build();
}
cb.Constructor _buildConstructor(
ConstructorElement2 constructorElement,
Set<String> allProxyNames,
ResolvedLibraryResult libraryResult,
String originalClassName,
) {
final membersToSkipForThisClass = _membersToSkip[originalClassName] ?? const <String>{};
return cb.Constructor((b) {
b
..constant = constructorElement.isConst
..name =
(constructorElement.name3 == null || constructorElement.name3!.isEmpty || constructorElement.name3 == "new")
? null
: constructorElement.name3;
for (final param in constructorElement.formalParameters) {
final paramName = param.name3;
if (paramName != null && membersToSkipForThisClass.contains(paramName)) {
continue;
}
final parameter = cb.Parameter((p) {
p
..name = param.name3!
..toThis = param.isInitializingFormal && param is! SuperFormalParameterElement2
..toSuper = param is SuperFormalParameterElement2
..required = param.isRequiredNamed
..named = param.isNamed;
if (!param.isInitializingFormal) {
p.type = _mapTypeToCodeBuilderType(param.type, allProxyNames);
}
if (param.hasDefaultValue && param.defaultValueCode != 'null') {
p.defaultTo = cb.Code(_transformDefaultValue(param.defaultValueCode!, allProxyNames));
}
});
if (param.isNamed) {
b.optionalParameters.add(parameter);
} else {
b.requiredParameters.add(parameter);
}
}
final declaration = libraryResult.getFragmentDeclaration(constructorElement.firstFragment);
if (declaration?.node is! ConstructorDeclaration) {
return;
}
final constructorNode = declaration!.node as ConstructorDeclaration;
final Set<String> initializedByParameter = constructorElement.formalParameters
.where((p) => p.isInitializingFormal)
.map((p) => p.name3!)
.toSet();
for (final initializer in constructorNode.initializers) {
bool shouldAdd = true;
if (initializer is ConstructorFieldInitializer) {
if (initializedByParameter.contains(initializer.fieldName.name)) {
shouldAdd = false;
}
}
if (shouldAdd) {
final sourceCode = initializer.toSource();
final transformedCode = _transformDefaultValue(sourceCode, allProxyNames);
b.initializers.add(cb.Code(transformedCode));
}
}
});
}
cb.Constructor _buildFactoryConstructor(
ConstructorElement2 constructor,
Set<String> allProxyNames,
ResolvedLibraryResult libraryResult,
String originalClassName,
) {
final membersToSkipForThisClass = _membersToSkip[originalClassName] ?? const <String>{};
return cb.Constructor((b) {
b
..factory = true
..name = (constructor.name3 == null || constructor.name3!.isEmpty || constructor.name3 == "new")
? null
: constructor.name3;
for (final param in constructor.formalParameters) {
final paramName = param.name3;
if (paramName != null && membersToSkipForThisClass.contains(paramName)) {
continue;
}
final parameter = cb.Parameter((p) {
p
..name = param.name3!
..required = param.isRequiredNamed
..named = param.isNamed;
p.type = _mapTypeToCodeBuilderType(param.type, allProxyNames);
if (param.hasDefaultValue && param.defaultValueCode != 'null') {
p.defaultTo = cb.Code(_transformDefaultValue(param.defaultValueCode!, allProxyNames));
}
});
if (param.isNamed) {
b.optionalParameters.add(parameter);
} else {
b.requiredParameters.add(parameter);
}
}
final declaration = libraryResult.getFragmentDeclaration(constructor.firstFragment);
if (declaration?.node is! ConstructorDeclaration) return;
final constructorNode = declaration!.node as ConstructorDeclaration;
if (constructorNode.redirectedConstructor != null) {
final redirectSource = constructorNode.redirectedConstructor!.toSource();
b.redirect = cb.refer(_transformDefaultValue(redirectSource, allProxyNames));
return;
}
final body = constructorNode.body;
if (body is ExpressionFunctionBody) {
b.lambda = true;
final expressionSource = body.expression.toSource();
b.body = cb.Code(_transformDefaultValue(expressionSource, allProxyNames));
} else if (body is BlockFunctionBody) {
final blockSource = body.block.toSource();
final statements = blockSource.substring(1, blockSource.length - 1).trim();
b.body = cb.Code(_transformDefaultValue(statements, allProxyNames));
} else if (body is EmptyFunctionBody) {}
});
}
cb.Method _buildMethodProxy(
ExecutableElement2 executable,
Set<String> allProxyNames,
ResolvedLibraryResult libraryResult,
) {
final declaration = libraryResult.getFragmentDeclaration(executable.firstFragment);
if (declaration?.node is! MethodDeclaration) {
print(' - WARNING: Could not find MethodDeclaration for ${executable.name3}');
return cb.Method((b) {});
}
final methodNode = declaration!.node as MethodDeclaration;
return cb.Method((b) {
b
..name = executable.name3
..static = executable.isStatic
..returns = _mapTypeToCodeBuilderType(executable.returnType, allProxyNames);
for (final param in executable.typeParameters2) {
b.types.add(
cb.TypeReference((t) {
t.symbol = param.name3;
final bound = param.bound;
if (bound != null && !bound.isDartCoreObject) {
t.bound = _mapTypeToCodeBuilderType(bound, allProxyNames);
}
}),
);
}
if (methodNode.isGetter) {
b.type = cb.MethodType.getter;
} else if (methodNode.isSetter) {
b.type = cb.MethodType.setter;
}
for (final param in executable.formalParameters) {
final parameter = cb.Parameter((p) {
p
..name = param.name3!
..required = param.isRequiredNamed
..named = param.isNamed
..type = _mapTypeToCodeBuilderType(param.type, allProxyNames);
if (param.hasDefaultValue && param.defaultValueCode != 'null') {
p.defaultTo = cb.Code(_transformDefaultValue(param.defaultValueCode!, allProxyNames));
}
});
if (param.isNamed) {
b.optionalParameters.add(parameter);
} else {
b.requiredParameters.add(parameter);
}
}
final body = methodNode.body;
if (body is ExpressionFunctionBody) {
b.lambda = true;
final expressionSource = body.expression.toSource();
b.body = cb.Code(_transformDefaultValue(expressionSource, allProxyNames));
} else if (body is BlockFunctionBody) {
final blockSource = body.block.toSource();
final statements = blockSource.substring(1, blockSource.length - 1).trim();
b.body = cb.Code(_transformDefaultValue(statements, allProxyNames));
}
});
}
cb.Reference _mapTypeToCodeBuilderType(DartType type, Set<String> allProxyNames, {bool forceNullable = false}) {
final isNullable = forceNullable || type.nullabilitySuffix == NullabilitySuffix.question;
if (type is FunctionType) {
return cb.FunctionType((b) {
b
..isNullable = isNullable
..returnType = _mapTypeToCodeBuilderType(type.returnType, allProxyNames);
for (final param in type.formalParameters) {
final paramType = _mapTypeToCodeBuilderType(param.type, allProxyNames);
if (param.isNamed) {
b.namedParameters[param.name3!] = paramType;
} else if (param.isOptionalPositional) {
b.optionalParameters.add(paramType);
} else {
b.requiredParameters.add(paramType);
}
}
});
}
final element = type.element3;
if (element == null || element.name3 == 'dynamic') return cb.refer('dynamic');
if (element.isPrivate) {
final supertype = (element as ClassElement2).supertype;
if (supertype != null && !supertype.isDartCoreObject) {
return _mapTypeToCodeBuilderType(supertype, allProxyNames, forceNullable: isNullable);
}
}
final elementName = element.name3!;
final libraryUri = element.library2!.uri;
if (libraryUri.isScheme('dart')) {
requiredImports.add(libraryUri.toString());
return cb.TypeReference((b) {
b
..symbol = elementName
..url = libraryUri.toString()
..isNullable = isNullable;
if (type is InterfaceType) {
b.types.addAll(type.typeArguments.map((t) => _mapTypeToCodeBuilderType(t, allProxyNames)));
}
});
}
final name = elementName;
final finalName = allProxyNames.contains(name) ? 'Script$name' : name;
return cb.TypeReference((b) {
b
..symbol = finalName
..isNullable = isNullable;
if (type is InterfaceType) {
b.types.addAll(type.typeArguments.map((t) => _mapTypeToCodeBuilderType(t, allProxyNames)));
}
});
}
String _transformDefaultValue(String defaultValue, Set<String> proxyNames) {
if (proxyNames.isEmpty) {
return defaultValue;
}
final sortedNames = proxyNames.toList()..sort((a, b) => b.length.compareTo(a.length));
final pattern = sortedNames.map((name) => RegExp.escape(name)).join('|');
final regex = RegExp('(?<!Script)\\b($pattern)\\b');
return defaultValue.replaceAllMapped(regex, (match) {
final matchedName = match.group(1)!;
if (matchedName.startsWith('_')) {
return 'Script${matchedName.substring(1)}';
} else {
return 'Script$matchedName';
}
});
}
class _TypeFinderVisitor extends RecursiveAstVisitor<void> {
final List<InterfaceElement2> queue;
final Set<InterfaceElement2> processedElements;
final AnalysisSession session;
final String projectPath;
_TypeFinderVisitor(this.queue, this.processedElements, this.session, this.projectPath);
@override
void visitNamedType(NamedType node) {
final element = node.element2;
if (element is InterfaceElement2) {
_collectTypes(element.thisType, queue, processedElements, session, projectPath);
}
super.visitNamedType(node);
}
@override
void visitPrefixedIdentifier(PrefixedIdentifier node) {
final element = node.prefix.element;
if (element is InterfaceElement2) {
_collectTypes(element.thisType, queue, processedElements, session, projectPath);
}
super.visitPrefixedIdentifier(node);
}
}
String _generateHeader() {
return '''
// Generated by tool/generate_proxies.dart
// ignore_for_file: unused_field
''';
}
extension on DartType {
bool get isNullable => nullabilitySuffix == NullabilitySuffix.question;
}
Set<InterfaceElement2> _resolveAmbiguities(Set<InterfaceElement2> elements, AnalysisSession session) {
final projectPath = session.analysisContext.contextRoot.root.path;
final uriConverter = session.uriConverter;
final groupedByName = groupBy(elements, (e) => e.name3);
final resolvedElements = <InterfaceElement2>{};
groupedByName.forEach((name, elementList) {
if (name == null || elementList.isEmpty) return;
if (elementList.length == 1) {
resolvedElements.add(elementList.first);
} else {
final localCandidates = elementList.where((e) {
final path = uriConverter.uriToPath(e.library2.uri);
return path != null && path.startsWith(projectPath);
}).toList();
if (localCandidates.length == 1) {
final localElement = localCandidates.first;
print(' - Resolving ambiguity for "$name": Prioritizing local version from ${localElement.library2.uri}');
resolvedElements.add(localElement);
} else if (localCandidates.isEmpty) {
print(
' - WARNING: Ambiguity for "$name" could not be resolved between external packages. Skipping proxy generation. Candidates: ${elementList.map((e) => e.library2.uri).join(', ')}',
);
} else {
print(
' - WARNING: Ambiguity for "$name" exists within your own project files. Skipping proxy generation. Candidates: ${localCandidates.map((e) => e.library2.uri).join(', ')}',
);
}
}
});
return resolvedElements;
}
dynamic _evaluateConstantExpression(
Expression? expression,
Map<String, dynamic> knownValues,
Set<String> typesToPrecalculate,
) {
if (expression == null) {
return null;
}
if (expression is IntegerLiteral) return expression.value;
if (expression is DoubleLiteral) return expression.value;
if (expression is BooleanLiteral) return expression.value;
if (expression is StringLiteral) return expression.stringValue;
if (expression is PrefixedIdentifier && expression.toSource() == 'double.infinity') {
return double.infinity;
}
if (expression is SimpleIdentifier) {
if (knownValues.containsKey(expression.name)) {
return knownValues[expression.name];
}
}
if (expression is BinaryExpression) {
final left = _evaluateConstantExpression(expression.leftOperand, knownValues, typesToPrecalculate);
final right = _evaluateConstantExpression(expression.rightOperand, knownValues, typesToPrecalculate);
if (left == null || right == null || left is! num || right is! num) {
return null;
}
switch (expression.operator.type) {
case TokenType.PLUS:
return left + right;
case TokenType.MINUS:
return left - right;
case TokenType.STAR:
return left * right;
case TokenType.SLASH:
return left / right;
default:
return null;
}
}
if (expression is InstanceCreationExpression) {
final staticType = expression.staticType;
if (staticType is! InterfaceType) return null;
final className = staticType.element3.name3;
if (className == null || !typesToPrecalculate.contains(className)) {
return null;
}
final positionalArgs = <dynamic>[];
final namedArgs = <String, dynamic>{};
bool canEvaluateAllArgs = true;
for (final arg in expression.argumentList.arguments) {
final argValue = _evaluateConstantExpression(
arg is NamedExpression ? arg.expression : arg,
knownValues,
typesToPrecalculate,
);
if (argValue == null) {
canEvaluateAllArgs = false;
break;
}
final valueToStore = argValue is Map && argValue['__constructor__'] == true
? argValue['positionalArgs'].first
: argValue;
if (arg is NamedExpression) {
namedArgs[arg.name.label.name] = valueToStore;
} else {
positionalArgs.add(valueToStore);
}
}
if (canEvaluateAllArgs) {
return {
'__constructor__': true,
'className': className,
'constructorName': expression.constructorName.name?.name,
'positionalArgs': positionalArgs,
'namedArgs': namedArgs,
};
}
}
return null;
}
Future<List<ClassElement2>> _findAllWidgetSubclasses(AnalysisSession session) async {
final projectPath = session.analysisContext.contextRoot.root.path;
final libFolder = session.resourceProvider.getFolder(p.join(projectPath, 'lib'));
final allWidgetClasses = <ClassElement2>[];
Future<void> findDartFiles(file_system.Folder folder) async {
final children = folder.getChildren();
for (final child in children) {
if (child is file_system.Folder) {
await findDartFiles(child);
} else if (child is file_system.File && child.path.endsWith('.dart')) {
final filePath = child.path;
final libraryResult = await session.getResolvedLibrary(filePath);
if (libraryResult is ResolvedLibraryResult) {
final libraryElement = libraryResult.element2;
for (final classElement in libraryElement.classes) {
if (!classElement.isAbstract && classElement.allSupertypes.any((type) => type.element3.name3 == 'Widget')) {
if (!allWidgetClasses.any((existing) => existing.name3 == classElement.name3)) {
allWidgetClasses.add(classElement);
}
}
}
}
}
}
}
await findDartFiles(libFolder);
return allWidgetClasses;
}
Future<List<ClassElement2>> _findAllProjectClasses(AnalysisSession session) async {
print('Scanning project for all class definitions...');
final projectPath = session.analysisContext.contextRoot.root.path;
final libFolder = session.resourceProvider.getFolder(p.join(projectPath, 'lib'));
final allProjectClasses = <ClassElement2>[];
final seenClassNames = <String>{};
Future<void> findDartFiles(file_system.Folder folder) async {
try {
final children = folder.getChildren();
for (final child in children) {
if (child is file_system.Folder) {
await findDartFiles(child);
} else if (child is file_system.File && child.path.endsWith('.dart')) {
final filePath = child.path;
final libraryResult = await session.getResolvedLibrary(filePath);
if (libraryResult is ResolvedLibraryResult) {
final libraryElement = libraryResult.element2;
for (final classElement in libraryElement.classes) {
final name = classElement.name3;
if (name != null && seenClassNames.add(name)) {
allProjectClasses.add(classElement);
}
}
}
}
}
} catch (e) {
print(' - Warning: Could not read children of folder ${folder.path}: $e');
}
}
await findDartFiles(libFolder);
print('Found ${allProjectClasses.length} total classes in the project.');
return allProjectClasses;
}
OK. I may take another stab at it soon. If you feel like implementing it yourself that is also fine, I don't know if/when I will do it myself. |
Ok. I doubt I'll take a stab at it anytime soon but I'll let you know if that changes |
Solves this test case:
See also #195. This doesn't close that issue because it doesn't work for top level variables for some reason. I think in those cases we need to actually compute the value(?), which this does not do. The current implementation just reorders the declarations. We would need some new
evaluateConstExpression
method that does this. The issue is I'm not sure I would be able to create an exhaustive method for that. I'm opening this PR to get feedback. Maybe it's good enough as is, maybe it needs more work.I did create a simple
evaluateConstExpression
myself for my own project as a workaround until making this PR but as mentioned it isn't exhaustive.code
Open to changing this implementation, of course, let me know what you think, Thanks!