Add --delete-tostring-package-uri option to kernel compilers
This change adds toString transformation to gen_kernel and
frontend_server tools in Dart SDK.
The implementation and tests are derived from
pkg/frontend_server/lib/src/to_string_transformer.dart and
https://github.com/flutter/engine/tree/master/flutter_frontend_server/test.
In addition to _KeepToString in dart:ui, toString transformation
now supports @pragma('flutter:keep-to-string') to exclude
certain toString methods from the transformation without depending
on dart:ui.
pkg/frontend_server/lib/src/to_string_transformer.dart is not
cleaned up in this change yet as it is still used by Flutter
engine. Cleanup will be done after Flutter engine is cleaned up,
after it is switched to the implementation of toString transformer
added in this change.
This is also a step towards a unified kernel compiler
(https://github.com/dart-lang/sdk/issues/39126).
TEST=pkg/vm/test/transformations/to_string_transformer_test.dart
Issue: https://github.com/dart-lang/sdk/issues/46022
Issue: https://github.com/dart-lang/sdk/issues/39126
Change-Id: Icbfd3fa193d54f1fc6b2d7fa0bace82b3704f91f
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/200525
Reviewed-by: Dan Field <dnfield@google.com>
Reviewed-by: Vyacheslav Egorov <vegorov@google.com>
Commit-Queue: Alexander Markov <alexmarkov@google.com>
diff --git a/pkg/frontend_server/lib/frontend_server.dart b/pkg/frontend_server/lib/frontend_server.dart
index 635cdd2..4b3a9b0 100644
--- a/pkg/frontend_server/lib/frontend_server.dart
+++ b/pkg/frontend_server/lib/frontend_server.dart
@@ -141,6 +141,13 @@
..addFlag('track-widget-creation',
help: 'Run a kernel transformer to track creation locations for widgets.',
defaultsTo: false)
+ ..addMultiOption(
+ 'delete-tostring-package-uri',
+ help: 'Replaces implementations of `toString` with `super.toString()` for '
+ 'specified package',
+ valueHelp: 'dart:ui',
+ defaultsTo: const <String>[],
+ )
..addFlag('enable-asserts',
help: 'Whether asserts will be enabled.', defaultsTo: false)
..addFlag('sound-null-safety',
@@ -531,6 +538,7 @@
results = await _runWithPrintRedirection(() => compileToKernel(
_mainSource, compilerOptions,
includePlatform: options['link-platform'],
+ deleteToStringPackageUris: options['delete-tostring-package-uri'],
aot: options['aot'],
useGlobalTypeFlowAnalysis: options['tfa'],
environmentDefines: environmentDefines,
diff --git a/pkg/vm/lib/kernel_front_end.dart b/pkg/vm/lib/kernel_front_end.dart
index e0368f7..0458451 100644
--- a/pkg/vm/lib/kernel_front_end.dart
+++ b/pkg/vm/lib/kernel_front_end.dart
@@ -65,6 +65,7 @@
import 'transformations/unreachable_code_elimination.dart'
as unreachable_code_elimination;
import 'transformations/deferred_loading.dart' as deferred_loading;
+import 'transformations/to_string_transformer.dart' as to_string_transformer;
/// Declare options consumed by [runCompiler].
void declareCompilerOptions(ArgParser args) {
@@ -129,6 +130,13 @@
args.addFlag('track-widget-creation',
help: 'Run a kernel transformer to track creation locations for widgets.',
defaultsTo: false);
+ args.addMultiOption(
+ 'delete-tostring-package-uri',
+ help: 'Replaces implementations of `toString` with `super.toString()` for '
+ 'specified package',
+ valueHelp: 'dart:ui',
+ defaultsTo: const <String>[],
+ );
args.addOption('invocation-modes',
help: 'Provides information to the front end about how it is invoked.',
defaultsTo: '');
@@ -258,6 +266,7 @@
final results = await compileToKernel(mainUri, compilerOptions,
includePlatform: additionalDills.isNotEmpty,
+ deleteToStringPackageUris: options['delete-tostring-package-uri'],
aot: aot,
useGlobalTypeFlowAnalysis: tfa,
environmentDefines: environmentDefines,
@@ -324,6 +333,7 @@
Future<KernelCompilationResults> compileToKernel(
Uri source, CompilerOptions options,
{bool includePlatform: false,
+ List<String> deleteToStringPackageUris: const <String>[],
bool aot: false,
bool useGlobalTypeFlowAnalysis: false,
Map<String, String> environmentDefines,
@@ -354,6 +364,11 @@
compilerResult?.loadedComponents, compilerResult?.sdkComponent,
includePlatform: includePlatform);
+ if (deleteToStringPackageUris.isNotEmpty && component != null) {
+ to_string_transformer.transformComponent(
+ component, deleteToStringPackageUris);
+ }
+
// Run global transformations only if component is correct.
if ((aot || minimalKernel) && component != null) {
await runGlobalTransformations(
diff --git a/pkg/vm/lib/transformations/to_string_transformer.dart b/pkg/vm/lib/transformations/to_string_transformer.dart
new file mode 100644
index 0000000..2931251
--- /dev/null
+++ b/pkg/vm/lib/transformations/to_string_transformer.dart
@@ -0,0 +1,82 @@
+// Copyright (c) 2021, 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.
+
+import 'package:kernel/ast.dart';
+
+/// Transformer/visitor for toString
+transformComponent(Component component, List<String> packageUris) {
+ component.visitChildren(ToStringVisitor(packageUris.toSet()));
+}
+
+/// A [RecursiveVisitor] that replaces [Object.toString] overrides with
+/// `super.toString()`.
+class ToStringVisitor extends RecursiveVisitor {
+ /// The [packageUris] must not be null.
+ ToStringVisitor(this._packageUris) : assert(_packageUris != null);
+
+ /// A set of package URIs to apply this transformer to, e.g. 'dart:ui' and
+ /// 'package:flutter/foundation.dart'.
+ final Set<String> _packageUris;
+
+ /// Turn 'dart:ui' into 'dart:ui', or
+ /// 'package:flutter/src/semantics_event.dart' into 'package:flutter'.
+ String _importUriToPackage(Uri importUri) =>
+ '${importUri.scheme}:${importUri.pathSegments.first}';
+
+ bool _isInTargetPackage(Procedure node) {
+ return _packageUris
+ .contains(_importUriToPackage(node.enclosingLibrary.importUri));
+ }
+
+ bool _hasKeepAnnotation(Procedure node) {
+ for (ConstantExpression expression
+ in node.annotations.whereType<ConstantExpression>()) {
+ if (expression.constant is! InstanceConstant) {
+ continue;
+ }
+ final InstanceConstant constant = expression.constant as InstanceConstant;
+ final className = constant.classNode.name;
+ final libraryUri =
+ constant.classNode.enclosingLibrary.importUri.toString();
+ if (className == '_KeepToString' && libraryUri == 'dart:ui') {
+ return true;
+ }
+ if (className == 'pragma' && libraryUri == 'dart:core') {
+ for (var fieldRef in constant.fieldValues.keys) {
+ if (fieldRef.asField.name.text == 'name') {
+ Constant name = constant.fieldValues[fieldRef];
+ return name is StringConstant &&
+ name.value == 'flutter:keep-to-string';
+ }
+ }
+ return false;
+ }
+ }
+ return false;
+ }
+
+ @override
+ void visitProcedure(Procedure node) {
+ if (node.name.text == 'toString' &&
+ node.enclosingClass != null &&
+ node.enclosingLibrary != null &&
+ !node.isStatic &&
+ !node.isAbstract &&
+ !node.enclosingClass.isEnum &&
+ _isInTargetPackage(node) &&
+ !_hasKeepAnnotation(node)) {
+ node.function.body.replaceWith(
+ ReturnStatement(
+ SuperMethodInvocation(
+ node.name,
+ Arguments(<Expression>[]),
+ ),
+ ),
+ );
+ }
+ }
+
+ @override
+ void defaultMember(Member node) {}
+}
diff --git a/pkg/vm/test/common_test_utils.dart b/pkg/vm/test/common_test_utils.dart
index 1c97ba4..a45a95d 100644
--- a/pkg/vm/test/common_test_utils.dart
+++ b/pkg/vm/test/common_test_utils.dart
@@ -38,7 +38,8 @@
{Target target,
bool enableSuperMixins = false,
List<String> experimentalFlags,
- Map<String, String> environmentDefines}) async {
+ Map<String, String> environmentDefines,
+ Uri packagesFileUri}) async {
final platformKernel =
computePlatformBinariesLocation().resolve('vm_platform_strong.dill');
target ??= new TestingVmTarget(new TargetFlags())
@@ -48,6 +49,7 @@
..target = target
..additionalDills = <Uri>[platformKernel]
..environmentDefines = environmentDefines
+ ..packagesFileUri = packagesFileUri
..explicitExperimentalFlags =
parseExperimentalFlags(parseExperimentalArguments(experimentalFlags),
onError: (String message) {
diff --git a/pkg/vm/test/transformations/to_string_transformer_test.dart b/pkg/vm/test/transformations/to_string_transformer_test.dart
new file mode 100644
index 0000000..ef94ca8
--- /dev/null
+++ b/pkg/vm/test/transformations/to_string_transformer_test.dart
@@ -0,0 +1,42 @@
+// Copyright (c) 2021, 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.
+
+import 'dart:io';
+
+import 'package:kernel/ast.dart';
+import 'package:kernel/kernel.dart';
+import 'package:kernel/verifier.dart';
+import 'package:test/test.dart';
+import 'package:vm/transformations/to_string_transformer.dart'
+ show transformComponent;
+
+import '../common_test_utils.dart';
+
+final Uri pkgVmUri = Platform.script.resolve('../..');
+
+runTestCase(List<String> packageUris, String expectationName) async {
+ final testCasesUri =
+ pkgVmUri.resolve('testcases/transformations/to_string_transformer/');
+ final packagesFileUri =
+ testCasesUri.resolve('.dart_tool/package_config.json');
+ Component component = await compileTestCaseToKernelProgram(
+ Uri.parse('package:to_string_transformer_test/main.dart'),
+ packagesFileUri: packagesFileUri);
+
+ transformComponent(component, packageUris);
+ verifyComponent(component);
+
+ final actual = kernelLibraryToString(component.mainMethod.enclosingLibrary);
+
+ compareResultWithExpectationsFile(
+ testCasesUri.resolve(expectationName), actual);
+}
+
+main() {
+ group('to-string-transformer', () {
+ runTestCase(['package:foo'], 'not_transformed');
+ runTestCase(
+ ['package:foo', 'package:to_string_transformer_test'], 'transformed');
+ });
+}
diff --git a/pkg/vm/testcases/transformations/to_string_transformer/.dart_tool/package_config.json b/pkg/vm/testcases/transformations/to_string_transformer/.dart_tool/package_config.json
new file mode 100644
index 0000000..b8a9fce
--- /dev/null
+++ b/pkg/vm/testcases/transformations/to_string_transformer/.dart_tool/package_config.json
@@ -0,0 +1,11 @@
+{
+ "configVersion": 2,
+ "packages": [
+ {
+ "name": "to_string_transformer_test",
+ "rootUri": "../",
+ "packageUri": "lib",
+ "languageVersion": "2.12"
+ }
+ ]
+}
diff --git a/pkg/vm/testcases/transformations/to_string_transformer/.gitignore b/pkg/vm/testcases/transformations/to_string_transformer/.gitignore
new file mode 100644
index 0000000..0a29033
--- /dev/null
+++ b/pkg/vm/testcases/transformations/to_string_transformer/.gitignore
@@ -0,0 +1 @@
+!.dart_tool
diff --git a/pkg/vm/testcases/transformations/to_string_transformer/lib/main.dart b/pkg/vm/testcases/transformations/to_string_transformer/lib/main.dart
new file mode 100644
index 0000000..96544f2
--- /dev/null
+++ b/pkg/vm/testcases/transformations/to_string_transformer/lib/main.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2021, 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.
+
+import 'dart:convert';
+
+const keepToString = pragma('flutter:keep-to-string');
+
+String toString() => 'I am static';
+
+abstract class IFoo {
+ @override
+ String toString();
+}
+
+class Foo implements IFoo {
+ @override
+ String toString() => 'I am a Foo';
+}
+
+enum FooEnum { A, B, C }
+
+class Keep {
+ @keepToString
+ @override
+ String toString() => 'I am a Keep';
+}
+
+void main() {
+ final IFoo foo = Foo();
+ print(foo.toString());
+ print(Keep().toString());
+ print(FooEnum.B.toString());
+}
diff --git a/pkg/vm/testcases/transformations/to_string_transformer/not_transformed.expect b/pkg/vm/testcases/transformations/to_string_transformer/not_transformed.expect
new file mode 100644
index 0000000..291ca37
--- /dev/null
+++ b/pkg/vm/testcases/transformations/to_string_transformer/not_transformed.expect
@@ -0,0 +1,68 @@
+library #lib /*isNonNullableByDefault*/;
+import self as self;
+import "dart:core" as core;
+
+import "dart:convert";
+
+abstract class IFoo extends core::Object {
+ synthetic constructor •() → self::IFoo
+ : super core::Object::•()
+ ;
+ @#C1
+ abstract method toString() → core::String;
+}
+class Foo extends core::Object implements self::IFoo {
+ synthetic constructor •() → self::Foo
+ : super core::Object::•()
+ ;
+ @#C1
+ method toString() → core::String
+ return "I am a Foo";
+}
+class FooEnum extends core::Object /*isEnum*/ {
+ final field core::int index;
+ final field core::String _name;
+ static const field core::List<self::FooEnum> values = #C11;
+ static const field self::FooEnum A = #C4;
+ static const field self::FooEnum B = #C7;
+ static const field self::FooEnum C = #C10;
+ const constructor •(core::int index, core::String _name) → self::FooEnum
+ : self::FooEnum::index = index, self::FooEnum::_name = _name, super core::Object::•()
+ ;
+ method toString() → core::String
+ return this.{self::FooEnum::_name};
+}
+class Keep extends core::Object {
+ synthetic constructor •() → self::Keep
+ : super core::Object::•()
+ ;
+ @#C14
+ @#C1
+ method toString() → core::String
+ return "I am a Keep";
+}
+static const field core::pragma keepToString = #C14;
+static method toString() → core::String
+ return "I am static";
+static method main() → void {
+ final self::IFoo foo = new self::Foo::•();
+ core::print(foo.{self::IFoo::toString}());
+ core::print(new self::Keep::•().{self::Keep::toString}());
+ core::print((#C7).{self::FooEnum::toString}());
+}
+constants {
+ #C1 = core::_Override {}
+ #C2 = 0
+ #C3 = "FooEnum.A"
+ #C4 = self::FooEnum {index:#C2, _name:#C3}
+ #C5 = 1
+ #C6 = "FooEnum.B"
+ #C7 = self::FooEnum {index:#C5, _name:#C6}
+ #C8 = 2
+ #C9 = "FooEnum.C"
+ #C10 = self::FooEnum {index:#C8, _name:#C9}
+ #C11 = <self::FooEnum*>[#C4, #C7, #C10]
+ #C12 = "flutter:keep-to-string"
+ #C13 = null
+ #C14 = core::pragma {name:#C12, options:#C13}
+}
diff --git a/pkg/vm/testcases/transformations/to_string_transformer/transformed.expect b/pkg/vm/testcases/transformations/to_string_transformer/transformed.expect
new file mode 100644
index 0000000..7ae1922
--- /dev/null
+++ b/pkg/vm/testcases/transformations/to_string_transformer/transformed.expect
@@ -0,0 +1,68 @@
+library #lib /*isNonNullableByDefault*/;
+import self as self;
+import "dart:core" as core;
+
+import "dart:convert";
+
+abstract class IFoo extends core::Object {
+ synthetic constructor •() → self::IFoo
+ : super core::Object::•()
+ ;
+ @#C1
+ abstract method toString() → core::String;
+}
+class Foo extends core::Object implements self::IFoo {
+ synthetic constructor •() → self::Foo
+ : super core::Object::•()
+ ;
+ @#C1
+ method toString() → core::String
+ return super.toString();
+}
+class FooEnum extends core::Object /*isEnum*/ {
+ final field core::int index;
+ final field core::String _name;
+ static const field core::List<self::FooEnum> values = #C11;
+ static const field self::FooEnum A = #C4;
+ static const field self::FooEnum B = #C7;
+ static const field self::FooEnum C = #C10;
+ const constructor •(core::int index, core::String _name) → self::FooEnum
+ : self::FooEnum::index = index, self::FooEnum::_name = _name, super core::Object::•()
+ ;
+ method toString() → core::String
+ return this.{self::FooEnum::_name};
+}
+class Keep extends core::Object {
+ synthetic constructor •() → self::Keep
+ : super core::Object::•()
+ ;
+ @#C14
+ @#C1
+ method toString() → core::String
+ return "I am a Keep";
+}
+static const field core::pragma keepToString = #C14;
+static method toString() → core::String
+ return "I am static";
+static method main() → void {
+ final self::IFoo foo = new self::Foo::•();
+ core::print(foo.{self::IFoo::toString}());
+ core::print(new self::Keep::•().{self::Keep::toString}());
+ core::print((#C7).{self::FooEnum::toString}());
+}
+constants {
+ #C1 = core::_Override {}
+ #C2 = 0
+ #C3 = "FooEnum.A"
+ #C4 = self::FooEnum {index:#C2, _name:#C3}
+ #C5 = 1
+ #C6 = "FooEnum.B"
+ #C7 = self::FooEnum {index:#C5, _name:#C6}
+ #C8 = 2
+ #C9 = "FooEnum.C"
+ #C10 = self::FooEnum {index:#C8, _name:#C9}
+ #C11 = <self::FooEnum*>[#C4, #C7, #C10]
+ #C12 = "flutter:keep-to-string"
+ #C13 = null
+ #C14 = core::pragma {name:#C12, options:#C13}
+}