Version 2.10.0-136.0.dev
Merge commit '0eec9ee53c4306abea1733b3b5cc2bfb57cbe380' into 'dev'
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/type_promotion/data/bug42653.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/type_promotion/data/bug42653.dart
new file mode 100644
index 0000000..45304e0
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/type_promotion/data/bug42653.dart
@@ -0,0 +1,39 @@
+// Copyright (c) 2020, 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.
+
+void forEachStatement(Object x) {
+ if (x is int) {
+ /*int*/ x;
+ for (x in [0]) {
+ /*int*/ x;
+ }
+ }
+}
+
+forEachElementInListLiteral(Object x) {
+ if (x is int) {
+ /*int*/ x;
+ return [
+ for (x in [0]) /*int*/ x
+ ];
+ }
+}
+
+forEachElementInMapLiteral(Object x) {
+ if (x is int) {
+ /*int*/ x;
+ return {
+ for (x in [0]) /*int*/ x: /*int*/ x
+ };
+ }
+}
+
+forEachElementInSetLiteral(Object x) {
+ if (x is int) {
+ /*int*/ x;
+ return {
+ for (x in [0]) /*int*/ x
+ };
+ }
+}
diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
index 1a6a820..7feaa01 100644
--- a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
+++ b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
@@ -4938,8 +4938,8 @@
reporter.reportError('must not be null');
return false;
}
- if (!(obj['kind'] is String)) {
- reporter.reportError('must be of type String');
+ if (!(obj['kind'] == 'create')) {
+ reporter.reportError('must be the literal \'create\'');
return false;
}
} finally {
@@ -5963,8 +5963,8 @@
reporter.reportError('must not be null');
return false;
}
- if (!(obj['kind'] is String)) {
- reporter.reportError('must be of type String');
+ if (!(obj['kind'] == 'delete')) {
+ reporter.reportError('must be the literal \'delete\'');
return false;
}
} finally {
@@ -13198,7 +13198,10 @@
final capabilities = json['capabilities'] != null
? ClientCapabilities.fromJson(json['capabilities'])
: null;
- final trace = json['trace'];
+ final trace = const {null, 'off', 'messages', 'verbose'}
+ .contains(json['trace'])
+ ? json['trace']
+ : throw '''${json['trace']} was not one of (null, 'off', 'messages', 'verbose')''';
final workspaceFolders = json['workspaceFolders']
?.map((item) => item != null ? WorkspaceFolder.fromJson(item) : null)
?.cast<WorkspaceFolder>()
@@ -13363,8 +13366,12 @@
}
reporter.push('trace');
try {
- if (obj['trace'] != null && !(obj['trace'] is String)) {
- reporter.reportError('must be of type String');
+ if (obj['trace'] != null &&
+ !((obj['trace'] == 'off' ||
+ obj['trace'] == 'messages' ||
+ obj['trace'] == 'verbose'))) {
+ reporter.reportError(
+ 'must be one of the literals \'off\', \'messages\', \'verbose\'');
return false;
}
} finally {
@@ -16647,8 +16654,8 @@
reporter.reportError('must not be null');
return false;
}
- if (!(obj['kind'] is String)) {
- reporter.reportError('must be of type String');
+ if (!(obj['kind'] == 'rename')) {
+ reporter.reportError('must be the literal \'rename\'');
return false;
}
} finally {
@@ -23813,8 +23820,8 @@
reporter.reportError('must not be null');
return false;
}
- if (!(obj['kind'] is String)) {
- reporter.reportError('must be of type String');
+ if (!(obj['kind'] == 'begin')) {
+ reporter.reportError('must be the literal \'begin\'');
return false;
}
} finally {
@@ -24090,8 +24097,8 @@
reporter.reportError('must not be null');
return false;
}
- if (!(obj['kind'] is String)) {
- reporter.reportError('must be of type String');
+ if (!(obj['kind'] == 'end')) {
+ reporter.reportError('must be the literal \'end\'');
return false;
}
} finally {
@@ -24464,8 +24471,8 @@
reporter.reportError('must not be null');
return false;
}
- if (!(obj['kind'] is String)) {
- reporter.reportError('must be of type String');
+ if (!(obj['kind'] == 'report')) {
+ reporter.reportError('must be the literal \'report\'');
return false;
}
} finally {
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/make_variable_nullable.dart b/pkg/analysis_server/lib/src/services/correction/dart/make_variable_nullable.dart
new file mode 100644
index 0000000..5a4d2ec
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/correction/dart/make_variable_nullable.dart
@@ -0,0 +1,106 @@
+// Copyright (c) 2020, 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.
+
+import 'package:analysis_server/src/services/correction/dart/abstract_producer.dart';
+import 'package:analysis_server/src/services/correction/fix.dart';
+import 'package:analyzer/dart/analysis/features.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/token.dart';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/nullability_suffix.dart';
+import 'package:analyzer/src/dart/element/type.dart';
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:analyzer_plugin/utilities/range_factory.dart';
+
+class MakeVariableNullable extends CorrectionProducer {
+ /// The name of the variable whose type is to be made nullable.
+ String _variableName;
+
+ @override
+ List<Object> get fixArguments => [_variableName];
+
+ @override
+ FixKind get fixKind => DartFixKind.MAKE_VARIABLE_NULLABLE;
+
+ @override
+ Future<void> compute(ChangeBuilder builder) async {
+ var node = coveredNode;
+ var parent = node?.parent;
+ if (unit.featureSet.isEnabled(Feature.non_nullable) &&
+ parent is AssignmentExpression &&
+ parent.rightHandSide == node) {
+ var leftHandSide = parent.leftHandSide;
+ if (leftHandSide is SimpleIdentifier) {
+ var element = leftHandSide.staticElement;
+ if (element is LocalVariableElement) {
+ var oldType = element.type;
+ var newType = (node as Expression).staticType;
+ if (node is NullLiteral) {
+ newType = (oldType as InterfaceTypeImpl)
+ .withNullability(NullabilitySuffix.question);
+ } else if (!typeSystem.isAssignableTo(
+ oldType, typeSystem.promoteToNonNull(newType))) {
+ return;
+ }
+ var declarationList =
+ _findDeclaration(element, parent.thisOrAncestorOfType<Block>());
+ if (declarationList == null || declarationList.variables.length > 1) {
+ return;
+ }
+ var variable = declarationList.variables[0];
+ _variableName = variable.name.name;
+ await builder.addDartFileEdit(file, (builder) {
+ var keyword = declarationList.keyword;
+ if (keyword != null && keyword.type == Keyword.VAR) {
+ builder.addReplacement(range.token(keyword), (builder) {
+ builder.writeType(newType);
+ });
+ } else if (keyword == null) {
+ if (declarationList.type == null) {
+ builder.addInsertion(variable.offset, (builder) {
+ builder.writeType(newType);
+ builder.write(' ');
+ });
+ } else {
+ builder.addSimpleInsertion(declarationList.type.end, '?');
+ }
+ }
+ });
+ }
+ }
+ }
+ }
+
+ /// Return the list of variable declarations containing the declaration of the
+ /// given [variable] that is located in the given [block] or in a surrounding
+ /// block. Return `null` if the declaration can't be found.
+ VariableDeclarationList _findDeclaration(
+ LocalVariableElement variable, Block block) {
+ if (variable == null) {
+ return null;
+ }
+ var currentBlock = block;
+ while (currentBlock != null) {
+ for (var statement in block.statements) {
+ if (statement is VariableDeclarationStatement) {
+ var variableList = statement.variables;
+ if (variableList != null) {
+ var variables = variableList.variables;
+ for (var declaration in variables) {
+ if (declaration.declaredElement == variable) {
+ return variableList;
+ }
+ }
+ }
+ }
+ }
+ currentBlock = currentBlock.parent.thisOrAncestorOfType<Block>();
+ }
+ return null;
+ }
+
+ /// Return an instance of this class. Used as a tear-off in `FixProcessor`.
+ static MakeVariableNullable newInstance() => MakeVariableNullable();
+}
diff --git a/pkg/analysis_server/lib/src/services/correction/fix.dart b/pkg/analysis_server/lib/src/services/correction/fix.dart
index 0f67130..7dd5825 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix.dart
@@ -323,6 +323,8 @@
'Move type arguments to after class name');
static const MAKE_VARIABLE_NOT_FINAL = FixKind(
'dart.fix.makeVariableNotFinal', 50, "Make variable '{0}' not final");
+ static const MAKE_VARIABLE_NULLABLE =
+ FixKind('dart.fix.makeVariableNullable', 50, "Make '{0}' nullable");
static const ORGANIZE_IMPORTS =
FixKind('dart.fix.organize.imports', 50, 'Organize Imports');
static const QUALIFY_REFERENCE =
diff --git a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
index adfaa6e..86ed829 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -80,6 +80,7 @@
import 'package:analysis_server/src/services/correction/dart/make_final.dart';
import 'package:analysis_server/src/services/correction/dart/make_return_type_nullable.dart';
import 'package:analysis_server/src/services/correction/dart/make_variable_not_final.dart';
+import 'package:analysis_server/src/services/correction/dart/make_variable_nullable.dart';
import 'package:analysis_server/src/services/correction/dart/move_type_arguments_to_class.dart';
import 'package:analysis_server/src/services/correction/dart/organize_imports.dart';
import 'package:analysis_server/src/services/correction/dart/qualify_reference.dart';
@@ -716,6 +717,7 @@
AddExplicitCast.newInstance,
AddNullCheck.newInstance,
ChangeTypeAnnotation.newInstance,
+ MakeVariableNullable.newInstance,
],
CompileTimeErrorCode.INVOCATION_OF_NON_FUNCTION_EXPRESSION: [
RemoveParenthesesInGetterInvocation.newInstance,
diff --git a/pkg/analysis_server/test/src/services/correction/fix/make_variable_nullable_test.dart b/pkg/analysis_server/test/src/services/correction/fix/make_variable_nullable_test.dart
new file mode 100644
index 0000000..133daf5
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/fix/make_variable_nullable_test.dart
@@ -0,0 +1,111 @@
+// Copyright (c) 2020, 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.
+
+import 'package:analysis_server/src/services/correction/fix.dart';
+import 'package:analyzer/src/dart/analysis/experiments.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'fix_processor.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(MakeVariableNullableTest);
+ });
+}
+
+@reflectiveTest
+class MakeVariableNullableTest extends FixProcessorTest {
+ @override
+ List<String> get experiments => [EnableString.non_nullable];
+
+ @override
+ FixKind get kind => DartFixKind.MAKE_VARIABLE_NULLABLE;
+
+ Future<void> test_lhsNotIdentifier() async {
+ await resolveTestUnit('''
+void f(C c) {
+ c.s = null;
+}
+class C {
+ String s = '';
+}
+''');
+ await assertNoFix();
+ }
+
+ Future<void> test_lhsNotLocalVariable() async {
+ await resolveTestUnit('''
+var s = '';
+void f() {
+ s = null;
+ print(s);
+}
+''');
+ await assertNoFix();
+ }
+
+ Future<void> test_multipleVariables() async {
+ await resolveTestUnit('''
+void f() {
+ var s = '', t = '';
+ s = null;
+ print(s);
+ print(t);
+}
+''');
+ await assertNoFix();
+ }
+
+ Future<void> test_noKeywordOrType() async {
+ await resolveTestUnit('''
+void f() {
+ late s = '';
+ s = null;
+ print(s);
+}
+''');
+ await assertHasFix('''
+void f() {
+ late String? s = '';
+ s = null;
+ print(s);
+}
+''');
+ }
+
+ Future<void> test_type() async {
+ await resolveTestUnit('''
+void f() {
+ String s = '';
+ s = null;
+ print(s);
+}
+''');
+ await assertHasFix('''
+void f() {
+ String? s = '';
+ s = null;
+ print(s);
+}
+''');
+ }
+
+ Future<void> test_var() async {
+ await resolveTestUnit('''
+void f() {
+ var s = '';
+ s = null;
+ print(s);
+}
+''');
+ await assertHasFix('''
+void f() {
+ String? s = '';
+ s = null;
+ print(s);
+}
+''');
+ }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
index 64dde02..7fcd197 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
@@ -94,6 +94,7 @@
import 'make_final_test.dart' as make_final;
import 'make_return_type_nullable_test.dart' as make_return_type_nullable;
import 'make_variable_not_final_test.dart' as make_variable_not_final;
+import 'make_variable_nullable_test.dart' as make_variable_nullable;
import 'move_type_arguments_to_class_test.dart' as move_type_arguments_to_class;
import 'organize_imports_test.dart' as organize_imports;
import 'qualify_reference_test.dart' as qualify_reference;
@@ -249,6 +250,7 @@
make_final.main();
make_return_type_nullable.main();
make_variable_not_final.main();
+ make_variable_nullable.main();
move_type_arguments_to_class.main();
organize_imports.main();
qualify_reference.main();
diff --git a/pkg/analysis_server/test/tool/lsp_spec/json_test.dart b/pkg/analysis_server/test/tool/lsp_spec/json_test.dart
index da31eff..1334077 100644
--- a/pkg/analysis_server/test/tool/lsp_spec/json_test.dart
+++ b/pkg/analysis_server/test/tool/lsp_spec/json_test.dart
@@ -142,6 +142,50 @@
expect(canParse, isTrue);
});
+ test('canParse allows matching literal strings', () {
+ // The CreateFile type is defined with `{ kind: 'create' }` so the only
+ // allowed value for `kind` is "create".
+ final canParse = CreateFile.canParse({
+ 'kind': 'create',
+ 'uri': 'file:///temp/foo',
+ }, nullLspJsonReporter);
+ expect(canParse, isTrue);
+ });
+
+ test('canParse disallows non-matching literal strings', () {
+ // The CreateFile type is defined with `{ kind: 'create' }` so the only
+ // allowed value for `kind` is "create".
+ final canParse = CreateFile.canParse({
+ 'kind': 'not-create',
+ 'uri': 'file:///temp/foo',
+ }, nullLspJsonReporter);
+ expect(canParse, isFalse);
+ });
+
+ test('canParse handles unions of literals', () {
+ // Key = value to test
+ // Value whether expected to parse
+ const testTraceValues = {
+ 'off': true,
+ 'messages': true,
+ 'verbose': true,
+ null: true,
+ 'invalid': false,
+ };
+ for (final testValue in testTraceValues.keys) {
+ final expected = testTraceValues[testValue];
+ final canParse = InitializeParams.canParse({
+ 'processId': null,
+ 'rootUri': null,
+ 'capabilities': <String, Object>{},
+ 'trace': testValue,
+ }, nullLspJsonReporter);
+ expect(canParse, expected,
+ reason: 'InitializeParams.canParse returned $canParse with a '
+ '"trace" value of "$testValue" but expected $expected');
+ }
+ });
+
test('canParse validates optional fields', () {
expect(
RenameFileOptions.canParse(<String, Object>{}, nullLspJsonReporter),
@@ -336,7 +380,7 @@
InitializeParamsClientInfo(name: 'server name', version: '1.2.3'),
rootPath: '!root',
capabilities: ClientCapabilities(),
- trace: '!trace',
+ trace: 'off',
workspaceFolders: [
WorkspaceFolder(uri: '!uri1', name: '!name1'),
WorkspaceFolder(uri: '!uri2', name: '!name2'),
diff --git a/pkg/analysis_server/test/tool/lsp_spec/matchers.dart b/pkg/analysis_server/test/tool/lsp_spec/matchers.dart
index 9744f04..e5ed507 100644
--- a/pkg/analysis_server/test/tool/lsp_spec/matchers.dart
+++ b/pkg/analysis_server/test/tool/lsp_spec/matchers.dart
@@ -9,6 +9,9 @@
Matcher isArrayOf(Matcher matcher) => ArrayTypeMatcher(wrapMatcher(matcher));
+Matcher isLiteralOf(Matcher typeMatcher, Object value) =>
+ LiteralTypeMatcher(typeMatcher, value);
+
Matcher isMapOf(Matcher indexMatcher, Matcher valueMatcher) =>
MapTypeMatcher(wrapMatcher(indexMatcher), wrapMatcher(valueMatcher));
@@ -43,6 +46,25 @@
}
}
+class LiteralTypeMatcher extends Matcher {
+ final Matcher _typeMatcher;
+ final String _value;
+ LiteralTypeMatcher(this._typeMatcher, this._value);
+
+ @override
+ Description describe(Description description) => description
+ .add('a literal where type is ')
+ .addDescriptionOf(_typeMatcher)
+ .add(' and value is $_value');
+
+ @override
+ bool matches(item, Map matchState) {
+ return item is LiteralType &&
+ _typeMatcher.matches(item.type, matchState) &&
+ item.literal == _value;
+ }
+}
+
class MapTypeMatcher extends Matcher {
final Matcher _indexMatcher, _valueMatcher;
const MapTypeMatcher(this._indexMatcher, this._valueMatcher);
diff --git a/pkg/analysis_server/test/tool/lsp_spec/typescript_test.dart b/pkg/analysis_server/test/tool/lsp_spec/typescript_test.dart
index 843ac88..b439e68 100644
--- a/pkg/analysis_server/test/tool/lsp_spec/typescript_test.dart
+++ b/pkg/analysis_server/test/tool/lsp_spec/typescript_test.dart
@@ -385,5 +385,49 @@
expect(output[0].commentNode.token.lexeme, equals('''// This is line 1
// This is line 2'''));
});
+
+ test('parses literal string values', () {
+ final input = '''
+export interface MyType {
+ kind: 'one';
+}
+ ''';
+ final output = parseString(input);
+ expect(output, hasLength(1));
+ expect(output[0], const TypeMatcher<Interface>());
+ final Interface interface = output[0];
+ expect(interface.name, equals('MyType'));
+ expect(interface.members, hasLength(1));
+ expect(interface.members[0], const TypeMatcher<Field>());
+ final Field field = interface.members[0];
+ expect(field.name, equals('kind'));
+ expect(field.allowsNull, isFalse);
+ expect(field.allowsUndefined, isFalse);
+ expect(field.type, isLiteralOf(isSimpleType('string'), "'one'"));
+ });
+
+ test('parses literal union values', () {
+ final input = '''
+export interface MyType {
+ kind: 'one' | 'two';
+}
+ ''';
+ final output = parseString(input);
+ expect(output, hasLength(1));
+ expect(output[0], const TypeMatcher<Interface>());
+ final Interface interface = output[0];
+ expect(interface.name, equals('MyType'));
+ expect(interface.members, hasLength(1));
+ expect(interface.members[0], const TypeMatcher<Field>());
+ final Field field = interface.members[0];
+ expect(field.name, equals('kind'));
+ expect(field.allowsNull, isFalse);
+ expect(field.allowsUndefined, isFalse);
+ expect(field.type, const TypeMatcher<LiteralUnionType>());
+ LiteralUnionType union = field.type;
+ expect(union.types, hasLength(2));
+ expect(union.types[0], isLiteralOf(isSimpleType('string'), "'one'"));
+ expect(union.types[1], isLiteralOf(isSimpleType('string'), "'two'"));
+ });
});
}
diff --git a/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart b/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart
index cefcce6..e25450b 100644
--- a/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart
+++ b/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart
@@ -120,6 +120,16 @@
return sortedList;
}
+String _getTypeCheckFailureMessage(TypeBase type) {
+ if (type is LiteralType) {
+ return 'must be the literal ${type.literal}';
+ } else if (type is LiteralUnionType) {
+ return 'must be one of the literals ${type.literalTypes.map((t) => t.literal).join(', ')}';
+ } else {
+ return 'must be of type ${type.dartTypeWithTypeArgs}';
+ }
+}
+
bool _isSimpleType(TypeBase type) {
const literals = ['num', 'String', 'bool'];
return type is Type && literals.contains(type.dartType);
@@ -224,7 +234,7 @@
..write(')) {')
..indent()
..writeIndentedln(
- "reporter.reportError('must be of type ${field.type.dartTypeWithTypeArgs}');")
+ "reporter.reportError('${_getTypeCheckFailureMessage(field.type).replaceAll("'", "\\'")}');")
..writeIndentedln('return false;')
..outdent()
..writeIndentedln('}')
@@ -310,8 +320,13 @@
final consts = namespace.members.cast<Const>().toList();
final allowsAnyValue = enumClassAllowsAnyValue(namespace.name);
final constructorName = allowsAnyValue ? '' : '._';
+ final firstValueType = consts.first.type;
+ // Enums can have constant values in their fields so if a field is a literal
+ // use its underlying type for type checking.
+ final requiredValueType =
+ firstValueType is LiteralType ? firstValueType.type : firstValueType;
final typeOfValues =
- resolveTypeAlias(consts.first.type, resolveEnumClasses: true);
+ resolveTypeAlias(requiredValueType, resolveEnumClasses: true);
buffer
..writeln('class ${namespace.name} {')
@@ -449,6 +464,9 @@
_writeFromJsonCode(buffer, type.valueType, 'value', allowsNull: allowsNull);
buffer.write(
'))?.cast<${type.indexType.dartTypeWithTypeArgs}, ${type.valueType.dartTypeWithTypeArgs}>()');
+ } else if (type is LiteralUnionType) {
+ _writeFromJsonCodeForLiteralUnion(buffer, type, valueCode,
+ allowsNull: allowsNull);
} else if (type is UnionType) {
_writeFromJsonCodeForUnion(buffer, type, valueCode, allowsNull: allowsNull);
} else {
@@ -456,6 +474,18 @@
}
}
+void _writeFromJsonCodeForLiteralUnion(
+ IndentableStringBuffer buffer, LiteralUnionType union, String valueCode,
+ {bool allowsNull}) {
+ final allowedValues = [
+ if (allowsNull) null,
+ ...union.literalTypes.map((t) => t.literal)
+ ];
+ buffer.write(
+ "const {${allowedValues.join(', ')}}.contains($valueCode) ? $valueCode : "
+ "throw '''\${$valueCode} was not one of (${allowedValues.join(', ')})'''");
+}
+
void _writeFromJsonCodeForUnion(
IndentableStringBuffer buffer, UnionType union, String valueCode,
{bool allowsNull}) {
@@ -720,6 +750,8 @@
buffer.write('true');
} else if (_isSimpleType(type)) {
buffer.write('$valueCode is $fullDartType');
+ } else if (type is LiteralType) {
+ buffer.write('$valueCode == ${type.literal}');
} else if (_isSpecType(type)) {
buffer.write('$dartType.canParse($valueCode, $reporter)');
} else if (type is ArrayType) {
diff --git a/pkg/analysis_server/tool/lsp_spec/typescript_parser.dart b/pkg/analysis_server/tool/lsp_spec/typescript_parser.dart
index 1f510d6..9e9d814 100644
--- a/pkg/analysis_server/tool/lsp_spec/typescript_parser.dart
+++ b/pkg/analysis_server/tool/lsp_spec/typescript_parser.dart
@@ -29,6 +29,8 @@
bool isAnyType(TypeBase t) =>
t is Type && (t.name == 'any' || t.name == 'object');
+bool isLiteralType(TypeBase t) => t is LiteralType;
+
bool isNullType(TypeBase t) => t is Type && t.name == 'null';
bool isUndefinedType(TypeBase t) => t is Type && t.name == 'undefined';
@@ -85,6 +87,7 @@
Token valueToken;
Const(Comment comment, this.nameToken, this.type, this.valueToken)
: super(comment);
+
@override
String get name => nameToken.lexeme;
@@ -172,6 +175,36 @@
: '';
}
+class LiteralType extends TypeBase {
+ final Type type;
+ final String literal;
+
+ LiteralType(this.type, this.literal);
+
+ @override
+ String get dartType => type.dartType;
+
+ @override
+ String get typeArgsString => type.typeArgsString;
+
+ @override
+ String get uniqueTypeIdentifier => '$literal:${super.uniqueTypeIdentifier}';
+}
+
+/// A special class of Union types where the values are all literals of the same
+/// type so the Dart field can be the base type rather than an EitherX<>.
+class LiteralUnionType extends UnionType {
+ final List<LiteralType> literalTypes;
+
+ LiteralUnionType(this.literalTypes) : super(literalTypes);
+
+ @override
+ String get dartType => types.first.dartType;
+
+ @override
+ String get typeArgsString => types.first.typeArgsString;
+}
+
class MapType extends TypeBase {
final TypeBase indexType;
final TypeBase valueType;
@@ -180,6 +213,7 @@
@override
String get dartType => 'Map';
+
@override
String get typeArgsString =>
'<${indexType.dartTypeWithTypeArgs}, ${valueType.dartTypeWithTypeArgs}>';
@@ -364,9 +398,7 @@
// simplify the unions.
remainingTypes.removeWhere((t) => !allowTypeInSignatures(t));
- type = remainingTypes.length > 1
- ? UnionType(remainingTypes)
- : remainingTypes.single;
+ type = _simplifyUnionTypes(remainingTypes);
} else if (isAnyType(type)) {
// There are values in the spec marked as `any` that allow nulls (for
// example, the result field on ResponseMessage can be null for a
@@ -376,25 +408,6 @@
return Field(leadingComment, name, type, canBeNull, canBeUndefined);
}
- /// Remove any duplicate types (for ex. if we map multiple types into dynamic)
- /// we don't want to end up with `dynamic | dynamic`. Key on dartType to
- /// ensure we different types that will map down to the same type.
- List<TypeBase> _getUniqueTypes(List<TypeBase> types) {
- final uniqueTypes = Map.fromEntries(
- types.map((t) => MapEntry(t.dartTypeWithTypeArgs, t)),
- ).values.toList();
-
- // If our list includes something that maps to dynamic as well as other
- // types, we should just treat the whole thing as dynamic as we get no value
- // typing Either4<bool, String, num, dynamic> but it becomes much more
- // difficult to use.
- if (uniqueTypes.any(isAnyType)) {
- return [uniqueTypes.firstWhere(isAnyType)];
- }
-
- return uniqueTypes;
- }
-
Indexer _indexer(String containerName, Comment leadingComment) {
final indexer = _field(containerName, leadingComment);
_consume(TokenType.RIGHT_BRACKET, 'Expected ]');
@@ -490,6 +503,29 @@
/// Returns the next token without advancing.
Token _peek() => _tokenAt(_current);
+ /// Remove any duplicate types (for ex. if we map multiple types into dynamic)
+ /// we don't want to end up with `dynamic | dynamic`. Key on dartType to
+ /// ensure we different types that will map down to the same type.
+ TypeBase _simplifyUnionTypes(List<TypeBase> types) {
+ final uniqueTypes = Map.fromEntries(
+ types.map((t) => MapEntry(t.uniqueTypeIdentifier, t)),
+ ).values.toList();
+
+ // If our list includes something that maps to dynamic as well as other
+ // types, we should just treat the whole thing as dynamic as we get no value
+ // typing Either4<bool, String, num, dynamic> but it becomes much more
+ // difficult to use.
+ if (uniqueTypes.any(isAnyType)) {
+ return uniqueTypes.firstWhere(isAnyType);
+ }
+
+ return uniqueTypes.length == 1
+ ? uniqueTypes.single
+ : uniqueTypes.every(isLiteralType)
+ ? LiteralUnionType(uniqueTypes.cast<LiteralType>())
+ : UnionType(uniqueTypes);
+ }
+
Token _tokenAt(int index) =>
index < _tokens.length ? _tokens[index] : Token.EOF;
@@ -562,17 +598,17 @@
// Some types are in (parens), so we just parse the contents as a nested type.
type = _type(containerName, fieldName);
_consume(TokenType.RIGHT_PAREN, 'Expected )');
- } else if (_match([TokenType.STRING])) {
+ } else if (_check(TokenType.STRING)) {
+ final token = _advance();
// In TS and the spec, literal strings can be types:
// export const PlainText: 'plaintext' = 'plaintext';
// trace?: 'off' | 'messages' | 'verbose';
- // the best we can do is use their base type (string).
- type = Type.identifier('string');
- } else if (_match([TokenType.NUMBER])) {
+ type = LiteralType(Type.identifier('string'), token.lexeme);
+ } else if (_check(TokenType.NUMBER)) {
+ final token = _advance();
// In TS and the spec, literal numbers can be types:
// export const Invoked: 1 = 1;
- // the best we can do is use their base type (number).
- type = Type.identifier('number');
+ type = LiteralType(Type.identifier('number'), token.lexeme);
} else if (_match([TokenType.LEFT_BRACKET])) {
// Tuples will just be converted to List/Array.
final tupleElementTypes = <TypeBase>[];
@@ -583,10 +619,7 @@
}
_consume(TokenType.RIGHT_BRACKET, 'Expected ]');
- final uniqueTypes = _getUniqueTypes(tupleElementTypes);
- var tupleType = uniqueTypes.length == 1
- ? uniqueTypes.single
- : UnionType(uniqueTypes);
+ var tupleType = _simplifyUnionTypes(tupleElementTypes);
type = ArrayType(tupleType);
} else {
var typeName = _consume(TokenType.IDENTIFIER, 'Expected identifier');
@@ -629,10 +662,7 @@
}
}
- final uniqueTypes = _getUniqueTypes(types);
-
- var type =
- uniqueTypes.length == 1 ? uniqueTypes.single : UnionType(uniqueTypes);
+ var type = _simplifyUnionTypes(types);
// Handle improved type mappings for things that aren't very tight in the spec.
if (improveTypes) {
@@ -957,6 +987,10 @@
String get dartType;
String get dartTypeWithTypeArgs => '$dartType$typeArgsString';
String get typeArgsString;
+
+ /// A unique identifier for this type. Used for folding types together
+ /// (for example two types that resolve to "dynamic" in Dart).
+ String get uniqueTypeIdentifier => dartTypeWithTypeArgs;
}
class UnionType extends TypeBase {
diff --git a/pkg/analyzer/lib/src/dart/resolver/assignment_expression_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/assignment_expression_resolver.dart
index 3c2ce69..fc73415 100644
--- a/pkg/analyzer/lib/src/dart/resolver/assignment_expression_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/assignment_expression_resolver.dart
@@ -59,6 +59,11 @@
var left = node.leftHandSide;
var right = node.rightHandSide;
+ if (left is PrefixedIdentifier) {
+ _resolve_PrefixedIdentifier(node, left);
+ return;
+ }
+
if (left is PropertyAccess) {
_resolve_PropertyAccess(node, left);
return;
@@ -311,6 +316,37 @@
}
}
+ void _resolve_PrefixedIdentifier(
+ AssignmentExpressionImpl node,
+ PrefixedIdentifier left,
+ ) {
+ left.prefix?.accept(_resolver);
+
+ var propertyName = left.identifier;
+ var operator = node.operator.type;
+ var hasRead = operator != TokenType.EQ;
+
+ var resolver = PropertyElementResolver(_resolver);
+ var result = resolver.resolvePrefixedIdentifier(
+ node: left,
+ hasRead: hasRead,
+ hasWrite: true,
+ );
+
+ var readElement = result.readElement;
+ var writeElement = result.writeElement;
+
+ if (hasRead) {
+ _resolver.setReadElement(left, readElement);
+ }
+ _resolver.setWriteElement(left, writeElement);
+
+ _setBackwardCompatibility(node, propertyName);
+
+ var right = node.rightHandSide;
+ _resolve3(node, left, operator, right);
+ }
+
void _resolve_PropertyAccess(
AssignmentExpressionImpl node,
PropertyAccess left,
@@ -323,9 +359,6 @@
_resolver.startNullAwarePropertyAccess(left);
- ExecutableElement readElement;
- ExecutableElement writeElement;
-
var resolver = PropertyElementResolver(_resolver);
var result = resolver.resolvePropertyAccess(
node: left,
@@ -333,14 +366,12 @@
hasWrite: true,
);
- readElement = result.readElement;
- writeElement = result.writeElement;
+ var readElement = result.readElement;
+ var writeElement = result.writeElement;
if (hasRead) {
_resolver.setReadElement(left, readElement);
}
-
- // var writeElement = result.setter ?? result.getter;
_resolver.setWriteElement(left, writeElement);
_setBackwardCompatibility(node, propertyName);
@@ -407,7 +438,9 @@
}
var parent = left.parent;
- if (parent is PropertyAccess && parent.propertyName == left) {
+ if (parent is PrefixedIdentifier && parent.identifier == left) {
+ _recordStaticType(parent, node.writeType);
+ } else if (parent is PropertyAccess && parent.propertyName == left) {
_recordStaticType(parent, node.writeType);
}
}
diff --git a/pkg/analyzer/lib/src/dart/resolver/for_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/for_resolver.dart
index 454eeef..8946ab4 100644
--- a/pkg/analyzer/lib/src/dart/resolver/for_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/for_resolver.dart
@@ -100,7 +100,7 @@
if (identifier != null) {
identifierElement = identifier.staticElement;
if (identifierElement is VariableElement) {
- valueType = identifierElement.type;
+ valueType = _resolver.localVariableTypeProvider.getType(identifier);
} else if (identifierElement is PropertyAccessorElement) {
var parameters = identifierElement.parameters;
if (parameters.isNotEmpty) {
diff --git a/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
index db73a0c..4d3fffb 100644
--- a/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
@@ -32,11 +32,37 @@
@required bool hasRead,
@required bool hasWrite,
}) {
+ var prefix = node.prefix;
+ var identifier = node.identifier;
+
+ var prefixElement = prefix.staticElement;
+ if (prefixElement is PrefixElement) {
+ var lookupResult = prefixElement.scope.lookup2(identifier.name);
+
+ var readElement = _resolver.toLegacyElement(lookupResult.getter);
+ var writeElement = _resolver.toLegacyElement(lookupResult.setter);
+
+ if (hasRead && readElement == null || hasWrite && writeElement == null) {
+ _errorReporter.reportErrorForNode(
+ CompileTimeErrorCode.UNDEFINED_PREFIXED_NAME,
+ identifier,
+ [identifier.name, prefixElement.name],
+ );
+ }
+
+ return PropertyElementResolverResult(
+ readElementRequested: readElement,
+ readElementRecovery: null,
+ writeElementRequested: writeElement,
+ writeElementRecovery: null,
+ );
+ }
+
return _resolve(
- target: node.prefix,
+ target: prefix,
isCascaded: false,
isNullAware: false,
- propertyName: node.identifier,
+ propertyName: identifier,
hasRead: hasRead,
hasWrite: hasWrite,
);
@@ -469,10 +495,10 @@
}
class PropertyElementResolverResult {
- final ExecutableElement readElementRequested;
- final ExecutableElement readElementRecovery;
- final ExecutableElement writeElementRequested;
- final ExecutableElement writeElementRecovery;
+ final Element readElementRequested;
+ final Element readElementRecovery;
+ final Element writeElementRequested;
+ final Element writeElementRecovery;
PropertyElementResolverResult({
this.readElementRequested,
@@ -481,11 +507,11 @@
this.writeElementRecovery,
});
- ExecutableElement get readElement {
+ Element get readElement {
return readElementRequested ?? readElementRecovery;
}
- ExecutableElement get writeElement {
+ Element get writeElement {
return writeElementRequested ?? writeElementRecovery;
}
}
diff --git a/pkg/analyzer/test/src/diagnostics/ambiguous_import_test.dart b/pkg/analyzer/test/src/diagnostics/ambiguous_import_test.dart
index 61d593a..01e301c 100644
--- a/pkg/analyzer/test/src/diagnostics/ambiguous_import_test.dart
+++ b/pkg/analyzer/test/src/diagnostics/ambiguous_import_test.dart
@@ -219,34 +219,99 @@
]);
}
- test_varRead() async {
- newFile("$testPackageLibPath/lib1.dart", content: '''
-library lib1;
-var v;''');
- newFile("$testPackageLibPath/lib2.dart", content: '''
-library lib2;
-var v;''');
+ test_variable_read() async {
+ newFile('$testPackageLibPath/a.dart', content: '''
+var x;
+''');
+
+ newFile('$testPackageLibPath/b.dart', content: '''
+var x;
+''');
+
await assertErrorsInCode('''
-import 'lib1.dart';
-import 'lib2.dart';
-f() { g(v); }
-g(p) {}''', [
+import 'a.dart';
+import 'b.dart';
+
+void f() {
+ x;
+}
+''', [
error(CompileTimeErrorCode.AMBIGUOUS_IMPORT, 48, 1),
]);
}
- test_varWrite() async {
- newFile("$testPackageLibPath/lib1.dart", content: '''
-library lib1;
-var v;''');
- newFile("$testPackageLibPath/lib2.dart", content: '''
-library lib2;
-var v;''');
+ test_variable_read_prefixed() async {
+ newFile('$testPackageLibPath/a.dart', content: '''
+var x;
+''');
+
+ newFile('$testPackageLibPath/b.dart', content: '''
+var x;
+''');
+
await assertErrorsInCode('''
-import 'lib1.dart';
-import 'lib2.dart';
-f() { v = 0; }''', [
- error(CompileTimeErrorCode.AMBIGUOUS_IMPORT, 46, 1),
+import 'a.dart' as p;
+import 'b.dart' as p;
+
+void f() {
+ p.x;
+}
+''', [
+ error(CompileTimeErrorCode.AMBIGUOUS_IMPORT, 60, 1),
+ ]);
+ }
+
+ test_variable_write() async {
+ newFile('$testPackageLibPath/a.dart', content: '''
+var x;
+''');
+
+ newFile('$testPackageLibPath/b.dart', content: '''
+var x;
+''');
+
+ await assertErrorsInCode('''
+import 'a.dart';
+import 'b.dart';
+
+void f() {
+ x = 0;
+ x += 1;
+ ++x;
+ x++;
+}
+''', [
+ error(CompileTimeErrorCode.AMBIGUOUS_IMPORT, 48, 1),
+ error(CompileTimeErrorCode.AMBIGUOUS_IMPORT, 57, 1),
+ error(CompileTimeErrorCode.AMBIGUOUS_IMPORT, 69, 1),
+ error(CompileTimeErrorCode.AMBIGUOUS_IMPORT, 74, 1),
+ ]);
+ }
+
+ test_variable_write_prefixed() async {
+ newFile('$testPackageLibPath/a.dart', content: '''
+var x;
+''');
+
+ newFile('$testPackageLibPath/b.dart', content: '''
+var x;
+''');
+
+ await assertErrorsInCode('''
+import 'a.dart' as p;
+import 'b.dart' as p;
+
+void f() {
+ p.x = 0;
+ p.x += 1;
+ ++p.x;
+ p.x++;
+}
+''', [
+ error(CompileTimeErrorCode.AMBIGUOUS_IMPORT, 60, 1),
+ error(CompileTimeErrorCode.AMBIGUOUS_IMPORT, 71, 1),
+ error(CompileTimeErrorCode.AMBIGUOUS_IMPORT, 85, 1),
+ error(CompileTimeErrorCode.AMBIGUOUS_IMPORT, 92, 1),
]);
}
}
diff --git a/pkg/analyzer/test/src/diagnostics/receiver_of_type_never_test.dart b/pkg/analyzer/test/src/diagnostics/receiver_of_type_never_test.dart
index c5129ed..1ea665c 100644
--- a/pkg/analyzer/test/src/diagnostics/receiver_of_type_never_test.dart
+++ b/pkg/analyzer/test/src/diagnostics/receiver_of_type_never_test.dart
@@ -432,7 +432,7 @@
findNode.simple('foo'),
readElement: null,
writeElement: null,
- type: 'Never',
+ type: 'dynamic',
);
assertAssignment(
diff --git a/pkg/front_end/lib/src/base/processed_options.dart b/pkg/front_end/lib/src/base/processed_options.dart
index 5e51442..373eed1 100644
--- a/pkg/front_end/lib/src/base/processed_options.dart
+++ b/pkg/front_end/lib/src/base/processed_options.dart
@@ -13,7 +13,12 @@
import 'package:kernel/binary/ast_from_binary.dart' show BinaryBuilder;
import 'package:kernel/kernel.dart'
- show CanonicalName, Component, Location, Version;
+ show
+ CanonicalName,
+ Component,
+ Location,
+ NonNullableByDefaultCompiledMode,
+ Version;
import 'package:kernel/target/targets.dart'
show NoneTarget, Target, TargetFlags;
@@ -357,6 +362,18 @@
_raw.experimentReleasedVersionForTesting);
}
+ Component _validateNullSafetyMode(Component component) {
+ if (nnbdMode == NnbdMode.Strong &&
+ !(component.mode == NonNullableByDefaultCompiledMode.Strong ||
+ component.mode == NonNullableByDefaultCompiledMode.Agnostic)) {
+ throw new FormatException(
+ 'Provided .dill file for the following libraries does not '
+ 'support sound null safety:\n'
+ '${component.libraries.join('\n')}');
+ }
+ return component;
+ }
+
/// Get an outline component that summarizes the SDK, if any.
// TODO(sigmund): move, this doesn't feel like an "option".
Future<Component> loadSdkSummary(CanonicalName nameRoot) async {
@@ -365,6 +382,7 @@
List<int> bytes = await loadSdkSummaryBytes();
if (bytes != null && bytes.isNotEmpty) {
_sdkSummaryComponent = loadComponent(bytes, nameRoot);
+ _validateNullSafetyMode(_sdkSummaryComponent);
}
}
return _sdkSummaryComponent;
@@ -374,6 +392,7 @@
if (_sdkSummaryComponent != null) {
throw new StateError("sdkSummary already loaded.");
}
+ _validateNullSafetyMode(platform);
_sdkSummaryComponent = platform;
}
@@ -389,7 +408,8 @@
uris.map((uri) => _readAsBytes(fileSystem.entityForUri(uri))));
_additionalDillComponents = allBytes
.where((bytes) => bytes != null)
- .map((bytes) => loadComponent(bytes, nameRoot))
+ .map((bytes) =>
+ _validateNullSafetyMode(loadComponent(bytes, nameRoot)))
.toList();
}
return _additionalDillComponents;
@@ -399,6 +419,7 @@
if (_additionalDillComponents != null) {
throw new StateError("inputAdditionalDillsComponents already loaded.");
}
+ components.forEach(_validateNullSafetyMode);
_additionalDillComponents = components;
}
@@ -414,7 +435,7 @@
disableLazyReading: false,
alwaysCreateNewNamedNodes: alwaysCreateNewNamedNodes)
.readComponent(component);
- return component;
+ return _validateNullSafetyMode(component);
}
/// Get the [UriTranslator] which resolves "package:" and "dart:" URIs.
diff --git a/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart b/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart
index a956473..f823190 100644
--- a/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart
+++ b/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart
@@ -6061,8 +6061,14 @@
LocalForInVariable(this.variableSet);
- DartType computeElementType(TypeInferrerImpl inferrer) =>
- variableSet.variable.type;
+ DartType computeElementType(TypeInferrerImpl inferrer) {
+ VariableDeclaration variable = variableSet.variable;
+ DartType promotedType;
+ if (inferrer.isNonNullableByDefault) {
+ promotedType = inferrer.flowAnalysis.promotedType(variable);
+ }
+ return promotedType ?? variable.type;
+ }
Expression inferAssignment(TypeInferrerImpl inferrer, DartType rhsType) {
Expression rhs = inferrer.ensureAssignable(
diff --git a/pkg/front_end/lib/src/kernel_generator_impl.dart b/pkg/front_end/lib/src/kernel_generator_impl.dart
index 5cef59a..76a3697 100644
--- a/pkg/front_end/lib/src/kernel_generator_impl.dart
+++ b/pkg/front_end/lib/src/kernel_generator_impl.dart
@@ -83,28 +83,11 @@
// sdkSummary between multiple invocations.
CanonicalName nameRoot = sdkSummary?.root ?? new CanonicalName.root();
if (sdkSummary != null) {
- if (options.nnbdMode == NnbdMode.Strong &&
- !(sdkSummary.mode == NonNullableByDefaultCompiledMode.Strong ||
- sdkSummary.mode == NonNullableByDefaultCompiledMode.Agnostic)) {
- throw new FormatException(
- 'Provided SDK .dill does not support sound null safety.');
- }
dillTarget.loader.appendLibraries(sdkSummary);
}
for (Component additionalDill
in await options.loadAdditionalDills(nameRoot)) {
- if (options.nnbdMode == NnbdMode.Strong &&
- !(additionalDill.mode == NonNullableByDefaultCompiledMode.Strong ||
- // In some VM tests the SDK dill appears as an additionalDill so
- // allow agnostic here as well.
- additionalDill.mode ==
- NonNullableByDefaultCompiledMode.Agnostic)) {
- throw new FormatException(
- 'Provided .dill file for the following libraries does not support '
- 'sound null safety:\n'
- '${additionalDill.libraries.join('\n')}');
- }
loadedComponents.add(additionalDill);
dillTarget.loader.appendLibraries(additionalDill);
}
diff --git a/pkg/front_end/lib/widget_cache.dart b/pkg/front_end/lib/widget_cache.dart
index 9ccfa38..158125b 100644
--- a/pkg/front_end/lib/widget_cache.dart
+++ b/pkg/front_end/lib/widget_cache.dart
@@ -61,7 +61,7 @@
/// `State` subtype.
///
/// Returns the class name if located, otherwise `null`.
- String checkWidgetCache(
+ String checkSingleWidgetTypeModified(
Component lastGoodComponent,
Component partialComponent,
ClassHierarchy classHierarchy,
diff --git a/pkg/frontend_server/lib/frontend_server.dart b/pkg/frontend_server/lib/frontend_server.dart
index af48251..c95b0a2 100644
--- a/pkg/frontend_server/lib/frontend_server.dart
+++ b/pkg/frontend_server/lib/frontend_server.dart
@@ -20,6 +20,7 @@
import 'package:front_end/src/api_prototype/compiler_options.dart'
show CompilerOptions, parseExperimentalFlags;
import 'package:front_end/src/api_unstable/vm.dart';
+import 'package:front_end/widget_cache.dart';
import 'package:kernel/ast.dart' show Library, Procedure, LibraryDependency;
import 'package:kernel/binary/ast_to_binary.dart';
import 'package:kernel/kernel.dart'
@@ -176,7 +177,10 @@
defaultsTo: false)
..addOption('dartdevc-module-format',
help: 'The module format to use on for the dartdevc compiler',
- defaultsTo: 'amd');
+ defaultsTo: 'amd')
+ ..addFlag('flutter-widget-cache',
+ help: 'Enable the widget cache to track changes to Widget subtypes',
+ defaultsTo: false);
String usage = '''
Usage: server [options] [input.dart]
@@ -345,6 +349,8 @@
IncrementalCompiler _generator;
JavaScriptBundler _bundler;
+ WidgetCache _widgetCache;
+
String _kernelBinaryFilename;
String _kernelBinaryFilenameIncremental;
String _kernelBinaryFilenameFull;
@@ -530,6 +536,9 @@
component.uriToSource.keys);
incrementalSerializer = _generator.incrementalSerializer;
+ if (options['flutter-widget-cache']) {
+ _widgetCache = WidgetCache(component);
+ }
} else {
if (options['link-platform']) {
// TODO(aam): Remove linkedDependencies once platform is directly embedded
@@ -904,6 +913,7 @@
await writeDillFile(results, _kernelBinaryFilename,
incrementalSerializer: _generator.incrementalSerializer);
}
+ _updateWidgetCache(deltaProgram);
_outputStream.writeln(boundaryKey);
await _outputDependenciesDelta(results.compiledSources);
@@ -1095,6 +1105,7 @@
@override
void acceptLastDelta() {
_generator.accept();
+ _widgetCache?.reset();
}
@override
@@ -1108,11 +1119,13 @@
@override
void invalidate(Uri uri) {
_generator.invalidate(uri);
+ _widgetCache?.invalidate(uri);
}
@override
void resetIncrementalCompiler() {
_generator.resetDeltaState();
+ _widgetCache?.reset();
_kernelBinaryFilename = _kernelBinaryFilenameFull;
}
@@ -1122,6 +1135,29 @@
incrementalSerialization: incrementalSerialization);
}
+ /// If the flutter widget cache is enabled, check if a single class was modified.
+ ///
+ /// The resulting class name is written as a String to
+ /// `_kernelBinaryFilename`.widget_cache, or else the file is deleted
+ /// if it exists.
+ void _updateWidgetCache(Component partialComponent) {
+ if (_widgetCache == null) {
+ return;
+ }
+ final String singleModifiedClassName =
+ _widgetCache.checkSingleWidgetTypeModified(
+ _generator.lastKnownGoodComponent,
+ partialComponent,
+ _generator.getClassHierarchy(),
+ );
+ final File outputFile = File('$_kernelBinaryFilename.widget_cache');
+ if (singleModifiedClassName != null) {
+ outputFile.writeAsStringSync(singleModifiedClassName);
+ } else if (outputFile.existsSync()) {
+ outputFile.deleteSync();
+ }
+ }
+
Uri _ensureFolderPath(String path) {
String uriPath = Uri.file(path).toString();
if (!uriPath.endsWith('/')) {
diff --git a/pkg/frontend_server/test/frontend_server_test.dart b/pkg/frontend_server/test/frontend_server_test.dart
index d4b17b7..90b4d5f 100644
--- a/pkg/frontend_server/test/frontend_server_test.dart
+++ b/pkg/frontend_server/test/frontend_server_test.dart
@@ -68,6 +68,24 @@
expect(capturedArgs.single['sdk-root'], equals('sdkroot'));
expect(capturedArgs.single['link-platform'], equals(true));
});
+
+ test('compile from command line with widget cache', () async {
+ final List<String> args = <String>[
+ 'server.dart',
+ '--sdk-root',
+ 'sdkroot',
+ '--flutter-widget-cache',
+ ];
+ await starter(args, compiler: compiler);
+ final List<dynamic> capturedArgs = verify(compiler.compile(
+ argThat(equals('server.dart')),
+ captureAny,
+ generator: anyNamed('generator'),
+ )).captured;
+ expect(capturedArgs.single['sdk-root'], equals('sdkroot'));
+ expect(capturedArgs.single['link-platform'], equals(true));
+ expect(capturedArgs.single['flutter-widget-cache'], equals(true));
+ });
});
group('interactive compile with mocked compiler', () {
@@ -230,6 +248,34 @@
inputStreamController.close();
});
+ test('recompile one file with widget cache does not fail', () async {
+ // The component will not contain the flutter framework sources so
+ // this should no-op.
+ final StreamController<List<int>> inputStreamController =
+ StreamController<List<int>>();
+ final ReceivePort recompileCalled = ReceivePort();
+
+ when(compiler.recompileDelta(entryPoint: null))
+ .thenAnswer((Invocation invocation) async {
+ recompileCalled.sendPort.send(true);
+ });
+ Future<int> result = starter(
+ <String>[...args, '--flutter-widget-cache'],
+ compiler: compiler,
+ input: inputStreamController.stream,
+ );
+ inputStreamController.add('recompile abc\nfile1.dart\nabc\n'.codeUnits);
+ await recompileCalled.first;
+
+ verifyInOrder(<void>[
+ compiler.invalidate(Uri.base.resolve('file1.dart')),
+ await compiler.recompileDelta(entryPoint: null),
+ ]);
+ inputStreamController.add('quit\n'.codeUnits);
+ expect(await result, 0);
+ inputStreamController.close();
+ });
+
test('recompile few files with new entrypoint', () async {
final StreamController<List<int>> inputStreamController =
StreamController<List<int>>();
@@ -923,6 +969,218 @@
inputStreamController.close();
});
+ test(
+ 'recompile request with flutter widget cache outputs change in class name',
+ () async {
+ var frameworkDirectory = Directory('${tempDir.path}/flutter');
+ var flutterFramework =
+ File('${frameworkDirectory.path}/lib/src/widgets/framework.dart')
+ ..createSync(recursive: true);
+ flutterFramework.writeAsStringSync('''
+abstract class Widget {}
+class StatelessWidget extends Widget {}
+class StatefulWidget extends Widget {}
+class State<T extends StatefulWidget> {}
+''');
+
+ var file = File('${tempDir.path}/foo.dart')..createSync();
+ file.writeAsStringSync("""
+import "package:flutter/src/widgets/framework.dart";
+
+void main() {}
+
+class FooWidget extends StatelessWidget {}
+
+class FizzWidget extends StatefulWidget {}
+
+class BarState extends State<FizzWidget> {}
+""");
+ var config = File('${tempDir.path}/package_config.json')..createSync();
+ config.writeAsStringSync('''
+{
+ "configVersion": 2,
+ "packages": [
+ {
+ "name": "flutter",
+ "rootUri": "${frameworkDirectory.uri}",
+ "packageUri": "lib/",
+ "languageVersion": "2.2"
+ }
+ ]
+}
+''');
+
+ var dillFile = File('${tempDir.path}/app.dill');
+ expect(dillFile.existsSync(), equals(false));
+ final List<String> args = <String>[
+ '--sdk-root=${sdkRoot.toFilePath()}',
+ '--incremental',
+ '--platform=${platformKernel.path}',
+ '--output-dill=${dillFile.path}',
+ '--flutter-widget-cache',
+ '--packages=${config.path}',
+ ];
+
+ final StreamController<List<int>> inputStreamController =
+ StreamController<List<int>>();
+ final StreamController<List<int>> stdoutStreamController =
+ StreamController<List<int>>();
+ final IOSink ioSink = IOSink(stdoutStreamController.sink);
+ StreamController<Result> receivedResults = StreamController<Result>();
+
+ final outputParser = OutputParser(receivedResults);
+ stdoutStreamController.stream
+ .transform(utf8.decoder)
+ .transform(const LineSplitter())
+ .listen(outputParser.listener);
+
+ Future<int> result =
+ starter(args, input: inputStreamController.stream, output: ioSink);
+ inputStreamController.add('compile ${file.path}\n'.codeUnits);
+ int count = 0;
+ receivedResults.stream.listen((Result compiledResult) {
+ if (count == 0) {
+ // First request is to 'compile', which results in full kernel file.
+ expect(dillFile.existsSync(), equals(true));
+ compiledResult.expectNoErrors(filename: dillFile.path);
+ count += 1;
+ inputStreamController.add('accept\n'.codeUnits);
+ file.writeAsStringSync("""
+import "package:flutter/src/widgets/framework.dart";
+
+void main() {}
+
+class FooWidget extends StatelessWidget {
+ // Added.
+}
+
+class FizzWidget extends StatefulWidget {}
+
+class BarState extends State<FizzWidget> {}
+""");
+ inputStreamController.add('recompile ${file.path} abc\n'
+ '${file.path}\n'
+ 'abc\n'
+ .codeUnits);
+ } else if (count == 1) {
+ expect(count, 1);
+ // Second request is to 'recompile', which results in incremental
+ // kernel file and invalidation of StatelessWidget.
+ var dillIncFile = File('${dillFile.path}.incremental.dill');
+ var widgetCacheFile =
+ File('${dillFile.path}.incremental.dill.widget_cache');
+ compiledResult.expectNoErrors(filename: dillIncFile.path);
+ expect(dillIncFile.existsSync(), equals(true));
+ expect(widgetCacheFile.existsSync(), equals(true));
+ expect(widgetCacheFile.readAsStringSync(), 'FooWidget');
+ count += 1;
+ inputStreamController.add('accept\n'.codeUnits);
+
+ file.writeAsStringSync("""
+import "package:flutter/src/widgets/framework.dart";
+
+void main() {}
+
+class FooWidget extends StatelessWidget {
+ // Added.
+}
+
+class FizzWidget extends StatefulWidget {
+ // Added.
+}
+
+class BarState extends State<FizzWidget> {}
+""");
+ inputStreamController.add('recompile ${file.path} abc\n'
+ '${file.path}\n'
+ 'abc\n'
+ .codeUnits);
+ } else if (count == 2) {
+ // Second request is to 'recompile', which results in incremental
+ // kernel file and invalidation of StatelessWidget.
+ var dillIncFile = File('${dillFile.path}.incremental.dill');
+ var widgetCacheFile =
+ File('${dillFile.path}.incremental.dill.widget_cache');
+ compiledResult.expectNoErrors(filename: dillIncFile.path);
+ expect(dillIncFile.existsSync(), equals(true));
+ expect(widgetCacheFile.existsSync(), equals(true));
+ expect(widgetCacheFile.readAsStringSync(), 'FizzWidget');
+ count += 1;
+ inputStreamController.add('accept\n'.codeUnits);
+
+ file.writeAsStringSync("""
+import "package:flutter/src/widgets/framework.dart";
+
+void main() {}
+
+class FooWidget extends StatelessWidget {
+ // Added.
+}
+
+class FizzWidget extends StatefulWidget {
+ // Added.
+}
+
+class BarState extends State<FizzWidget> {
+ // Added.
+}
+""");
+ inputStreamController.add('recompile ${file.path} abc\n'
+ '${file.path}\n'
+ 'abc\n'
+ .codeUnits);
+ } else if (count == 3) {
+ // Third request is to 'recompile', which results in incremental
+ // kernel file and invalidation of State class.
+ var dillIncFile = File('${dillFile.path}.incremental.dill');
+ var widgetCacheFile =
+ File('${dillFile.path}.incremental.dill.widget_cache');
+ compiledResult.expectNoErrors(filename: dillIncFile.path);
+ expect(dillIncFile.existsSync(), equals(true));
+ expect(widgetCacheFile.existsSync(), equals(true));
+ expect(widgetCacheFile.readAsStringSync(), 'FizzWidget');
+ count += 1;
+ inputStreamController.add('accept\n'.codeUnits);
+
+ file.writeAsStringSync("""
+import "package:flutter/src/widgets/framework.dart";
+
+void main() {}
+
+// Added
+
+class FooWidget extends StatelessWidget {
+ // Added.
+}
+
+class FizzWidget extends StatefulWidget {
+ // Added.
+}
+
+class BarState extends State<FizzWidget> {
+ // Added.
+}
+""");
+ inputStreamController.add('recompile ${file.path} abc\n'
+ '${file.path}\n'
+ 'abc\n'
+ .codeUnits);
+ } else if (count == 4) {
+ // Fourth request is to 'recompile', which results in incremental
+ // kernel file and no widget cache
+ var dillIncFile = File('${dillFile.path}.incremental.dill');
+ var widgetCacheFile =
+ File('${dillFile.path}.incremental.dill.widget_cache');
+ compiledResult.expectNoErrors(filename: dillIncFile.path);
+ expect(dillIncFile.existsSync(), equals(true));
+ expect(widgetCacheFile.existsSync(), equals(false));
+ inputStreamController.add('quit\n'.codeUnits);
+ }
+ });
+ expect(await result, 0);
+ inputStreamController.close();
+ });
+
test('unsafe-package-serialization', () async {
// Package A.
var file = File('${tempDir.path}/pkgA/a.dart')
diff --git a/tools/VERSION b/tools/VERSION
index 6597a0f..d2bf2b9 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
MAJOR 2
MINOR 10
PATCH 0
-PRERELEASE 135
+PRERELEASE 136
PRERELEASE_PATCH 0
\ No newline at end of file