| // Copyright (c) 2023, 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:io'; |
| |
| Never usage() { |
| print('Usage: ${Platform.executable} ${Platform.script} <test-file/folder>'); |
| exit(1); |
| } |
| |
| /// This script updates LINE_* and OFFSET_* constants in the given test or |
| /// directory. |
| void main(List<String> args) { |
| if (args.length != 1) { |
| usage(); |
| } |
| |
| final inputFolder = Directory(args[0]); |
| if (inputFolder.existsSync()) { |
| return processFolder(inputFolder); |
| } |
| final inputFile = File(args[0]); |
| if (inputFile.existsSync() && args[0].endsWith('.dart')) { |
| return processFile(inputFile); |
| } |
| usage(); |
| } |
| |
| void processFolder(Directory inputFolder) { |
| for (var file in inputFolder.listSync()) { |
| if (file is File && file.path.endsWith('.dart')) { |
| processFile(file); |
| } |
| } |
| } |
| |
| void processFile(File inputFile) { |
| final rawContent = inputFile.readAsStringSync(); |
| final content = rawContent.trim().split('\n'); |
| |
| const autogeneratedStart = '// AUTOGENERATED START'; |
| const autogeneratedEnd = '// AUTOGENERATED END'; |
| |
| final lineConstantPattern = RegExp(r'^const( int)? LINE_\w+ = \d+;$'); |
| final offsetConstantPattern = RegExp(r'^const( int)? OFFSET_\w+ = \d+;$'); |
| |
| final prefix = content |
| .takeWhile( |
| (line) => |
| !lineConstantPattern.hasMatch(line) && |
| !offsetConstantPattern.hasMatch(line) && |
| autogeneratedStart != line, |
| ) |
| .toList(); |
| |
| final suffix = content |
| .skip(prefix.length) |
| .skipWhile( |
| (line) => |
| line.startsWith('//') || |
| lineConstantPattern.hasMatch(line) || |
| offsetConstantPattern.hasMatch(line), |
| ) |
| .toList(); |
| |
| final lineCommentPattern = |
| RegExp(r' // (LINE_\w+)\.?$|/\*\s*(LINE_\w+)\s*\*/'); |
| final offsetCommentPattern = |
| RegExp(r'// (OFFSET_\w+)\.?$|(/\*\s*(OFFSET_\w+)\s*\*/\s*)'); |
| final firstNonSpace = RegExp(r'(\s*)\S'); |
| final lineMapping = <String, int>{}; |
| final offsetMapping = <String, int>{}; |
| var suffixTextLengthSoFar = 0; |
| for (var i = 0; i < suffix.length; i++) { |
| final line = suffix[i]; |
| final m = lineCommentPattern.firstMatch(line); |
| if (m != null) { |
| lineMapping[(m[1] ?? m[2])!] = i; |
| } |
| for (var match in offsetCommentPattern.allMatches(line)) { |
| if (match[1] != null) { |
| // E.g. '// OFFSET_FOO'. |
| // Points to the offset on the start of the next line. |
| offsetMapping[match[1]!] = suffixTextLengthSoFar + line.length + i + 1; |
| } else if (match[3] != null) { |
| // E.g. '/* OFFSET_INLINE */'. |
| // Points to the offset on the start of the next "word". |
| if (match.end == line.length && suffix.length > i + 1) { |
| // At end of line with another line. Go to the next "word" |
| // on the next line. |
| final nextLine = suffix[i + 1]; |
| offsetMapping[match[3]!] = suffixTextLengthSoFar + |
| line.length + |
| (firstNonSpace.firstMatch(nextLine)?.end ?? 0) + |
| i; |
| } else { |
| offsetMapping[match[3]!] = suffixTextLengthSoFar + match.end + i; |
| } |
| } |
| } |
| suffixTextLengthSoFar += line.length; |
| } |
| |
| if (lineMapping.isEmpty && offsetMapping.isEmpty) return; |
| |
| final fileUri = Uri.base.resolveUri(inputFile.uri); |
| final fileUriString = fileUri.toString(); |
| final sdkRootString = |
| Uri.base.resolveUri(Platform.script).resolve('../../../').toString(); |
| |
| var testFileString = '<test.dart>'; |
| if (sdkRootString.length < fileUriString.length && |
| fileUriString.substring(0, sdkRootString.length) == sdkRootString) { |
| testFileString = fileUriString.substring(sdkRootString.length); |
| } |
| |
| final header = [ |
| ...prefix, |
| autogeneratedStart, |
| '//', |
| '// Update these constants by running:', |
| '//', |
| '// dart pkg/vm_service/test/update_line_numbers.dart $testFileString', |
| '//', |
| ]; |
| |
| // Mapping currently contains 0 based indices into suffix. |
| // Convert them into 1 based line numbers taking into account that we will |
| // generate a header + one line for each LINE_* constant plus |
| // autogeneratedEnd marker. |
| lineMapping.updateAll( |
| (_, value) => |
| 1 + |
| header.length + |
| lineMapping.length + |
| offsetMapping.length + |
| 1 + |
| value, |
| ); |
| |
| final offsetFixedLengthInt = 4; |
| |
| if (offsetMapping.isNotEmpty) { |
| final newHeaderMockLength = [ |
| ...header, |
| for (var entry in lineMapping.entries) |
| 'const ${entry.key} = ${entry.value};', |
| for (var entry in offsetMapping.entries) |
| 'const ${entry.key} = ${entry.value.toString().padLeft(offsetFixedLengthInt, '0')};', |
| autogeneratedEnd, |
| ].join('\n').length; |
| |
| // Mapping currently contains 0 based char-indices into suffix. |
| // Convert them to take into account that we will |
| // generate a header + one line for each constant plus |
| // autogeneratedEnd marker. |
| offsetMapping.updateAll( |
| (_, value) => newHeaderMockLength + 1 + value, |
| ); |
| } |
| |
| final newContent = [ |
| ...header, |
| for (var entry in lineMapping.entries) |
| 'const ${entry.key} = ${entry.value};', |
| for (var entry in offsetMapping.entries) |
| 'const ${entry.key} = ${entry.value.toString().padLeft(offsetFixedLengthInt, '0')};', |
| autogeneratedEnd, |
| ...suffix, |
| '', |
| ].join('\n'); |
| if (newContent != rawContent) { |
| inputFile.writeAsString(newContent); |
| print('Updated: ${inputFile.path}'); |
| } else { |
| print('Unchanged: ${inputFile.path}'); |
| } |
| } |