[flutter_tools] ensure generated entrypoint matches test and web entrypoint language version (#59291)
Ensure that the language version of the test/web generated entrypoint matches the language version of the test file to run, or the overall package language version if no annotation is provided.
diff --git a/dev/bots/test.dart b/dev/bots/test.dart
index 8e50148..6670ef0 100644
--- a/dev/bots/test.dart
+++ b/dev/bots/test.dart
@@ -544,7 +544,7 @@
Future<void> _runFrameworkTests() async {
final bq.BigqueryApi bigqueryApi = await _getBigqueryApi();
- final List<String> nullSafetyOptions = <String>['--enable-experiment=non-nullable', '--no-sound-null-safety'];
+ final List<String> nullSafetyOptions = <String>['--enable-experiment=non-nullable'];
final List<String> trackWidgetCreationAlternatives = <String>['--track-widget-creation', '--no-track-widget-creation'];
Future<void> runWidgets() async {
diff --git a/dev/integration_tests/non_nullable/test/test_test.dart b/dev/integration_tests/non_nullable/test/test_test.dart
index 1a89396..a6f7411 100644
--- a/dev/integration_tests/non_nullable/test/test_test.dart
+++ b/dev/integration_tests/non_nullable/test/test_test.dart
@@ -2,11 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-// @dart=2.9
+// @dart=2.8
import 'package:flutter_test/flutter_test.dart';
-String? x;
-
void main() {
testWidgets('trivial', (WidgetTester tester) async {
expect(true, true);
diff --git a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
index a28fb74..48f83a3 100644
--- a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
+++ b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
@@ -23,6 +23,7 @@
import '../build_info.dart';
import '../cache.dart';
import '../convert.dart';
+import '../dart/language_version.dart';
import '../dart/pub.dart';
import '../devfs.dart';
import '../device.dart';
@@ -581,6 +582,10 @@
}
final String entrypoint = <String>[
+ determineLanguageVersion(
+ globals.fs.file(mainUri),
+ packageConfig[flutterProject.manifest.appName],
+ ),
'// Flutter web bootstrap script for $importedEntrypoint.',
'',
"import 'dart:ui' as ui;",
diff --git a/packages/flutter_tools/lib/src/dart/language_version.dart b/packages/flutter_tools/lib/src/dart/language_version.dart
new file mode 100644
index 0000000..b3f57a6
--- /dev/null
+++ b/packages/flutter_tools/lib/src/dart/language_version.dart
@@ -0,0 +1,68 @@
+// Copyright 2014 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 'package:file/file.dart';
+import 'package:package_config/package_config.dart';
+
+final RegExp _languageVersion = RegExp(r'\/\/\s*@dart');
+final RegExp _declarationEnd = RegExp('(import)|(library)|(part)');
+const String _blockCommentStart = '/*';
+const String _blockCommentEnd = '*/';
+
+/// Attempts to read the language version of a dart [file], returning
+/// the entire comment.
+///
+/// If this is not present, falls back to the language version defined in
+/// [package]. If [package] is not provided and there is no
+/// language version header, returns `null`. This does not specifically check
+/// for language declarations other than library, part, or import.
+///
+/// The specification for the language version tag is defined at:
+/// https://github.com/dart-lang/language/blob/master/accepted/future-releases/language-versioning/feature-specification.md#individual-library-language-version-override
+String determineLanguageVersion(File file, Package package) {
+ int blockCommentDepth = 0;
+ for (final String line in file.readAsLinesSync()) {
+ final String trimmedLine = line.trim();
+ if (trimmedLine.isEmpty) {
+ continue;
+ }
+ // Check for the start or end of a block comment. Within a block
+ // comment, all language version declarations are ignored. Block
+ // comments can be nested, and the start or end may occur on
+ // the same line. This does not handle the case of invalid
+ // block comment combinations like `*/ /*` since that will cause
+ // a compilation error anyway.
+ bool sawBlockComment = false;
+ final int startMatches = _blockCommentStart.allMatches(trimmedLine).length;
+ final int endMatches = _blockCommentEnd.allMatches(trimmedLine).length;
+ if (startMatches > 0) {
+ blockCommentDepth += startMatches;
+ sawBlockComment = true;
+ }
+ if (endMatches > 0) {
+ blockCommentDepth -= endMatches;
+ sawBlockComment = true;
+ }
+ if (blockCommentDepth != 0 || sawBlockComment) {
+ continue;
+ }
+ // Check for a match with the language version.
+ final Match match = _languageVersion.matchAsPrefix(trimmedLine);
+ if (match != null) {
+ return trimmedLine;
+ }
+
+ // Check for a declaration which ends the search for a language
+ // version.
+ if (_declarationEnd.matchAsPrefix(trimmedLine) != null) {
+ break;
+ }
+ }
+
+ // If the language version cannot be found, use the package version.
+ if (package != null) {
+ return '// @dart = ${package.languageVersion}';
+ }
+ return null;
+}
diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart
index 9cb4d41..40d471e 100644
--- a/packages/flutter_tools/lib/src/test/flutter_platform.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart
@@ -5,14 +5,14 @@
import 'dart:async';
import 'package:meta/meta.dart';
+import 'package:package_config/package_config.dart';
import 'package:stream_channel/stream_channel.dart';
-import 'package:vm_service/vm_service.dart' as vm_service;
-
import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/environment.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/plugin/platform_helpers.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
-import 'package:test_core/src/runner/plugin/platform_helpers.dart'; // ignore: implementation_imports
-import 'package:test_core/src/runner/environment.dart'; // ignore: implementation_imports
+import 'package:vm_service/vm_service.dart' as vm_service;
import '../base/common.dart';
import '../base/file_system.dart';
@@ -20,6 +20,7 @@
import '../build_info.dart';
import '../compile.dart';
import '../convert.dart';
+import '../dart/language_version.dart';
import '../dart/package_map.dart';
import '../globals.dart' as globals;
import '../project.dart';
@@ -151,7 +152,7 @@
@required InternetAddress host,
File testConfigFile,
bool updateGoldens = false,
- bool nullSafety = false,
+ String languageVersionHeader = '',
}) {
assert(testUrl != null);
assert(host != null);
@@ -164,6 +165,7 @@
final StringBuffer buffer = StringBuffer();
buffer.write('''
+$languageVersionHeader
import 'dart:async';
import 'dart:convert'; // ignore: dart_convert_import
import 'dart:io'; // ignore: dart_io_import
@@ -181,17 +183,11 @@
import '${Uri.file(testConfigFile.path)}' as test_config;
''');
}
- // This type is sensitive to the non-nullable experiment.
- final String beforeLoadTypedef = nullSafety
- ? 'Future<dynamic> Function()?'
- : 'Future<dynamic> Function()';
buffer.write('''
/// Returns a serialized test suite.
-StreamChannel<dynamic> serializeSuite(Function getMain(),
- {bool hidePrints = true, $beforeLoadTypedef beforeLoad}) {
- return RemoteListener.start(getMain,
- hidePrints: hidePrints, beforeLoad: beforeLoad);
+StreamChannel<dynamic> serializeSuite(Function getMain()) {
+ return RemoteListener.start(getMain);
}
/// Capture any top-level errors (mostly lazy syntax errors, since other are
@@ -402,11 +398,17 @@
@visibleForTesting
Future<HttpServer> bind(InternetAddress host, int port) => HttpServer.bind(host, port);
+ PackageConfig _packageConfig;
+
Future<_AsyncError> _startTest(
String testPath,
StreamChannel<dynamic> controller,
int ourTestCount,
) async {
+ _packageConfig ??= await loadPackageConfigWithLogging(
+ globals.fs.file(globalPackagesPath),
+ logger: globals.logger,
+ );
globals.printTrace('test $ourTestCount: starting test $testPath');
_AsyncError outOfBandError; // error that we couldn't send to the harness that we need to send via our future
@@ -748,15 +750,20 @@
Uri testUrl,
}) {
assert(testUrl.scheme == 'file');
+ final File file = globals.fs.file(testUrl);
return generateTestBootstrap(
testUrl: testUrl,
testConfigFile: findTestConfigFile(globals.fs.file(testUrl)),
host: host,
updateGoldens: updateGoldens,
- nullSafety: extraFrontEndOptions?.contains('--enable-experiment=non-nullable') ?? false,
+ languageVersionHeader: determineLanguageVersion(
+ file,
+ _packageConfig[flutterProject?.manifest?.appName],
+ ),
);
}
+
File _cachedFontConfig;
@override
diff --git a/packages/flutter_tools/test/general.shard/dart/language_version_test.dart b/packages/flutter_tools/test/general.shard/dart/language_version_test.dart
new file mode 100644
index 0000000..2bfc87d
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/dart/language_version_test.dart
@@ -0,0 +1,228 @@
+// Copyright 2014 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 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/dart/language_version.dart';
+import 'package:package_config/package_config.dart';
+
+import '../../src/common.dart';
+
+void main() {
+ testWithoutContext('detects language version in comment', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+// @dart = 2.9
+''');
+
+ expect(determineLanguageVersion(file, null), '// @dart = 2.9');
+ });
+
+ testWithoutContext('detects technically invalid language version', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+// @dart
+''');
+
+ expect(determineLanguageVersion(file, null), '// @dart');
+ });
+
+ testWithoutContext('detects language version with leading whitespace', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+ // @dart = 2.9
+''');
+
+ expect(determineLanguageVersion(file, null), '// @dart = 2.9');
+ });
+
+ testWithoutContext('detects language version with tabs', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+//\t@dart = 2.9
+''');
+
+ expect(determineLanguageVersion(file, null), '//\t@dart = 2.9');
+ });
+
+ testWithoutContext('detects language version with tons of whitespace', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+// @dart = 23
+''');
+
+ expect(determineLanguageVersion(file, null), '// @dart = 23');
+ });
+
+ testWithoutContext('does not detect language version in dartdoc', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+/// @dart = 2.9
+''');
+
+ expect(determineLanguageVersion(file, null), null);
+ });
+
+ testWithoutContext('does not detect language version in block comment', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+/*
+// @dart = 2.9
+*/
+''');
+
+ expect(determineLanguageVersion(file, null), null);
+ });
+
+ testWithoutContext('does not detect language version in nested block comment', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+/*
+/*
+// @dart = 2.9
+*/
+*/
+''');
+
+ expect(determineLanguageVersion(file, null), null);
+ });
+
+ testWithoutContext('detects language version after nested block comment', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+/* /*
+*/
+*/
+// @dart = 2.9
+''');
+
+ expect(determineLanguageVersion(file, null), '// @dart = 2.9');
+ });
+
+ testWithoutContext('does not crash with unbalanced opening block comments', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+/*
+/*
+*/
+// @dart = 2.9
+''');
+
+ expect(determineLanguageVersion(file, null), null);
+ });
+
+ testWithoutContext('does not crash with unbalanced closing block comments', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+/*
+*/
+*/
+// @dart = 2.9
+''');
+
+ expect(determineLanguageVersion(file, null), null);
+ });
+
+ testWithoutContext('does not detect language version in single line block comment', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+/* // @dart = 2.9 */
+''');
+
+ expect(determineLanguageVersion(file, null), null);
+ });
+
+ testWithoutContext('does not detect language version after import declaration', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+import 'dart:ui' as ui;
+
+// @dart = 2.9
+''');
+
+ expect(determineLanguageVersion(file, null), null);
+ });
+
+ testWithoutContext('does not detect language version after part declaration', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+part of 'foo.dart';
+
+// @dart = 2.9
+''');
+
+ expect(determineLanguageVersion(file, null), null);
+ });
+
+ testWithoutContext('does not detect language version after library declaration', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+
+library funstuff;
+
+// @dart = 2.9
+''');
+
+ expect(determineLanguageVersion(file, null), null);
+ });
+
+ testWithoutContext('looks up language version from package if not found in file', () {
+ final FileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('example.dart')
+ ..writeAsStringSync('''
+// Some license
+''');
+ final Package package = Package(
+ 'foo',
+ Uri.parse('file://foo/'),
+ languageVersion: LanguageVersion(2, 7),
+ );
+
+ expect(determineLanguageVersion(file, package), '// @dart = 2.7');
+ });
+}