| // Copyright (c) 2014, 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 'dart:collection'; | |
| import 'package:analyzer_plugin/protocol/protocol_common.dart'; | |
| import 'package:test/test.dart'; | |
| import 'domain_completion_util.dart'; | |
| /// A base class for classes containing completion tests. | |
| abstract class CompletionTestCase extends AbstractCompletionDomainTest { | |
| static const String _cursorMarker = '!'; | |
| List<String> get suggestedCompletions => suggestions | |
| .map((CompletionSuggestion suggestion) => suggestion.completion) | |
| .toList(); | |
| void assertHasCompletion( | |
| String completion, { | |
| ElementKind? elementKind, | |
| bool? isDeprecated, | |
| Matcher? libraryUri, | |
| }) { | |
| var expectedOffset = completion.indexOf(_cursorMarker); | |
| if (expectedOffset >= 0) { | |
| if (completion.contains(_cursorMarker, expectedOffset + 1)) { | |
| fail( | |
| "Invalid completion, contains multiple cursor positions: '$completion'", | |
| ); | |
| } | |
| completion = completion.replaceFirst(_cursorMarker, ''); | |
| } else { | |
| expectedOffset = completion.length; | |
| } | |
| CompletionSuggestion? matchingSuggestion; | |
| for (var suggestion in suggestions) { | |
| if (suggestion.completion == completion) { | |
| if (matchingSuggestion == null) { | |
| matchingSuggestion = suggestion; | |
| } else { | |
| // It is OK to have a class and its default constructor suggestions. | |
| if (matchingSuggestion.element?.kind == ElementKind.CLASS && | |
| suggestion.element?.kind == ElementKind.CONSTRUCTOR || | |
| matchingSuggestion.element?.kind == ElementKind.CONSTRUCTOR && | |
| suggestion.element?.kind == ElementKind.CLASS) { | |
| return; | |
| } | |
| fail( | |
| "Expected exactly one '$completion' but found multiple:\n $suggestedCompletions", | |
| ); | |
| } | |
| } | |
| } | |
| if (matchingSuggestion == null) { | |
| fail("Expected '$completion' but found none:\n $suggestedCompletions"); | |
| } | |
| expect(matchingSuggestion.selectionOffset, equals(expectedOffset)); | |
| expect(matchingSuggestion.selectionLength, equals(0)); | |
| if (elementKind != null) { | |
| expect(matchingSuggestion.element!.kind, elementKind); | |
| } | |
| if (isDeprecated != null) { | |
| expect(matchingSuggestion.isDeprecated, isDeprecated); | |
| } | |
| if (libraryUri != null) { | |
| expect(matchingSuggestion.libraryUri, libraryUri); | |
| } | |
| } | |
| void assertHasNoCompletion(String completion) { | |
| if (suggestions.any( | |
| (CompletionSuggestion suggestion) => suggestion.completion == completion, | |
| )) { | |
| fail( | |
| "Did not expect completion '$completion' but found:\n $suggestedCompletions", | |
| ); | |
| } | |
| } | |
| /// Discard any results that do not start with the characters the user has | |
| /// "already typed". | |
| void filterResults(String content) { | |
| var charsAlreadyTyped = content | |
| .substring(replacementOffset!, completionOffset) | |
| .toLowerCase(); | |
| suggestions = suggestions | |
| .where( | |
| (CompletionSuggestion suggestion) => | |
| suggestion.completion.toLowerCase().startsWith(charsAlreadyTyped), | |
| ) | |
| .toList(); | |
| } | |
| Future<void> runTest( | |
| LocationSpec spec, [ | |
| Map<String, String>? extraFiles, | |
| ]) async { | |
| await super.setUp(); | |
| try { | |
| extraFiles?.forEach((String fileName, String content) { | |
| newFile(fileName, content); | |
| }); | |
| newFile(testFile.path, spec.source); | |
| completionOffset = spec.testLocation; | |
| await getSuggestions( | |
| path: testFile.path, | |
| completionOffset: completionOffset, | |
| maxResults: 1 << 10, | |
| ); | |
| filterResults(spec.source); | |
| for (var result in spec.positiveResults) { | |
| assertHasCompletion(result); | |
| } | |
| for (var result in spec.negativeResults) { | |
| assertHasNoCompletion(result); | |
| } | |
| } finally { | |
| await super.tearDown(); | |
| } | |
| } | |
| } | |
| /// A specification of the completion results expected at a given location. | |
| class LocationSpec { | |
| String id; | |
| int testLocation = -1; | |
| List<String> positiveResults = <String>[]; | |
| List<String> negativeResults = <String>[]; | |
| late String source; | |
| new(this.id); | |
| /// Parse a set of tests from the given `originalSource`. Return a list of the | |
| /// specifications that were parsed. | |
| /// | |
| /// The source string has test locations embedded in it, which are identified | |
| /// by '!X' where X is a single character. Each X is matched to positive or | |
| /// negative results in the array of [validationStrings]. Validation strings | |
| /// contain the name of a prediction with a two character prefix. The first | |
| /// character of the prefix corresponds to an X in the [originalSource]. The | |
| /// second character is either a '+' or a '-' indicating whether the string is | |
| /// a positive or negative result. If logical not is needed in the source it | |
| /// can be represented by '!!'. | |
| /// | |
| /// The [originalSource] is the source for a test that contains test | |
| /// locations. The [validationStrings] are the positive and negative | |
| /// predictions. | |
| static List<LocationSpec> from( | |
| String originalSource, | |
| List<String> validationStrings, | |
| ) { | |
| Map<String, LocationSpec> tests = HashMap<String, LocationSpec>(); | |
| var modifiedSource = originalSource; | |
| var modifiedPosition = 0; | |
| while (true) { | |
| var index = modifiedSource.indexOf('!', modifiedPosition); | |
| if (index < 0) { | |
| break; | |
| } | |
| var n = 1; // only delete one char for double-bangs | |
| var id = modifiedSource.substring(index + 1, index + 2); | |
| if (id != '!') { | |
| n = 2; | |
| var test = LocationSpec(id); | |
| tests[id] = test; | |
| test.testLocation = index; | |
| } else { | |
| modifiedPosition = index + 1; | |
| } | |
| modifiedSource = | |
| modifiedSource.substring(0, index) + | |
| modifiedSource.substring(index + n); | |
| } | |
| if (modifiedSource == originalSource) { | |
| throw StateError('No tests in source: $originalSource'); | |
| } | |
| for (var result in validationStrings) { | |
| if (result.length < 3) { | |
| throw StateError('Invalid location result: $result'); | |
| } | |
| var id = result.substring(0, 1); | |
| var sign = result.substring(1, 2); | |
| var value = result.substring(2); | |
| var test = tests[id]; | |
| if (test == null) { | |
| throw StateError('Invalid location result id: $id for: $result'); | |
| } | |
| test.source = modifiedSource; | |
| if (sign == '+') { | |
| test.positiveResults.add(value); | |
| } else if (sign == '-') { | |
| test.negativeResults.add(value); | |
| } else { | |
| var err = 'Invalid location result sign: $sign for: $result'; | |
| throw StateError(err); | |
| } | |
| } | |
| var badPoints = <String>[]; | |
| var badResults = <String>[]; | |
| for (var test in tests.values) { | |
| if (test.testLocation == -1) { | |
| badPoints.add(test.id); | |
| } | |
| if (test.positiveResults.isEmpty && test.negativeResults.isEmpty) { | |
| badResults.add(test.id); | |
| } | |
| } | |
| if (!(badPoints.isEmpty && badResults.isEmpty)) { | |
| var err = StringBuffer(); | |
| if (badPoints.isNotEmpty) { | |
| err.write('No test location for tests:'); | |
| for (var ch in badPoints) { | |
| err | |
| ..write(' ') | |
| ..write(ch); | |
| } | |
| err.write(' '); | |
| } | |
| if (badResults.isNotEmpty) { | |
| err.write('No results for tests:'); | |
| for (var ch in badResults) { | |
| err | |
| ..write(' ') | |
| ..write(ch); | |
| } | |
| } | |
| throw StateError(err.toString()); | |
| } | |
| return tests.values.toList(); | |
| } | |
| } |