blob: 4916f2a49d3f0203dd3bc508d842ee5f32045650 [file] [log] [blame]
// 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 'dart:io' as io;
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
class NodeTextExpectationsCollector {
static const updatingIsEnabled = true;
static const assertMethods = [
_AssertMethod(
className: 'ContextResolutionTest',
methodName: 'assertDriverStateString',
argumentIndex: 1,
),
_AssertMethod(
className: 'ElementsBaseTest',
methodName: 'checkElementText',
argumentIndex: 1,
),
_AssertMethod(
className: 'FileResolutionTest',
methodName: 'assertStateString',
argumentIndex: 0,
),
_AssertMethod(
className: 'IndexTest',
methodName: 'assertElementIndexText',
argumentIndex: 1,
),
_AssertMethod(
className: 'InheritanceManager3Test_ExtensionType',
methodName: 'assertInterfaceText',
argumentIndex: 1,
),
_AssertMethod(
className: 'MacroDeclarationsIntrospectTest',
methodName: '_assertIntrospectText',
argumentIndex: 1,
),
_AssertMethod(
className: 'MacroTypesIntrospectTest',
methodName: '_assertIntrospectText',
argumentIndex: 1,
),
_AssertMethod(
className: 'ParserDiagnosticsTest',
methodName: 'assertParsedNodeText',
argumentIndex: 1,
),
_AssertMethod(
className: 'ResolutionTest',
methodName: 'assertParsedNodeText',
argumentIndex: 1,
),
_AssertMethod(
className: 'ResolutionTest',
methodName: 'assertResolvedNodeText',
argumentIndex: 1,
),
_AssertMethod(
className: 'SearchTest',
methodName: 'assertDeclarationsText',
argumentIndex: 2,
),
_AssertMethod(
className: 'SearchTest',
methodName: 'assertElementReferencesText',
argumentIndex: 1,
),
_AssertMethod(
className: 'SearchTest',
methodName: 'assertUnresolvedMemberReferencesText',
argumentIndex: 1,
),
];
static final Map<String, _File> _files = {};
static void add(String actual) {
if (!updatingIsEnabled) {
return;
}
final traceLines = '${StackTrace.current}'.split('\n');
for (var traceIndex = 0; traceIndex < traceLines.length; traceIndex++) {
final traceLine = traceLines[traceIndex];
for (final assertMethod in assertMethods) {
if (!traceLine.contains(' ${assertMethod.stackTracePattern} ')) {
continue;
}
// Find the invocation of the assert method in the stack trace.
var invocationTraceIndex = traceIndex + 1;
if (traceLines[invocationTraceIndex] == '<asynchronous suspension>') {
invocationTraceIndex++;
}
final invocationTraceLine = traceLines[invocationTraceIndex];
// Parse the invocation stack trace line.
final locationMatch = RegExp(
r'(file://.+_test.dart):(\d+):',
).firstMatch(invocationTraceLine);
if (locationMatch == null) {
fail('Cannot parse: $invocationTraceLine');
}
final path = Uri.parse(locationMatch.group(1)!).toFilePath();
final line = int.parse(locationMatch.group(2)!);
final file = _getFile(path);
final invocation = file.findInvocation(
invocationLine: line,
);
if (invocation == null) {
fail('Cannot find MethodInvocation.');
}
if (invocation.methodName.name != assertMethod.methodName) {
fail(
'Expected: ${assertMethod.methodName}\n'
'Actual: ${invocation.methodName.name}\n',
);
}
final arguments = invocation.argumentList.arguments;
final argument = arguments[assertMethod.argumentIndex];
if (argument is! SimpleStringLiteral) {
fail('Not a literal: ${argument.runtimeType}');
}
file.addReplacement(
_Replacement(
argument.contentsOffset,
argument.contentsEnd,
actual,
),
);
}
}
}
static void _apply() {
for (final file in _files.values) {
file.applyReplacements();
}
_files.clear();
}
static _File _getFile(String path) {
return _files[path] ??= _File(path);
}
}
@reflectiveTest
class UpdateNodeTextExpectations {
test_applyReplacements() {
NodeTextExpectationsCollector._apply();
}
}
class _AssertMethod {
final String className;
final String methodName;
final int argumentIndex;
const _AssertMethod({
required this.className,
required this.methodName,
required this.argumentIndex,
});
String get stackTracePattern => '$className.$methodName';
}
class _File {
final String path;
final String content;
final LineInfo lineInfo;
final CompilationUnit unit;
final List<_Replacement> replacements = [];
factory _File(String path) {
final content = io.File(path).readAsStringSync();
final collection = AnalysisContextCollection(
resourceProvider: PhysicalResourceProvider.INSTANCE,
includedPaths: [path],
);
final analysisContext = collection.contextFor(path);
final analysisSession = analysisContext.currentSession;
final parseResult = analysisSession.getParsedUnit(path);
parseResult as ParsedUnitResult;
return _File._(
path: path,
content: content,
lineInfo: LineInfo.fromContent(content),
unit: parseResult.unit,
);
}
_File._({
required this.path,
required this.content,
required this.lineInfo,
required this.unit,
});
void addReplacement(_Replacement replacement) {
// Check if there is the same replacement.
for (final existing in replacements) {
if (existing.offset == replacement.offset) {
// Sanity check.
if (existing.end != replacement.end) {
fail(
'At offset: ${existing.offset}\n'
'Existing end: ${existing.end}\n'
'New end: ${replacement.end}\n',
);
}
if (existing.text != replacement.text) {
fail(
'At offset: ${existing.offset}\n'
'Existing text:\n${existing.text}\n'
'New text:\n${replacement.end}\n',
);
}
// We already have the same replacement, exit.
return;
}
}
// This is a new replacement, add it.
replacements.add(replacement);
}
void applyReplacements() {
replacements.sort((a, b) => b.offset - a.offset);
var newCode = content;
for (final replacement in replacements) {
newCode = newCode.substring(0, replacement.offset) +
replacement.text +
newCode.substring(replacement.end);
}
io.File(path).writeAsStringSync(newCode);
}
MethodInvocation? findInvocation({
required int invocationLine,
}) {
final visitor = _InvocationVisitor(
lineInfo: lineInfo,
requestedLine: invocationLine,
);
unit.accept(visitor);
return visitor.result;
}
}
class _InvocationVisitor extends RecursiveAstVisitor<void> {
final LineInfo lineInfo;
final int requestedLine;
MethodInvocation? result;
_InvocationVisitor({
required this.lineInfo,
required this.requestedLine,
});
@override
void visitMethodInvocation(MethodInvocation node) {
if (result != null) {
return;
}
final nodeLine = lineInfo.getLocation(node.offset).lineNumber;
if (nodeLine == requestedLine) {
result = node;
}
super.visitMethodInvocation(node);
}
}
class _Replacement {
final int offset;
final int end;
final String text;
_Replacement(this.offset, this.end, this.text);
}