// Copyright (c) 2020, 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/context_root.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/utilities/extensions/diagnostic.dart';
import 'package:args/args.dart';

/// Compute and print lexical and semantic information about a package.
Future<void> main(List<String> args) async {
  var parser = createArgParser();
  var result = parser.parse(args);

  if (validArguments(parser, result)) {
    var out = io.stdout;
    var rootPath = result.rest[0];
    out.writeln('Analyzing root: "$rootPath"');

    var computer = CodeShapeMetricsComputer();
    var stopwatch = Stopwatch();
    stopwatch.start();
    await computer.compute(rootPath);
    stopwatch.stop();
    var duration = Duration(milliseconds: stopwatch.elapsedMilliseconds);
    out.writeln('  Analysis performed in $duration');
    computer.writeResults(out);
    await out.flush();
  }
  io.exit(0);
}

/// Create a parser that can be used to parse the command-line arguments.
ArgParser createArgParser() {
  var parser = ArgParser();
  parser.addOption('help', abbr: 'h', help: 'Print this help message.');
  return parser;
}

/// Print usage information for this tool.
void printUsage(ArgParser parser, {String? error}) {
  if (error != null) {
    print(error);
    print('');
  }
  print('usage: dart code_metrics.dart [options] packagePath');
  print('');
  print('Compute and print lexical and semantic information about a package.');
  print('');
  print(parser.usage);
}

/// Return `true` if the command-line arguments (represented by the [result] and
/// parsed by the [parser]) are valid.
bool validArguments(ArgParser parser, ArgResults result) {
  if (result.wasParsed('help')) {
    printUsage(parser);
    return false;
  } else if (result.rest.length != 1) {
    printUsage(parser, error: 'No package path specified.');
    return false;
  }
  var rootPath = result.rest[0];
  if (!io.Directory(rootPath).existsSync()) {
    printUsage(parser, error: 'The directory "$rootPath" does not exist.');
    return false;
  }
  return true;
}

/// An object that records the data used to compute the metrics.
class CodeShapeData {
  /// A table mapping the name of a node class to the number of instances of
  /// that class that were visited.
  Map<String, int> nodeData = {};

  /// A table mapping the name of a node class and the name of the node's
  /// parent's class to the number of instances of the node class that had a
  /// parent of the parent class.
  Map<String, Map<String, int>> parentData = {};

  /// A table mapping the name of a node class, the name of a property of that
  /// node, and the class of the property's value to number of instances of the
  /// node class that had a value of the property class for the property.
  Map<String, Map<String, Map<String, int>>> childData = {};

  /// A table mapping the name of a node class, the name of a list-valued
  /// property of that node, and the length of the list to number of instances
  /// of the node class that had a value of that length for the property.
  Map<String, Map<String, Map<int, int>>> lengthData = {};

  /// Information about children of nodes that were not correctly recorded.
  Set<String> missedChildren = {};

  /// Initialize a newly created set of relevance data to be empty.
  new();

  /// Record that an element of the given [node] was found in the given
  /// [context].
  void recordNode(String nodeClassName, String property, AstNode node) {
    var childClass = node.runtimeType.toString();
    if (childClass.endsWith('Impl')) {
      childClass = childClass.substring(0, childClass.length - 4);
    }
    _recordChildData(nodeClassName, property, childClass);
  }

  /// Record that a node that is an instance of the [nodeClassName] was visited.
  void recordNodeClass(String nodeClassName) {
    nodeData[nodeClassName] = (nodeData[nodeClassName] ?? 0) + 1;
  }

  void recordNodeList(String nodeClassName, String property, NodeList list) {
    _recordListLength(nodeClassName, property, list);
    for (var element in list) {
      recordNode(nodeClassName, property, element);
    }
  }

  void recordParentClass(String nodeClassName, String parentClassName) {
    var classMap = parentData.putIfAbsent(nodeClassName, () => {});
    classMap[parentClassName] = (classMap[parentClassName] ?? 0) + 1;
  }

  void recordToken(String nodeClassName, String property, Token? token) {
    var lexeme = token?.lexeme ?? 'null';
    _recordChildData(nodeClassName, property, lexeme);
  }

  void _recordChildData(
    String nodeClassName,
    String property,
    String childValue,
  ) {
    var classMap = childData.putIfAbsent(nodeClassName, () => {});
    var propertyMap = classMap.putIfAbsent(property, () => {});
    propertyMap[childValue] = (propertyMap[childValue] ?? 0) + 1;
  }

  /// Record that an element of the given [node] was found in the given
  /// [context].
  void _recordListLength(String nodeClassName, String property, NodeList list) {
    var classMap = lengthData.putIfAbsent(nodeClassName, () => {});
    var propertyMap = classMap.putIfAbsent(property, () => {});
    var length = list.length;
    propertyMap[length] = (propertyMap[length] ?? 0) + 1;
  }
}

/// An object that visits a compilation unit in order to record the data used to
/// compute the metrics.
class CodeShapeDataCollector extends RecursiveAstVisitor<void> {
  /// The data being collected.
  final CodeShapeData data;

  /// Initialize a newly created collector to add data points to the given
  /// [data].
  new(this.data);

  @override
  void visitAdjacentStrings(AdjacentStrings node) {
    _visitChildren(node, {'strings': node.strings});
    super.visitAdjacentStrings(node);
  }

  @override
  void visitAnnotation(Annotation node) {
    _visitChildren(node, {
      'name': node.name,
      'constructorName': node.constructorName,
      'arguments': node.arguments,
    });
    super.visitAnnotation(node);
  }

  @override
  void visitArgumentList(ArgumentList node) {
    _visitChildren(node, {'arguments': node.arguments});
    super.visitArgumentList(node);
  }

  @override
  void visitAsExpression(AsExpression node) {
    _visitChildren(node, {'expression': node.expression, 'type': node.type});
    super.visitAsExpression(node);
  }

  @override
  void visitAssertInitializer(AssertInitializer node) {
    _visitChildren(node, {
      'condition': node.condition,
      'message': node.message,
    });
    super.visitAssertInitializer(node);
  }

  @override
  void visitAssertStatement(AssertStatement node) {
    _visitChildren(node, {
      'condition': node.condition,
      'message': node.message,
    });
    super.visitAssertStatement(node);
  }

  @override
  void visitAssignmentExpression(AssignmentExpression node) {
    _visitChildren(node, {
      'leftHandSide': node.leftHandSide,
      'operator': node.operator,
      'rightHandSide': node.rightHandSide,
    });
    super.visitAssignmentExpression(node);
  }

  @override
  void visitAwaitExpression(AwaitExpression node) {
    _visitChildren(node, {'expression': node.expression});
    super.visitAwaitExpression(node);
  }

  @override
  void visitBinaryExpression(BinaryExpression node) {
    _visitChildren(node, {
      'leftOperand': node.leftOperand,
      'operator': node.operator,
      'rightOperand': node.rightOperand,
    });
    super.visitBinaryExpression(node);
  }

  @override
  void visitBlock(Block node) {
    _visitChildren(node, {'statement': node.statements});
    super.visitBlock(node);
  }

  @override
  void visitBlockFunctionBody(BlockFunctionBody node) {
    _visitChildren(node, {
      'keyword': node.keyword,
      'star': node.star,
      'block': node.block,
    });
    super.visitBlockFunctionBody(node);
  }

  @override
  void visitBooleanLiteral(BooleanLiteral node) {
    _visitChildren(node, {'literal': node.literal});
    super.visitBooleanLiteral(node);
  }

  @override
  void visitBreakStatement(BreakStatement node) {
    _visitChildren(node, {'label': node.label});
    super.visitBreakStatement(node);
  }

  @override
  void visitCascadeExpression(CascadeExpression node) {
    _visitChildren(node, {
      'target': node.target,
      'cascadeSections': node.cascadeSections,
    });
    super.visitCascadeExpression(node);
  }

  @override
  void visitCatchClause(CatchClause node) {
    _visitChildren(node, {
      'exceptionType': node.exceptionType,
      'exceptionParameter': node.exceptionParameter,
      'stackTraceParameter': node.stackTraceParameter,
      'body': node.body,
    });
    super.visitCatchClause(node);
  }

  @override
  void visitClassDeclaration(ClassDeclaration node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'abstractKeyword': node.abstractKeyword,
      'name': node.namePart.typeName,
      'typeParameters': node.namePart.typeParameters,
      'extendsClause': node.extendsClause,
      'withClause': node.withClause,
      'implementsClause': node.implementsClause,
      'nativeClause': node.nativeClause,
      'members': node.body.members,
    });
    super.visitClassDeclaration(node);
  }

  @override
  void visitClassTypeAlias(ClassTypeAlias node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'abstractKeyword': node.abstractKeyword,
      'name': node.name,
      'typeParameters': node.typeParameters,
      'superclass': node.superclass,
      'withClause': node.withClause,
      'implementsClause': node.implementsClause,
    });
    super.visitClassTypeAlias(node);
  }

  @override
  void visitComment(Comment node) {
    _visitChildren(node, {'references': node.references});
    super.visitComment(node);
  }

  @override
  void visitCommentReference(CommentReference node) {
    _visitChildren(node, {
      'newKeyword': node.newKeyword,
      'expression': node.expression,
    });
    super.visitCommentReference(node);
  }

  @override
  void visitCompilationUnit(CompilationUnit node) {
    _visitChildren(node, {
      'scriptTag': node.scriptTag,
      'directives': node.directives,
      'declarations': node.declarations,
    });
    super.visitCompilationUnit(node);
  }

  @override
  void visitConditionalExpression(ConditionalExpression node) {
    _visitChildren(node, {
      'condition': node.condition,
      'thenExpression': node.thenExpression,
      'elseExpression': node.elseExpression,
    });
    super.visitConditionalExpression(node);
  }

  @override
  void visitConfiguration(Configuration node) {
    _visitChildren(node, {
      'name': node.name,
      'value': node.value,
      'uri': node.uri,
    });
    super.visitConfiguration(node);
  }

  @override
  void visitConstructorDeclaration(ConstructorDeclaration node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'externalKeyword': node.externalKeyword,
      'constKeyword': node.constKeyword,
      'factoryKeyword': node.factoryKeyword,
      'returnType': ?node.typeName,
      'name': node.name,
      'parameters': node.parameters,
      'initializers': node.initializers,
      'redirectedConstructor': node.redirectedConstructor,
      'body': node.body,
    });
    super.visitConstructorDeclaration(node);
  }

  @override
  void visitConstructorFieldInitializer(ConstructorFieldInitializer node) {
    _visitChildren(node, {
      'thisKeyword': node.thisKeyword,
      'fieldName': node.fieldName,
      'expression': node.expression,
    });
    super.visitConstructorFieldInitializer(node);
  }

  @override
  void visitConstructorName(ConstructorName node) {
    _visitChildren(node, {'type': node.type, 'name': node.name});
    super.visitConstructorName(node);
  }

  @override
  void visitContinueStatement(ContinueStatement node) {
    _visitChildren(node, {'label': node.label});
    super.visitContinueStatement(node);
  }

  @override
  void visitDeclaredIdentifier(DeclaredIdentifier node) {
    _visitChildren(node, {
      'keyword': node.keyword,
      'type': node.type,
      'name': node.name,
    });
    super.visitDeclaredIdentifier(node);
  }

  @override
  void visitDoStatement(DoStatement node) {
    _visitChildren(node, {'body': node.body, 'condition': node.condition});
    super.visitDoStatement(node);
  }

  @override
  void visitDottedName(DottedName node) {
    _visitChildren(node, {});
    super.visitDottedName(node);
  }

  @override
  void visitDoubleLiteral(DoubleLiteral node) {
    _visitChildren(node, {});
    super.visitDoubleLiteral(node);
  }

  @override
  void visitEmptyFunctionBody(EmptyFunctionBody node) {
    _visitChildren(node, {});
    super.visitEmptyFunctionBody(node);
  }

  @override
  void visitEmptyStatement(EmptyStatement node) {
    _visitChildren(node, {});
    super.visitEmptyStatement(node);
  }

  @override
  void visitEnumConstantDeclaration(EnumConstantDeclaration node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'name': node.name,
    });
    super.visitEnumConstantDeclaration(node);
  }

  @override
  void visitEnumDeclaration(EnumDeclaration node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'name': node.namePart.typeName,
      'constants': node.body.constants,
    });
    super.visitEnumDeclaration(node);
  }

  @override
  void visitExportDirective(ExportDirective node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'uri': node.uri,
      'configurations': node.configurations,
      'combinators': node.combinators,
    });
    super.visitExportDirective(node);
  }

  @override
  void visitExpressionFunctionBody(ExpressionFunctionBody node) {
    _visitChildren(node, {
      'keyword': node.keyword,
      'star': node.star,
      'expression': node.expression,
    });
    super.visitExpressionFunctionBody(node);
  }

  @override
  void visitExpressionStatement(ExpressionStatement node) {
    _visitChildren(node, {'expression': node.expression});
    super.visitExpressionStatement(node);
  }

  @override
  void visitExtendsClause(ExtendsClause node) {
    _visitChildren(node, {'superclass': node.superclass});
    super.visitExtendsClause(node);
  }

  @override
  void visitExtensionDeclaration(ExtensionDeclaration node) {
    _visitChildren(node, {
      'name': node.name,
      'typeParameters': node.typeParameters,
      'onClause': node.onClause,
      'member': node.body.members,
    });
    super.visitExtensionDeclaration(node);
  }

  @override
  void visitExtensionOverride(ExtensionOverride node) {
    _visitChildren(node, {
      'importPrefix': node.importPrefix,
      'name': node.name,
      'typeArguments': node.typeArguments,
      'argumentList': node.argumentList,
    });
    super.visitExtensionOverride(node);
  }

  @override
  void visitFieldDeclaration(FieldDeclaration node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'abstractKeyword': node.abstractKeyword,
      'covariantKeyword': node.covariantKeyword,
      'externalKeyword': node.externalKeyword,
      'staticKeyword': node.staticKeyword,
      'fields': node.fields,
    });
    super.visitFieldDeclaration(node);
  }

  @override
  void visitFieldFormalParameter(FieldFormalParameter node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'covariantKeyword': node.covariantKeyword,
      'requiredKeyword': node.requiredKeyword,
      'constFinalOrVarKeyword': node.constFinalOrVarKeyword,
      'type': node.type,
      'thisKeyword': node.thisKeyword,
      'period': node.period,
      'name': node.name,
      'functionTypedSuffix': node.functionTypedSuffix,
      'defaultClause': node.defaultClause,
    });
    super.visitFieldFormalParameter(node);
  }

  @override
  void visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) {
    _visitChildren(node, {
      'loopVariable': node.loopVariable,
      'iterable': node.iterable,
    });
    super.visitForEachPartsWithDeclaration(node);
  }

  @override
  void visitForEachPartsWithIdentifier(ForEachPartsWithIdentifier node) {
    _visitChildren(node, {
      'identifier': node.identifier,
      'iterable': node.iterable,
    });
    super.visitForEachPartsWithIdentifier(node);
  }

  @override
  void visitForElement(ForElement node) {
    _visitChildren(node, {
      'awaitKeyword': node.awaitKeyword,
      'forLoopParts': node.forLoopParts,
      'body': node.body,
    });
    super.visitForElement(node);
  }

  @override
  void visitFormalParameterDefaultClause(FormalParameterDefaultClause node) {
    _visitChildren(node, {'separator': node.separator, 'value': node.value});
    super.visitFormalParameterDefaultClause(node);
  }

  @override
  void visitFormalParameterList(FormalParameterList node) {
    _visitChildren(node, {'parameters': node.parameters});
    super.visitFormalParameterList(node);
  }

  @override
  void visitForPartsWithDeclarations(ForPartsWithDeclarations node) {
    _visitChildren(node, {
      'variables': node.variables,
      'condition': node.condition,
      'updater': node.updaters,
    });
    super.visitForPartsWithDeclarations(node);
  }

  @override
  void visitForPartsWithExpression(ForPartsWithExpression node) {
    _visitChildren(node, {
      'initialization': node.initialization,
      'condition': node.condition,
      'updater': node.updaters,
    });
    super.visitForPartsWithExpression(node);
  }

  @override
  void visitForStatement(ForStatement node) {
    _visitChildren(node, {
      'awaitKeyword': node.awaitKeyword,
      'forLoopParts': node.forLoopParts,
      'body': node.body,
    });
    super.visitForStatement(node);
  }

  @override
  void visitFunctionDeclaration(FunctionDeclaration node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'externalKeyword': node.externalKeyword,
      'propertyKeyword': node.propertyKeyword,
      'name': node.name,
      'functionExpression': node.functionExpression,
      'returnType': node.returnType,
    });
    super.visitFunctionDeclaration(node);
  }

  @override
  void visitFunctionDeclarationStatement(FunctionDeclarationStatement node) {
    _visitChildren(node, {'functionDeclaration': node.functionDeclaration});
    super.visitFunctionDeclarationStatement(node);
  }

  @override
  void visitFunctionExpression(FunctionExpression node) {
    _visitChildren(node, {
      'typeParameters': node.typeParameters,
      'parameters': node.parameters,
      'body': node.body,
    });
    super.visitFunctionExpression(node);
  }

  @override
  void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) {
    _visitChildren(node, {
      'function': node.function,
      'typeArguments': node.typeArguments,
      'argumentList': node.argumentList,
    });
    super.visitFunctionExpressionInvocation(node);
  }

  @override
  void visitFunctionTypeAlias(FunctionTypeAlias node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'returnType': node.returnType,
      'name': node.name,
      'typeParameters': node.typeParameters,
      'parameters': node.parameters,
    });
    super.visitFunctionTypeAlias(node);
  }

  @override
  void visitFunctionTypedFormalParameterSuffix(
    FunctionTypedFormalParameterSuffix node,
  ) {
    _visitChildren(node, {
      'typeParameters': node.typeParameters,
      'formalParameters': node.formalParameters,
      'question': node.question,
    });
    super.visitFunctionTypedFormalParameterSuffix(node);
  }

  @override
  void visitGenericFunctionType(GenericFunctionType node) {
    _visitChildren(node, {
      'returnType': node.returnType,
      'typeParameters': node.typeParameters,
      'parameters': node.parameters,
      'question': node.question,
    });
    super.visitGenericFunctionType(node);
  }

  @override
  void visitGenericTypeAlias(GenericTypeAlias node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'name': node.name,
      'typeParameters': node.typeParameters,
      'functionType': node.functionType,
    });
    super.visitGenericTypeAlias(node);
  }

  @override
  void visitHideCombinator(HideCombinator node) {
    _visitChildren(node, {'hiddenNames': node.hiddenNames});
    super.visitHideCombinator(node);
  }

  @override
  void visitIfElement(IfElement node) {
    _visitChildren(node, {
      'condition': node.expression,
      'thenElement': node.thenElement,
      'elseElement': node.elseElement,
    });
    super.visitIfElement(node);
  }

  @override
  void visitIfStatement(IfStatement node) {
    _visitChildren(node, {
      'condition': node.expression,
      'thenStatement': node.thenStatement,
      'elseStatement': node.elseStatement,
    });
    super.visitIfStatement(node);
  }

  @override
  void visitImplementsClause(ImplementsClause node) {
    _visitChildren(node, {'interfaces': node.interfaces});
    super.visitImplementsClause(node);
  }

  @override
  void visitImportDirective(ImportDirective node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'uri': node.uri,
      'deferredKeyword': node.deferredKeyword,
      'asKeyword': node.asKeyword,
      'prefix': node.prefix,
      'configurations': node.configurations,
      'combinators': node.combinators,
    });
    super.visitImportDirective(node);
  }

  @override
  void visitIndexExpression(IndexExpression node) {
    _visitChildren(node, {'target': node.target, 'index': node.index});
    super.visitIndexExpression(node);
  }

  @override
  void visitInstanceCreationExpression(InstanceCreationExpression node) {
    _visitChildren(node, {
      'keyword': node.keyword,
      'constructorName': node.constructorName,
      'argumentList': node.argumentList,
    });
    super.visitInstanceCreationExpression(node);
  }

  @override
  void visitIntegerLiteral(IntegerLiteral node) {
    var value = node.value;
    if (value == -1 || value == 0 || value == 1) {
      _visitChildren(node, {'literal': node.literal});
    } else {
      _visitChildren(node, {});
    }
    super.visitIntegerLiteral(node);
  }

  @override
  void visitInterpolationExpression(InterpolationExpression node) {
    _visitChildren(node, {'expression': node.expression});
    super.visitInterpolationExpression(node);
  }

  @override
  void visitInterpolationString(InterpolationString node) {
    _visitChildren(node, {});
    super.visitInterpolationString(node);
  }

  @override
  void visitIsExpression(IsExpression node) {
    _visitChildren(node, {'expression': node.expression, 'type': node.type});
    super.visitIsExpression(node);
  }

  @override
  void visitLabel(Label node) {
    _visitChildren(node, {'label': node.name});
    super.visitLabel(node);
  }

  @override
  void visitLabeledStatement(LabeledStatement node) {
    _visitChildren(node, {'statement': node.statement});
    super.visitLabeledStatement(node);
  }

  @override
  void visitLibraryDirective(LibraryDirective node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'name': node.name,
    });
    super.visitLibraryDirective(node);
  }

  @override
  void visitListLiteral(ListLiteral node) {
    _visitChildren(node, {
      'typeArguments': node.typeArguments,
      'elements': node.elements,
    });
    super.visitListLiteral(node);
  }

  @override
  void visitMapLiteralEntry(MapLiteralEntry node) {
    _visitChildren(node, {'key': node.key, 'value': node.value});
    super.visitMapLiteralEntry(node);
  }

  @override
  void visitMethodDeclaration(MethodDeclaration node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'externalKeyword': node.externalKeyword,
      'modifierKeyword': node.modifierKeyword,
      'returnType': node.returnType,
      'name': node.name,
      'operatorKeyword': node.operatorKeyword,
      'typeParameters': node.typeParameters,
      'parameters': node.parameters,
      'body': node.body,
    });
    super.visitMethodDeclaration(node);
  }

  @override
  void visitMethodInvocation(MethodInvocation node) {
    _visitChildren(node, {
      'target': node.target,
      'methodName': node.methodName,
      'typeArguments': node.typeArguments,
      'argumentList': node.argumentList,
    });
    super.visitMethodInvocation(node);
  }

  @override
  void visitMixinDeclaration(MixinDeclaration node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'name': node.name,
      'typeParameters': node.typeParameters,
      'onClause': node.onClause,
      'implementsClause': node.implementsClause,
      'members': node.body.members,
    });
    super.visitMixinDeclaration(node);
  }

  @override
  void visitMixinOnClause(MixinOnClause node) {
    _visitChildren(node, {'superclassConstraints': node.superclassConstraints});
    super.visitMixinOnClause(node);
  }

  @override
  void visitNamedArgument(NamedArgument node) {
    _visitChildren(node, {'argumentExpression': node.argumentExpression});
    super.visitNamedArgument(node);
  }

  @override
  void visitNamedType(NamedType node) {
    _visitChildren(node, {
      'importPrefix': node.importPrefix,
      'name': node.name,
      'typeArguments': node.typeArguments,
      'question': node.question,
    });
    super.visitNamedType(node);
  }

  @override
  void visitNativeClause(NativeClause node) {
    _visitChildren(node, {'name': node.name});
    super.visitNativeClause(node);
  }

  @override
  void visitNativeFunctionBody(NativeFunctionBody node) {
    _visitChildren(node, {});
    super.visitNativeFunctionBody(node);
  }

  @override
  void visitNullLiteral(NullLiteral node) {
    _visitChildren(node, {});
    super.visitNullLiteral(node);
  }

  @override
  void visitParenthesizedExpression(ParenthesizedExpression node) {
    _visitChildren(node, {'expression': node.expression});
    super.visitParenthesizedExpression(node);
  }

  @override
  void visitPartDirective(PartDirective node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'uri': node.uri,
    });
    super.visitPartDirective(node);
  }

  @override
  void visitPartOfDirective(PartOfDirective node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'libraryName': node.libraryName,
      'uri': node.uri,
    });
    super.visitPartOfDirective(node);
  }

  @override
  void visitPostfixExpression(PostfixExpression node) {
    _visitChildren(node, {'operand': node.operand, 'operator': node.operator});
    super.visitPostfixExpression(node);
  }

  @override
  void visitPrefixedIdentifier(PrefixedIdentifier node) {
    _visitChildren(node, {
      'prefix': node.prefix,
      'identifier': node.identifier,
    });
    super.visitPrefixedIdentifier(node);
  }

  @override
  void visitPrefixExpression(PrefixExpression node) {
    _visitChildren(node, {'operator': node.operator, 'operand': node.operand});
    super.visitPrefixExpression(node);
  }

  @override
  void visitPropertyAccess(PropertyAccess node) {
    _visitChildren(node, {
      'target': node.target,
      'propertyName': node.propertyName,
    });
    super.visitPropertyAccess(node);
  }

  @override
  void visitRedirectingConstructorInvocation(
    RedirectingConstructorInvocation node,
  ) {
    _visitChildren(node, {
      'constructorName': node.constructorName,
      'argumentList': node.argumentList,
    });
    super.visitRedirectingConstructorInvocation(node);
  }

  @override
  void visitRegularFormalParameter(RegularFormalParameter node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'covariantKeyword': node.covariantKeyword,
      'requiredKeyword': node.requiredKeyword,
      'constFinalOrVarKeyword': node.constFinalOrVarKeyword,
      'type': node.type,
      'name': node.name,
      'functionTypedSuffix': node.functionTypedSuffix,
      'defaultClause': node.defaultClause,
    });
    super.visitRegularFormalParameter(node);
  }

  @override
  void visitRethrowExpression(RethrowExpression node) {
    _visitChildren(node, {});
    super.visitRethrowExpression(node);
  }

  @override
  void visitReturnStatement(ReturnStatement node) {
    _visitChildren(node, {'expression': node.expression});
    super.visitReturnStatement(node);
  }

  @override
  void visitScriptTag(ScriptTag node) {
    _visitChildren(node, {});
    super.visitScriptTag(node);
  }

  @override
  void visitSetOrMapLiteral(SetOrMapLiteral node) {
    _visitChildren(node, {
      'typeArguments': node.typeArguments,
      'elements': node.elements,
    });
    super.visitSetOrMapLiteral(node);
  }

  @override
  void visitShowCombinator(ShowCombinator node) {
    _visitChildren(node, {'shownNames': node.shownNames});
    super.visitShowCombinator(node);
  }

  @override
  void visitSimpleStringLiteral(SimpleStringLiteral node) {
    _visitChildren(node, {});
    super.visitSimpleStringLiteral(node);
  }

  @override
  void visitSpreadElement(SpreadElement node) {
    _visitChildren(node, {'expression': node.expression});
    super.visitSpreadElement(node);
  }

  @override
  void visitStringInterpolation(StringInterpolation node) {
    _visitChildren(node, {'elements': node.elements});
    super.visitStringInterpolation(node);
  }

  @override
  void visitSuperConstructorInvocation(SuperConstructorInvocation node) {
    _visitChildren(node, {
      'constructorName': node.constructorName,
      'argumentList': node.argumentList,
    });
    super.visitSuperConstructorInvocation(node);
  }

  @override
  void visitSuperExpression(SuperExpression node) {
    _visitChildren(node, {});
    super.visitSuperExpression(node);
  }

  @override
  void visitSuperFormalParameter(SuperFormalParameter node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'covariantKeyword': node.covariantKeyword,
      'requiredKeyword': node.requiredKeyword,
      'constFinalOrVarKeyword': node.constFinalOrVarKeyword,
      'type': node.type,
      'superKeyword': node.superKeyword,
      'period': node.period,
      'name': node.name,
      'functionTypedSuffix': node.functionTypedSuffix,
      'defaultClause': node.defaultClause,
    });
    super.visitSuperFormalParameter(node);
  }

  @override
  void visitSwitchCase(SwitchCase node) {
    _visitChildren(node, {
      'labels': node.labels,
      'expression': node.expression,
      'statements': node.statements,
    });
    super.visitSwitchCase(node);
  }

  @override
  void visitSwitchDefault(SwitchDefault node) {
    _visitChildren(node, {'statements': node.statements});
    super.visitSwitchDefault(node);
  }

  @override
  void visitSwitchStatement(SwitchStatement node) {
    _visitChildren(node, {
      'expression': node.expression,
      'members': node.members,
    });
    super.visitSwitchStatement(node);
  }

  @override
  void visitSymbolLiteral(SymbolLiteral node) {
    _visitChildren(node, {});
    super.visitSymbolLiteral(node);
  }

  @override
  void visitThisExpression(ThisExpression node) {
    _visitChildren(node, {});
    super.visitThisExpression(node);
  }

  @override
  void visitThrowExpression(ThrowExpression node) {
    _visitChildren(node, {'expression': node.expression});
    super.visitThrowExpression(node);
  }

  @override
  void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'externalKeyword': node.externalKeyword,
      'variables': node.variables,
    });
    super.visitTopLevelVariableDeclaration(node);
  }

  @override
  void visitTryStatement(TryStatement node) {
    _visitChildren(node, {
      'body': node.body,
      'catchClauses': node.catchClauses,
      'finallyBlock': node.finallyBlock,
    });
    super.visitTryStatement(node);
  }

  @override
  void visitTypeArgumentList(TypeArgumentList node) {
    _visitChildren(node, {'arguments': node.arguments});
    super.visitTypeArgumentList(node);
  }

  @override
  void visitTypeParameter(TypeParameter node) {
    _visitChildren(node, {'name': node.name, 'bound': node.bound});
    super.visitTypeParameter(node);
  }

  @override
  void visitTypeParameterList(TypeParameterList node) {
    _visitChildren(node, {'typeParameters': node.typeParameters});
    super.visitTypeParameterList(node);
  }

  @override
  void visitVariableDeclaration(VariableDeclaration node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'name': node.name,
      'initializer': node.initializer,
    });
    super.visitVariableDeclaration(node);
  }

  @override
  void visitVariableDeclarationList(VariableDeclarationList node) {
    _visitChildren(node, {
      'documentationComment': node.documentationComment,
      'metadata': node.metadata,
      'type': node.type,
      'variables': node.variables,
    });
    super.visitVariableDeclarationList(node);
  }

  @override
  void visitVariableDeclarationStatement(VariableDeclarationStatement node) {
    _visitChildren(node, {'variables': node.variables});
    super.visitVariableDeclarationStatement(node);
  }

  @override
  void visitWhileStatement(WhileStatement node) {
    _visitChildren(node, {'condition': node.condition, 'body': node.body});
    super.visitWhileStatement(node);
  }

  @override
  void visitWithClause(WithClause node) {
    _visitChildren(node, {'mixinTypes': node.mixinTypes});
    super.visitWithClause(node);
  }

  @override
  void visitYieldStatement(YieldStatement node) {
    _visitChildren(node, {'star': node.star, 'expression': node.expression});
    super.visitYieldStatement(node);
  }

  String _className(AstNode node) {
    var className = node.runtimeType.toString();
    if (className.endsWith('Impl')) {
      className = className.substring(0, className.length - 4);
    }
    return className;
  }

  /// Visit the children of a node. The node is an instance of the class named
  /// [parentClass] and the children are in the [childMap], keyed by the name of
  /// the child property.
  void _visitChildren(AstNode node, Map<String, Object?> childMap) {
    var nodeClassName = _className(node);

    data.recordNodeClass(nodeClassName);

    var parent = node.parent;
    if (parent != null) {
      data.recordParentClass(nodeClassName, _className(parent));
    }

    var visitChildren = <AstNode>[];
    for (var entry in childMap.entries) {
      var property = entry.key;
      var child = entry.value;
      if (child is AstNode) {
        data.recordNode(nodeClassName, property, child);
        visitChildren.add(child);
      } else if (child is NodeList) {
        data.recordNodeList(nodeClassName, property, child);
        visitChildren.addAll(child);
      } else if (child is Token?) {
        data.recordToken(nodeClassName, property, child);
      } else {
        throw ArgumentError('Unknown class of child: ${child.runtimeType}');
      }
    }
    // Validate that all AST node children were included in the [childMap].
    for (var entity in node.childEntities) {
      if (entity is AstNode && !visitChildren.contains(entity)) {
        data.missedChildren.add('$nodeClassName (${entity.runtimeType})');
      }
    }
  }
}

/// An object used to compute metrics for a single file or directory.
class CodeShapeMetricsComputer {
  /// The metrics data that was computed.
  final CodeShapeData data = CodeShapeData();

  /// Initialize a newly created metrics computer that can compute the metrics
  /// in one or more files and directories.
  new();

  /// Compute the metrics for the file(s) in the [rootPath].
  Future<void> compute(String rootPath) async {
    var collection = AnalysisContextCollection(
      includedPaths: [rootPath],
      resourceProvider: PhysicalResourceProvider.INSTANCE,
    );
    var collector = CodeShapeDataCollector(data);
    for (var context in collection.contexts) {
      await _computeInContext(context.contextRoot, collector);
    }
  }

  /// Write a report of the metrics that were computed to the [sink].
  void writeResults(StringSink sink) {
    // Write error information.
    _writeMissedChildren(sink);

    // Write normal information.
    _writeChildData(sink);
  }

  /// Compute the metrics for the files in the context [root], creating a
  /// separate context collection to prevent accumulating memory. The metrics
  /// should be captured in the [collector]. Include additional details in the
  /// output if [verbose] is `true`.
  Future<void> _computeInContext(
    ContextRoot root,
    CodeShapeDataCollector collector,
  ) async {
    // Create a new collection to avoid consuming large quantities of memory.
    var collection = AnalysisContextCollection(
      includedPaths: root.includedPaths.toList(),
      resourceProvider: PhysicalResourceProvider.INSTANCE,
    );
    var context = collection.contexts[0];
    var pathContext = context.contextRoot.resourceProvider.pathContext;
    for (var filePath in context.contextRoot.analyzedFiles()) {
      if (file_paths.isDart(pathContext, filePath)) {
        try {
          var resolvedUnitResult = await context.currentSession.getResolvedUnit(
            filePath,
          );
          //
          // Check for errors that cause the file to be skipped.
          //
          if (resolvedUnitResult is! ResolvedUnitResult) {
            print('File $filePath skipped because it could not be analyzed.');
            print('');
            continue;
          } else if (resolvedUnitResult.diagnostics.errors.isNotEmpty) {
            print('File $filePath skipped due to errors:');
            for (var diagnostic in resolvedUnitResult.diagnostics) {
              print('  ${diagnostic.toString()}');
            }
            print('');
            continue;
          }

          resolvedUnitResult.unit.accept(collector);
        } catch (exception) {
          print('Exception caught analyzing: "$filePath"');
          print(exception.toString());
        }
      }
    }
  }

  /// Convert the contents of a single [map] into the values for each row in the
  /// column occupied by the map.
  List<String> _convertMap<T extends Object>(String context, Map<T, int> map) {
    var columns = <String>[];
    var entries = map.entries.toList()
      ..sort((first, second) {
        return second.value.compareTo(first.value);
      });
    var total = 0;
    for (var entry in entries) {
      total += entry.value;
    }
    columns.add('$context ($total)');
    for (var entry in entries) {
      var value = entry.value;
      var percent = _formatPercent(value, total);
      columns.add('  $percent%: ${entry.key} ($value)');
    }
    return columns;
  }

  /// Compute and format a percentage for the fraction [value] / [total].
  String _formatPercent(int value, int total) {
    var percent = ((value / total) * 100).toStringAsFixed(1);
    if (percent.length == 3) {
      percent = '  $percent';
    } else if (percent.length == 4) {
      percent = ' $percent';
    }
    return percent;
  }

  /// Write the child data to the [sink].
  void _writeChildData(StringSink sink) {
    sink.writeln();
    sink.writeln('Child data');

    // TODO(brianwilkerson): This misses all node kinds for which zero instances
    //  were visited.
    var nodeData = data.nodeData;
    var parentData = data.parentData;
    var childData = data.childData;
    var lengthData = data.lengthData;
    var nodeClasses = nodeData.keys.toList()..sort();
    for (var nodeClass in nodeClasses) {
      var count = nodeData[nodeClass];
      sink.writeln();
      sink.writeln('$nodeClass ($count)');

      var parentMap = parentData[nodeClass];
      if (parentMap != null) {
        var lines = _convertMap('parent', parentMap);
        for (var line in lines) {
          sink.writeln('  $line');
        }
      }

      var childMap = childData[nodeClass] ?? {};
      var listMap = lengthData[nodeClass] ?? {};
      var childNames = {...childMap.keys, ...listMap.keys}.toList()..sort();
      for (var childName in childNames) {
        var listLengths = listMap[childName];
        if (listLengths != null) {
          var lines = _convertMap(childName, listLengths);
          for (var line in lines) {
            sink.writeln('  $line');
          }
        }

        var childTypes = childMap[childName];
        if (childTypes != null) {
          var lines = _convertMap(childName, childTypes);
          for (var line in lines) {
            sink.writeln('  $line');
          }
        }
      }
    }
  }

  /// Write a report of the metrics that were computed to the [sink].
  void _writeMissedChildren(StringSink sink) {
    var missedChildren = data.missedChildren;
    if (missedChildren.isNotEmpty) {
      sink.writeln();
      sink.writeln('Missed children in the following classes:');
      for (var nodeClass in missedChildren.toList()..sort()) {
        sink.writeln('  $nodeClass');
      }
    }
  }
}
