| // 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:analyzer/dart/analysis/results.dart'; | |
| import 'package:analyzer/dart/ast/ast.dart'; | |
| import 'package:analyzer/source/line_info.dart'; | |
| import 'package:analyzer/source/source_range.dart'; | |
| import 'package:analyzer/src/test_utilities/platform.dart'; | |
| import 'package:collection/collection.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*/`). A single range can be marked with `[!` and | |
| /// `!]`, which behave the same as `/*[0*/` and `/*0]*/`. | |
| /// | |
| /// In addition, the pattern `/**/` will be removed from the test code. This is | |
| /// be used for two purposes. | |
| /// | |
| /// First, it can prevent certain code from being interpreted as markup. For | |
| /// example, if the test code includes `[!` (such as in a list literal whose | |
| /// first element begins with a unary prefix operator), you can use `[/**/!` to | |
| /// prevent the `[!` from being interpreted as markup. Similarly, you can use | |
| /// `!/**/]` in places where `!]` should appear in the unmarked code. To | |
| /// prevent a `^` from being interpreted as markup, you can use `^/**/`. | |
| /// | |
| /// Second, it can be used at the end of a line of code that contains trailing | |
| /// whitespace. Without some form of marker the formatter will remove the | |
| /// trailing whitespace. | |
| class TestCode { | |
| static final _positionShorthand = '^'; | |
| static final _positionPattern = RegExp(r'/\*(\d+)\*/'); | |
| static final _rangeStartShorthand = '[!'; | |
| static final _rangeEndShorthand = '!]'; | |
| static final _rangeStartPattern = RegExp(r'/\*\[(\d+)\*/'); | |
| static final _rangeEndPattern = RegExp(r'/\*(\d+)\]\*/'); | |
| static final _zeroWidthMarker = '/**/'; | |
| /// An empty code block with a single position at offset 0. | |
| static final empty = TestCode.parse('^'); | |
| /// The code with markers removed. | |
| final String code; | |
| /// The code with markers like `^`. | |
| /// These markers are used to fill [positions] and [ranges]. | |
| final String markedCode; | |
| /// 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.markedCode, | |
| required this.code, | |
| required this.positions, | |
| required this.ranges, | |
| }); | |
| TestCodePosition get position => positions.single; | |
| TestCodeRange get range => ranges.single; | |
| static TestCode parse( | |
| String markedCode, { | |
| bool positionShorthand = true, | |
| bool rangeShorthand = true, | |
| bool zeroWidthMarker = true, | |
| }) { | |
| var scanner = _StringScanner(markedCode); | |
| var codeBuffer = StringBuffer(); | |
| var positionOffsets = <int, int>{}; | |
| var rangeStartOffsets = <int, int>{}; | |
| var rangeEndOffsets = <int, int>{}; | |
| late int start; | |
| int scannedNumber() => int.parse(scanner.lastMatch![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 (positionShorthand && zeroWidthMarker && scanner.scan('^/**/')) { | |
| codeBuffer.write('^'); | |
| } else if (positionShorthand && scanner.scan(_positionShorthand)) { | |
| recordPosition(0); | |
| } else if (scanner.scan(_positionPattern)) { | |
| recordPosition(scannedNumber()); | |
| } else if (rangeShorthand && scanner.scan(_rangeStartShorthand)) { | |
| recordRangeStart(0); | |
| } else if (rangeShorthand && scanner.scan(_rangeEndShorthand)) { | |
| recordRangeEnd(0); | |
| } else if (scanner.scan(_rangeStartPattern)) { | |
| recordRangeStart(scannedNumber()); | |
| } else if (scanner.scan(_rangeEndPattern)) { | |
| recordRangeEnd(scannedNumber()); | |
| } else if (zeroWidthMarker && scanner.scan(_zeroWidthMarker)) { | |
| // Don't record any information about zero-width markers, simply remove | |
| // the marker from the unmarked code. | |
| } else { | |
| codeBuffer.writeCharCode(scanner.readChar()); | |
| } | |
| } | |
| var unendedRanges = rangeStartOffsets.keys | |
| .whereNot(rangeEndOffsets.keys.contains) | |
| .toList(); | |
| if (unendedRanges.isNotEmpty) { | |
| throw ArgumentError( | |
| 'Code contains range starts numbered $unendedRanges without ends', | |
| ); | |
| } | |
| var code = codeBuffer.toString(); | |
| var lineInfo = LineInfo.fromContent(code); | |
| var positions = positionOffsets.map( | |
| (number, offset) => MapEntry(number, TestCodePosition(lineInfo, offset)), | |
| ); | |
| var ranges = rangeStartOffsets.map( | |
| (number, offset) => MapEntry( | |
| number, | |
| TestCodeRange( | |
| lineInfo, | |
| code.substring(offset, rangeEndOffsets[number]!), | |
| SourceRange(offset, rangeEndOffsets[number]! - offset), | |
| ), | |
| ), | |
| ); | |
| return TestCode._( | |
| code: code, | |
| markedCode: markedCode, | |
| positions: positions.values.toList(), | |
| ranges: ranges.values.toList(), | |
| ); | |
| } | |
| /// A version of [TestCode.parse] that normalizes newlines in the code to | |
| /// match that for the current platform so that tests on Windows test using | |
| /// `\r\n` and tests on other platforms test using `\n`. | |
| static TestCode parseNormalized( | |
| String markedCode, { | |
| bool positionShorthand = true, | |
| bool rangeShorthand = true, | |
| bool zeroWidthMarker = true, | |
| }) { | |
| return parse( | |
| normalizeNewlinesForPlatform(markedCode), | |
| positionShorthand: positionShorthand, | |
| rangeShorthand: rangeShorthand, | |
| zeroWidthMarker: zeroWidthMarker, | |
| ); | |
| } | |
| } | |
| /// A marked position in the test code. | |
| class TestCodePosition { | |
| /// Line break information for the test code. | |
| final LineInfo lineInfo; | |
| /// The 0-based offset of the marker. | |
| /// | |
| /// Offsets are based on [TestCode.code], with all parsed markers removed. | |
| final int offset; | |
| TestCodePosition(this.lineInfo, this.offset); | |
| AstNode? findNode(ParsedUnitResult unitResult) { | |
| // ignore: analyzer_to_be_deprecated_use | |
| return unitResult.unit.nodeCovering(offset: offset); | |
| } | |
| } | |
| class TestCodeRange { | |
| /// Line break information for the test code. | |
| final LineInfo lineInfo; | |
| /// 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; | |
| TestCodeRange(this.lineInfo, this.text, this.sourceRange); | |
| } | |
| /// A simple scanner to read characters and [Pattern]s from a source string. | |
| class _StringScanner { | |
| final String _source; | |
| int _position = 0; | |
| int? _lastMatchPosition; | |
| Match? _lastMatch; | |
| _StringScanner(this._source); | |
| /// Returns whether the end of the string has been reached. | |
| bool get isDone => _position == _source.length; | |
| /// Returns the last match from scaling [scan] if the position has not | |
| /// advanced since. | |
| Match? get lastMatch { | |
| return _position == _lastMatchPosition ? _lastMatch : null; | |
| } | |
| /// Scans forwards a single character, returning its character code. | |
| int readChar() { | |
| if (isDone) { | |
| throw StateError('Unable to scan past the end of string'); | |
| } | |
| return _source.codeUnitAt(_position++); | |
| } | |
| /// Scans forwards to the end of the match if the string from the current | |
| /// position starts with [pattern]. | |
| /// | |
| /// Returns whether any match occurred. | |
| bool scan(Pattern pattern) { | |
| if (isDone) { | |
| throw StateError('Unable to scan past the end of string'); | |
| } | |
| var match = pattern.matchAsPrefix(_source, _position); | |
| if (match != null) { | |
| _position = match.end; | |
| _lastMatch = match; | |
| _lastMatchPosition = _position; | |
| return true; | |
| } | |
| return false; | |
| } | |
| } |