未验证 提交 034f913b 编写于 作者: D Dan Field 提交者: GitHub

Teach frontend compiler to replace `toString` with `super.toString` for selected packages (#17068)

Adds annotation `keepToString` to opt out.
上级 f00a1358
......@@ -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
......
......@@ -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
......
......@@ -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<int> 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 <String>[],
);
try {
options = frontend.argParser.parse(args);
} catch (error) {
......@@ -115,6 +122,8 @@ Future<int> starter(
return 1;
}
final Set<String> deleteToStringPackageUris = (options['delete-tostring-package-uri'] as List<String>).toSet();
if (options['train'] as bool) {
if (!options.rest.isNotEmpty) {
throw Exception('Must specify input.dart');
......@@ -137,7 +146,10 @@ Future<int> 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<int> 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<int> 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<void> {
/// 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;
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(<Expression>[]),
),
),
);
}
}
@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<String> _packageUris;
@override
void transform(Component component) {
assert(_child is! ToStringTransformer);
if (_packageUris.isNotEmpty) {
component.visitChildren(ToStringVisitor(_packageUris));
}
_child?.transform(component);
}
}
# Generated by pub on 2020-01-15 10:08:29.776333.
flutter_frontend_fixtures:lib/
// 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(<String, String>{
'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';
}
// 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<String> 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<String> uiAndFlutter = <String>{
'dart:ui',
'package:flutter',
};
test('No packages', () {
final ToStringTransformer transformer = ToStringTransformer(null, <String>{});
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, <String>{});
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 <Expression>[]);
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 <Expression>[]);
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 <Expression>[]);
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 <Expression>[]);
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 <Expression>[]);
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(<Expression>[
ConstantExpression(
InstanceConstant(
Reference()..node = annotation,
<DartType>[],
<Reference, Constant>{},
),
),
]);
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<SuperMethodInvocation>());
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 <Expression>[]);
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 <Expression>[]);
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, <String>[
frontendServer,
'--sdk-root=$sdkRoot',
'--target=flutter',
'--packages=$dotPackages',
'--output-dill=$regularDill',
mainDart,
]));
final ProcessResult runResult = Process.runSync(dart, <String>[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, <String>[
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, <String>[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 {}
// 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();
}
......@@ -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",
......
......@@ -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';
......
// 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();
}
......@@ -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';
......
......@@ -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."
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册