Unverified Commit 3fb389c7 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[null-safety] implement null-safe autodetection for the web (#70126)

Fixes #69416
Fixes #70121
parent ff953e4f
......@@ -95,10 +95,10 @@ class WebEntrypointTarget extends Target {
logger: environment.logger,
);
final FlutterProject flutterProject = FlutterProject.current();
final String languageVersion = determineLanguageVersion(
final LanguageVersion languageVersion = determineLanguageVersion(
environment.fileSystem.file(targetFile),
packageConfig[flutterProject.manifest.appName],
) ?? '';
);
// Use the PackageConfig to find the correct package-scheme import path
// for the user application. If the application has a mix of package-scheme
......@@ -122,7 +122,7 @@ class WebEntrypointTarget extends Target {
final String generatedImport = packageConfig.toPackageUri(generatedUri)?.toString()
?? generatedUri.toString();
contents = '''
$languageVersion
// @dart=${languageVersion.major}.${languageVersion.minor}
import 'dart:ui' as ui;
......@@ -139,7 +139,7 @@ Future<void> main() async {
''';
} else {
contents = '''
$languageVersion
// @dart=${languageVersion.major}.${languageVersion.minor}
import 'dart:ui' as ui;
......
......@@ -5,22 +5,24 @@
import 'package:file/file.dart';
import 'package:package_config/package_config.dart';
final RegExp _languageVersion = RegExp(r'\/\/\s*@dart');
final RegExp _languageVersion = RegExp(r'\/\/\s*@dart\s*=\s*([0-9])\.([0-9]+)');
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.
/// The first language version where null safety was available by default.
final LanguageVersion nullSafeVersion = LanguageVersion(2, 12);
/// Attempts to read the language version of a dart [file].
///
/// 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
/// language version header, returns 2.12. 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) {
LanguageVersion determineLanguageVersion(File file, Package package) {
int blockCommentDepth = 0;
for (final String line in file.readAsLinesSync()) {
final String trimmedLine = line.trim();
......@@ -50,7 +52,17 @@ String determineLanguageVersion(File file, Package package) {
// Check for a match with the language version.
final Match match = _languageVersion.matchAsPrefix(trimmedLine);
if (match != null) {
return trimmedLine;
final String rawMajor = match.group(1);
final String rawMinor = match.group(2);
try {
final int major = int.parse(rawMajor);
final int minor = int.parse(rawMinor);
return LanguageVersion(major, minor);
} on FormatException {
// Language comment was invalid in a way that the regexp did not
// anticipate.
break;
}
}
// Check for a declaration which ends the search for a language
......@@ -62,7 +74,8 @@ String determineLanguageVersion(File file, Package package) {
// If the language version cannot be found, use the package version.
if (package != null) {
return '// @dart = ${package.languageVersion}';
return package.languageVersion;
}
return null;
// Default to 2.12
return nullSafeVersion;
}
......@@ -669,12 +669,13 @@ class _ResidentWebRunner extends ResidentWebRunner {
path: '/' + mainUri.pathSegments.last,
);
}
final LanguageVersion languageVersion = determineLanguageVersion(
globals.fs.file(mainUri),
packageConfig[flutterProject.manifest.appName],
);
final String entrypoint = <String>[
determineLanguageVersion(
globals.fs.file(mainUri),
packageConfig[flutterProject.manifest.appName],
),
'// @dart=${languageVersion.major}.${languageVersion.minor}',
'// Flutter web bootstrap script for $importedEntrypoint.',
'',
"import 'dart:ui' as ui;",
......
......@@ -27,6 +27,8 @@ import 'build_system/targets/localizations.dart';
import 'bundle.dart';
import 'cache.dart';
import 'compile.dart';
import 'dart/language_version.dart';
import 'dart/package_map.dart';
import 'devfs.dart';
import 'device.dart';
import 'features.dart';
......@@ -101,14 +103,28 @@ class FlutterDevice {
platformDillArtifact = Artifact.webPlatformSoundKernelDill;
extraFrontEndOptions = buildInfo.extraFrontEndOptions;
} else {
// TODO(jonahwilliams): null-safe auto detection does not currently
// work on the web. Always opt out of null safety if it was not
// specifically requested.
platformDillArtifact = Artifact.webPlatformKernelDill;
extraFrontEndOptions = <String>[
...?buildInfo.extraFrontEndOptions,
'--no-sound-null-safety',
];
final PackageConfig packageConfig = await loadPackageConfigWithLogging(
globals.fs.file(buildInfo.packagesPath),
logger: globals.logger,
);
final File entrypointFile = globals.fs.file(target);
final LanguageVersion languageVersion = determineLanguageVersion(
entrypointFile,
packageConfig.packageOf(entrypointFile.absolute.uri),
);
if (languageVersion.major >= nullSafeVersion.major && languageVersion.minor >= nullSafeVersion.minor) {
platformDillArtifact = Artifact.webPlatformSoundKernelDill;
extraFrontEndOptions = <String>[
...?buildInfo.extraFrontEndOptions,
'--sound-null-safety',
];
} else {
platformDillArtifact = Artifact.webPlatformKernelDill;
extraFrontEndOptions = <String>[
...?buildInfo.extraFrontEndOptions,
'--no-sound-null-safety',
];
}
}
generator = ResidentCompiler(
......
......@@ -713,19 +713,19 @@ class FlutterPlatform extends PlatformPlugin {
}) {
assert(testUrl.scheme == 'file');
final File file = globals.fs.file(testUrl);
final LanguageVersion languageVersion = determineLanguageVersion(
file,
_packageConfig[flutterProject?.manifest?.appName],
);
return generateTestBootstrap(
testUrl: testUrl,
testConfigFile: findTestConfigFile(globals.fs.file(testUrl)),
host: host,
updateGoldens: updateGoldens,
languageVersionHeader: determineLanguageVersion(
file,
_packageConfig[flutterProject?.manifest?.appName],
),
languageVersionHeader: '// @dart=${languageVersion.major}.${languageVersion.minor}'
);
}
File _cachedFontConfig;
@override
......
......@@ -238,7 +238,7 @@ void main() {
final String generated = environment.buildDir.childFile('main.dart').readAsStringSync();
// Language version
expect(generated, contains('// @dart = 2.7'));
expect(generated, contains('// @dart=2.7'));
}));
test('WebEntrypointTarget generates an entrypoint without plugins and without init platform', () => testbed.run(() async {
......
......@@ -19,10 +19,34 @@ void main() {
// @dart = 2.9
''');
expect(determineLanguageVersion(file, null), '// @dart = 2.9');
expect(determineLanguageVersion(file, null), LanguageVersion(2, 9));
});
testWithoutContext('detects technically invalid language version', () {
testWithoutContext('detects language version in comment without spacing', () {
final FileSystem fileSystem = MemoryFileSystem.test();
final File file = fileSystem.file('example.dart')
..writeAsStringSync('''
// Some license
// @dart=2.9
''');
expect(determineLanguageVersion(file, null), LanguageVersion(2, 9));
});
testWithoutContext('detects language version in comment with more numbers', () {
final FileSystem fileSystem = MemoryFileSystem.test();
final File file = fileSystem.file('example.dart')
..writeAsStringSync('''
// Some license
// @dart=2.12
''');
expect(determineLanguageVersion(file, null), LanguageVersion(2, 12));
});
testWithoutContext('does not detect invalid language version', () {
final FileSystem fileSystem = MemoryFileSystem.test();
final File file = fileSystem.file('example.dart')
..writeAsStringSync('''
......@@ -31,7 +55,7 @@ void main() {
// @dart
''');
expect(determineLanguageVersion(file, null), '// @dart');
expect(determineLanguageVersion(file, null), LanguageVersion(2, 12));
});
testWithoutContext('detects language version with leading whitespace', () {
......@@ -43,7 +67,7 @@ void main() {
// @dart = 2.9
''');
expect(determineLanguageVersion(file, null), '// @dart = 2.9');
expect(determineLanguageVersion(file, null), LanguageVersion(2, 9));
});
testWithoutContext('detects language version with tabs', () {
......@@ -55,7 +79,7 @@ void main() {
//\t@dart = 2.9
''');
expect(determineLanguageVersion(file, null), '//\t@dart = 2.9');
expect(determineLanguageVersion(file, null), LanguageVersion(2, 9));
});
testWithoutContext('detects language version with tons of whitespace', () {
......@@ -64,10 +88,10 @@ void main() {
..writeAsStringSync('''
// Some license
// @dart = 23
// @dart = 2.23
''');
expect(determineLanguageVersion(file, null), '// @dart = 23');
expect(determineLanguageVersion(file, null), LanguageVersion(2, 23));
});
testWithoutContext('does not detect language version in dartdoc', () {
......@@ -79,7 +103,7 @@ void main() {
/// @dart = 2.9
''');
expect(determineLanguageVersion(file, null), null);
expect(determineLanguageVersion(file, null), LanguageVersion(2, 12));
});
testWithoutContext('does not detect language version in block comment', () {
......@@ -93,7 +117,7 @@ void main() {
*/
''');
expect(determineLanguageVersion(file, null), null);
expect(determineLanguageVersion(file, null), LanguageVersion(2, 12));
});
testWithoutContext('does not detect language version in nested block comment', () {
......@@ -109,7 +133,7 @@ void main() {
*/
''');
expect(determineLanguageVersion(file, null), null);
expect(determineLanguageVersion(file, null), LanguageVersion(2, 12));
});
testWithoutContext('detects language version after nested block comment', () {
......@@ -124,7 +148,7 @@ void main() {
// @dart = 2.9
''');
expect(determineLanguageVersion(file, null), '// @dart = 2.9');
expect(determineLanguageVersion(file, null), LanguageVersion(2, 9));
});
testWithoutContext('does not crash with unbalanced opening block comments', () {
......@@ -139,7 +163,7 @@ void main() {
// @dart = 2.9
''');
expect(determineLanguageVersion(file, null), null);
expect(determineLanguageVersion(file, null), LanguageVersion(2, 12));
});
testWithoutContext('does not crash with unbalanced closing block comments', () {
......@@ -154,7 +178,7 @@ void main() {
// @dart = 2.9
''');
expect(determineLanguageVersion(file, null), null);
expect(determineLanguageVersion(file, null), LanguageVersion(2, 12));
});
testWithoutContext('does not detect language version in single line block comment', () {
......@@ -166,7 +190,7 @@ void main() {
/* // @dart = 2.9 */
''');
expect(determineLanguageVersion(file, null), null);
expect(determineLanguageVersion(file, null), LanguageVersion(2, 12));
});
testWithoutContext('does not detect language version after import declaration', () {
......@@ -180,7 +204,7 @@ import 'dart:ui' as ui;
// @dart = 2.9
''');
expect(determineLanguageVersion(file, null), null);
expect(determineLanguageVersion(file, null), LanguageVersion(2, 12));
});
testWithoutContext('does not detect language version after part declaration', () {
......@@ -194,7 +218,7 @@ part of 'foo.dart';
// @dart = 2.9
''');
expect(determineLanguageVersion(file, null), null);
expect(determineLanguageVersion(file, null), LanguageVersion(2, 12));
});
testWithoutContext('does not detect language version after library declaration', () {
......@@ -208,7 +232,7 @@ library funstuff;
// @dart = 2.9
''');
expect(determineLanguageVersion(file, null), null);
expect(determineLanguageVersion(file, null), LanguageVersion(2, 12));
});
testWithoutContext('looks up language version from package if not found in file', () {
......@@ -223,6 +247,6 @@ library funstuff;
languageVersion: LanguageVersion(2, 7),
);
expect(determineLanguageVersion(file, package), '// @dart = 2.7');
expect(determineLanguageVersion(file, package), LanguageVersion(2, 7));
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment