Files
sdk/pkg/analysis_server/test/completion_test_support.dart
Paul Berry afcfbbeba8 Migrate developer experience packages to new constructor decl syntax.
(Part of https://github.com/dart-lang/sdk/issues/63288)

This change migrates the packages owned by the developer experience
team to use the new constructor declaration syntax, described in
https://github.com/dart-lang/language/blob/main/accepted/future-releases/primary-constructors/feature-specification.md#abbreviations-of-in-body-constructor-declarations.

This change was performed in an automated fashion, by (a) bumping the
packages' SDK constraints to `3.13.0-0`, (b) enabling the lints
`unnecessary_type_name_in_constructor` and
`unnecessary_const_in_enum_constructor`, (c) fixing the resulting lint
failures using `dart fix`, and then (d) reformatting the affected
files.

To ease code review, I've reverted unrelated formatting changes.

Since this change requires bumping SDK constaints to `3.13.0-0`, it
was only performed on packages that are *not* published on
pub. (Packages that *are* published on pub should remain on lower
language versions until at least after the stable version of 3.13 is
released, so that we don't block users on the stable channel from
receiving updates to those packages.)

Change-Id: Ibb4daebafd239da58251e838ea6a3f336a6a6964
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/505046
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
SLSA-Policy-Verified: SLSA Policy Verification Service <devtools-gerritcodereview-exitgate@google.com>
2026-05-27 14:52:58 -07:00

237 lines
7.6 KiB
Dart

// 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();
}
}