blob: 7c01034b04ed7f7aadcc8d6c7a250ec2b241087e [file] [log] [blame]
// 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 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
import 'package:analyzer_utilities/package_root.dart' as package_root;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(SelectionCoverageTest);
});
}
class AstImplData {
final List<ClassElement> instantiableInterfaces = [];
AstImplData();
}
class AstInterfaceData {
/// A table mapping the element for a class to a list of the elements for the
/// supertypes of that class.
final Map<ClassElement, List<ClassElement>> supertypes = {};
/// A table mapping class element to a list of the getters declared directly
/// in that class that return a `NodeList`.
final Map<ClassElement, List<ExecutableElement>> declaredLists = {};
AstInterfaceData();
List<ExecutableElement> nodeListsFor(ClassElement class_) {
var lists = <ExecutableElement>[];
_addListsFor(lists, class_, <ClassElement>{});
return lists;
}
void _addListsFor(List<ExecutableElement> lists, ClassElement class_,
Set<ClassElement> visited) {
if (!visited.add(class_)) {
return;
}
lists.addAll(declaredLists[class_] ?? const []);
var supertypes = this.supertypes[class_];
if (supertypes != null) {
for (var supertype in supertypes) {
_addListsFor(lists, supertype, visited);
}
}
}
}
@reflectiveTest
class SelectionCoverageTest {
AstImplData processAstImpl(ResolvedUnitResult result) {
var data = AstImplData();
for (var declaration in result.unit.declarations) {
if (declaration is ClassDeclaration &&
declaration.abstractKeyword == null) {
var interfaceName = declaration.name.lexeme;
if (!interfaceName.endsWith('Impl')) {
continue;
}
interfaceName = interfaceName.substring(0, interfaceName.length - 4);
var implementsClause = declaration.implementsClause;
if (implementsClause != null) {
for (var type in implementsClause.interfaces) {
var element = type.type?.element;
if (element is ClassElement && element.name == interfaceName) {
data.instantiableInterfaces.add(element);
}
}
}
}
}
return data;
}
AstInterfaceData processAstInterface(ResolvedUnitResult result) {
var data = AstInterfaceData();
for (var declaration in result.unit.declarations) {
if (declaration is ClassDeclaration) {
// Build the subtype map.
var subtypeElement = declaration.declaredElement!;
var implementsClause = declaration.implementsClause;
if (implementsClause != null) {
for (var supertype in implementsClause.interfaces) {
var supertypeElement = supertype.type?.element;
if (supertypeElement is ClassElement) {
data.supertypes
.putIfAbsent(subtypeElement, () => [])
.add(supertypeElement);
}
}
}
// Build the node list map.
var nodeLists = <ExecutableElement>[];
for (var member in declaration.members) {
if (member is MethodDeclaration && member.isGetter) {
var returnType = member.returnType;
if (returnType != null &&
returnType.toSource().startsWith('NodeList<')) {
nodeLists.add(member.declaredElement!);
}
}
}
if (nodeLists.isNotEmpty) {
data.declaredLists[subtypeElement] = nodeLists;
}
}
}
return data;
}
SelectionData processSelection(ResolvedUnitResult result) {
var data = SelectionData();
for (var declaration in result.unit.declarations) {
if (declaration is ClassDeclaration &&
declaration.name.lexeme == '_ChildrenFinder') {
for (var member in declaration.members) {
if (member is MethodDeclaration &&
member.name.lexeme.startsWith('visit')) {
var visitedClass = member
.parameters?.parameters.first.declaredElement?.type.element;
if (visitedClass is ClassElement) {
var visitor = VisitMethodVisitor();
member.body.accept(visitor);
data.visitedLists[visitedClass] = visitor.visitedLists;
}
}
}
}
}
return data;
}
@TestTimeout(Timeout.factor(4))
Future<void> test_visitorCoverage() async {
var provider = PhysicalResourceProvider.INSTANCE;
var pathContext = provider.pathContext;
var packageRoot = pathContext.normalize(package_root.packageRoot);
var pathToAsInterface = pathContext.join(
packageRoot, 'analyzer', 'lib', 'dart', 'ast', 'ast.dart');
var pathToAsImpl = pathContext.join(
packageRoot, 'analyzer', 'lib', 'src', 'dart', 'ast', 'ast.dart');
var pathToSelection = pathContext.join(packageRoot, 'analysis_server',
'lib', 'src', 'utilities', 'selection.dart');
var collection =
AnalysisContextCollection(includedPaths: [pathToSelection]);
var context = collection.contexts.first;
var astInterfaceResult =
await context.currentSession.getResolvedUnit(pathToAsInterface);
var astImplResult =
await context.currentSession.getResolvedUnit(pathToAsImpl);
var selectionResult =
await context.currentSession.getResolvedUnit(pathToSelection);
var astInterfaceData =
processAstInterface(astInterfaceResult as ResolvedUnitResult);
var astImplData = processAstImpl(astImplResult as ResolvedUnitResult);
var selectionData = processSelection(selectionResult as ResolvedUnitResult);
var visitedLists = selectionData.visitedLists;
var inheritanceManager = InheritanceManager3();
var buffer = StringBuffer();
for (var interface in astImplData.instantiableInterfaces) {
if (interface.name == 'Comment' ||
interface.name == 'VariableDeclaration') {
// The class `Comment` has references, but we don't support selecting a
// portion of a comment in order to operate on it.
//
// The class `VariableDeclaration` has metadata, but the list is never
// populated, so we don't visit the class, and hence are required to
// special case it here. If the class hierarchy is ever cleaned up, we
// can remove this special casing.
continue;
}
var declaredNodeLists = astInterfaceData.nodeListsFor(interface);
if (declaredNodeLists.isEmpty) {
continue;
}
var visitedNodeLists = visitedLists[interface];
if (visitedNodeLists == null) {
var interfaceName = interface.name;
buffer.writeln('Missing implementation of visit$interfaceName:');
buffer.writeln();
buffer.writeln('@override');
buffer.writeln('void visit$interfaceName($interfaceName node) {');
for (var nodeList in declaredNodeLists) {
buffer.writeln(' _fromList(node.${nodeList.name});');
}
buffer.writeln('}');
buffer.writeln();
} else {
var unvisitedNodeLists = {...declaredNodeLists};
for (var visitedNodeList in visitedNodeLists) {
unvisitedNodeLists.remove(visitedNodeList);
var overridden = inheritanceManager.getOverridden2(
visitedNodeList.enclosingElement3 as InterfaceElement,
Name(visitedNodeList.library.source.uri, visitedNodeList.name));
if (overridden != null) {
unvisitedNodeLists.removeAll(overridden);
}
}
if (unvisitedNodeLists.isNotEmpty) {
buffer.writeln('Missing lines in visit${interface.name}:');
buffer.writeln();
for (var nodeList in unvisitedNodeLists) {
buffer.writeln(' _fromList(node.${nodeList.name});');
}
buffer.writeln();
}
}
}
if (buffer.length > 0) {
fail(buffer.toString());
}
}
}
class SelectionData {
final Map<ClassElement, List<ExecutableElement>> visitedLists = {};
SelectionData();
}
class VisitMethodVisitor extends RecursiveAstVisitor<void> {
List<ExecutableElement> visitedLists = [];
VisitMethodVisitor();
@override
void visitMethodInvocation(MethodInvocation node) {
if (node.methodName.name == '_fromList') {
var argument = node.argumentList.arguments.first;
if (argument is PrefixedIdentifier) {
visitedLists
.add(argument.identifier.staticElement as ExecutableElement);
} else if (argument is PropertyAccess) {
visitedLists
.add(argument.propertyName.staticElement as ExecutableElement);
}
}
}
}