// 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 positions; /// A map of ranges marked in code, indexed by their number. final List 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 = {}; final rangeStartOffsets = {}; final rangeEndOffsets = {}; 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); }