// Copyright (c) 2018, 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. // @dart = 2.9 import 'dart:async'; import 'dart:io'; import 'package:dev_compiler/src/kernel/command.dart'; import 'package:dev_compiler/src/kernel/js_typerep.dart'; import 'package:dev_compiler/src/kernel/nullable_inference.dart'; import 'package:dev_compiler/src/kernel/target.dart'; import 'package:front_end/src/api_unstable/ddc.dart' as fe; import 'package:kernel/class_hierarchy.dart'; import 'package:kernel/core_types.dart'; import 'package:kernel/kernel.dart'; import 'package:kernel/src/printer.dart'; import 'package:kernel/target/targets.dart'; import 'package:kernel/type_environment.dart'; import 'package:test/test.dart'; const AstTextStrategy astTextStrategy = AstTextStrategy( includeLibraryNamesInTypes: true, includeLibraryNamesInMembers: true, useMultiline: false); void main() { test('empty main', () async { await expectNotNull('main() {}', ''); }); group('literal', () { test('null', () async { await expectNotNull('main() { print(null); }', ''); }); test('bool', () async { await expectNotNull('main() { print(false); }', 'false'); }); test('int', () async { await expectNotNull('main() { print(42); }', '42'); }); test('double', () async { await expectNotNull('main() { print(123.0); }', '123.0'); }); test('String', () async { await expectNotNull('main() { print("hi"); }', '"hi"'); }); test('List', () async { await expectNotNull( 'main() { print([42, null]); }', '[42, null], 42'); }); test('Map', () async { await expectNotNull('main() { print({"x": null}); }', '{"x": null}, "x"'); }); test('Symbol', () async { await expectNotNull('main() { print(#hi); }', '#hi'); }); test('Type', () async { await expectNotNull('main() { print(Object); }', 'dart.core::Object*'); }); }); test('this', () async { await expectNotNull('class C { m() { return this; } }', 'this'); }); test('is', () async { await expectNotNull('main() { 42 is int; null is int; }', '42 is dart.core::int*, 42, null is dart.core::int*'); }); test('as', () async { await expectNotNull( 'main() { 42 as int; null as int; }', '42 as dart.core::int*, 42'); }); test('constructor', () async { await expectNotNull( 'library a; class C {} main() { new C(); }', 'new a::C()'); }); group('operator', () { test('==', () async { // This is not a correct non-null assumption when user-defined operators, // are present, see: https://github.com/dart-lang/sdk/issues/31854 await expectAllNotNull('main() { 1 == 1; }'); }); test('!', () async { await expectAllNotNull('main() { !false; }'); }); test('!=', () async { await expectAllNotNull('main() { 1 != 2; }'); }); test('&&', () async { await expectAllNotNull('main() { true && true; }'); }); test('||', () async { await expectAllNotNull('main() { true || true; }'); }); test('? :', () async { await expectAllNotNull('main() { true ? true : false; }'); }); }); test('bool', () async { await expectAllNotNull('main() { true.toString(); false.hashCode; }'); }); group('int', () { test('arithmetic', () async { await expectAllNotNull( 'main() { -0; 1 + 2; 3 - 4; 5 * 6; 7 / 8; 9 % 10; 11 ~/ 12; }'); }); test('bitwise', () async { await expectAllNotNull( 'main() { 1 & 2; 3 | 4; 5 ^ 6; ~7; 8 << 9; 10 >> 11; }'); }); test('comparison', () async { await expectAllNotNull('main() { 1 < 2; 3 > 4; 5 <= 6; 7 >= 8; }'); }); test('getters', () async { await expectAllNotNull( 'main() { 1.isOdd; 1.isEven; 1.isNegative; 1.isNaN; 1.isInfinite; ' '1.isFinite; 1.sign; 1.bitLength; 1.hashCode; }'); }); test('methods', () async { await expectAllNotNull( 'main() { 1.compareTo(2); 1.remainder(2); 1.abs(); 1.toInt(); ' '1.ceil(); 1.floor(); 1.truncate(); 1.round(); 1.ceilToDouble(); ' '1.floorToDouble(); 1.truncateToDouble(); 1.roundToDouble(); ' '1.toDouble(); 1.clamp(2, 2); 1.toStringAsFixed(2); ' '1.toStringAsExponential(); 1.toStringAsPrecision(2); 1.toString(); ' '1.toRadixString(2); 1.toUnsigned(2); 1.toSigned(2); 1.modPow(2, 2); ' '1.modInverse(2); 1.gcd(2); }'); }); }); group('double', () { test('arithmetic', () async { await expectAllNotNull( 'main() { -0.0; 1.0 + 2.0; 3.0 - 4.0; 5.0 * 6.0; 7.0 / 8.0; ' '9.0 % 10.0; 11.0 ~/ 12.0; }'); }); test('comparison', () async { await expectAllNotNull( 'main() { 1.0 < 2.0; 3.0 > 4.0; 5.0 <= 6.0; 7.0 >= 8.0; }'); }); test('getters', () async { await expectAllNotNull( 'main() { (1.0).isNegative; (1.0).isNaN; (1.0).isInfinite; ' '(1.0).isFinite; (1.0).sign; (1.0).hashCode; }'); }); test('methods', () async { await expectAllNotNull( 'main() { (1.0).compareTo(2.0); (1.0).remainder(2.0); (1.0).abs(); ' '(1.0).toInt(); (1.0).ceil(); (1.0).floor(); (1.0).truncate(); ' '(1.0).round(); (1.0).ceilToDouble(); (1.0).floorToDouble(); ' '(1.0).truncateToDouble(); (1.0).roundToDouble(); (1.0).toDouble(); ' '(1.0).clamp(2.0, 2.0); (1.0).toStringAsFixed(2); (1.0).toString(); ' '(1.0).toStringAsExponential(); (1.0).toStringAsPrecision(2); }'); }); }); group('num', () { test('arithmetic', () async { await expectAllNotNull( 'main() { num n = 1; -n; n + n; n - n; n * n; n / n; n % n; n % n; ' 'n ~/ n; }'); }); test('comparison', () async { await expectAllNotNull( 'main() { num n = 1; n < n; n > n; n <= n; n >= n; }'); }); test('getters', () async { await expectAllNotNull( 'main() { num n = 1; n.isNegative; n.isNaN; n.isInfinite; ' 'n.isFinite; n.sign; n.hashCode; }'); }); test('methods', () async { await expectAllNotNull( 'main() { num n = 1; n.compareTo(n); n.remainder(n); n.abs(); ' 'n.toInt(); n.ceil(); n.floor(); n.truncate(); ' 'n.round(); n.ceilToDouble(); n.floorToDouble(); ' 'n.truncateToDouble(); n.roundToDouble(); n.toDouble(); ' 'n.clamp(n, n); n.toStringAsFixed(n); n.toString(); ' 'n.toStringAsExponential(); n.toStringAsPrecision(n); }'); }); }); group('String', () { test('concatenation', () async { await expectAllNotNull('main() { "1" "2"; }'); }); test('interpolation', () async { await expectAllNotNull('main() { "1${2}"; }'); }); test('getters', () async { await expectAllNotNull( 'main() { "".codeUnits; "".hashCode; "".isEmpty; "".isNotEmpty; ' '"".length; "".runes; }'); }); test('operators', () async { await expectAllNotNull('main() { "" + ""; "" * 2; "" == ""; "x"[0]; }'); }); test('methods', () async { await expectAllNotNull('''main() { String s = ''; s.codeUnitAt(0); s.contains(s); s.endsWith(s); s.indexOf(s); s.lastIndexOf(s); s.padLeft(1); s.padRight(1); s.replaceAll(s, s); s.replaceAllMapped(s, (_) => s); s.replaceFirst(s, s); s.replaceFirstMapped(s, (_) => s); s.replaceRange(1, 2, s); s.split(s); s.splitMapJoin(s, onMatch: (_) => s, onNonMatch: (_) => s); s.startsWith(s); s.substring(1); s.toLowerCase(); s.toUpperCase(); s.trim(); s.trimLeft(); s.trimRight(); // compareTo relies on the interface target being String.compareTo // except that method does not exist unless we insert too many // forwarding stubs. // // s.compareTo(s); s.toString(); // Pattern methods (allMatches, matchAsPrefix) are not recognized. }'''); }); }); test('identical', () async { await expectNotNull('main() { identical(null, null); }', 'dart.core::identical(null, null)'); }); test('throw', () async { await expectNotNull('main() { print(throw null); }', 'throw null'); }); test('rethrow', () async { await expectNotNull('main() { try {} catch (e) { rethrow; } }', 'rethrow'); }); test('function expression', () async { await expectNotNull( 'main() { () => null; f() {}; f; }', 'Null () => null, f'); }); test('cascades (kernel BlockExpression)', () async { // `null..toString()` evaluates to `null` so it is nullable. await expectNotNull('main() { null..toString(); }', ''); await expectAllNotNull('main() { 1..toString(); }'); }); group('variable', () { test('declaration not-null', () async { await expectNotNull('main() { var x = 42; print(x); }', '42, x'); }); test('declaration null', () async { await expectNotNull('main() { var x = null; print(x); }', ''); }); test('declaration without initializer', () async { await expectNotNull('main() { var x; x = 1; print(x); }', 'x = 1, 1'); }); test('assignment non-null', () async { await expectNotNull( 'main() { var x = 42; x = 1; print(x); }', '42, x = 1, 1, x'); }); test('assignment null', () async { await expectNotNull('main() { var x = 42; x = null; print(x); }', '42'); }); test('flow insensitive', () async { await expectNotNull('''main() { var x = 1; if (true) { print(x); } else { x = null; print(x); } }''', '1, true'); }); test('declaration from variable', () async { await expectNotNull('''main() { var x = 1; var y = x; print(y); x = null; }''', '1'); }); test('declaration from variable nested', () async { await expectNotNull('''main() { var x = 1; var y = (x = null) == null; print(x); print(y); }''', '1, (x = null) == null, y'); }); test('declaration from variable transitive', () async { await expectNotNull('''main() { var x = 1; var y = x; var z = y; print(z); x = null; }''', '1'); }); test('declaration between variable transitive nested', () async { await expectNotNull('''main() { var x = 1; var y = 1; var z = y = x; print(z); x = null; }''', '1, 1'); }); test('for not-null', () async { await expectAllNotNull('''main() { for (var i = 0; i < 10; i++) { i; } }'''); }); test('for nullable', () async { await expectNotNull( '''main() { for (var i = 0; i < 10; i++) { if (i >= 10) i = null; } }''', // arithmetic operation results on `i` are themselves not null, even // though `i` is nullable. '0, i.{dart.core::num.<}(10), 10, i = i.{dart.core::num.+}(1), ' 'i.{dart.core::num.+}(1), 1, i.{dart.core::num.>=}(10), 10'); }); test('for-in', () async { await expectNotNull('''main() { for (var i in []) { print(i); } }''', '[]'); }); test('inner functions', () async { await expectNotNull('''main() { var y = 0; f(x) { var g = () => print('g'); g(); print(x); print(y); var z = 1; print(z); } f; f(42); }''', '0, void () => dart.core::print("g"), "g", g, y, 1, z, f, 42'); }); test('assignment to closure variable', () async { await expectNotNull('''main() { var y = 0; f(x) { y = x; } f; f(42); print(y); }''', '0, f, 42'); }); test('declaration visits initializer', () async { await expectAllNotNull('''main() { var x = () { var y = 1; return y; }; x; }'''); }); test('assignment visits value', () async { await expectAllNotNull('''main() { var x = () => 42; x = () { var y = 1; return y; }; }'''); }); test('assignment visits value with closure variable set', () async { await expectNotNull('''main() { var x = () => 42; var y = (() => x = null); }''', 'dart.core::int* () => 42, 42, Null () => x = null'); }); test('do not depend on unrelated variables', () async { await expectNotNull('''main() { var x; var y = identical(x, null); y; // this is still non-null even though `x` is nullable }''', 'dart.core::identical(x, null), y'); }); test('do not depend on unrelated variables updated later', () async { await expectNotNull('''main() { var x = 1; var y = identical(x, 1); x = null; y; // this is still non-null even though `x` is nullable }''', '1, dart.core::identical(x, 1), 1, y'); }); }); group('functions parameters in SDK', () { setUp(() { // Using annotations here to test how the parameter is detected when // compiling functions from the SDK. // A regression test for: https://github.com/dart-lang/sdk/issues/37700 useAnnotations = true; }); tearDown(() { useAnnotations = false; }); test('optional with default value', () async { await expectNotNull(''' f(x, [y = 1]) { x; y; } ''', '1'); }); test('named with default value', () async { await expectNotNull(''' f(x, {y = 1}) { x; y; } ''', '1'); }); }); group('notNull', () { setUp(() { useAnnotations = true; }); tearDown(() { useAnnotations = false; }); var imports = "import 'package:meta/meta.dart';"; group('(previously known kernel annotation bug)', () { test('variable without initializer', () async { await expectNotNull( '$imports main() { @notNull var x; print(x); }', 'x'); }); test('variable with initializer', () async { // TODO(jmesserly): this does not work in the Analyzer backend. await expectNotNull( '$imports main() { @notNull var x = null; print(x); }', 'x'); }); test('parameters', () async { await expectNotNull( '$imports f(@notNull x, [@notNull y, @notNull z = 42]) ' '{ x; y; z; }', '42, x, y, z'); }); test('named parameters', () async { await expectNotNull( '$imports f({@notNull x, @notNull y: 42}) { x; y; }', '42, x, y'); }); }); test('top-level field', () async { await expectNotNull( 'library a; $imports @notNull int x; main() { x; }', 'a::x'); }); test('getter', () async { await expectNotNull( 'library b; $imports @notNull get x => null; main() { x; }', 'b::x'); }); test('function', () async { await expectNotNull( 'library a; $imports @notNull f() {} main() { f(); }', 'a::f()'); }); test('method', () async { await expectNotNull( 'library b; $imports class C { @notNull m() {} } ' 'main() { var c = new C(); c.m(); }', 'new b::C(), c.{b::C.m}(), c'); }); }); } /// Given the Dart [code], expects the [expectedNotNull] kernel expression list /// to be produced in the set of expressions that cannot be null by DDC's null /// inference. Future expectNotNull(String code, String expectedNotNull) async { code = '// @dart = 2.9\n$code'; var result = await kernelCompile(code); var collector = NotNullCollector(result.librariesFromDill); result.component.accept(collector); var actualNotNull = collector.notNullExpressions // ConstantExpressions print the table offset - we want to compare // against the underlying constant value instead. .map((e) { if (e is ConstantExpression) { var c = e.constant; if (c is DoubleConstant && c.value.isFinite && c.value.truncateToDouble() == c.value) { // Print integer values as integers return BigInt.from(c.value).toString(); } return c.toText(astTextStrategy); } return e.toText(astTextStrategy); }) // Filter out our own NotNull annotations. The library prefix changes // per test, so just filter on the suffix. .where((s) => !s.endsWith('_NotNull{}')) .join(', '); expect(actualNotNull, equals(expectedNotNull)); } /// Given the Dart [code], expects all the expressions inferred to be not-null. Future expectAllNotNull(String code) async { code = '// @dart = 2.9\n$code'; var result = (await kernelCompile(code)); result.component.accept(ExpectAllNotNull(result.librariesFromDill)); } bool useAnnotations = false; NullableInference inference; class _TestRecursiveVisitor extends RecursiveVisitor { final Set librariesFromDill; int _functionNesting = 0; TypeEnvironment _typeEnvironment; StatefulStaticTypeContext _staticTypeContext; _TestRecursiveVisitor(this.librariesFromDill); @override void visitComponent(Component node) { var coreTypes = CoreTypes(node); var hierarchy = ClassHierarchy(node, coreTypes); var jsTypeRep = JSTypeRep( fe.TypeSchemaEnvironment(coreTypes, hierarchy), hierarchy, ); _typeEnvironment = jsTypeRep.types; _staticTypeContext = StatefulStaticTypeContext.stacked(_typeEnvironment); inference ??= NullableInference(jsTypeRep, _staticTypeContext); if (useAnnotations) { inference.allowNotNullDeclarations = useAnnotations; inference.allowPackageMetaAnnotations = useAnnotations; } super.visitComponent(node); } @override void visitLibrary(Library node) { _staticTypeContext.enterLibrary(node); if (librariesFromDill.contains(node) || node.importUri.scheme == 'package' && node.importUri.pathSegments[0] == 'meta') { return; } super.visitLibrary(node); _staticTypeContext.leaveLibrary(node); } @override void visitField(Field node) { _staticTypeContext.enterMember(node); super.visitField(node); _staticTypeContext.leaveMember(node); } @override void visitConstructor(Constructor node) { _staticTypeContext.enterMember(node); super.visitConstructor(node); _staticTypeContext.leaveMember(node); } @override void visitProcedure(Procedure node) { _staticTypeContext.enterMember(node); super.visitProcedure(node); _staticTypeContext.leaveMember(node); } @override void visitFunctionNode(FunctionNode node) { _functionNesting++; if (_functionNesting == 1) { inference.enterFunction(node); } super.visitFunctionNode(node); if (_functionNesting == 1) inference.exitFunction(node); _functionNesting--; } } class NotNullCollector extends _TestRecursiveVisitor { final notNullExpressions = []; NotNullCollector(Set librariesFromDill) : super(librariesFromDill); @override void defaultExpression(Expression node) { if (!inference.isNullable(node)) { notNullExpressions.add(node); } super.defaultExpression(node); } } class ExpectAllNotNull extends _TestRecursiveVisitor { ExpectAllNotNull(Set librariesFromDill) : super(librariesFromDill); @override void defaultExpression(Expression node) { expect(inference.isNullable(node), false, reason: 'expression `$node` should be inferred as not-null'); super.defaultExpression(node); } } fe.InitializedCompilerState _compilerState; final _fileSystem = fe.MemoryFileSystem(Uri.file('/memory/')); class CompileResult { final Component component; final Set librariesFromDill; CompileResult(this.component, this.librariesFromDill); } Future kernelCompile(String code) async { var succeeded = true; void diagnosticMessageHandler(fe.DiagnosticMessage message) { if (message.severity == fe.Severity.error) { succeeded = false; } fe.printDiagnosticMessage(message, print); } var sdkUri = Uri.file('/memory/dart_sdk.dill'); var sdkFile = _fileSystem.entityForUri(sdkUri); if (!await sdkFile.exists()) { sdkFile.writeAsBytesSync( File(defaultSdkSummaryPath(soundNullSafety: false)).readAsBytesSync()); } var packagesUri = Uri.file('/memory/.packages'); var packagesFile = _fileSystem.entityForUri(packagesUri); if (!await packagesFile.exists()) { packagesFile.writeAsStringSync('meta:/memory/meta/lib'); _fileSystem .entityForUri(Uri.file('/memory/meta/lib/meta.dart')) .writeAsStringSync(''' class _NotNull { const _NotNull(); } const notNull = const _NotNull(); class _NullCheck { const _NullCheck(); } const nullCheck = const _NullCheck(); '''); } var mainUri = Uri.file('/memory/test.dart'); _fileSystem.entityForUri(mainUri).writeAsStringSync(code); var oldCompilerState = _compilerState; _compilerState = await fe.initializeCompiler(oldCompilerState, false, null, sdkUri, packagesUri, null, [], DevCompilerTarget(TargetFlags()), fileSystem: _fileSystem, explicitExperimentalFlags: const {}, environmentDefines: const {}, nnbdMode: fe.NnbdMode.Weak); if (!identical(oldCompilerState, _compilerState)) inference = null; var result = await fe.compile(_compilerState, [mainUri], diagnosticMessageHandler); expect(succeeded, true); var librariesFromDill = result.computeLibrariesFromDill(); return CompileResult(result.component, librariesFromDill); }