Version 2.10.0-110.0.dev
Merge commit '4d72ed38b80a7d6d377b92747868dbd392a058c5' into 'dev'
diff --git a/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart b/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
index 30cd1fc..08fc72b 100644
--- a/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
+++ b/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
@@ -96,6 +96,7 @@
LintNames.prefer_equal_for_default_values:
ReplaceColonWithEquals.newInstance,
LintNames.prefer_final_fields: MakeFinal.newInstance,
+ LintNames.prefer_final_locals: MakeFinal.newInstance,
LintNames.prefer_for_elements_to_map_fromIterable:
ConvertMapFromIterableToForLiteral.newInstance,
LintNames.prefer_generic_function_type_aliases:
diff --git a/pkg/analysis_server/lib/src/services/correction/fix/data_driven/transform_set_parser.dart b/pkg/analysis_server/lib/src/services/correction/fix/data_driven/transform_set_parser.dart
index 0690f27..b81936e 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix/data_driven/transform_set_parser.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix/data_driven/transform_set_parser.dart
@@ -5,6 +5,7 @@
import 'package:analysis_server/src/services/correction/fix/data_driven/add_type_parameter.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/change.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/element_descriptor.dart';
+import 'package:analysis_server/src/services/correction/fix/data_driven/modify_parameters.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/parameter_reference.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/rename.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/transform.dart';
@@ -17,11 +18,13 @@
/// A parser used to read a transform set from a file.
class TransformSetParser {
+ static const String _argumentValueKey = 'argumentValue';
static const String _changesKey = 'changes';
static const String _classKey = 'class';
+ static const String _constantKey = 'constant';
static const String _constructorKey = 'constructor';
+ static const String _defaultValueKey = 'defaultValue';
static const String _elementKey = 'element';
- static const String _enumConstantKey = 'constant';
static const String _enumKey = 'enum';
static const String _extensionKey = 'extension';
static const String _fieldKey = 'field';
@@ -38,6 +41,7 @@
static const String _nameKey = 'name';
static const String _newNameKey = 'newName';
static const String _setterKey = 'setter';
+ static const String _styleKey = 'style';
static const String _titleKey = 'title';
static const String _transformsKey = 'transforms';
static const String _typedefKey = 'typedef';
@@ -49,15 +53,17 @@
/// the possible containers of that element.
static const Map<String, List<String>> _containerKeyMap = {
_constructorKey: [_inClassKey],
- _enumConstantKey: [_inEnumKey],
+ _constantKey: [_inEnumKey],
_fieldKey: [_inClassKey, _inExtensionKey, _inMixinKey],
_getterKey: [_inClassKey, _inExtensionKey, _inMixinKey],
_methodKey: [_inClassKey, _inExtensionKey, _inMixinKey],
_setterKey: [_inClassKey, _inExtensionKey, _inMixinKey],
};
+ static const String _addParameterKind = 'addParameter';
static const String _addTypeParameterKind = 'addTypeParameter';
static const String _argumentKind = 'argument';
+ static const String _removeParameterKind = 'removeParameter';
static const String _renameKind = 'rename';
static const int currentVersion = 1;
@@ -65,6 +71,10 @@
/// The error reporter to which diagnostics will be reported.
final ErrorReporter errorReporter;
+ /// The parameter modifications associated with the current transform, or
+ /// `null` if the current transform does not yet have any such modifications.
+ List<ParameterModification> _parameterModifications;
+
/// Initialize a newly created parser to report diagnostics to the
/// [errorReporter].
TransformSetParser(this.errorReporter);
@@ -160,6 +170,52 @@
return foundKeys[0];
}
+ /// Translate the [node] into a add-parameter modification.
+ void _translateAddParameterChange(YamlMap node) {
+ _singleKey(node, [_indexKey, _nameKey]);
+ _reportUnsupportedKeys(node, const {_indexKey, _kindKey, _nameKey});
+ var index = _translateInteger(node.valueAt(_indexKey), _indexKey);
+ if (index == null) {
+ return;
+ }
+ var name = _translateString(node.valueAt(_nameKey), _nameKey);
+ if (name == null) {
+ return;
+ }
+ var style = _translateString(node.valueAt(_styleKey), _styleKey);
+ if (style == null) {
+ return;
+ }
+ var isRequired = style.startsWith('required_');
+ var isPositional = style.endsWith('_positional');
+ // TODO(brianwilkerson) I originally thought we'd need a default value, but
+ // it seems like we ought to be able to get it from the overridden method,
+ // so investigate removing this field.
+ var defaultValue = _translateValueExtractor(
+ node.valueAt(_defaultValueKey), _defaultValueKey);
+ if (isRequired && defaultValue != null) {
+ // TODO(brianwilkerson) Report that required parameters can't have a
+ // default value.
+ return;
+ }
+ var argumentValue = _translateValueExtractor(
+ node.valueAt(_argumentValueKey), _argumentValueKey);
+ // TODO(brianwilkerson) We really ought to require an argument value for
+ // optional positional parameters too for the case where the added
+ // parameter is being added before the end of the list and call sites might
+ // already be providing a value for subsequent parameters. Unfortunately we
+ // can't know at this point whether there are subsequent parameters in
+ // order to require it only when it's potentially necessary.
+ if (isRequired && argumentValue == null) {
+ // TODO(brianwilkerson) Report that required parameters must have an
+ // argument value.
+ return;
+ }
+ _parameterModifications ??= [];
+ _parameterModifications.add(AddParameter(
+ index, name, isRequired, isPositional, defaultValue, argumentValue));
+ }
+
/// Translate the [node] into an add-type-parameter change. Return the
/// resulting change, or `null` if the [node] does not represent a valid
/// add-type-parameter change.
@@ -219,6 +275,12 @@
return _translateAddTypeParameterChange(node);
} else if (kind == _renameKind) {
return _translateRenameChange(node);
+ } else if (kind == _addParameterKind) {
+ _translateAddParameterChange(node);
+ return null;
+ } else if (kind == _removeParameterKind) {
+ _translateRemoveParameterChange(node);
+ return null;
}
// TODO(brianwilkerson) Report the invalid change kind.
return null;
@@ -248,7 +310,7 @@
}
var elementKey = _singleKey(node, [
_classKey,
- _enumConstantKey,
+ _constantKey,
_constructorKey,
_enumKey,
_extensionKey,
@@ -270,7 +332,7 @@
var containerName =
_translateString(node.valueAt(containerKey), containerKey);
if (containerName == null) {
- if ([_constructorKey, _enumConstantKey, _methodKey, _fieldKey]
+ if ([_constructorKey, _constantKey, _methodKey, _fieldKey]
.contains(elementKey)) {
// TODO(brianwilkerson) Report that no container was found.
return null;
@@ -338,6 +400,25 @@
}
}
+ /// Translate the [node] into a remove-parameter modification.
+ void _translateRemoveParameterChange(YamlMap node) {
+ _singleKey(node, [_indexKey, _nameKey]);
+ _reportUnsupportedKeys(node, const {_indexKey, _kindKey, _nameKey});
+ ParameterReference reference;
+ var index = _translateInteger(node.valueAt(_indexKey), _indexKey);
+ if (index != null) {
+ reference = PositionalParameterReference(index);
+ } else {
+ var name = _translateString(node.valueAt(_nameKey), _nameKey);
+ if (name == null) {
+ return;
+ }
+ reference = NamedParameterReference(name);
+ }
+ _parameterModifications ??= [];
+ _parameterModifications.add(RemoveParameter(reference));
+ }
+
/// Translate the [node] into a rename change. Return the resulting change, or
/// `null` if the [node] does not represent a valid rename change.
Rename _translateRenameChange(YamlMap node) {
@@ -380,12 +461,16 @@
_reportUnsupportedKeys(node, const {_changesKey, _elementKey, _titleKey});
var title = _translateString(node.valueAt(_titleKey), _titleKey);
var element = _translateElement(node.valueAt(_elementKey), _elementKey);
- var changes = _translateList<Change>(
+ var changes = _translateList(
node.valueAt(_changesKey), _changesKey, _translateChange);
if (changes == null) {
// The error has already been reported.
return null;
}
+ if (_parameterModifications != null) {
+ changes.add(ModifyParameters(modifications: _parameterModifications));
+ _parameterModifications = null;
+ }
return Transform(title: title, element: element, changes: changes);
} else if (node == null) {
// TODO(brianwilkerson) Report the missing YAML.
diff --git a/pkg/analysis_server/lib/src/services/correction/fix/data_driven/value_extractor.dart b/pkg/analysis_server/lib/src/services/correction/fix/data_driven/value_extractor.dart
index ad5243b..cc9b79f 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix/data_driven/value_extractor.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix/data_driven/value_extractor.dart
@@ -19,14 +19,27 @@
@override
String from(AstNode node, CorrectionUtils utils) {
- if (node is InvocationExpression) {
- var expression = parameter.argumentFrom(node.argumentList);
+ var argumentList = _getArgumentList(node);
+ if (argumentList != null) {
+ var expression = parameter.argumentFrom(argumentList);
if (expression != null) {
return utils.getNodeText(expression);
}
}
return null;
}
+
+ /// Return the argument list associated with the given [node].
+ ArgumentList _getArgumentList(AstNode node) {
+ if (node is ArgumentList) {
+ return node;
+ } else if (node is InvocationExpression) {
+ return node.argumentList;
+ } else if (node is InstanceCreationExpression) {
+ return node.argumentList;
+ }
+ return null;
+ }
}
/// A value extractor that returns a pre-computed piece of code.
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 4740406..a979965 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -504,9 +504,11 @@
],
CompileTimeErrorCode.EXTRA_POSITIONAL_ARGUMENTS: [
AddMissingParameter.newInstance,
+ DataDriven.newInstance,
],
CompileTimeErrorCode.EXTRA_POSITIONAL_ARGUMENTS_COULD_BE_NAMED: [
AddMissingParameter.newInstance,
+ DataDriven.newInstance,
],
CompileTimeErrorCode.IMPLEMENTS_NON_CLASS: [
DataDriven.newInstance,
diff --git a/pkg/analysis_server/test/src/services/correction/fix/bulk/make_final_test.dart b/pkg/analysis_server/test/src/services/correction/fix/bulk/make_final_test.dart
index d31d543..cae164a 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/bulk/make_final_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/bulk/make_final_test.dart
@@ -10,6 +10,7 @@
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(PreferFinalFieldsTest);
+ defineReflectiveTests(PreferFinalLocalsTest);
});
}
@@ -37,3 +38,24 @@
''');
}
}
+
+@reflectiveTest
+class PreferFinalLocalsTest extends BulkFixProcessorTest {
+ @override
+ String get lintCode => LintNames.prefer_final_locals;
+
+ Future<void> test_singleFile() async {
+ await resolveTestUnit('''
+f() {
+ var x = 0;
+ var y = x;
+}
+''');
+ await assertHasFix('''
+f() {
+ final x = 0;
+ final y = x;
+}
+''');
+ }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/data_driven/end_to_end_test.dart b/pkg/analysis_server/test/src/services/correction/fix/data_driven/end_to_end_test.dart
index 1f0eb58..b5ae21c 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/data_driven/end_to_end_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/data_driven/end_to_end_test.dart
@@ -14,6 +14,45 @@
@reflectiveTest
class EndToEndTest extends DataDrivenFixProcessorTest {
+ Future<void> test_addParameter() async {
+ setPackageContent('''
+class C {
+ void m(int x, int y) {}
+}
+''');
+ addPackageDataFile('''
+version: 1
+transforms:
+- title: 'Add parameter'
+ element:
+ uris: ['$importUri']
+ method: 'm'
+ inClass: 'C'
+ changes:
+ - kind: 'addParameter'
+ index: 1
+ name: 'y'
+ style: required_positional
+ argumentValue:
+ kind: 'argument'
+ index: 0
+''');
+ await resolveTestUnit('''
+import '$importUri';
+
+void f(C c) {
+ c.m(0);
+}
+''');
+ await assertHasFix('''
+import '$importUri';
+
+void f(C c) {
+ c.m(0, 0);
+}
+''');
+ }
+
Future<void> test_addTypeParameter() async {
setPackageContent('''
class C {
@@ -53,6 +92,40 @@
''');
}
+ Future<void> test_removeParameter() async {
+ setPackageContent('''
+class C {
+ void m(int x) {}
+}
+''');
+ addPackageDataFile('''
+version: 1
+transforms:
+- title: 'Add argument'
+ element:
+ uris: ['$importUri']
+ method: 'm'
+ inClass: 'C'
+ changes:
+ - kind: 'removeParameter'
+ index: 1
+''');
+ await resolveTestUnit('''
+import '$importUri';
+
+void f(C c) {
+ c.m(0, 1);
+}
+''');
+ await assertHasFix('''
+import '$importUri';
+
+void f(C c) {
+ c.m(0);
+}
+''');
+ }
+
Future<void> test_rename() async {
setPackageContent('''
class New {}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/data_driven/transform_set_parser_test.dart b/pkg/analysis_server/test/src/services/correction/fix/data_driven/transform_set_parser_test.dart
index 58dc538..e5e8e1f 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/data_driven/transform_set_parser_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/data_driven/transform_set_parser_test.dart
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/src/services/correction/fix/data_driven/add_type_parameter.dart';
+import 'package:analysis_server/src/services/correction/fix/data_driven/modify_parameters.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/parameter_reference.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/rename.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/transform_set_error_code.dart';
@@ -21,6 +22,146 @@
@reflectiveTest
class TransformSetParserTest extends AbstractTransformSetParserTest {
+ void test_addParameter_optionalNamed() {
+ parse('''
+version: 1
+transforms:
+- title: 'Add'
+ element:
+ uris: ['test.dart']
+ function: 'f'
+ changes:
+ - kind: 'addParameter'
+ index: 0
+ name: 'p'
+ style: optional_named
+ argumentValue:
+ kind: 'argument'
+ index: 1
+''');
+ var transforms = result.transformsFor('f', ['test.dart']);
+ expect(transforms, hasLength(1));
+ var transform = transforms[0];
+ expect(transform.title, 'Add');
+ expect(transform.changes, hasLength(1));
+ var change = transform.changes[0] as ModifyParameters;
+ var modifications = change.modifications;
+ expect(modifications, hasLength(1));
+ var modification = modifications[0] as AddParameter;
+ expect(modification.index, 0);
+ expect(modification.name, 'p');
+ expect(modification.isRequired, false);
+ expect(modification.isPositional, false);
+ var value = modification.argumentValue as ArgumentExtractor;
+ var parameter = value.parameter as PositionalParameterReference;
+ expect(parameter.index, 1);
+ }
+
+ void test_addParameter_optionalPositional() {
+ parse('''
+version: 1
+transforms:
+- title: 'Add'
+ element:
+ uris: ['test.dart']
+ function: 'f'
+ changes:
+ - kind: 'addParameter'
+ index: 0
+ name: 'p'
+ style: optional_positional
+ defaultValue:
+ kind: 'argument'
+ index: 1
+''');
+ var transforms = result.transformsFor('f', ['test.dart']);
+ expect(transforms, hasLength(1));
+ var transform = transforms[0];
+ expect(transform.title, 'Add');
+ expect(transform.changes, hasLength(1));
+ var change = transform.changes[0] as ModifyParameters;
+ var modifications = change.modifications;
+ expect(modifications, hasLength(1));
+ var modification = modifications[0] as AddParameter;
+ expect(modification.index, 0);
+ expect(modification.name, 'p');
+ expect(modification.isRequired, false);
+ expect(modification.isPositional, true);
+ var value = modification.defaultValue as ArgumentExtractor;
+ var parameter = value.parameter as PositionalParameterReference;
+ expect(parameter.index, 1);
+ }
+
+ void test_addParameter_requiredNamed() {
+ parse('''
+version: 1
+transforms:
+- title: 'Add'
+ element:
+ uris: ['test.dart']
+ function: 'f'
+ changes:
+ - kind: 'addParameter'
+ index: 0
+ name: 'p'
+ style: required_named
+ argumentValue:
+ kind: 'argument'
+ index: 1
+''');
+ var transforms = result.transformsFor('f', ['test.dart']);
+ expect(transforms, hasLength(1));
+ var transform = transforms[0];
+ expect(transform.title, 'Add');
+ expect(transform.changes, hasLength(1));
+ var change = transform.changes[0] as ModifyParameters;
+ var modifications = change.modifications;
+ expect(modifications, hasLength(1));
+ var modification = modifications[0] as AddParameter;
+ expect(modification.index, 0);
+ expect(modification.name, 'p');
+ expect(modification.isRequired, true);
+ expect(modification.isPositional, false);
+ var value = modification.argumentValue as ArgumentExtractor;
+ var parameter = value.parameter as PositionalParameterReference;
+ expect(parameter.index, 1);
+ }
+
+ void test_addParameter_requiredPositional() {
+ parse('''
+version: 1
+transforms:
+- title: 'Add'
+ element:
+ uris: ['test.dart']
+ function: 'f'
+ changes:
+ - kind: 'addParameter'
+ index: 0
+ name: 'p'
+ style: required_positional
+ argumentValue:
+ kind: 'argument'
+ index: 1
+''');
+ var transforms = result.transformsFor('f', ['test.dart']);
+ expect(transforms, hasLength(1));
+ var transform = transforms[0];
+ expect(transform.title, 'Add');
+ expect(transform.changes, hasLength(1));
+ var change = transform.changes[0] as ModifyParameters;
+ var modifications = change.modifications;
+ expect(modifications, hasLength(1));
+ var modification = modifications[0] as AddParameter;
+ expect(modification.index, 0);
+ expect(modification.name, 'p');
+ expect(modification.isRequired, true);
+ expect(modification.isPositional, true);
+ var value = modification.argumentValue as ArgumentExtractor;
+ var parameter = value.parameter as PositionalParameterReference;
+ expect(parameter.index, 1);
+ }
+
void test_addTypeParameter_fromNamedArgument() {
parse('''
version: 1
@@ -155,6 +296,56 @@
]);
}
+ void test_removeParameter_named() {
+ parse('''
+version: 1
+transforms:
+- title: 'Remove'
+ element:
+ uris: ['test.dart']
+ function: 'f'
+ changes:
+ - kind: 'removeParameter'
+ name: 'p'
+''');
+ var transforms = result.transformsFor('f', ['test.dart']);
+ expect(transforms, hasLength(1));
+ var transform = transforms[0];
+ expect(transform.title, 'Remove');
+ expect(transform.changes, hasLength(1));
+ var change = transform.changes[0] as ModifyParameters;
+ var modifications = change.modifications;
+ expect(modifications, hasLength(1));
+ var modification = modifications[0] as RemoveParameter;
+ var parameter = modification.parameter as NamedParameterReference;
+ expect(parameter.name, 'p');
+ }
+
+ void test_removeParameter_positional() {
+ parse('''
+version: 1
+transforms:
+- title: 'Remove'
+ element:
+ uris: ['test.dart']
+ function: 'f'
+ changes:
+ - kind: 'removeParameter'
+ index: 0
+''');
+ var transforms = result.transformsFor('f', ['test.dart']);
+ expect(transforms, hasLength(1));
+ var transform = transforms[0];
+ expect(transform.title, 'Remove');
+ expect(transform.changes, hasLength(1));
+ var change = transform.changes[0] as ModifyParameters;
+ var modifications = change.modifications;
+ expect(modifications, hasLength(1));
+ var modification = modifications[0] as RemoveParameter;
+ var parameter = modification.parameter as PositionalParameterReference;
+ expect(parameter.index, 0);
+ }
+
void test_rename() {
parse('''
version: 1
diff --git a/sdk/lib/_internal/vm/bin/socket_patch.dart b/sdk/lib/_internal/vm/bin/socket_patch.dart
index abd886f..0d888f2 100644
--- a/sdk/lib/_internal/vm/bin/socket_patch.dart
+++ b/sdk/lib/_internal/vm/bin/socket_patch.dart
@@ -576,149 +576,255 @@
}
static Future<ConnectionTask<_NativeSocket>> startConnect(
- dynamic host, int port, dynamic sourceAddress) {
+ dynamic host, int port, dynamic sourceAddress) async {
if (host is String) {
host = escapeLinkLocalAddress(host);
}
_throwOnBadPort(port);
- if (sourceAddress != null && sourceAddress is! _InternetAddress) {
- if (sourceAddress is String) {
- sourceAddress = new InternetAddress(sourceAddress);
+ if (sourceAddress is String) {
+ sourceAddress = InternetAddress(sourceAddress);
+ } else if (sourceAddress != null && sourceAddress is! _InternetAddress) {
+ throw ArgumentError(
+ 'sourceAddress $sourceAddress must be either String or InternetAddress');
+ }
+
+ // The stream containing Internet addresses
+ final stream = await _lookup(host);
+ var streamSubscription;
+ final completer = Completer<_NativeSocket>();
+ // The first error that occurs.
+ var error = null;
+ // The map contains currently attempted connections.
+ final connecting = HashMap<_NativeSocket, Timer>();
+ // A queue contains Internet addresses that waits for connecting.
+ // `stream` keeps pushing element into queue, and connectNext() consume
+ // them.
+ final queue = <InternetAddress>[];
+ // The flag that stream of addresses is done.
+ var streamClosed = false;
+ // connectNext() will exhaust all elements from the queue. In the case of an
+ // element taking so long that connectNext() has finished trying all
+ // addresses, an explicit call to connectNext() is needed to restart the
+ // process.
+ var tryConnect = true;
+
+ // It will try each element in the `queue` to establish the connection.
+ // There can be multiple Internet addresses are under processing. Whenever
+ // a connection is successfully established (by receiving a write event),
+ // all pending connections stored in the `connecting` will be cancelled.
+ //
+ // 1. Check the status of `queue`, `completer` and `connecting`.
+ // Return an error if all elements have been tried.
+ // Reset tryConnect so that next element will be examined if `stream`
+ // is still open.
+ // 2. Create a socket.
+ // Try next address by issuing another connectNext() if this failed and
+ // keep the first error.
+ // 3. Register a timer, if socket creation succeeds, to try next address.
+ // 4. Set up a handler for socket. It will cancel all other pending
+ // connections and timers if it succeeds. Otherwise, keep the first
+ // error and issue connectNext().
+ void connectNext() {
+ if (completer.isCompleted) return;
+ if (queue.isEmpty) {
+ if (streamClosed && connecting.isEmpty) {
+ assert(error != null);
+ completer.completeError(error);
+ } else if (!streamClosed) {
+ // If new addresses comes after all elements in the queue have been
+ // processed, issue another connectNext().
+ tryConnect = true;
+ }
+ return;
+ }
+ final _InternetAddress address = queue.removeAt(0) as _InternetAddress;
+ var socket = _NativeSocket.normal(address);
+ var result;
+ if (sourceAddress == null) {
+ if (address.type == InternetAddressType.unix) {
+ result = socket.nativeCreateUnixDomainConnect(
+ address.address, _Namespace._namespace);
+ } else {
+ result = socket.nativeCreateConnect(
+ address._in_addr, port, address._scope_id);
+ }
+ } else {
+ assert(sourceAddress is _InternetAddress);
+ if (address.type == InternetAddressType.unix) {
+ assert(sourceAddress.type == InternetAddressType.unix);
+ result = socket.nativeCreateUnixDomainBindConnect(
+ address.address, sourceAddress.address, _Namespace._namespace);
+ } else {
+ result = socket.nativeCreateBindConnect(address._in_addr, port,
+ sourceAddress._in_addr, address._scope_id);
+ }
+ }
+ if (result is OSError) {
+ // Keep first error, if present.
+ if (error == null) {
+ int errorCode = result.errorCode;
+ if (sourceAddress != null &&
+ errorCode != null &&
+ socket.isBindError(errorCode)) {
+ error = createError(result, "Bind failed", sourceAddress);
+ } else {
+ error = createError(result, "Connection failed", address, port);
+ }
+ }
+ connectNext();
+ } else {
+ // Query the local port for error messages.
+ try {
+ socket.port;
+ } catch (e) {
+ error ??= createError(e, "Connection failed", address, port);
+ connectNext();
+ return;
+ }
+ // Set up timer for when we should retry the next address
+ // (if any).
+ final duration =
+ address.isLoopback ? _retryDurationLoopback : _retryDuration;
+ final timer = Timer(duration, connectNext);
+ setupResourceInfo(socket);
+
+ connecting[socket] = timer;
+ // Setup handlers for receiving the first write event which
+ // indicate that the socket is fully connected.
+ socket.setHandlers(write: () {
+ timer.cancel();
+ connecting.remove(socket);
+ // From 'man 2 connect':
+ // After select(2) indicates writability, use getsockopt(2) to read
+ // the SO_ERROR option at level SOL_SOCKET to determine whether
+ // connect() completed successfully (SO_ERROR is zero) or
+ // unsuccessfully.
+ OSError osError = socket.nativeGetError();
+ if (osError.errorCode != 0) {
+ socket.close();
+ error ??= osError;
+ // No timer is active for triggering next tryConnect(), do it
+ // manually.
+ if (connecting.isEmpty) connectNext();
+ return;
+ }
+ socket.setListening(read: false, write: false);
+ completer.complete(socket);
+ connecting.forEach((s, t) {
+ t.cancel();
+ s.close();
+ s.setHandlers();
+ s.setListening(read: false, write: false);
+ });
+ connecting.clear();
+ }, error: (e, st) {
+ timer.cancel();
+ socket.close();
+ // Keep first error, if present.
+ error ??= e;
+ connecting.remove(socket);
+ if (connecting.isEmpty) connectNext();
+ });
+ socket.setListening(read: false, write: true);
}
}
- return new Future.value(host).then((host) {
- if (host is _InternetAddress) return [host];
- return lookup(host).then((addresses) {
+
+ void onCancel() {
+ connecting.forEach((s, t) {
+ t.cancel();
+ s.close();
+ s.setHandlers();
+ s.setListening(read: false, write: false);
+ if (error == null) {
+ error = createError(null,
+ "Connection attempt cancelled, host: ${host}, port: ${port}");
+ }
+ });
+ connecting.clear();
+ if (!completer.isCompleted) {
+ completer.completeError(error! as Object);
+ }
+ }
+
+ // The stream is constructed in the _lookup() and should not emit the error.
+ streamSubscription = stream.listen((address) {
+ queue.add(address);
+ if (tryConnect) {
+ tryConnect = false;
+ connectNext();
+ }
+ }, onError: (e) {
+ streamSubscription.cancel();
+ streamClosed = true;
+ // The error comes from lookup() and we just rethrow the error.
+ throw e;
+ }, onDone: () {
+ streamClosed = true;
+ // In case that stream closes later than connectNext() exhausts all
+ // Internet addresses, check whether an error is thrown.
+ if (queue.isEmpty && connecting.isEmpty && !completer.isCompleted) {
+ completer.completeError(error);
+ }
+ });
+
+ return Future.value(
+ ConnectionTask<_NativeSocket>._(completer.future, onCancel));
+ }
+
+ // The [host] should be either a String or an _InternetAddress.
+ static Future<Stream> _lookup(host) async {
+ if (host is _InternetAddress) {
+ return Stream.value(host);
+ } else {
+ assert(host is String);
+ // If host is an IPv4 or IPv6 literal, bypass the lookup.
+ final inAddr = _InternetAddress._parse(host);
+ if (inAddr != null && inAddr.length == _InternetAddress._IPv4AddrLength) {
+ var addresses = await lookup(host, type: InternetAddressType.IPv4);
if (addresses.isEmpty) {
throw createError(null, "Failed host lookup: '$host'");
}
- return addresses;
- });
- }).then((addresses) {
- var completer = new Completer<_NativeSocket>();
- var it = (addresses as List<InternetAddress>).iterator;
- var error = null;
- var connecting = new HashMap();
-
- void connectNext() {
- if (!it.moveNext()) {
- if (connecting.isEmpty) {
- assert(error != null);
- completer.completeError(error);
- }
- return;
+ return Stream.fromIterable(addresses);
+ }
+ if (inAddr != null && inAddr.length == _InternetAddress._IPv6AddrLength) {
+ var addresses = await lookup(host, type: InternetAddressType.IPv6);
+ if (addresses.isEmpty) {
+ throw createError(null, "Failed host lookup: '$host'");
}
- final _InternetAddress address = it.current as _InternetAddress;
- var socket = new _NativeSocket.normal(address);
- var result;
- if (sourceAddress == null) {
- if (address.type == InternetAddressType.unix) {
- result = socket.nativeCreateUnixDomainConnect(
- address.address, _Namespace._namespace);
- } else {
- result = socket.nativeCreateConnect(
- address._in_addr, port, address._scope_id);
- }
- } else {
- assert(sourceAddress is _InternetAddress);
- if (address.type == InternetAddressType.unix) {
- assert(sourceAddress.type == InternetAddressType.unix);
- result = socket.nativeCreateUnixDomainBindConnect(
- address.address, sourceAddress.address, _Namespace._namespace);
- } else {
- result = socket.nativeCreateBindConnect(address._in_addr, port,
- sourceAddress._in_addr, address._scope_id);
- }
- }
- if (result is OSError) {
- // Keep first error, if present.
- if (error == null) {
- int errorCode = result.errorCode;
- if (sourceAddress != null &&
- errorCode != null &&
- socket.isBindError(errorCode)) {
- error = createError(result, "Bind failed", sourceAddress);
- } else {
- error = createError(result, "Connection failed", address, port);
- }
- }
- connectNext();
- } else {
- // Query the local port for error messages.
- try {
- socket.port;
- } catch (e) {
- if (error == null) {
- error = createError(e, "Connection failed", address, port);
- }
- connectNext();
- }
- // Set up timer for when we should retry the next address
- // (if any).
- var duration =
- address.isLoopback ? _retryDurationLoopback : _retryDuration;
- var timer = new Timer(duration, connectNext);
- setupResourceInfo(socket);
-
- connecting[socket] = timer;
- // Setup handlers for receiving the first write event which
- // indicate that the socket is fully connected.
- socket.setHandlers(write: () {
- timer.cancel();
- connecting.remove(socket);
- // From 'man 2 connect':
- // After select(2) indicates writability, use getsockopt(2) to read
- // the SO_ERROR option at level SOL_SOCKET to determine whether
- // connect() completed successfully (SO_ERROR is zero) or
- // unsuccessfully.
- OSError osError = socket.nativeGetError();
- if (osError.errorCode != 0) {
- socket.close();
- if (error == null) error = osError;
- if (connecting.isEmpty) connectNext();
- return;
- }
- socket.setListening(read: false, write: false);
- completer.complete(socket);
- connecting.forEach((s, t) {
- t.cancel();
- s.close();
- s.setHandlers();
- s.setListening(read: false, write: false);
- });
- connecting.clear();
- }, error: (e, st) {
- timer.cancel();
- socket.close();
- // Keep first error, if present.
- if (error == null) error = e;
- connecting.remove(socket);
- if (connecting.isEmpty) connectNext();
- });
- socket.setListening(read: false, write: true);
- }
+ return Stream.fromIterable(addresses);
}
- void onCancel() {
- connecting.forEach((s, t) {
- t.cancel();
- s.close();
- s.setHandlers();
- s.setListening(read: false, write: false);
- if (error == null) {
- error = createError(null,
- "Connection attempt cancelled, host: ${host}, port: ${port}");
- }
- });
- connecting.clear();
- if (!completer.isCompleted) {
- completer.completeError(error);
+ if (Platform.isIOS) {
+ return _concurrentLookup(host);
+ } else {
+ var addresses = await lookup(host, type: InternetAddressType.any);
+ if (addresses.isEmpty) {
+ throw createError(null, "Failed host lookup: '$host'");
}
+ return Stream.fromIterable(addresses);
}
+ }
+ }
- connectNext();
- return new ConnectionTask<_NativeSocket>._(completer.future, onCancel);
+ static Stream<InternetAddress> _concurrentLookup(String host) {
+ final controller = StreamController<InternetAddress>();
+ // Lookup IPv4 and IPv6 concurrently
+ Future.wait([
+ for (final type in [InternetAddressType.IPv4, InternetAddressType.IPv6])
+ lookup(host, type: type).then((list) {
+ for (final address in list) {
+ controller.add(address);
+ }
+ return list.isNotEmpty;
+ })
+ ]).then((successes) {
+ if (!successes.contains(true)) {
+ // Neither lookup found an address.
+ throw createError(null, "Failed host lookup: '$host'");
+ }
+ controller.close();
});
+ return controller.stream;
}
static Future<_NativeSocket> connect(
diff --git a/sdk/lib/io/socket.dart b/sdk/lib/io/socket.dart
index d2a0feb..48f428f 100644
--- a/sdk/lib/io/socket.dart
+++ b/sdk/lib/io/socket.dart
@@ -203,7 +203,7 @@
* change over time.
*/
external static Future<List<InternetAddress>> lookup(String host,
- {InternetAddressType type: InternetAddressType.any});
+ {InternetAddressType type = InternetAddressType.any});
/**
* Clones the given [address] with the new [host].
@@ -264,9 +264,9 @@
* specified type. Default is [InternetAddressType.any].
*/
external static Future<List<NetworkInterface>> list(
- {bool includeLoopback: false,
- bool includeLinkLocal: false,
- InternetAddressType type: InternetAddressType.any});
+ {bool includeLoopback = false,
+ bool includeLinkLocal = false,
+ InternetAddressType type = InternetAddressType.any});
}
/**
@@ -315,7 +315,7 @@
* distributed over multiple isolates this way.
*/
external static Future<RawServerSocket> bind(address, int port,
- {int backlog: 0, bool v6Only: false, bool shared: false});
+ {int backlog = 0, bool v6Only = false, bool shared = false});
/**
* Returns the port used by this socket.
@@ -391,7 +391,8 @@
}
external static Future<ServerSocket> _bind(address, int port,
- {int backlog: 0, bool v6Only: false, bool shared: false});
+ {int backlog = 0, bool v6Only = false, bool shared = false});
+
/**
* Returns the port used by this socket.
*/
@@ -558,11 +559,21 @@
/**
* Events for the [RawSocket].
+ *
+ * These event objects are by the [Stream] behavior of [RawSocket] (for example
+ * [RawSocket.listen], [RawSocket.forEach]) when the socket's state change.
*/
class RawSocketEvent {
+ /// An event indicates the socket is ready to be read.
static const RawSocketEvent read = const RawSocketEvent._(0);
+
+ /// An event indicates the socket is ready to write.
static const RawSocketEvent write = const RawSocketEvent._(1);
+
+ /// An event indicates the reading from the socket is closed
static const RawSocketEvent readClosed = const RawSocketEvent._(2);
+
+ /// An event indicates the socket is closed.
static const RawSocketEvent closed = const RawSocketEvent._(3);
@Deprecated("Use read instead")
@@ -610,15 +621,17 @@
}
}
-/**
- * A [RawSocket] is an unbuffered interface to a TCP socket.
- *
- * The raw socket delivers the data stream in the same chunks as the underlying
- * operating system.
- *
- * It is not the same as a
- * [POSIX raw socket](http://man7.org/linux/man-pages/man7/raw.7.html).
- */
+/// A TCP connection.
+///
+/// A *socket connection* connects a *local* socket to a *remote* socket.
+/// Data, as [Uint8List]s, is received by the local socket and made
+/// available by the [read] method, and can be sent to the remote socket
+/// through the [write] method.
+///
+/// The [Stream] interface of this class provides event notification about when
+/// a certain change has happened, for example when data has become available
+/// ([RawSocketEvent.read]) or when the remote end has stopped listening
+/// ([RawSocketEvent.close]).
abstract class RawSocket implements Stream<RawSocketEvent> {
/**
* Set or get, if the [RawSocket] should listen for [RawSocketEvent.read]
@@ -768,12 +781,12 @@
void setRawOption(RawSocketOption option);
}
-/**
- * A high-level class for communicating over a TCP socket.
- *
- * The [Socket] exposes both a [Stream] and a [IOSink] interface, making it
- * ideal for using together with other [Stream]s.
- */
+/// A TCP connection between two sockets.
+///
+/// A *socket connection* connects a *local* socket to a *remote* socket.
+/// Data, as [Uint8List]s, is received by the local socket, made available
+/// by the [Stream] interface of this class, and can be sent to the remote
+/// socket through the [IOSink] interface of this class.
abstract class Socket implements Stream<Uint8List>, IOSink {
/**
* Creates a new socket connection to the host and port and returns a [Future]
@@ -903,14 +916,15 @@
Future get done;
}
-/**
- * Datagram package. Data sent to and received from datagram sockets
- * contains the internet address and port of the destination or source
- * togeter with the data.
- */
+/// A data packet which is received by a [RawDatagramSocket].
class Datagram {
+ /// The actual bytes of the message.
Uint8List data;
+
+ /// The address of the socket which sends the data.
InternetAddress address;
+
+ /// The port of the socket which sends the data.
int port;
Datagram(this.data, this.address, this.port);
@@ -990,11 +1004,29 @@
void set broadcastEnabled(bool value);
/**
- * Creates a new raw datagram socket binding it to an address and
- * port.
+ * Binds a socket to the given [host] and the [port].
+ *
+ * When the socket is bound and has started listening on [port], the returned
+ * future completes with the [RawDatagramSocket] of the bound socket.
+ *
+ * The [host] can either be a [String] or an [InternetAddress]. If [host] is a
+ * [String], [bind] will perform a [InternetAddress.lookup] and use the first
+ * value in the list. To listen on the loopback interface, which will allow
+ * only incoming connections from the local host, use the value
+ * [InternetAddress.loopbackIPv4] or [InternetAddress.loopbackIPv6].
+ * To allow for incoming connection from any network use either one of
+ * the values [InternetAddress.anyIPv4] or [InternetAddress.anyIPv6] to
+ * bind to all interfaces, or use the IP address of a specific interface.
+ *
+ * The [reuseAddress] should be set for all listeners that bind to the same
+ * address. Otherwise, it will fail with a [SocketException].
+ *
+ * The [reusePort] specifies whether the port can be reused.
+ *
+ * The [ttl] sets `time to live` of a datagram sent on the socket.
*/
external static Future<RawDatagramSocket> bind(host, int port,
- {bool reuseAddress: true, bool reusePort: false, int ttl: 1});
+ {bool reuseAddress = true, bool reusePort = false, int ttl = 1});
/**
* Returns the port used by this socket.
@@ -1062,13 +1094,34 @@
void setRawOption(RawSocketOption option);
}
+/// Exception thrown when a socket operation fails.
class SocketException implements IOException {
+ /// Description of the error.
final String message;
+
+ /// The underlying OS error.
+ ///
+ /// If this exception is not thrown due to an OS error, the value is `null`.
final OSError? osError;
+
+ /// The address of the socket giving rise to the exception.
+ ///
+ /// This is either the source or destination address of a socket,
+ /// or it can be `null` if no socket end-point was involved in the cause of
+ /// the exception.
final InternetAddress? address;
+
+ /// The port of the socket giving rise to the exception.
+ ///
+ /// This is either the source or destination address of a socket,
+ /// or it can be `null` if no socket end-point was involved in the cause of
+ /// the exception.
final int? port;
+ /// Creates a [SocketException] with the provided values.
const SocketException(this.message, {this.osError, this.address, this.port});
+
+ /// Creates an exception reporting that a socket was used after it was closed.
const SocketException.closed()
: message = 'Socket has been closed',
osError = null,
diff --git a/tools/VERSION b/tools/VERSION
index 7aea38a..13e4a72 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
MAJOR 2
MINOR 10
PATCH 0
-PRERELEASE 109
+PRERELEASE 110
PRERELEASE_PATCH 0
\ No newline at end of file