64a2e5c114
Change-Id: I45ca24d2360f7201a563145a19c5291f83b9465c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/259424 Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
183 lines
5.8 KiB
Dart
183 lines
5.8 KiB
Dart
// Copyright (c) 2022, 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/lsp_protocol/protocol.dart'
|
|
show Position, Range;
|
|
import 'package:analysis_server/src/lsp/mapping.dart';
|
|
import 'package:analyzer/source/line_info.dart';
|
|
import 'package:analyzer/source/source_range.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:string_scanner/string_scanner.dart';
|
|
|
|
/// A class for parsing and representing test code that contains special markup
|
|
/// to simplify marking up positions and ranges in test code.
|
|
///
|
|
/// Positions and ranges are marked with brackets inside block comments:
|
|
///
|
|
/// ```
|
|
/// position ::= '/*' integer '*/
|
|
/// rangeStart ::= '/*[' integer '*/
|
|
/// rangeEnd ::= '/*' integer ']*/
|
|
/// ```
|
|
///
|
|
/// Numbers start at 0 and positions and range starts must be consecutive.
|
|
/// The same numbers can be used to represent both positions and ranges.
|
|
///
|
|
/// For convenience, a single position can also be marked with a `^` (which
|
|
/// behaves the same as `/*0*/).
|
|
class TestCode {
|
|
static final _positionPattern = RegExp(r'\/\*(\d+)\*\/');
|
|
static final _rangeStartPattern = RegExp(r'\/\*\[(\d+)\*\/');
|
|
static final _rangeEndPattern = RegExp(r'\/\*(\d+)\]\*\/');
|
|
final String code;
|
|
final String rawCode;
|
|
|
|
/// A map of positions marked in code, indexed by their number.
|
|
final List<TestCodePosition> positions;
|
|
|
|
/// A map of ranges marked in code, indexed by their number.
|
|
final List<TestCodeRange> ranges;
|
|
|
|
TestCode._({
|
|
required this.rawCode,
|
|
required this.code,
|
|
required this.positions,
|
|
required this.ranges,
|
|
});
|
|
|
|
TestCodePosition get position => positions.single;
|
|
TestCodeRange get range => ranges.single;
|
|
|
|
static TestCode parse(String rawCode, {bool treatCaretAsPosition = true}) {
|
|
final scanner = StringScanner(rawCode);
|
|
final codeBuffer = StringBuffer();
|
|
final positionOffsets = <int, int>{};
|
|
final rangeStartOffsets = <int, int>{};
|
|
final rangeEndOffsets = <int, int>{};
|
|
late int start;
|
|
|
|
int scannedNumber() => int.parse(scanner.lastMatch!.group(1)!);
|
|
|
|
void recordPosition(int number) {
|
|
if (positionOffsets.containsKey(number)) {
|
|
throw ArgumentError(
|
|
'Code contains multiple positions numbered $number');
|
|
} else if (number > positionOffsets.length) {
|
|
throw ArgumentError(
|
|
'Code contains position numbered $number but expected ${positionOffsets.length}');
|
|
}
|
|
positionOffsets[number] = start;
|
|
}
|
|
|
|
void recordRangeStart(int number) {
|
|
if (rangeStartOffsets.containsKey(number)) {
|
|
throw ArgumentError(
|
|
'Code contains multiple range starts numbered $number');
|
|
} else if (number > rangeStartOffsets.length) {
|
|
throw ArgumentError(
|
|
'Code contains range start numbered $number but expected ${rangeStartOffsets.length}');
|
|
}
|
|
rangeStartOffsets[number] = start;
|
|
}
|
|
|
|
void recordRangeEnd(int number) {
|
|
if (rangeEndOffsets.containsKey(number)) {
|
|
throw ArgumentError(
|
|
'Code contains multiple range ends numbered $number');
|
|
}
|
|
if (!rangeStartOffsets.containsKey(number)) {
|
|
throw ArgumentError(
|
|
'Code contains range end numbered $number without a preceeding start');
|
|
}
|
|
rangeEndOffsets[number] = start;
|
|
}
|
|
|
|
while (!scanner.isDone) {
|
|
start = codeBuffer.length;
|
|
if (treatCaretAsPosition && scanner.scan('^')) {
|
|
recordPosition(0);
|
|
} else if (scanner.scan(_positionPattern)) {
|
|
recordPosition(scannedNumber());
|
|
} else if (scanner.scan(_rangeStartPattern)) {
|
|
recordRangeStart(scannedNumber());
|
|
} else if (scanner.scan(_rangeEndPattern)) {
|
|
recordRangeEnd(scannedNumber());
|
|
} else {
|
|
codeBuffer.writeCharCode(scanner.readChar());
|
|
}
|
|
}
|
|
|
|
final unendedRanges =
|
|
rangeStartOffsets.keys.whereNot(rangeEndOffsets.keys.contains).toList();
|
|
if (unendedRanges.isNotEmpty) {
|
|
throw ArgumentError(
|
|
'Code contains range starts numbered $unendedRanges without ends');
|
|
}
|
|
|
|
final code = codeBuffer.toString();
|
|
final lineInfo = LineInfo.fromContent(code);
|
|
|
|
final positions = positionOffsets.map(
|
|
(number, offset) => MapEntry(
|
|
number,
|
|
TestCodePosition(
|
|
offset,
|
|
toPosition(lineInfo.getLocation(offset)),
|
|
),
|
|
),
|
|
);
|
|
|
|
final ranges = rangeStartOffsets.map(
|
|
(number, offset) => MapEntry(
|
|
number,
|
|
TestCodeRange(
|
|
code.substring(offset, rangeEndOffsets[number]!),
|
|
SourceRange(offset, rangeEndOffsets[number]! - offset),
|
|
toRange(lineInfo, offset, rangeEndOffsets[number]! - offset),
|
|
),
|
|
),
|
|
);
|
|
|
|
return TestCode._(
|
|
code: code,
|
|
rawCode: rawCode,
|
|
positions: positions.values.toList(),
|
|
ranges: ranges.values.toList(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A marked position in the test code.
|
|
class TestCodePosition {
|
|
/// The 0-based offset of the marker.
|
|
///
|
|
/// Offsets are based on [TestCode.code], with all parsed markers removed.
|
|
final int offset;
|
|
|
|
/// The LSP [Position] of the marker.
|
|
///
|
|
/// Positions are based on [TestCode.code], with all parsed markers removed.
|
|
final Position lsp;
|
|
|
|
TestCodePosition(this.offset, this.lsp);
|
|
}
|
|
|
|
class TestCodeRange {
|
|
/// The text from [TestCode.code] covered by this range.
|
|
final String text;
|
|
|
|
/// The [SourceRange] indicated by the markers.
|
|
///
|
|
/// Offsets/lengths are based on [TestCode.code], with all parsed markers
|
|
/// removed.
|
|
final SourceRange sourceRange;
|
|
|
|
/// The LSP [Range] indicated by the markers.
|
|
///
|
|
/// Ranges are based on [TestCode.code], with all parsed markers removed.
|
|
final Range lsp;
|
|
|
|
TestCodeRange(this.text, this.sourceRange, this.lsp);
|
|
}
|