// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. library kernel.transformations.method_call; import 'dart:math' as math; import '../ast.dart'; import '../class_hierarchy.dart'; import '../core_types.dart'; import '../kernel.dart'; import '../visitor.dart'; /// Problems with the method rewrite transformation: /// /// * Cannot rewrite invocations to things called "call" because of tear-offs /// and whatnot that when invoked turns into variableName.call(...). /// /// * Cannot rewrite invocations to things sharing a name with a field because /// one could have called clazz.fieldName(...). /// /// * Rewrites will make stacktraces look weird. /// /// * Rewrites will make noSuchMethod look weird --- e.g. calling a non-existing /// function foo(a: 42) turns into foo%0%a(42), i.e. the method name has /// changed ("foo" vs "foo%0%a") and the arguments has changed (named "a" vs /// positional). /// NOTE: At least for now this can be fixed by changing the /// invocation_mirror_patch file. Doing this I can make all dill, language /// and co19 tests pass! /// /// Somewhat weird: /// /// * Inserts methods that redirect to the correct noSuchMethod invocation /// so that program #1 example below will work. /// The reason it otherwise wouldn't is that b.foo(499, named1: 88) is /// rewritten to b.foo%1%named1(499, 88) which is not legal for class B /// (thus the method would not be create there) but IS legal for Bs parent /// class (A), so it would be created there. The call will thus go to the /// super (A) but shouldn't as foo was overwritten in B. /// /// Program #1 example: /// class A { /// foo(required1, { named1: 499}) => print("Hello from class A"); /// } /// /// class B extends A { /// foo(required1) => print("Hello from class B"); /// } /// /// main() { /// var b = new B(); /// b.foo(499, named1: 88); /// } Program transformProgram( CoreTypes coreTypes, ClassHierarchy hierarchy, Program program, [debug = false]) { new MethodCallTransformer(coreTypes, hierarchy, debug).visitProgram(program); return program; } class MethodCallTransformer extends Transformer { final CoreTypes coreTypes; /// Keep track of "visited" procedures and constructors to not visit already /// visited stuff, nor visit newly created stubs. Set _visited = new Set(); /// Some things currently cannot be rewritten. Calls to methods called "call" /// (because invoking tear-offs and closures and whatnot becomes .call) /// as well as clashes with field names as a field can contain a function /// and one can then do a clazz.fieldName(...). Set blacklistedSelectors = new Set.from(["call"]); /// Map from a "originally named" procedure to the "%original" procedure /// for procedures that was moved. Map _movedBodies = {}; /// Map from a "originally named" constructor to the "%original" /// constructor for constructors that was moved. Map _movedConstructors = {}; /// For static method transformations: /// Maps a procedure to the mapping of argument signature to procedure stub. Map> _staticProcedureCalls = {}; /// For constructor transformations: /// Maps a constructor to a mapping of argument signature to constructor stub. Map> _constructorCalls = {}; /// For non-static method transformations: /// Maps from method name to the set of legal number of positional arguments. Map> _methodToLegalPositionalArgumentCount = {}; /// For non-static method transformations: /// Maps from name of method to the set of new target names seen for at least /// one instance (i.e. rewriting has been performed from key to all values in /// the mapped to set at least once). Map> _rewrittenMethods = {}; /// For non-static method transformations: /// Maps a procedure to the mapping of argument signature to procedure stub. Map> _superProcedureCalls = {}; /// Whether in debug mode, i.e. if we can insert extra print statements for /// debugging purposes. bool _debug; /// For noSuchMethod calls. ClassHierarchy hierarchy; Constructor _invocationMirrorConstructor; // cached Procedure _listFrom; // cached MethodCallTransformer(this.coreTypes, this.hierarchy, this._debug); @override TreeNode visitProgram(Program node) { // First move body of all procedures that takes optional positional or named // parameters and record which non-static procedure names have optional // positional arguments. // Do the same for constructors. Then also rewrite constructor initializers // using LocalInitializer and sort named arguments in those initializers for (final library in node.libraries) { for (final procedure in new List.from(library.procedures)) { _moveAndTransformProcedure(procedure); } for (final clazz in library.classes) { for (final field in clazz.fields) { blacklistedSelectors.add(field.name.name); } for (final procedure in new List.from(clazz.procedures)) { // This call creates new procedures _moveAndTransformProcedure(procedure); _recordNonStaticProcedureAndVariableArguments(procedure); } for (final constructor in new List.from(clazz.constructors)) { // This call creates new constructors _moveAndTransformConstructor(constructor); } for (final constructor in clazz.constructors) { _rewriteConstructorInitializations(constructor); } } } // Rewrite calls node.transformChildren(this); // Now for all method calls that was rewritten, make sure those call // destinations actually exist, i.e. for each method with a matching name // where the called-with-arguments is legal, create a stub for (final library in node.libraries) { for (final clazz in library.classes) { for (final procedure in new List.from(clazz.procedures)) { // This call creates new procedures _createNeededNonStaticStubs(procedure); } } } return node; } @override TreeNode visitProcedure(Procedure node) { if (!_visited.contains(node)) { _visited.add(node); node.transformChildren(this); } return node; } @override TreeNode visitConstructor(Constructor node) { if (!_visited.contains(node)) { _visited.add(node); node.transformChildren(this); } return node; } @override TreeNode visitStaticInvocation(StaticInvocation node) { node.transformChildren(this); if (!_isMethod(node.target)) return node; if (!_hasAnyOptionalParameters(node.target.function)) return node; if (!_callIsLegal(node.target.function, node.arguments)) return node; // Rewrite with let if needed (without named arguments it won't do anything) Expression rewrittenNode = _rewriteWithLetAndSort(node, node.arguments); // Create/lookup target and set it as the new target node.target = _getNewTargetForStaticLikeInvocation( node.target, node.arguments, _staticProcedureCalls); // Now turn any named parameters into positional parameters _turnNamedArgumentsIntoPositional(node.arguments); return rewrittenNode; } @override TreeNode visitDirectMethodInvocation(DirectMethodInvocation node) { node.transformChildren(this); if (!_isMethod(node.target)) return node; if (!_hasAnyOptionalParameters(node.target.function)) return node; if (!_callIsLegal(node.target.function, node.arguments)) return node; // Rewrite with let if needed (without named arguments it won't do anything) Expression rewrittenNode = _rewriteWithLetAndSort(node, node.arguments); // Create/lookup target and set it as the new target node.target = _getNewTargetForStaticLikeInvocation( node.target, node.arguments, _superProcedureCalls); // Now turn any named parameters into positional parameters instead _turnNamedArgumentsIntoPositional(node.arguments); return rewrittenNode; } @override TreeNode visitSuperMethodInvocation(SuperMethodInvocation node) { // SuperMethodInvocation was changed since I originally wrote this, // and now it seems to never be called anyway. throw "visitSuperMethodInvocation is not implemented!"; } @override TreeNode visitMethodInvocation(MethodInvocation node) { node.transformChildren(this); final name = node.name.name; // Don't renamed calls to methods that clashes in name with a field // or is called "call". if (blacklistedSelectors.contains(name)) return node; // Rewrite with let if needed (without named arguments it won't do anything) Expression rewrittenNode = _rewriteWithLetAndSort(node, node.arguments); String argumentsSignature = _createArgumentsSignature(node.arguments); if (node.arguments.named.isEmpty) { // Positional: Don't rewrite if no procedure with that name can be called // with a variable number of arguments, or where the number of arguments // called with here isn't a legal number of arguments to any such // procedure. // Note for named arguments: Named arguments are always rewritten // (except for 'call' methods) so there's no such check final okCounts = _methodToLegalPositionalArgumentCount[name]; if (okCounts == null || !okCounts.contains(node.arguments.positional.length)) { return node; } } // Rewrite this call final originalName = node.name; node.name = _createName(node.name, argumentsSignature); // Remember that we rewrote this call _rewrittenMethods .putIfAbsent(originalName, () => new Set()) .add(argumentsSignature); // Now turn any named parameters into positional parameters instead _turnNamedArgumentsIntoPositional(node.arguments); return rewrittenNode; } @override TreeNode visitConstructorInvocation(ConstructorInvocation node) { node.transformChildren(this); if (!_callIsLegal(node.target.function, node.arguments)) return node; Expression rewrittenNode; if (node.isConst) { // Sort named arguments by name => it's const so there's no side-effects! // but DO NOT rewrite with let! node.arguments.named.sort((a, b) => a.name.compareTo(b.name)); rewrittenNode = node; } else { rewrittenNode = _rewriteWithLetAndSort(node, node.arguments); } node.target = _getNewTargetForConstructor(node.target, node.arguments); // Now turn named parameters into positional parameters instead _turnNamedArgumentsIntoPositional(node.arguments); return rewrittenNode; } @override TreeNode visitSuperInitializer(SuperInitializer node) { // Note that sorting was done in _rewriteConstructorInitializations node.transformChildren(this); if (!_callIsLegal(node.target.function, node.arguments)) return node; node.target = _getNewTargetForConstructor(node.target, node.arguments); // Now turn named parameters into positional parameters instead _turnNamedArgumentsIntoPositional(node.arguments); return node; } @override TreeNode visitRedirectingInitializer(RedirectingInitializer node) { // Note that sorting was done in _rewriteConstructorInitializations node.transformChildren(this); if (!_callIsLegal(node.target.function, node.arguments)) return node; node.target = _getNewTargetForConstructor(node.target, node.arguments); // Now turn named parameters into positional parameters instead _turnNamedArgumentsIntoPositional(node.arguments); return node; } /// Gets the new target for an invocation, using cache or creating a new one. /// /// Assumes that any let-rewrite, named argument sorting etc has been done /// already. Procedure _getNewTargetForStaticLikeInvocation(Procedure target, Arguments arguments, Map> cache) { final createdProcedures = cache.putIfAbsent(target, () => {}); // Rewrite target final argumentsSignature = _createArgumentsSignature(arguments); return createdProcedures[argumentsSignature] ?? _createAndCacheInvocationProcedure( argumentsSignature, arguments.positional.length, arguments.named.map((e) => e.name).toList(), target, _movedBodies[target], createdProcedures, true); } /// Rewrite the [Argument]s turning named arguments into positional arguments. /// /// Note that if the [Argument]s does not take any named parameters this /// method does nothing. void _turnNamedArgumentsIntoPositional(Arguments arguments) { for (final named in arguments.named) { arguments.positional.add(named.value..parent = arguments); } arguments.named.clear(); } /// Gets the new target for an invocation, using cache or creating a new one. /// /// Assumes that any let-rewrite, named argument sorting etc has been done /// already. Constructor _getNewTargetForConstructor( Constructor target, Arguments arguments) { if (!_isNotExternal(target)) return target; if (!_hasAnyOptionalParameters(target.function)) return target; final argumentsSignature = _createArgumentsSignature(arguments); final createdConstructor = _constructorCalls.putIfAbsent(target, () => {}); return createdConstructor[argumentsSignature] ?? _createAndCacheInvocationConstructor( argumentsSignature, arguments.positional.length, arguments.named.map((e) => e.name).toList(), target, _movedConstructors[target], createdConstructor, true); } /// Create a signature for the [Arguments]. /// /// Assumes that any needed sorting etc has already been done. /// /// Looks like x%positionalCount%named --- but it shouldn't matter if always /// using these methods String _createArgumentsSignature(Arguments arguments) { String namedString = arguments.named.map((e) => e.name).join("%"); return "${arguments.positional.length}%$namedString"; } /// Parse the argument signature. /// /// First element will be the string representation of the number of /// positional arguments used. /// The rest will be the named arguments, except that with no named arguments /// there still is a 2nd entry: the empty string... List _parseArgumentsSignature(String argumentsSignature) { return argumentsSignature.split("%"); } /// Rewrites an expression with let, replacing expressions in the [Arguments]. /// /// Sorts the named arguments after rewriting with let. /// /// Note that this method does nothing if there are no named arguments, or the /// named arguments list contain only a single named argument as any sorting /// would have no effect. As such, the let-rewrite will also have no effect. /// In such a case the return value is [original]. Expression _rewriteWithLetAndSort(Expression original, Arguments arguments) { final named = arguments.named; // Only bother if names can be unordered if (named.length < 2) return original; // Rewrite named with let in given order Let let; for (int i = named.length - 1; i >= 0; i--) { VariableDeclaration letDeclaration = new VariableDeclaration.forValue(named[i].value); named[i].value = new VariableGet(letDeclaration)..parent = arguments; let = new Let(letDeclaration, let ?? original); } // Sort named arguments by name named.sort((a, b) => a.name.compareTo(b.name)); // Now also add the given positional arguments into the let final expressions = arguments.positional; for (int i = expressions.length - 1; i >= 0; i--) { VariableDeclaration letDeclaration = new VariableDeclaration.forValue(expressions[i]); expressions[i] = new VariableGet(letDeclaration)..parent = arguments; let = new Let(letDeclaration, let ?? original); } return let; } /// Creates all needed stubs for non static procedures. /// /// More specifically: If calls have been made to a procedure with the same /// name as this procedure with both 1 and 2 arguments, where both of these /// are legal inputs to this procedure, create stubs for both of them, /// each of which calls with whatever default parameter values are defined for /// the non-given arguments. void _createNeededNonStaticStubs(Procedure procedure) { final incomingCalls = _rewrittenMethods[procedure.name]; if (incomingCalls != null && procedure.kind == ProcedureKind.Method && !procedure.isStatic) { final createdOnSuper = _superProcedureCalls[procedure]; final names = procedure.function.namedParameters.map((e) => e.name).toSet(); // A procedure with this name was called on at least one object with // an argument signature like any in [incomingCalls] nextArgumentSignature: for (final argumentsSignature in incomingCalls) { // Skip if it was created in a super call already if (createdOnSuper != null && createdOnSuper.containsKey(argumentsSignature)) { continue; } final elements = _parseArgumentsSignature(argumentsSignature); int positional = int.parse(elements[0]); if (positional < procedure.function.requiredParameterCount || positional > procedure.function.positionalParameters.length) { // We don't take that number of positional parameters! // Call noSuchMethod in case anyone called on object with wrong // parameters, but where superclass does take these parameters. _createNoSuchMethodStub( argumentsSignature, positional, elements.sublist(1), procedure); continue; } if (elements.length > 2 || elements[1] != "") { // Named: Could the call be for this method? for (int i = 1; i < elements.length; i++) { String name = elements[i]; // Using a name that we don't have? if (!names.contains(name)) { // Call noSuchMethod in case anyone called on object with wrong // parameters, but where superclass does take these parameters. _createNoSuchMethodStub(argumentsSignature, positional, elements.sublist(1), procedure); continue nextArgumentSignature; } } } // Potential legal call => make stub // Note the ?? here: E.g. contains on list doesn't take optionals so it // wasn't moved, but calls were rewritten because contains on string // takes either 1 or 2 arguments. final destination = _movedBodies[procedure] ?? procedure; _createAndCacheInvocationProcedure(argumentsSignature, positional, elements.sublist(1), procedure, destination, {}, false); } } } /// Records how this procedure can be called (if it is non-static). /// /// More specifically: Assuming that the procedure given is non-static taking /// a variable number of positional parameters, record all number of arguments /// that is legal, e.g. foo(int a, [int b]) is legal for 1 and 2 parameters. /// If it takes named parameters, remember how many positional there is so /// we also know to rewrite calls without the named arguments. void _recordNonStaticProcedureAndVariableArguments(Procedure procedure) { if (_isMethod(procedure) && !procedure.isStatic && _hasAnyOptionalParameters(procedure.function)) { final name = procedure.name.name; final okCounts = _methodToLegalPositionalArgumentCount.putIfAbsent( name, () => new Set()); for (int i = procedure.function.requiredParameterCount; i <= procedure.function.positionalParameters.length; i++) { okCounts.add(i); } } } /// Move body of procedure to new procedure and call that from this procedure. /// /// More specifically: For all procedures with optional positional parameters, /// or named parameters, create a new procedure without optional positional /// parameters and named parameters and move the body of the original /// procedure into this new procedure. /// Then make the body of the original procedure call the new procedure. /// /// The idea is that all rewrites should call the moved procedure instead, /// bypassing the optional/named arguments entirely. void _moveAndTransformProcedure(Procedure procedure) { if (_isMethod(procedure) && _hasAnyOptionalParameters(procedure.function)) { final function = procedure.function; // Create variable lists final newParameterDeclarations = []; final newNamedParameterDeclarations = []; final newParameterVariableGets = []; final targetParameters = function.positionalParameters; final targetNamedParameters = function.namedParameters; _moveVariableInitialization( targetParameters, targetNamedParameters, newParameterDeclarations, newNamedParameterDeclarations, newParameterVariableGets, procedure.function); // Create new procedure looking like the old one // (with the old body and parameters) FunctionNode functionNode = _createShallowFunctionCopy(function); final newProcedure = new Procedure( _createOriginalName(procedure), ProcedureKind.Method, functionNode, isAbstract: procedure.isAbstract, isStatic: procedure.isStatic, isConst: procedure.isConst, fileUri: procedure.fileUri); // Add procedure to the code _addMember(procedure, newProcedure); // Map moved body _movedBodies[procedure] = newProcedure; // Transform original procedure if (procedure.isAbstract && procedure.function.body == null) { // do basically nothing then procedure.function.positionalParameters = newParameterDeclarations; procedure.function.namedParameters = newNamedParameterDeclarations; } else if (procedure.isStatic) { final expression = new StaticInvocation( newProcedure, new Arguments(newParameterVariableGets)); final statement = new ReturnStatement(expression) ..parent = procedure.function; procedure.function.body = statement; procedure.function.positionalParameters = newParameterDeclarations; procedure.function.namedParameters = newNamedParameterDeclarations; } else { final expression = new DirectMethodInvocation(new ThisExpression(), newProcedure, new Arguments(newParameterVariableGets)); final statement = new ReturnStatement(expression) ..parent = procedure.function; procedure.function.body = statement; procedure.function.positionalParameters = newParameterDeclarations; procedure.function.namedParameters = newNamedParameterDeclarations; } if (_debug) { // Debug flag set: Print something to the terminal before returning to // easily detect if rewrites are missing. Expression debugPrint = _getPrintExpression( "DEBUG! Procedure shouldn't have been called...", procedure); procedure.function.body = new Block( [new ExpressionStatement(debugPrint), procedure.function.body]) ..parent = procedure.function; } // Mark original procedure as seen (i.e. don't transform it further) _visited.add(procedure); } } /// Rewrite constructor initializers by introducing variables and sorting. /// /// For any* [SuperInitializer] or [RedirectingInitializer], extract the /// parameters, put them into variables, then sorting the named parameters. /// The idea is to sort the named parameters without changing any invocation /// order. /// /// * only with at least 2 named arguments, otherwise sorting would do nothing void _rewriteConstructorInitializations(Constructor constructor) { if (_isNotExternal(constructor)) { // Basically copied from "super_calls.dart" List initializers = constructor.initializers; int foundIndex = -1; Arguments arguments; for (int i = initializers.length - 1; i >= 0; --i) { Initializer initializer = initializers[i]; if (initializer is SuperInitializer) { foundIndex = i; arguments = initializer.arguments; break; } else if (initializer is RedirectingInitializer) { foundIndex = i; arguments = initializer.arguments; break; } } if (foundIndex == -1) return; // Rewrite using variables if using named parameters (so we can sort them) // (note that with 1 named it cannot be unsorted so we don't bother) if (arguments.named.length < 2) return; int argumentCount = arguments.positional.length + arguments.named.length; // Make room for [argumentCount] [LocalInitializer]s before the // super/redirector call. initializers.length += argumentCount; initializers.setRange( foundIndex + argumentCount, // destination start (inclusive) initializers.length, // destination end (exclusive) initializers, // source list foundIndex); // source start index // Fill in the [argumentCount] reserved slots with the evaluation // expressions of the arguments to the super/redirector constructor call int storeIndex = foundIndex; for (int i = 0; i < arguments.positional.length; ++i) { var variable = new VariableDeclaration.forValue(arguments.positional[i]); arguments.positional[i] = new VariableGet(variable)..parent = arguments; initializers[storeIndex++] = new LocalInitializer(variable) ..parent = constructor; } for (int i = 0; i < arguments.named.length; ++i) { NamedExpression argument = arguments.named[i]; var variable = new VariableDeclaration.forValue(argument.value); arguments.named[i].value = new VariableGet(variable)..parent = argument; initializers[storeIndex++] = new LocalInitializer(variable) ..parent = constructor; } // Sort the named arguments arguments.named.sort((a, b) => a.name.compareTo(b.name)); } } /// Move body of constructor to new one and call that from this one. /// /// More specifically: For all constructors with optional positional /// parameters, or named parameters, create a new constructor without optional /// positional parameters and named parameters, and move the body of the /// original constructor into this new constructor. /// Then make the original constructor redirect to the new constructor. /// /// The idea is that all rewrites should call the moved constructor instead, /// bypassing the optional/named arguments entirely. /// /// This method is very similar to _moveAndTransformProcedure void _moveAndTransformConstructor(Constructor constructor) { if (_isNotExternal(constructor) && _hasAnyOptionalParameters(constructor.function)) { final function = constructor.function; // Create variable lists final newParameterDeclarations = []; final newNamedParameterDeclarations = []; final newParameterVariableGets = []; final targetParameters = function.positionalParameters; final targetNamedParameters = function.namedParameters; _moveVariableInitialization( targetParameters, targetNamedParameters, newParameterDeclarations, newNamedParameterDeclarations, newParameterVariableGets, constructor.function); // Create new constructor looking like the old one // (with the old body, parameters and initializers) FunctionNode functionNode = _createShallowFunctionCopy(function); final newConstructor = new Constructor(functionNode, name: _createOriginalName(constructor), isConst: constructor.isConst, isExternal: constructor.isExternal, initializers: constructor.initializers); // Add constructor to the code _addMember(constructor, newConstructor); // Map moved body _movedConstructors[constructor] = newConstructor; // Transform original constructor constructor.function.body = null; constructor.function.positionalParameters = newParameterDeclarations; constructor.function.namedParameters = newNamedParameterDeclarations; constructor.initializers = [ new RedirectingInitializer( newConstructor, new Arguments(newParameterVariableGets)) ..parent = constructor ]; if (_debug) { // Debug flag set: Print something to the terminal before returning to // easily detect if rewrites are missing. Expression debugPrint = _getPrintExpression( "DEBUG! Constructor shouldn't have been called...", constructor); var variable = new VariableDeclaration.forValue(debugPrint); final debugInitializer = new LocalInitializer(variable) ..parent = constructor; final redirector = constructor.initializers[0]; constructor.initializers = [debugInitializer, redirector]; } // Mark original procedure as seen (i.e. don't transform it further) _visited.add(constructor); } } /// Creates a new [FunctionNode] based on the given one. /// /// Parameters are taken directly (i.e. after returning the parameters will /// have a new parent (the returned value), but still be referenced in the /// original [FunctionNode]. /// The same goes for the body of the function. /// The caller should take steps to remedy this after this call. /// /// The parameters are no longer optional and named parameters have been /// sorted and turned into regular parameters in the returned [FunctionNode]. FunctionNode _createShallowFunctionCopy(FunctionNode function) { final newParameters = new List.from(function.positionalParameters); final named = new List.from(function.namedParameters); named.sort((a, b) => a.name.compareTo(b.name)); newParameters.addAll(named); final functionNode = new FunctionNode(function.body, positionalParameters: newParameters, namedParameters: [], requiredParameterCount: newParameters.length, returnType: function.returnType, asyncMarker: function.asyncMarker, dartAsyncMarker: function.dartAsyncMarker); return functionNode; } /// Creates new variables, moving old initializers into them /// /// Specifically: Given lists for output, create new variables based on /// original parameters. Any new variable will receive the original variables /// initializer, and the original variable will have its initializer set to /// null. /// Named parameters have been sorted in [newParameterVariableGets]. void _moveVariableInitialization( List originalParameters, List originalNamedParameters, List newParameterDeclarations, List newNamedParameterDeclarations, List newParameterVariableGets, TreeNode newStuffParent) { for (final orgVar in originalParameters) { final variableDeclaration = new VariableDeclaration(orgVar.name, initializer: orgVar.initializer, type: orgVar.type, isFinal: orgVar.isFinal, isConst: orgVar.isConst) ..parent = newStuffParent; variableDeclaration.initializer?.parent = variableDeclaration; newParameterDeclarations.add(variableDeclaration); orgVar.initializer = null; newParameterVariableGets.add(new VariableGet(variableDeclaration)); } // Named expressions in newParameterVariableGets should be sorted final tmp = new List<_Pair>(); for (final orgVar in originalNamedParameters) { final variableDeclaration = new VariableDeclaration(orgVar.name, initializer: orgVar.initializer, type: orgVar.type, isFinal: orgVar.isFinal, isConst: orgVar.isConst) ..parent = newStuffParent; variableDeclaration.initializer?.parent = variableDeclaration; newNamedParameterDeclarations.add(variableDeclaration); orgVar.initializer = null; tmp.add(new _Pair(orgVar.name, new VariableGet(variableDeclaration))); } tmp.sort((a, b) => a.key.compareTo(b.key)); for (final item in tmp) { newParameterVariableGets.add(item.value); } } /// Creates a stub redirecting to noSuchMethod. /// /// Needed because if B extends A, both have a foo method, but taking /// different optional parameters, a call on an instance of B with parameters /// for A should actually result in a noSuchMethod call, but if only A has /// the rewritten method name, that method will be called... /// TODO: We only have to create these stubs for arguments that a procedures /// super allows, otherwise it will become a noSuchMethod automatically! Procedure _createNoSuchMethodStub( String argumentsSignature, int positionalCount, List givenNamedParameters, Procedure existing) { // Build parameter lists final newParameterDeclarations = []; final newParameterVariableGets = []; for (int i = 0; i < positionalCount + givenNamedParameters.length; i++) { final variableDeclaration = new VariableDeclaration("v%$i"); newParameterDeclarations.add(variableDeclaration); newParameterVariableGets.add(new VariableGet(variableDeclaration)); } var procedureName = _createName(existing.name, argumentsSignature); // Find noSuchMethod to call Member noSuchMethod = hierarchy.getDispatchTarget( existing.enclosingClass, new Name("noSuchMethod")); Arguments argumentsToNoSuchMethod; if (noSuchMethod.function.positionalParameters.length == 1 && noSuchMethod.function.namedParameters.isEmpty) { // We have a correct noSuchMethod method. ConstructorInvocation invocation = _createInvocation( procedureName.name, new Arguments(newParameterVariableGets)); argumentsToNoSuchMethod = new Arguments([invocation]); } else { // Get noSuchMethod on Object then... noSuchMethod = hierarchy.getDispatchTarget( coreTypes.objectClass, new Name("noSuchMethod")); ConstructorInvocation invocation = _createInvocation( procedureName.name, new Arguments(newParameterVariableGets)); ConstructorInvocation invocationPrime = _createInvocation("noSuchMethod", new Arguments([invocation])); argumentsToNoSuchMethod = new Arguments([invocationPrime]); } // Create return statement to call noSuchMethod ReturnStatement statement; final expression = new DirectMethodInvocation( new ThisExpression(), noSuchMethod, argumentsToNoSuchMethod); statement = new ReturnStatement(expression); // Build procedure final functionNode = new FunctionNode(statement, positionalParameters: newParameterDeclarations, namedParameters: [], requiredParameterCount: newParameterDeclarations.length, returnType: existing.function.returnType, asyncMarker: existing.function.asyncMarker, dartAsyncMarker: existing.function.dartAsyncMarker); final procedure = new Procedure( procedureName, ProcedureKind.Method, functionNode, isStatic: existing.isStatic, fileUri: existing.fileUri); // Add procedure to the code _addMember(existing, procedure); // Mark the new procedure as visited already (i.e. don't rewrite it again!) _visited.add(procedure); return procedure; } /// Creates an "new _InvocationMirror(...)" invocation. ConstructorInvocation _createInvocation( String methodName, Arguments callArguments) { if (_invocationMirrorConstructor == null) { Class clazz = coreTypes.invocationMirrorClass; _invocationMirrorConstructor = clazz.constructors[0]; } // The _InvocationMirror constructor takes the following arguments: // * Method name (a string). // * An arguments descriptor - a list consisting of: // - number of arguments (including receiver). // - number of positional arguments (including receiver). // - pairs (2 entries in the list) of // * named arguments name. // * index of named argument in arguments list. // * A list of arguments, where the first ones are the positional arguments. // * Whether it's a super invocation or not. int numPositionalArguments = callArguments.positional.length + 1; int numArguments = numPositionalArguments + callArguments.named.length; List argumentsDescriptor = [ new IntLiteral(numArguments), new IntLiteral(numPositionalArguments) ]; List arguments = []; arguments.add(new ThisExpression()); for (Expression pos in callArguments.positional) { arguments.add(pos); } for (NamedExpression named in callArguments.named) { argumentsDescriptor.add(new StringLiteral(named.name)); argumentsDescriptor.add(new IntLiteral(arguments.length)); arguments.add(named.value); } return new ConstructorInvocation( _invocationMirrorConstructor, new Arguments([ new StringLiteral(methodName), _fixedLengthList(argumentsDescriptor), _fixedLengthList(arguments), new BoolLiteral(false) ])); } /// Create a fixed length list containing given expressions. Expression _fixedLengthList(List list) { _listFrom ??= coreTypes.listFromConstructor; return new StaticInvocation( _listFrom, new Arguments([new ListLiteral(list)], named: [new NamedExpression("growable", new BoolLiteral(false))], types: [const DynamicType()])); } /// Creates a new procedure taking given arguments, caching it. /// /// Copies any non-given default values for parameters into the new procedure /// to be able to call the [realTarget] without using optionals and named /// parameters. Procedure _createAndCacheInvocationProcedure( String argumentsSignature, int positionalCount, List givenNamedParameters, Procedure target, Procedure realTarget, Map createdProcedures, bool doSpecialCaseForAllParameters) { // Special case: Calling with all parameters if (doSpecialCaseForAllParameters && positionalCount == target.function.positionalParameters.length && givenNamedParameters.length == target.function.namedParameters.length) { // We don't cache this procedure as this could make it look like // something with name argumentsSignature actually exists // while it doesn't (which is bad as we could then decide that we don't // need to create a stub even though we do!) return realTarget; } // Create and cache (save) constructor // Build parameter lists final newParameterDeclarations = []; final newParameterVariableGets = []; _extractAndCreateParameters(positionalCount, newParameterDeclarations, newParameterVariableGets, target, givenNamedParameters); // Create return statement to call real target ReturnStatement statement; if (target.isAbstract && target.function?.body == null) { // statement should just be null then } else if (target.isStatic) { final expression = new StaticInvocation( realTarget, new Arguments(newParameterVariableGets)); statement = new ReturnStatement(expression); } else { final expression = new DirectMethodInvocation(new ThisExpression(), realTarget, new Arguments(newParameterVariableGets)); statement = new ReturnStatement(expression); } // Build procedure final functionNode = new FunctionNode(statement, positionalParameters: newParameterDeclarations, namedParameters: [], requiredParameterCount: newParameterDeclarations.length, returnType: target.function.returnType, asyncMarker: target.function.asyncMarker, dartAsyncMarker: target.function.dartAsyncMarker); final procedure = new Procedure( _createName(target.name, argumentsSignature), ProcedureKind.Method, functionNode, isAbstract: target.isAbstract, isStatic: target.isStatic, isConst: target.isConst, fileUri: target.fileUri); // Add procedure to the code _addMember(target, procedure); // Cache it for future reference createdProcedures[argumentsSignature] = procedure; // Mark the new procedure as visited already (i.e. don't rewrite it again!) _visited.add(procedure); return procedure; } /// Creates a new constructor taking given arguments, caching it. /// /// Copies any non-given default values for parameters into the new /// constructor to be able to call the [realTarget] without using optionals /// and named parameters. Constructor _createAndCacheInvocationConstructor( String argumentsSignature, int positionalCount, List givenNamedParameters, Constructor target, Constructor realTarget, Map createdConstructor, bool doSpecialCaseForAllParameters) { // Special case: Calling with all parameters if (doSpecialCaseForAllParameters && positionalCount == target.function.positionalParameters.length && givenNamedParameters.length == target.function.namedParameters.length) { createdConstructor[argumentsSignature] = realTarget; return realTarget; } // Create and cache (save) constructor // Build parameter lists final newParameterDeclarations = []; final newParameterVariableGets = []; _extractAndCreateParameters(positionalCount, newParameterDeclarations, newParameterVariableGets, target, givenNamedParameters); // Build constructor final functionNode = new FunctionNode(null, positionalParameters: newParameterDeclarations, namedParameters: [], requiredParameterCount: newParameterDeclarations.length, returnType: target.function.returnType, asyncMarker: target.function.asyncMarker, dartAsyncMarker: target.function.dartAsyncMarker); final constructor = new Constructor(functionNode, name: _createName(target.name, argumentsSignature), isConst: target.isConst, isExternal: target.isExternal, initializers: [ new RedirectingInitializer( realTarget, new Arguments(newParameterVariableGets)) ]); // Add procedure to the code _addMember(target, constructor); // Cache it for future reference createdConstructor[argumentsSignature] = constructor; // Mark the new procedure as visited already (i.e. don't rewrite it again!) _visited.add(constructor); return constructor; } /// Extracts and creates parameters into the first two given lists. /// /// What is done: /// Step 1: Re-create the parameters given (i.e. the non-optional positional /// ones) - i.e. create a new variable with the same name etc, put it in /// [newParameterDeclarations]; create VariableGet for that and put it in /// [newParameterVariableGets] /// Step 2: Re-create the positional parameters NOT given, i.e. insert /// defaults and add to [newParameterVariableGets] only. /// Step 3: Re-create the named arguments (in sorted order). For actually /// given named parameters, do as in step 1, for not-given named parameters /// do as in step 2. /// /// NOTE: [newParameterDeclarations] and [newParameterVariableGets] are OUTPUT /// lists. void _extractAndCreateParameters( int positionalCount, List newParameterDeclarations, List newParameterVariableGets, Member target, List givenNamedParameters) { // First re-create the parameters given (i.e. the non-optional positional ones) final targetParameters = target.function.positionalParameters; positionalCount = math.min(positionalCount, targetParameters.length); for (int i = 0; i < positionalCount; i++) { final orgVar = targetParameters[i]; final variableDeclaration = new VariableDeclaration(orgVar.name, type: orgVar.type, isFinal: orgVar.isFinal, isConst: orgVar.isConst); newParameterDeclarations.add(variableDeclaration); newParameterVariableGets.add(new VariableGet(variableDeclaration)); } // Default parameters for the rest of them _fillInPositionalParameters( positionalCount, target, newParameterVariableGets); // Then all named parameters (given here or not) final orgNamed = new List.from(target.function.namedParameters); orgNamed.sort((a, b) => a.name.compareTo(b.name)); final givenArgumentsIterator = givenNamedParameters.iterator; givenArgumentsIterator.moveNext(); for (VariableDeclaration named in orgNamed) { if (givenArgumentsIterator.current == named.name) { // We have that one: Use it and move the iterator final variableDeclaration = new VariableDeclaration(named.name); newParameterDeclarations.add(variableDeclaration); newParameterVariableGets.add(new VariableGet(variableDeclaration)); givenArgumentsIterator.moveNext(); } else { // We don't have that one: Fill it in _fillInSingleParameter(named, newParameterVariableGets, target); } } } /// Adds the new member the same place as the existing member void _addMember(Member existingMember, Member newMember) { if (existingMember.enclosingClass != null) { existingMember.enclosingClass.addMember(newMember); } else { existingMember.enclosingLibrary.addMember(newMember); } } /// Create expressions based on the default values from the given [Member]. /// /// More specifically: static gets and nulls will be "copied" whereas other /// things (e.g. literals or things like "a+b") will be moved from the /// original member as argument initializers to const fields and both the /// original member and the expression-copy will use static gets to these. void _fillInPositionalParameters( int startFrom, Member copyFrom, List fillInto) { final targetParameters = copyFrom.function.positionalParameters; for (int i = startFrom; i < targetParameters.length; i++) { final parameter = targetParameters[i]; _fillInSingleParameter(parameter, fillInto, copyFrom); } } /// Create expression based on the default values from the given variable. /// /// More specifically: a static get or null will be "copied" whereas other /// things (e.g. literals or things like "a+b") will be moved from the /// original member as an argument initializer to a const field and both the /// original member and the expression-copy will use a static get to it. void _fillInSingleParameter(VariableDeclaration parameter, List fillInto, Member copyFrom) { if (parameter.initializer is StaticGet) { // Reference to const => recreate it StaticGet staticGet = parameter.initializer; fillInto.add(new StaticGet(staticGet.target)); } else if (parameter.initializer == null) { // No default given => output null fillInto.add(new NullLiteral()); } else if (parameter.initializer is IntLiteral) { // Int literal => recreate (or else class ByteBuffer in typed_data will // get 2 fields and the C++ code will complain!) IntLiteral value = parameter.initializer; fillInto.add(new IntLiteral(value.value)); } else { // Advanced stuff => move to static const field and reference that final initializer = parameter.initializer; final f = new Field( new Name('${copyFrom.name.name}%_${parameter.name}', copyFrom.enclosingLibrary), type: parameter.type, initializer: initializer, isFinal: false, isConst: true, isStatic: true, fileUri: copyFrom.enclosingClass?.fileUri ?? copyFrom.enclosingLibrary.fileUri); initializer.parent = f; // Add field to the code if (copyFrom.enclosingClass != null) { copyFrom.enclosingClass.addMember(f); } else { copyFrom.enclosingLibrary.addMember(f); } // Use it at the call site fillInto.add(new StaticGet(f)); // Now replace the initializer in the method to a StaticGet parameter.initializer = new StaticGet(f)..parent = parameter; } } /// Create an "original name" for a member. /// /// Specifically, for a member "x" just returns "x%original"; Name _createOriginalName(Member member) { return new Name("${member.name.name}%original", member.enclosingLibrary); } /// Create a [Name] based on current name and argument signature. Name _createName(Name name, String argumentsSignature) { String nameString = '${name.name}%$argumentsSignature'; return new Name(nameString, name.library); } /// Is the procedure a method? bool _isMethod(Procedure procedure) => procedure.kind == ProcedureKind.Method; /// Is the procedure NOT marked as external? bool _isNotExternal(Constructor constructor) => !constructor.isExternal; /// Does the target function have any optional arguments? (positional/named) bool _hasAnyOptionalParameters(FunctionNode targetFunction) => _hasOptionalParameters(targetFunction) || _hasNamedParameters(targetFunction); /// Does the target function have optional positional arguments? bool _hasOptionalParameters(FunctionNode targetFunction) => targetFunction.positionalParameters.length > targetFunction.requiredParameterCount; /// Does the target function have named parameters? bool _hasNamedParameters(FunctionNode targetFunction) => targetFunction.namedParameters.isNotEmpty; bool _callIsLegal(FunctionNode targetFunction, Arguments arguments) { if ((targetFunction.requiredParameterCount > arguments.positional.length) || (targetFunction.positionalParameters.length < arguments.positional.length)) { // Given too few or too many positional arguments return false; } // Do we give named that we don't take? Set givenNamed = arguments.named.map((v) => v.name).toSet(); Set takenNamed = targetFunction.namedParameters.map((v) => v.name).toSet(); givenNamed.removeAll(takenNamed); return givenNamed.isEmpty; } // Below methods used to add debug prints etc Library _getDartCoreLibrary(Program program) { if (program == null) return null; return program.libraries.firstWhere((lib) => lib.importUri.scheme == 'dart' && lib.importUri.path == 'core'); } Procedure _getProcedureInLib(Library lib, String name) { if (lib == null) return null; return lib.procedures .firstWhere((procedure) => procedure.name.name == name); } Procedure _getProcedureInClassInLib( Library lib, String className, String procedureName) { if (lib == null) return null; Class clazz = lib.classes.firstWhere((clazz) => clazz.name == className); return clazz.procedures .firstWhere((procedure) => procedure.name.name == procedureName); } Expression _getPrintExpression(String msg, TreeNode treeNode) { TreeNode program = treeNode; while (program is! Program) program = program.parent; var finalMsg = msg; if (treeNode is Member) { finalMsg += " [ ${treeNode.name.name} ]"; if (treeNode.enclosingClass != null) { finalMsg += " [ class ${treeNode.enclosingClass.name} ]"; } if (treeNode.enclosingLibrary != null) { finalMsg += " [ lib ${treeNode.enclosingLibrary.name} ]"; } } var stacktrace = new StaticGet(_getProcedureInClassInLib( _getDartCoreLibrary(program), 'StackTrace', 'current')); var printStackTrace = new StaticInvocation( _getProcedureInLib(_getDartCoreLibrary(program), 'print'), new Arguments([ new StringConcatenation([ new StringLiteral(finalMsg), new StringLiteral("\n"), stacktrace, new StringLiteral("\n") ]) ])); return printStackTrace; } } class _Pair { final K key; final V value; _Pair(this.key, this.value); }