From 034f913b98ec7512e5f6865efbc0e774bdff34b6 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Tue, 17 Mar 2020 15:40:09 -0700 Subject: [PATCH] Teach frontend compiler to replace `toString` with `super.toString` for selected packages (#17068) Adds annotation `keepToString` to opt out. --- analysis_options.yaml | 10 +- ci/licenses_golden/licenses_flutter | 2 + flutter_frontend_server/lib/server.dart | 104 ++++++- .../test/fixtures/.gitignore | 1 + .../test/fixtures/.packages | 2 + .../test/fixtures/lib/main.dart | 26 ++ .../test/to_string_test.dart | 289 ++++++++++++++++++ lib/ui/annotations.dart | 46 +++ lib/ui/dart_ui.gni | 1 + lib/ui/ui.dart | 1 + lib/web_ui/lib/src/ui/annotations.dart | 46 +++ lib/web_ui/lib/ui.dart | 1 + testing/run_tests.py | 18 ++ 13 files changed, 539 insertions(+), 8 deletions(-) create mode 100644 flutter_frontend_server/test/fixtures/.gitignore create mode 100644 flutter_frontend_server/test/fixtures/.packages create mode 100644 flutter_frontend_server/test/fixtures/lib/main.dart create mode 100644 flutter_frontend_server/test/to_string_test.dart create mode 100644 lib/ui/annotations.dart create mode 100644 lib/web_ui/lib/src/ui/annotations.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 64c440949..c125e1d52 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,9 +10,13 @@ # private fields, especially on the Window object): analyzer: - # this test pretends to be part of dart:ui and results in lots of false - # positives. - exclude: [ testing/dart/window_hooks_integration_test.dart ] + exclude: [ + # this test pretends to be part of dart:ui and results in lots of false + # positives. + testing/dart/window_hooks_integration_test.dart, + # Fixture depends on dart:ui and raises false positives. + flutter_frontend_server/test/fixtures/lib/main.dart + ] strong-mode: implicit-casts: false implicit-dynamic: false diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 0dac1de97..21bb859d5 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -269,6 +269,7 @@ FILE: ../../../flutter/lib/io/dart_io.cc FILE: ../../../flutter/lib/io/dart_io.h FILE: ../../../flutter/lib/snapshot/libraries.json FILE: ../../../flutter/lib/snapshot/snapshot.h +FILE: ../../../flutter/lib/ui/annotations.dart FILE: ../../../flutter/lib/ui/channel_buffers.dart FILE: ../../../flutter/lib/ui/compositing.dart FILE: ../../../flutter/lib/ui/compositing/scene.cc @@ -494,6 +495,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/util.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/validators.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/window.dart +FILE: ../../../flutter/lib/web_ui/lib/src/ui/annotations.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/channel_buffers.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/compositing.dart diff --git a/flutter_frontend_server/lib/server.dart b/flutter_frontend_server/lib/server.dart index db095bcd0..0c50557a7 100644 --- a/flutter_frontend_server/lib/server.dart +++ b/flutter_frontend_server/lib/server.dart @@ -8,9 +8,6 @@ import 'dart:async'; import 'dart:io' hide FileSystemEntity; import 'package:args/args.dart'; -import 'package:path/path.dart' as path; - -import 'package:vm/incremental_compiler.dart'; import 'package:frontend_server/frontend_server.dart' as frontend show FrontendCompiler, @@ -19,6 +16,9 @@ import 'package:frontend_server/frontend_server.dart' as frontend argParser, usage, ProgramTransformer; +import 'package:kernel/ast.dart'; +import 'package:path/path.dart' as path; +import 'package:vm/incremental_compiler.dart'; /// Wrapper around [FrontendCompiler] that adds [widgetCreatorTracker] kernel /// transformation to the compilation. @@ -107,6 +107,13 @@ Future starter( frontend.ProgramTransformer transformer, }) async { ArgResults options; + frontend.argParser.addMultiOption( + 'delete-tostring-package-uri', + help: 'Replaces implementations of `toString` with `super.toString()` for ' + 'specified package', + valueHelp: 'dart:ui', + defaultsTo: const [], + ); try { options = frontend.argParser.parse(args); } catch (error) { @@ -115,6 +122,8 @@ Future starter( return 1; } + final Set deleteToStringPackageUris = (options['delete-tostring-package-uri'] as List).toSet(); + if (options['train'] as bool) { if (!options.rest.isNotEmpty) { throw Exception('Must specify input.dart'); @@ -137,7 +146,10 @@ Future starter( '--gen-bytecode', '--bytecode-options=source-positions,local-var-info,debugger-stops,instance-field-initializers,keep-unreachable-code,avoid-closure-call-instructions', ]); - compiler ??= _FlutterFrontendCompiler(output); + compiler ??= _FlutterFrontendCompiler( + output, + transformer: ToStringTransformer(null, deleteToStringPackageUris), + ); await compiler.compile(input, options); compiler.acceptLastDelta(); @@ -156,7 +168,7 @@ Future starter( } compiler ??= _FlutterFrontendCompiler(output, - transformer: transformer, + transformer: ToStringTransformer(transformer, deleteToStringPackageUris), useDebuggerModuleNames: options['debugger-module-names'] as bool, unsafePackageSerialization: options['unsafe-package-serialization'] as bool); @@ -169,3 +181,85 @@ Future starter( frontend.listenAndCompile(compiler, input ?? stdin, options, completer); return completer.future; } + +// Transformer/visitor for toString +// If we add any more of these, they really should go into a separate library. + +/// 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 _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()) { + if (expression.constant is! InstanceConstant) { + continue; + } + final InstanceConstant constant = expression.constant as InstanceConstant; + if (constant.classNode.name == '_KeepToString' && constant.classNode.enclosingLibrary.importUri.toString() == 'dart:ui') { + return true; + } + } + return false; + } + + @override + void visitProcedure(Procedure node) { + if ( + node.name.name == 'toString' && + node.enclosingClass != null && + node.enclosingLibrary != null && + !node.isStatic && + !node.isAbstract && + _isInTargetPackage(node) && + !_hasKeepAnnotation(node) + ) { + node.function.body.replaceWith( + ReturnStatement( + SuperMethodInvocation( + node.name, + Arguments([]), + ), + ), + ); + } + } + + @override + void defaultMember(Member node) {} +} + +/// Replaces [Object.toString] overrides with calls to super for the specified +/// [packageUris]. +class ToStringTransformer extends frontend.ProgramTransformer { + /// The [packageUris] parameter must not be null, but may be empty. + ToStringTransformer(this._child, this._packageUris) : assert(_packageUris != null); + + final frontend.ProgramTransformer _child; + + /// A set of package URIs to apply this transformer to, e.g. 'dart:ui' and + /// 'package:flutter/foundation.dart'. + final Set _packageUris; + + @override + void transform(Component component) { + assert(_child is! ToStringTransformer); + if (_packageUris.isNotEmpty) { + component.visitChildren(ToStringVisitor(_packageUris)); + } + _child?.transform(component); + } +} diff --git a/flutter_frontend_server/test/fixtures/.gitignore b/flutter_frontend_server/test/fixtures/.gitignore new file mode 100644 index 000000000..160ff8892 --- /dev/null +++ b/flutter_frontend_server/test/fixtures/.gitignore @@ -0,0 +1 @@ +!.packages diff --git a/flutter_frontend_server/test/fixtures/.packages b/flutter_frontend_server/test/fixtures/.packages new file mode 100644 index 000000000..6164a98c2 --- /dev/null +++ b/flutter_frontend_server/test/fixtures/.packages @@ -0,0 +1,2 @@ +# Generated by pub on 2020-01-15 10:08:29.776333. +flutter_frontend_fixtures:lib/ diff --git a/flutter_frontend_server/test/fixtures/lib/main.dart b/flutter_frontend_server/test/fixtures/lib/main.dart new file mode 100644 index 000000000..bfa11f7d1 --- /dev/null +++ b/flutter_frontend_server/test/fixtures/lib/main.dart @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors. 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'; +import 'dart:ui'; + +void main() { + final Paint paint = Paint()..color = Color(0xFFFFFFFF); + print(jsonEncode({ + 'Paint.toString': paint.toString(), + 'Foo.toString': Foo().toString(), + 'Keep.toString': Keep().toString(), + })); +} + +class Foo { + @override + String toString() => 'I am a Foo'; +} + +class Keep { + @keepToString + @override + String toString() => 'I am a Keep'; +} diff --git a/flutter_frontend_server/test/to_string_test.dart b/flutter_frontend_server/test/to_string_test.dart new file mode 100644 index 000000000..ac70b2369 --- /dev/null +++ b/flutter_frontend_server/test/to_string_test.dart @@ -0,0 +1,289 @@ +// Copyright 2013 The Flutter Authors. 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:flutter_frontend_server/server.dart'; +import 'package:frontend_server/frontend_server.dart' as frontend show ProgramTransformer; +import 'package:kernel/kernel.dart'; +import 'package:mockito/mockito.dart'; +import 'package:path/path.dart' as path; + +import 'package:test/test.dart'; + +void main(List args) async { + if (args.length != 2) { + stderr.writeln('The first argument must be the path to the forntend server dill.'); + stderr.writeln('The second argument must be the path to the flutter_patched_sdk'); + exit(-1); + } + + const Set uiAndFlutter = { + 'dart:ui', + 'package:flutter', + }; + + test('No packages', () { + final ToStringTransformer transformer = ToStringTransformer(null, {}); + + final MockComponent component = MockComponent(); + transformer.transform(component); + verifyNever(component.visitChildren(any)); + }); + + test('dart:ui package', () { + final ToStringTransformer transformer = ToStringTransformer(null, uiAndFlutter); + + final MockComponent component = MockComponent(); + transformer.transform(component); + verify(component.visitChildren(any)).called(1); + }); + + test('Child transformer', () { + final MockTransformer childTransformer = MockTransformer(); + final ToStringTransformer transformer = ToStringTransformer(childTransformer, {}); + + final MockComponent component = MockComponent(); + transformer.transform(component); + verifyNever(component.visitChildren(any)); + verify(childTransformer.transform(component)).called(1); + }); + + test('ToStringVisitor ignores non-toString procedures', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + when(procedure.name).thenReturn(Name('main')); + when(procedure.annotations).thenReturn(const []); + + visitor.visitProcedure(procedure); + verifyNever(procedure.enclosingLibrary); + }); + + test('ToStringVisitor ignores top level toString', () { + // i.e. a `toString` method specified at the top of a library, like: + // + // void main() {} + // String toString() => 'why?'; + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('package:some_package/src/blah.dart')); + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(Name('toString')); + when(procedure.annotations).thenReturn(const []); + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(null); + when(procedure.isAbstract).thenReturn(false); + when(procedure.isStatic).thenReturn(false); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + verifyNever(statement.replaceWith(any)); + }); + + test('ToStringVisitor ignores abstract toString', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('package:some_package/src/blah.dart')); + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(Name('toString')); + when(procedure.annotations).thenReturn(const []); + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(Class()); + when(procedure.isAbstract).thenReturn(true); + when(procedure.isStatic).thenReturn(false); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + verifyNever(statement.replaceWith(any)); + }); + + test('ToStringVisitor ignores static toString', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('package:some_package/src/blah.dart')); + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(Name('toString')); + when(procedure.annotations).thenReturn(const []); + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(Class()); + when(procedure.isAbstract).thenReturn(false); + when(procedure.isStatic).thenReturn(true); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + verifyNever(statement.replaceWith(any)); + }); + + test('ToStringVisitor ignores non-specified libraries', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('package:some_package/src/blah.dart')); + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(Name('toString')); + when(procedure.annotations).thenReturn(const []); + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(Class()); + when(procedure.isAbstract).thenReturn(false); + when(procedure.isStatic).thenReturn(false); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + verifyNever(statement.replaceWith(any)); + }); + + test('ToStringVisitor ignores @keepToString', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('dart:ui')); + final Name name = Name('toString'); + final Class annotation = Class(name: '_KeepToString')..parent = Library(Uri.parse('dart:ui')); + + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(name); + when(procedure.annotations).thenReturn([ + ConstantExpression( + InstanceConstant( + Reference()..node = annotation, + [], + {}, + ), + ), + ]); + + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(Class()); + when(procedure.isAbstract).thenReturn(false); + when(procedure.isStatic).thenReturn(false); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + verifyNever(statement.replaceWith(any)); + }); + + void _validateReplacement(MockStatement body) { + final ReturnStatement replacement = verify(body.replaceWith(captureAny)).captured.single as ReturnStatement; + expect(replacement.expression, isA()); + final SuperMethodInvocation superMethodInvocation = replacement.expression as SuperMethodInvocation; + expect(superMethodInvocation.name.name, 'toString'); + } + + test('ToStringVisitor replaces toString in specified libraries (dart:ui)', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('dart:ui')); + final Name name = Name('toString'); + + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(name); + when(procedure.annotations).thenReturn(const []); + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(Class()); + when(procedure.isAbstract).thenReturn(false); + when(procedure.isStatic).thenReturn(false); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + _validateReplacement(statement); + }); + + test('ToStringVisitor replaces toString in specified libraries (package:flutter)', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('package:flutter/src/foundation.dart')); + final Name name = Name('toString'); + + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(name); + when(procedure.annotations).thenReturn(const []); + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(Class()); + when(procedure.isAbstract).thenReturn(false); + when(procedure.isStatic).thenReturn(false); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + _validateReplacement(statement); + }); + + group('Integration tests', () { + final String dart = Platform.resolvedExecutable; + final String frontendServer = args[0]; + final String sdkRoot = args[1]; + final String basePath = path.canonicalize(path.join(path.dirname(Platform.script.path), '..')); + final String fixtures = path.join(basePath, 'test', 'fixtures'); + final String mainDart = path.join(fixtures, 'lib', 'main.dart'); + final String dotPackages = path.join(fixtures, '.packages'); + final String regularDill = path.join(fixtures, 'toString.dill'); + final String transformedDill = path.join(fixtures, 'toStringTransformed.dill'); + + + void _checkProcessResult(ProcessResult result) { + if (result.exitCode != 0) { + stdout.writeln(result.stdout); + stderr.writeln(result.stderr); + } + expect(result.exitCode, 0); + } + + test('Without flag', () async { + _checkProcessResult(Process.runSync(dart, [ + frontendServer, + '--sdk-root=$sdkRoot', + '--target=flutter', + '--packages=$dotPackages', + '--output-dill=$regularDill', + mainDart, + ])); + final ProcessResult runResult = Process.runSync(dart, [regularDill]); + _checkProcessResult(runResult); + expect( + runResult.stdout.trim(), + '{"Paint.toString":"Paint(Color(0xffffffff))",' + '"Foo.toString":"I am a Foo",' + '"Keep.toString":"I am a Keep"}', + ); + }); + + test('With flag', () async { + _checkProcessResult(Process.runSync(dart, [ + frontendServer, + '--sdk-root=$sdkRoot', + '--target=flutter', + '--packages=$dotPackages', + '--output-dill=$transformedDill', + '--delete-tostring-package-uri', 'dart:ui', + '--delete-tostring-package-uri', 'package:flutter_frontend_fixtures', + mainDart, + ])); + final ProcessResult runResult = Process.runSync(dart, [transformedDill]); + _checkProcessResult(runResult); + expect( + runResult.stdout.trim(), + '{"Paint.toString":"Instance of \'Paint\'",' + '"Foo.toString":"Instance of \'Foo\'",' + '"Keep.toString":"I am a Keep"}', + ); + }); + }); +} + +class MockComponent extends Mock implements Component {} +class MockTransformer extends Mock implements frontend.ProgramTransformer {} +class MockProcedure extends Mock implements Procedure {} +class MockFunctionNode extends Mock implements FunctionNode {} +class MockStatement extends Mock implements Statement {} diff --git a/lib/ui/annotations.dart b/lib/ui/annotations.dart new file mode 100644 index 000000000..825c8637b --- /dev/null +++ b/lib/ui/annotations.dart @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO(dnfield): Remove unused_import ignores when https://github.com/dart-lang/sdk/issues/35164 is resolved. + +// @dart = 2.6 +part of dart.ui; + +// TODO(dnfield): Update this if/when we default this to on in the tool, +// see: https://github.com/flutter/flutter/issues/52759 +/// Annotation used by Flutter's Dart compiler to indicate that an +/// [Object.toString] override should not be replaced with a supercall. +/// +/// Since `dart:ui` and `package:flutter` override `toString` purely for +/// debugging purposes, the frontend compiler is instructed to replace all +/// `toString` bodies with `return super.toString()` during compilation. This +/// significantly reduces release code size, and would make it impossible to +/// implement a meaningful override of `toString` for release mode without +/// disabling the feature and losing the size savings. If a package uses this +/// feature and has some unavoidable need to keep the `toString` implementation +/// for a specific class, applying this annotation will direct the compiler +/// to leave the method body as-is. +/// +/// For example, in the following class the `toString` method will remain as +/// `return _buffer.toString();`, even if the `--delete-tostring-package-uri` +/// option would otherwise apply and replace it with `return super.toString()`. +/// +/// ```dart +/// class MyStringBuffer { +/// StringBuffer _buffer = StringBuffer(); +/// +/// // ... +/// +/// @keepToString +/// @override +/// String toString() { +/// return _buffer.toString(); +/// } +/// } +/// ``` +const _KeepToString keepToString = _KeepToString(); + +class _KeepToString { + const _KeepToString(); +} diff --git a/lib/ui/dart_ui.gni b/lib/ui/dart_ui.gni index 3eef51f00..28cb82e9c 100644 --- a/lib/ui/dart_ui.gni +++ b/lib/ui/dart_ui.gni @@ -3,6 +3,7 @@ # found in the LICENSE file. dart_ui_files = [ + "//flutter/lib/ui/annotations.dart", "//flutter/lib/ui/channel_buffers.dart", "//flutter/lib/ui/compositing.dart", "//flutter/lib/ui/geometry.dart", diff --git a/lib/ui/ui.dart b/lib/ui/ui.dart index 2f8692c80..2d46db040 100644 --- a/lib/ui/ui.dart +++ b/lib/ui/ui.dart @@ -23,6 +23,7 @@ import 'dart:math' as math; import 'dart:nativewrappers'; import 'dart:typed_data'; +part 'annotations.dart'; part 'channel_buffers.dart'; part 'compositing.dart'; part 'geometry.dart'; diff --git a/lib/web_ui/lib/src/ui/annotations.dart b/lib/web_ui/lib/src/ui/annotations.dart new file mode 100644 index 000000000..10b9ef269 --- /dev/null +++ b/lib/web_ui/lib/src/ui/annotations.dart @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO(dnfield): Remove unused_import ignores when https://github.com/dart-lang/sdk/issues/35164 is resolved. + +// @dart = 2.6 +part of ui; + +// TODO(dnfield): Update this if/when we default this to on in the tool, +// see: https://github.com/flutter/flutter/issues/52759 +/// Annotation used by Flutter's Dart compiler to indicate that an +/// [Object.toString] override should not be replaced with a supercall. +/// +/// Since `dart:ui` and `package:flutter` override `toString` purely for +/// debugging purposes, the frontend compiler is instructed to replace all +/// `toString` bodies with `return super.toString()` during compilation. This +/// significantly reduces release code size, and would make it impossible to +/// implement a meaningful override of `toString` for release mode without +/// disabling the feature and losing the size savings. If a package uses this +/// feature and has some unavoidable need to keep the `toString` implementation +/// for a specific class, applying this annotation will direct the compiler +/// to leave the method body as-is. +/// +/// For example, in the following class the `toString` method will remain as +/// `return _buffer.toString();`, even if the `--delete-tostring-package-uri` +/// option would otherwise apply and replace it with `return super.toString()`. +/// +/// ```dart +/// class MyStringBuffer { +/// StringBuffer _buffer = StringBuffer(); +/// +/// // ... +/// +/// @keepToString +/// @override +/// String toString() { +/// return _buffer.toString(); +/// } +/// } +/// ``` +const _KeepToString keepToString = _KeepToString(); + +class _KeepToString { + const _KeepToString(); +} diff --git a/lib/web_ui/lib/ui.dart b/lib/web_ui/lib/ui.dart index 4acb5e246..8565c42d0 100644 --- a/lib/web_ui/lib/ui.dart +++ b/lib/web_ui/lib/ui.dart @@ -24,6 +24,7 @@ export 'src/engine.dart' webOnlySetPluginHandler, webOnlyInitializeEngine; +part 'src/ui/annotations.dart'; part 'src/ui/canvas.dart'; part 'src/ui/channel_buffers.dart'; part 'src/ui/compositing.dart'; diff --git a/testing/run_tests.py b/testing/run_tests.py index 90c3a6067..658a982ff 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -330,6 +330,23 @@ def RunDartTests(build_dir, filter, verbose_dart_snapshot): RunDartTest(build_dir, dart_test_file, verbose_dart_snapshot, True) RunDartTest(build_dir, dart_test_file, verbose_dart_snapshot, False) + +def RunFrontEndServerTests(build_dir): + test_dir = os.path.join(buildroot_dir, 'flutter', 'flutter_frontend_server') + dart_tests = glob.glob('%s/test/*_test.dart' % test_dir) + for dart_test_file in dart_tests: + opts = [ + dart_test_file, + os.path.join(build_dir, 'gen', 'frontend_server.dart.snapshot'), + os.path.join(build_dir, 'flutter_patched_sdk')] + RunEngineExecutable( + build_dir, + os.path.join('dart-sdk', 'bin', 'dart'), + None, + flags=opts, + cwd=test_dir) + + def RunConstFinderTests(build_dir): test_dir = os.path.join(buildroot_dir, 'flutter', 'tools', 'const_finder', 'test') opts = [ @@ -376,6 +393,7 @@ def main(): dart_filter = args.dart_filter.split(',') if args.dart_filter else None RunDartTests(build_dir, dart_filter, args.verbose_dart_snapshot) RunConstFinderTests(build_dir) + RunFrontEndServerTests(build_dir) if 'java' in types: assert not IsWindows(), "Android engine files can't be compiled on Windows." -- GitLab