| // 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_testing/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 = []; | |
| new(); | |
| } | |
| 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 = {}; | |
| new(); | |
| 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.namePart.typeName.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.declaredFragment!.element; | |
| 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.body.members) { | |
| if (member is MethodDeclaration && member.isGetter) { | |
| var returnType = member.returnType; | |
| if (returnType != null && | |
| returnType.toSource().startsWith('NodeList<')) { | |
| nodeLists.add(member.declaredFragment!.element); | |
| } | |
| } | |
| } | |
| 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.namePart.typeName.lexeme == '_ChildrenFinder') { | |
| for (var member in declaration.body.members) { | |
| if (member is MethodDeclaration && | |
| member.name.lexeme.startsWith('visit')) { | |
| var visitedClass = member | |
| .parameters | |
| ?.parameters | |
| .first | |
| .declaredFragment | |
| ?.element | |
| .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 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 enclosingElement = | |
| visitedNodeList.enclosingElement as InterfaceElement; | |
| var overridden = enclosingElement.getOverridden( | |
| Name(visitedNodeList.library.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 = {}; | |
| new(); | |
| } | |
| class VisitMethodVisitor extends RecursiveAstVisitor<void> { | |
| List<ExecutableElement> visitedLists = []; | |
| new(); | |
| @override | |
| void visitMethodInvocation(MethodInvocation node) { | |
| if (node.methodName.name == '_fromList') { | |
| var argument = node.argumentList.arguments.first; | |
| if (argument is PrefixedIdentifier) { | |
| visitedLists.add(argument.identifier.element as ExecutableElement); | |
| } else if (argument is PropertyAccess) { | |
| visitedLists.add(argument.propertyName.element as ExecutableElement); | |
| } | |
| } | |
| } | |
| } |