Version 1.9.0-dev.8.2
svn merge -c 43763,43787,43790,43791,43792 https://dart.googlecode.com/svn/branches/bleeding_edge trunk
R=kustermann@google.com
Review URL: https://codereview.chromium.org//931103003
git-svn-id: http://dart.googlecode.com/svn/trunk@43808 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/pkg/analysis_server/lib/src/domain_completion.dart b/pkg/analysis_server/lib/src/domain_completion.dart
index 2c5f528..e1ef9fc5 100644
--- a/pkg/analysis_server/lib/src/domain_completion.dart
+++ b/pkg/analysis_server/lib/src/domain_completion.dart
@@ -158,7 +158,7 @@
/**
* Process a `completion.getSuggestions` request.
*/
- Response processRequest(Request request) {
+ Response processRequest(Request request, [CompletionManager manager]) {
performance = new CompletionPerformance();
// extract params
CompletionGetSuggestionsParams params =
@@ -168,7 +168,9 @@
AnalysisContext context = server.getAnalysisContext(params.file);
Source source = server.getSource(params.file);
recordRequest(performance, context, source, params.offset);
- CompletionManager manager = completionManagerFor(context, source);
+ if (manager == null) {
+ manager = completionManagerFor(context, source);
+ }
CompletionRequest completionRequest =
new CompletionRequest(params.offset, performance);
int notificationCount = 0;
diff --git a/pkg/analysis_server/lib/src/services/completion/common_usage_computer.dart b/pkg/analysis_server/lib/src/services/completion/common_usage_computer.dart
new file mode 100644
index 0000000..5c7d340
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/completion/common_usage_computer.dart
@@ -0,0 +1,160 @@
+// Copyright (c) 2015, 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.
+
+library services.completion.computer.dart.relevance;
+
+import 'package:analysis_server/src/protocol_server.dart' as protocol;
+import 'package:analysis_server/src/protocol_server.dart' show
+ CompletionSuggestion, CompletionSuggestionKind;
+import 'package:analysis_server/src/services/completion/dart_completion_manager.dart';
+import 'package:analyzer/src/generated/ast.dart';
+import 'package:analyzer/src/generated/element.dart';
+
+/**
+ * A map of <library>.<classname> to an ordered list of method names,
+ * field names, getter names, and named constructors.
+ * The names are ordered from most relevant to least relevant.
+ * Names not listed are considered equally less relevant than those listed.
+ */
+const Map<String, List<String>> defaultSelectorRelevance = const {//
+// Sample implementation which updates the relevance of the following
+// new Random().nextInt(...)
+// new Random().nextDouble(...)
+// new Random().nextBool() - not commonly used thus omitted from list
+// Entries should look something like this
+// 'dart.math.Random': const ['nextInt', 'nextDouble'],
+// 'dart.async.Future': const ['value', 'wait'],
+};
+
+/**
+ * A computer for adjusting the relevance of completions computed by others
+ * based upon common Dart usage patterns.
+ */
+class CommonUsageComputer {
+ /**
+ * A map of <library>.<classname> to an ordered list of method names,
+ * field names, getter names, and named constructors.
+ * The names are ordered from most relevant to least relevant.
+ * Names not listed are considered equally less relevant than those listed.
+ */
+ Map<String, List<String>> selectorRelevance;
+
+ CommonUsageComputer([this.selectorRelevance = defaultSelectorRelevance]);
+
+ /**
+ * Adjusts the relevance based on the given completion context.
+ * The compilation unit and completion node
+ * in the given completion context may not be resolved.
+ * This method should execute quickly and not block waiting for any analysis.
+ */
+ void computeFast(DartCompletionRequest request) {
+ _update(request);
+ }
+
+ /**
+ * Adjusts the relevance based on the given completion context.
+ * The compilation unit and completion node
+ * in the given completion context are resolved.
+ */
+ void computeFull(DartCompletionRequest request) {
+ _update(request);
+ }
+
+ /**
+ * Adjusts the relevance based on the given completion context.
+ * The compilation unit and completion node
+ * in the given completion context may not be resolved.
+ */
+ void _update(DartCompletionRequest request) {
+ var visitor = new _BestTypeVisitor(request.target.entity);
+ DartType type = request.target.containingNode.accept(visitor);
+ if (type != null) {
+ Element typeElem = type.element;
+ if (typeElem != null) {
+ LibraryElement libElem = typeElem.library;
+ if (libElem != null) {
+ _updateInvocationRelevance(request, type, libElem);
+ }
+ }
+ }
+ }
+
+ /**
+ * Adjusts the relevance of all method suggestions based upon the given
+ * target type and library.
+ */
+ void _updateInvocationRelevance(DartCompletionRequest request, DartType type,
+ LibraryElement libElem) {
+ String typeName = type.name;
+ List<String> selectors = selectorRelevance['${libElem.name}.${typeName}'];
+ if (selectors != null) {
+ for (CompletionSuggestion suggestion in request.suggestions) {
+ protocol.Element element = suggestion.element;
+ if (element != null &&
+ (element.kind == protocol.ElementKind.CONSTRUCTOR ||
+ element.kind == protocol.ElementKind.FIELD ||
+ element.kind == protocol.ElementKind.GETTER ||
+ element.kind == protocol.ElementKind.METHOD ||
+ element.kind == protocol.ElementKind.SETTER) &&
+ suggestion.kind == CompletionSuggestionKind.INVOCATION &&
+ suggestion.declaringType == typeName) {
+ int index = selectors.indexOf(suggestion.completion);
+ if (index != -1) {
+ suggestion.relevance = DART_RELEVANCE_COMMON_USAGE - index;
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * An [AstVisitor] used to determine the best defining type of a node.
+ */
+class _BestTypeVisitor extends GeneralizingAstVisitor {
+
+ /**
+ * The entity which the completed text will replace (or which will be
+ * displaced once the completed text is inserted). This may be an AstNode or
+ * a Token, or it may be null if the cursor is after all tokens in the file.
+ * See field of the same name in [CompletionTarget].
+ */
+ final Object entity;
+
+ _BestTypeVisitor(this.entity);
+
+ DartType visitConstructorName(ConstructorName node) {
+ if (node.period != null && node.name == entity) {
+ TypeName typeName = node.type;
+ if (typeName != null) {
+ return typeName.type;
+ }
+ }
+ return null;
+ }
+
+ DartType visitNode(AstNode node) {
+ return null;
+ }
+
+ DartType visitPrefixedIdentifier(PrefixedIdentifier node) {
+ if (node.identifier == entity) {
+ SimpleIdentifier prefix = node.prefix;
+ if (prefix != null) {
+ return prefix.bestType;
+ }
+ }
+ return null;
+ }
+
+ DartType visitPropertyAccess(PropertyAccess node) {
+ if (node.propertyName == entity) {
+ Expression target = node.realTarget;
+ if (target != null) {
+ return target.bestType;
+ }
+ }
+ return null;
+ }
+}
diff --git a/pkg/analysis_server/lib/src/services/completion/dart_completion_manager.dart b/pkg/analysis_server/lib/src/services/completion/dart_completion_manager.dart
index 3c20785..818d8a6 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart_completion_manager.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart_completion_manager.dart
@@ -9,6 +9,7 @@
import 'package:analysis_server/src/protocol.dart';
import 'package:analysis_server/src/services/completion/arglist_computer.dart';
import 'package:analysis_server/src/services/completion/combinator_computer.dart';
+import 'package:analysis_server/src/services/completion/common_usage_computer.dart';
import 'package:analysis_server/src/services/completion/completion_manager.dart';
import 'package:analysis_server/src/services/completion/completion_target.dart';
import 'package:analysis_server/src/services/completion/dart_completion_cache.dart';
@@ -20,24 +21,24 @@
import 'package:analysis_server/src/services/search/search_engine.dart';
import 'package:analyzer/src/generated/ast.dart';
import 'package:analyzer/src/generated/engine.dart';
-import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/scanner.dart';
+import 'package:analyzer/src/generated/source.dart';
-// Relevance highest to lowest
-const int DART_RELEVANCE_HIGH = 2000;
-const int DART_RELEVANCE_LOCAL_VARIABLE = 1059;
-const int DART_RELEVANCE_PARAMETER = 1059;
-const int DART_RELEVANCE_INHERITED_FIELD = 1058;
-const int DART_RELEVANCE_LOCAL_FIELD = 1058;
-const int DART_RELEVANCE_INHERITED_ACCESSOR = 1057;
-const int DART_RELEVANCE_INHERITED_METHOD = 1057;
-const int DART_RELEVANCE_LOCAL_ACCESSOR = 1057;
-const int DART_RELEVANCE_LOCAL_METHOD = 1057;
-const int DART_RELEVANCE_LOCAL_FUNCTION = 1056;
-const int DART_RELEVANCE_LOCAL_TOP_LEVEL_VARIABLE = 1056;
-const int DART_RELEVANCE_KEYWORD = 1055;
+const int DART_RELEVANCE_COMMON_USAGE = 1200;
const int DART_RELEVANCE_DEFAULT = 1000;
+const int DART_RELEVANCE_HIGH = 2000;
+const int DART_RELEVANCE_INHERITED_ACCESSOR = 1057;
+const int DART_RELEVANCE_INHERITED_FIELD = 1058;
+const int DART_RELEVANCE_INHERITED_METHOD = 1057;
+const int DART_RELEVANCE_KEYWORD = 1055;
+const int DART_RELEVANCE_LOCAL_ACCESSOR = 1057;
+const int DART_RELEVANCE_LOCAL_FIELD = 1058;
+const int DART_RELEVANCE_LOCAL_FUNCTION = 1056;
+const int DART_RELEVANCE_LOCAL_METHOD = 1057;
+const int DART_RELEVANCE_LOCAL_TOP_LEVEL_VARIABLE = 1056;
+const int DART_RELEVANCE_LOCAL_VARIABLE = 1059;
const int DART_RELEVANCE_LOW = 500;
+const int DART_RELEVANCE_PARAMETER = 1059;
/**
* The base class for computing code completion suggestions.
@@ -69,9 +70,10 @@
final SearchEngine searchEngine;
final DartCompletionCache cache;
List<DartCompletionComputer> computers;
+ CommonUsageComputer commonUsageComputer;
DartCompletionManager(AnalysisContext context, this.searchEngine,
- Source source, this.cache, [this.computers])
+ Source source, this.cache, [this.computers, this.commonUsageComputer])
: super(context, source) {
if (computers == null) {
computers = [
@@ -82,6 +84,9 @@
new ImportedComputer(),
new InvocationComputer()];
}
+ if (commonUsageComputer == null) {
+ commonUsageComputer = new CommonUsageComputer();
+ }
}
/**
@@ -141,6 +146,7 @@
return c.computeFast(request);
});
});
+ commonUsageComputer.computeFast(request);
sendResults(request, todo.isEmpty);
return todo;
});
@@ -179,6 +185,7 @@
request.performance.logElapseTime(completeTag);
bool last = --count == 0;
if (changed || last) {
+ commonUsageComputer.computeFull(request);
sendResults(request, last);
}
});
diff --git a/pkg/analysis_server/test/mock_sdk.dart b/pkg/analysis_server/test/mock_sdk.dart
index 90b5eac..4589c22 100644
--- a/pkg/analysis_server/test/mock_sdk.dart
+++ b/pkg/analysis_server/test/mock_sdk.dart
@@ -108,6 +108,8 @@
import 'dart:math';
class Future<T> {
+ factory Future.delayed(Duration duration, [T computation()]) => null;
+ factory Future.value([value]) => null;
static Future wait(List<Future> futures) => null;
}
@@ -143,7 +145,11 @@
external double cos(num x);
external double sin(num x);
external double sqrt(num x);
-class Random {}
+class Random {
+ bool nextBool() => true;
+ double nextDouble() => 2.0;
+ int nextInt() => 1;
+}
''');
static const _MockSdkLibrary LIB_HTML =
diff --git a/pkg/analysis_server/test/services/completion/common_usage_computer_test.dart b/pkg/analysis_server/test/services/completion/common_usage_computer_test.dart
new file mode 100644
index 0000000..039911c
--- /dev/null
+++ b/pkg/analysis_server/test/services/completion/common_usage_computer_test.dart
@@ -0,0 +1,249 @@
+// Copyright (c) 2015, 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.
+
+library test.services.completion.computer.dart.relevance;
+
+import 'dart:async';
+
+import 'package:analysis_server/src/constants.dart';
+import 'package:analysis_server/src/domain_completion.dart';
+import 'package:analysis_server/src/protocol.dart';
+import 'package:analysis_server/src/services/completion/common_usage_computer.dart';
+import 'package:analysis_server/src/services/completion/dart_completion_cache.dart';
+import 'package:analysis_server/src/services/completion/dart_completion_manager.dart';
+import 'package:analysis_server/src/services/index/index.dart';
+import 'package:analysis_server/src/services/index/local_memory_index.dart';
+import 'package:analyzer/src/generated/engine.dart';
+import 'package:analyzer/src/generated/source.dart';
+import 'package:unittest/unittest.dart';
+
+import '../../analysis_abstract.dart';
+import '../../mocks.dart';
+import '../../reflective_tests.dart';
+
+main() {
+ groupSep = ' | ';
+ runReflectiveTests(CommonUsageComputerTest);
+}
+
+@reflectiveTest
+class CommonUsageComputerTest extends AbstractAnalysisTest {
+ String completionId;
+ int completionOffset;
+ int replacementOffset;
+ int replacementLength;
+ List<CompletionSuggestion> suggestions = [];
+ bool suggestionsDone = false;
+
+ String addTestFile(String content) {
+ completionOffset = content.indexOf('^');
+ expect(completionOffset, isNot(equals(-1)), reason: 'missing ^');
+ int nextOffset = content.indexOf('^', completionOffset + 1);
+ expect(nextOffset, equals(-1), reason: 'too many ^');
+ return super.addTestFile(
+ content.substring(0, completionOffset) +
+ content.substring(completionOffset + 1));
+ }
+
+ void assertHasResult(CompletionSuggestionKind kind, String completion,
+ [int relevance = DART_RELEVANCE_DEFAULT, bool isDeprecated = false,
+ bool isPotential = false]) {
+ var cs;
+ suggestions.forEach((s) {
+ if (s.completion == completion) {
+ if (cs == null) {
+ cs = s;
+ } else {
+ fail('expected exactly one $completion but found > 1');
+ }
+ }
+ });
+ if (cs == null) {
+ var completions = suggestions.map((s) => s.completion).toList();
+ fail('expected "$completion" but found\n $completions');
+ }
+ expect(cs.kind, equals(kind));
+ expect(cs.relevance, equals(relevance));
+ expect(cs.selectionOffset, equals(completion.length));
+ expect(cs.selectionLength, equals(0));
+ expect(cs.isDeprecated, equals(isDeprecated));
+ expect(cs.isPotential, equals(isPotential));
+ }
+
+ void assertNoResult(String completion) {
+ if (suggestions.any((cs) => cs.completion == completion)) {
+ fail('did not expect completion: $completion');
+ }
+ }
+
+ void assertValidId(String id) {
+ expect(id, isNotNull);
+ expect(id.isNotEmpty, isTrue);
+ }
+
+ @override
+ Index createIndex() {
+ return createLocalMemoryIndex();
+ }
+
+ Future getSuggestions(Map<String, List<String>> selectorRelevance) async {
+ await waitForTasksFinished();
+ CompletionGetSuggestionsParams params =
+ new CompletionGetSuggestionsParams(testFile, completionOffset);
+ Request request = params.toRequest('0');
+ CompletionDomainHandler domainHandler = new CompletionDomainHandler(server);
+ handler = domainHandler;
+
+ AnalysisContext context = server.getAnalysisContext(params.file);
+ Source source = server.getSource(params.file);
+ DartCompletionManager completionManager = new DartCompletionManager(
+ context,
+ server.searchEngine,
+ source,
+ new DartCompletionCache(context, source),
+ null,
+ new CommonUsageComputer(selectorRelevance));
+
+ Response response =
+ domainHandler.processRequest(request, completionManager);
+ expect(response, isResponseSuccess('0'));
+ completionId = response.id;
+ assertValidId(completionId);
+ await pumpEventQueue();
+ expect(suggestionsDone, isTrue);
+ }
+
+ void processNotification(Notification notification) {
+ if (notification.event == COMPLETION_RESULTS) {
+ var params = new CompletionResultsParams.fromNotification(notification);
+ String id = params.id;
+ assertValidId(id);
+ if (id == completionId) {
+ expect(suggestionsDone, isFalse);
+ replacementOffset = params.replacementOffset;
+ replacementLength = params.replacementLength;
+ suggestionsDone = params.isLast;
+ expect(suggestionsDone, isNotNull);
+ suggestions = params.results;
+ }
+ }
+ }
+
+ @override
+ void setUp() {
+ super.setUp();
+ createProject();
+ }
+
+ test_ConstructorName() async {
+ // SimpleIdentifier ConstructorName InstanceCreationExpression
+ addTestFile('import "dart:async"; class A {x() {new Future.^}}');
+ await getSuggestions({
+ 'dart.async.Future': ['value', 'wait']
+ });
+ expect(replacementOffset, equals(completionOffset));
+ expect(replacementLength, equals(0));
+ assertHasResult(CompletionSuggestionKind.INVOCATION, 'delayed');
+ assertHasResult(
+ CompletionSuggestionKind.INVOCATION,
+ 'value',
+ DART_RELEVANCE_COMMON_USAGE);
+ assertNoResult('Future');
+ assertNoResult('Object');
+ assertNoResult('A');
+ }
+
+ test_PrefixedIdentifier_field() async {
+ // SimpleIdentifier PrefixedIdentifeir ExpressionStatement
+ addTestFile('class A {static int s1; static int s2; x() {A.^}}');
+ await getSuggestions({
+ '.A': ['s2']
+ });
+ expect(replacementOffset, equals(completionOffset));
+ expect(replacementLength, equals(0));
+ assertHasResult(CompletionSuggestionKind.INVOCATION, 's1');
+ assertHasResult(
+ CompletionSuggestionKind.INVOCATION,
+ 's2',
+ DART_RELEVANCE_COMMON_USAGE);
+ assertNoResult('Future');
+ assertNoResult('Object');
+ assertNoResult('A');
+ }
+
+ test_PrefixedIdentifier_getter() async {
+ // SimpleIdentifier PrefixedIdentifeir ExpressionStatement
+ addTestFile('class A {int get g1 => 1; int get g2 => 2; x() {new A().^}}');
+ await getSuggestions({
+ '.A': ['g2']
+ });
+ expect(replacementOffset, equals(completionOffset));
+ expect(replacementLength, equals(0));
+ assertHasResult(CompletionSuggestionKind.INVOCATION, 'g1');
+ assertHasResult(
+ CompletionSuggestionKind.INVOCATION,
+ 'g2',
+ DART_RELEVANCE_COMMON_USAGE);
+ assertNoResult('Future');
+ assertNoResult('Object');
+ assertNoResult('A');
+ }
+
+ test_PrefixedIdentifier_setter() async {
+ // SimpleIdentifier PrefixedIdentifeir ExpressionStatement
+ addTestFile('class A {set s1(v) {}; set s2(v) {}; x() {new A().^}}');
+ await getSuggestions({
+ '.A': ['s2']
+ });
+ expect(replacementOffset, equals(completionOffset));
+ expect(replacementLength, equals(0));
+ assertHasResult(CompletionSuggestionKind.INVOCATION, 's1');
+ assertHasResult(
+ CompletionSuggestionKind.INVOCATION,
+ 's2',
+ DART_RELEVANCE_COMMON_USAGE);
+ assertNoResult('Future');
+ assertNoResult('Object');
+ assertNoResult('A');
+ }
+
+ test_PrefixedIdentifier_static_method() async {
+ // SimpleIdentifier PrefixedIdentifeir ExpressionStatement
+ addTestFile('import "dart:async"; class A {x() {Future.^}}');
+ await getSuggestions({
+ 'dart.async.Future': ['value', 'wait']
+ });
+ expect(replacementOffset, equals(completionOffset));
+ expect(replacementLength, equals(0));
+ assertHasResult(
+ CompletionSuggestionKind.INVOCATION,
+ 'wait',
+ DART_RELEVANCE_COMMON_USAGE - 1);
+ assertNoResult('Future');
+ assertNoResult('Object');
+ assertNoResult('A');
+ }
+
+ test_PropertyAccess() async {
+ // SimpleIdentifier PropertyAccess ExpressionStatement
+ addTestFile('import "dart:math"; class A {x() {new Random().^}}');
+ await getSuggestions({
+ 'dart.math.Random': ['nextInt', 'nextDouble']
+ });
+ expect(replacementOffset, equals(completionOffset));
+ expect(replacementLength, equals(0));
+ assertHasResult(CompletionSuggestionKind.INVOCATION, 'nextBool');
+ assertHasResult(
+ CompletionSuggestionKind.INVOCATION,
+ 'nextDouble',
+ DART_RELEVANCE_COMMON_USAGE - 1);
+ assertHasResult(
+ CompletionSuggestionKind.INVOCATION,
+ 'nextInt',
+ DART_RELEVANCE_COMMON_USAGE);
+ assertNoResult('Random');
+ assertNoResult('Object');
+ assertNoResult('A');
+ }
+}
diff --git a/pkg/analysis_server/test/services/completion/completion_test_util.dart b/pkg/analysis_server/test/services/completion/completion_test_util.dart
index 67a7a06..573f604 100644
--- a/pkg/analysis_server/test/services/completion/completion_test_util.dart
+++ b/pkg/analysis_server/test/services/completion/completion_test_util.dart
@@ -9,6 +9,7 @@
import 'package:analysis_server/src/protocol.dart' as protocol show Element,
ElementKind;
import 'package:analysis_server/src/protocol.dart' hide Element, ElementKind;
+import 'package:analysis_server/src/services/completion/common_usage_computer.dart';
import 'package:analysis_server/src/services/completion/completion_manager.dart';
import 'package:analysis_server/src/services/completion/dart_completion_cache.dart';
import 'package:analysis_server/src/services/completion/dart_completion_manager.dart';
@@ -450,7 +451,8 @@
searchEngine,
testSource,
cache,
- [computer]);
+ [computer],
+ new CommonUsageComputer({}));
var result = _completionManager.computeFast(request);
expect(request.replacementOffset, isNotNull);
expect(request.replacementLength, isNotNull);
@@ -2496,6 +2498,19 @@
});
}
+ test_new_instance() {
+ addTestSource('import "dart:math"; class A {x() {new Random().^}}');
+ computeFast();
+ return computeFull((bool result) {
+ assertSuggestInvocationMethod('nextBool', 'Random', 'bool');
+ assertSuggestInvocationMethod('nextDouble', 'Random', 'double');
+ assertSuggestInvocationMethod('nextInt', 'Random', 'int');
+ assertNotSuggested('Random');
+ assertNotSuggested('Object');
+ assertNotSuggested('A');
+ });
+ }
+
test_partFile_TypeName() {
// SimpleIdentifier TypeName ConstructorName
addSource('/testB.dart', '''
diff --git a/pkg/analysis_server/test/services/completion/test_all.dart b/pkg/analysis_server/test/services/completion/test_all.dart
index ef908ff..269e8c0 100644
--- a/pkg/analysis_server/test/services/completion/test_all.dart
+++ b/pkg/analysis_server/test/services/completion/test_all.dart
@@ -8,6 +8,7 @@
import 'arglist_computer_test.dart' as arglist_test;
import 'combinator_computer_test.dart' as combinator_test;
+import 'common_usage_computer_test.dart' as common_usage_computer_test;
import 'completion_computer_test.dart' as completion_computer_test;
import 'completion_manager_test.dart' as completion_manager_test;
import 'completion_target_test.dart' as completion_target_test;
@@ -23,6 +24,7 @@
group('completion', () {
arglist_test.main();
combinator_test.main();
+ common_usage_computer_test.main();
completion_computer_test.main();
completion_manager_test.main();
completion_target_test.main();
diff --git a/tools/VERSION b/tools/VERSION
index d21d407..9130654 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -28,4 +28,4 @@
MINOR 9
PATCH 0
PRERELEASE 8
-PRERELEASE_PATCH 1
+PRERELEASE_PATCH 2