// 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 'dart:math' as math;

import 'package:_fe_analyzer_shared/src/base/syntactic_entity.dart';
import 'package:analysis_server/src/protocol_server.dart'
    show convertElementToElementKind, ElementKind;
import 'package:analysis_server/src/services/completion/dart/feature_computer.dart';
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/dart/element/element.dart'
    show
        Element,
        ExecutableElement,
        ExtensionElement,
        InterfaceElement,
        LibraryElement,
        FormalParameterElement,
        SetterElement;
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/dart/element/type_provider.dart';
import 'package:analyzer/dart/element/type_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/utilities/extensions/diagnostic.dart';
import 'package:analyzer/src/utilities/extensions/flutter.dart';
import 'package:args/args.dart';

import 'output_utilities.dart';

/// Compute metrics to determine whether they should be used to compute a
/// relevance score for completion suggestions.
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 = RelevanceMetricsComputer();
    var stopwatch = Stopwatch();
    stopwatch.start();
    await computer.compute(rootPath, verbose: result['verbose'] as bool);
    stopwatch.stop();

    var duration = Duration(milliseconds: stopwatch.elapsedMilliseconds);
    out.writeln('Metrics computed in $duration');
    computer.writeMetrics(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.');
  parser.addFlag(
    'verbose',
    abbr: 'v',
    help: 'Print additional information about the analysis',
    negatable: false,
  );
  return parser;
}

/// Print usage information for this tool.
void printUsage(ArgParser parser, {String? error}) {
  if (error != null) {
    print(error);
    print('');
  }
  print('usage: dart relevance_metrics.dart [options] packagePath');
  print('');
  print('Compute metrics to determine whether they should be used to compute');
  print('a relevance score for completion suggestions.');
  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 RelevanceData {
  /// A number identifying the version of this code that produced a given JSON
  /// encoded file. The number should be incremented whenever the shape of the
  /// JSON file is changed.
  // ignore: unreachable_from_main
  static const String currentVersion = '1';

  /// A table mapping match distances to counts by kind of distance.
  Map<String, Map<String, int>> byDistance = {};

  /// A table mapping element kinds to counts by context.
  Map<String, Map<String, int>> byElementKind = {};

  /// A table mapping token types to counts by context.
  Map<String, Map<String, int>> byTokenType = {};

  /// A table mapping match types to counts by kind of type match.
  Map<String, Map<String, int>> byTypeMatch = {};

  /// A table mapping the depth of a target type to the distance between the
  /// target type and the member type.
  Map<int, Map<int, int>> distanceByDepthMap = {};

  /// A table mapping counter names to counts.
  Map<String, int> simpleCounts = {};

  /// A table mapping the length of identifiers to the number of identifiers
  /// found of that length.
  Map<int, int> identifierLengths = {};

  /// A table mapping distances from an identifier to the nearest previous token
  /// with the same lexeme to the number of times that distance was found.
  Map<int, int> tokenDistances = {};

  /// A table mapping percentage data names to the percentage data collected.
  final Map<String, _PercentageData> _percentageData = {};

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

  /// Increment the count associated with the given [name] by one.
  void incrementCount(String name) {
    simpleCounts[name] = (simpleCounts[name] ?? 0) + 1;
  }

  /// Record that a reference to an element was found and that the distance
  /// between that reference and the declaration site is the given [distance].
  /// The [descriptor] is used to describe the kind of distance being measured.
  void recordDistance(String descriptor, int distance) {
    var contextMap = byDistance.putIfAbsent(descriptor, () => {});
    var key = distance.toString();
    contextMap[key] = (contextMap[key] ?? 0) + 1;
  }

  /// Given a member accessed on a target, record the distance between the
  /// target class and the member class by the depth of the target class.
  void recordDistanceByDepth(int targetDepth, int memberDistance) {
    var innerMap = distanceByDepthMap.putIfAbsent(memberDistance, () => {});
    innerMap[targetDepth] = (innerMap[targetDepth] ?? 0) + 1;
  }

  /// Record that an element of the given [kind] was found in the given
  /// [context].
  void recordElementKind(String context, ElementKind kind) {
    var contextMap = byElementKind.putIfAbsent(context, () => {});
    var key = kind.name;
    contextMap[key] = (contextMap[key] ?? 0) + 1;
  }

  /// Record that an identifier of the given [length] was found.
  void recordIdentifierOfLength(int length) {
    identifierLengths[length] = (identifierLengths[length] ?? 0) + 1;
  }

  /// Record that a data point for the percentage data with the given [name] was
  /// found. If [wasPositive] is `true` then the data point is a positive data
  /// point.
  void recordPercentage(String name, bool wasPositive) {
    var data = _percentageData.putIfAbsent(name, () => _PercentageData());
    data.addDataPoint(wasPositive);
  }

  /// Record information about the distance between recurring tokens.
  void recordTokenStream(int distance) {
    tokenDistances[distance] = (tokenDistances[distance] ?? 0) + 1;
  }

  /// Record that a token of the given [type] was found in the given [context].
  void recordTokenType(String context, TokenType type) {
    var contextMap = byTokenType.putIfAbsent(context, () => {});
    var key = type.name;
    contextMap[key] = (contextMap[key] ?? 0) + 1;
  }

  /// Record whether the given [kind] or type match applied to a given argument
  /// (that is, whether [matches] is `true`).
  void recordTypeMatch(String kind, String matchKind) {
    var contextMap = byTypeMatch.putIfAbsent(kind, () => {});
    contextMap[matchKind] = (contextMap[matchKind] ?? 0) + 1;
  }
}

/// An object that visits a compilation unit in order to record the data used to
/// compute the metrics.
class RelevanceDataCollector extends RecursiveAstVisitor<void> {
  static const List<Keyword> declarationKeywords = [
    Keyword.MIXIN,
    Keyword.TYPEDEF,
  ];

  static const List<Keyword> directiveKeywords = [
    Keyword.EXPORT,
    Keyword.IMPORT,
    Keyword.LIBRARY,
    Keyword.PART,
  ];

  static const List<Keyword> exportKeywords = [
    Keyword.AS,
    Keyword.HIDE,
    Keyword.SHOW,
  ];

  static const List<Keyword> expressionKeywords = [
    Keyword.AWAIT,
    Keyword.SUPER,
  ];

  static const List<Keyword> functionBodyKeywords = [
    Keyword.ASYNC,
    Keyword.SYNC,
  ];

  static const List<Keyword> importKeywords = [
    Keyword.AS,
    Keyword.HIDE,
    Keyword.SHOW,
  ];

  static const List<Keyword> memberKeywords = [
    Keyword.FACTORY,
    Keyword.GET,
    Keyword.OPERATOR,
    Keyword.SET,
    Keyword.STATIC,
  ];

  static const List<Keyword> noKeywords = [];

  static const List<Keyword> statementKeywords = [Keyword.AWAIT, Keyword.YIELD];

  /// The relevance data being collected.
  final RelevanceData data;

  /// The library containing the compilation unit being visited.
  late LibraryElement enclosingLibrary;

  /// A flag indicating whether we are currently in a context in which type
  /// parameters are visible.
  bool inGenericContext = false;

  /// The type provider associated with the current compilation unit.
  late TypeProvider typeProvider;

  /// The type system associated with the current compilation unit.
  late TypeSystem typeSystem;

  /// The object used to compute the values of features.
  late FeatureComputer featureComputer;

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

  @override
  void visitAdjacentStrings(AdjacentStrings node) {
    // There are no completions.
    super.visitAdjacentStrings(node);
  }

  @override
  void visitAnnotation(Annotation node) {
    _recordDataForNode('Annotation (name)', node.name);
    super.visitAnnotation(node);
  }

  @override
  void visitArgumentList(ArgumentList node) {
    var context = _argumentListContext(node);
    var parent = node.parent;
    var inWidgetConstructor =
        parent is InstanceCreationExpression && parent.staticType.isWidgetType;
    for (var argument in node.arguments) {
      var realArgument = argument.argumentExpression;
      var argumentKind = argument is NamedArgument ? 'named' : 'unnamed';
      _recordDataForNode(
        'ArgumentList (all, $argumentKind)',
        realArgument,
        allowedKeywords: expressionKeywords,
      );
      _recordDataForNode(
        'ArgumentList ($context, $argumentKind)',
        realArgument,
        allowedKeywords: expressionKeywords,
      );
      _recordTypeMatch(realArgument);
      if (inWidgetConstructor) {
        _recordDataForNode(
          'ArgumentList (widget constructor, $argumentKind)',
          realArgument,
          allowedKeywords: expressionKeywords,
        );
      }
    }
    super.visitArgumentList(node);
  }

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

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

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

  @override
  void visitAssignmentExpression(AssignmentExpression node) {
    _recordDataForNode(
      'AssignmentExpression (rhs)',
      node.rightHandSide,
      allowedKeywords: expressionKeywords,
    );
    var operatorType = node.operator.type;
    if (operatorType != TokenType.EQ &&
        operatorType != TokenType.QUESTION_QUESTION_EQ) {
      _recordTypeMatch(node.rightHandSide);
    }
    super.visitAssignmentExpression(node);
  }

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

  @override
  void visitBinaryExpression(BinaryExpression node) {
    var operator = node.operator.lexeme;
    _recordDataForNode(
      'BinaryExpression ($operator)',
      node.rightOperand,
      allowedKeywords: expressionKeywords,
    );
    if (node.operator.isUserDefinableOperator) {
      _recordTypeMatch(node.rightOperand);
    }
    super.visitBinaryExpression(node);
  }

  @override
  void visitBlock(Block node) {
    for (var statement in node.statements) {
      // Function declaration statements that have no return type begin with an
      // identifier but don't have an element kind associated with the
      // identifier.
      _recordDataForNode(
        'Block (statement)',
        statement,
        allowedKeywords: statementKeywords,
      );
    }
    super.visitBlock(node);
  }

  @override
  void visitBlockFunctionBody(BlockFunctionBody node) {
    _recordTokenType(
      'BlockFunctionBody (start)',
      node,
      allowedKeywords: functionBodyKeywords,
    );
    super.visitBlockFunctionBody(node);
  }

  @override
  void visitBooleanLiteral(BooleanLiteral node) {
    _recordTokenType('BooleanLiteral (start)', node);
    super.visitBooleanLiteral(node);
  }

  @override
  void visitBreakStatement(BreakStatement node) {
    // The token following the `break` (if there is one) is always a label.
    super.visitBreakStatement(node);
  }

  @override
  void visitCascadeExpression(CascadeExpression node) {
    for (var cascade in node.cascadeSections) {
      _recordDataForNode('CascadeExpression (section)', cascade);
    }
    super.visitCascadeExpression(node);
  }

  @override
  void visitCatchClause(CatchClause node) {
    _recordDataForNode('CatchClause (on)', node.exceptionType);
    super.visitCatchClause(node);
  }

  @override
  void visitClassDeclaration(ClassDeclaration node) {
    var wasInGenericContext = inGenericContext;
    inGenericContext = inGenericContext || node.namePart.typeParameters != null;
    data.recordPercentage(
      'Classes with type parameters',
      node.namePart.typeParameters != null,
    );
    var context = 'name';
    if (node.extendsClause != null) {
      _recordTokenType(
        'ClassDeclaration ($context)',
        node.extendsClause,
        allowedKeywords: [Keyword.EXTENDS],
      );
      context = 'extends';
    }
    if (node.withClause != null) {
      _recordTokenType('ClassDeclaration ($context)', node.withClause);
      context = 'with';
    }
    _recordTokenType(
      'ClassDeclaration ($context)',
      node.implementsClause,
      allowedKeywords: [Keyword.IMPLEMENTS],
    );

    for (var member in node.body.members) {
      _recordDataForNode(
        'ClassDeclaration (member)',
        member,
        allowedKeywords: memberKeywords,
      );
    }
    super.visitClassDeclaration(node);
    inGenericContext = wasInGenericContext;
  }

  @override
  void visitClassTypeAlias(ClassTypeAlias node) {
    var wasInGenericContext = inGenericContext;
    inGenericContext = inGenericContext || node.typeParameters != null;
    _recordDataForNode('ClassTypeAlias (superclass)', node.superclass);
    var context = 'superclass';
    _recordTokenType('ClassDeclaration ($context)', node.withClause);
    context = 'with';
    _recordTokenType('ClassDeclaration ($context)', node.implementsClause);
    super.visitClassTypeAlias(node);
    inGenericContext = wasInGenericContext;
  }

  @override
  void visitComment(Comment node) {
    // There are no completions.
    super.visitComment(node);
  }

  @override
  void visitCommentReference(CommentReference node) {
    void recordDataForCommentReference(String context, AstNode node) {
      _recordElementKind(context, node);
      _recordTokenType(context, node);
    }

    recordDataForCommentReference('CommentReference (name)', node.expression);
    super.visitCommentReference(node);
  }

  @override
  void visitCompilationUnit(CompilationUnit node) {
    enclosingLibrary = node.declaredFragment!.element;
    typeProvider = enclosingLibrary.typeProvider;
    typeSystem = enclosingLibrary.typeSystem;
    featureComputer = FeatureComputer(typeSystem, typeProvider);

    var hasPrefix = false;
    for (var directive in node.directives) {
      if (directive is ImportDirective && directive.prefix != null) {
        hasPrefix = true;
      }
      _recordTokenType(
        'CompilationUnit (directive)',
        directive,
        allowedKeywords: directiveKeywords,
      );
    }
    for (var declaration in node.declarations) {
      _recordDataForNode(
        'CompilationUnit (declaration)',
        declaration,
        allowedKeywords: declarationKeywords,
      );
    }
    data.recordPercentage(
      'Compilation units with at least one prefix',
      hasPrefix,
    );
    super.visitCompilationUnit(node);
  }

  @override
  void visitConditionalExpression(ConditionalExpression node) {
    _recordDataForNode(
      'ConditionalExpression (then)',
      node.thenExpression,
      allowedKeywords: expressionKeywords,
    );
    _recordDataForNode(
      'ConditionalExpression (else)',
      node.elseExpression,
      allowedKeywords: expressionKeywords,
    );
    super.visitConditionalExpression(node);
  }

  @override
  void visitConfiguration(Configuration node) {
    // There are no completions.
    super.visitConfiguration(node);
  }

  @override
  void visitConstructorDeclaration(ConstructorDeclaration node) {
    for (var initializer in node.initializers) {
      _recordTokenType('ConstructorDeclaration (initializer)', initializer);
    }
    super.visitConstructorDeclaration(node);
  }

  @override
  void visitConstructorFieldInitializer(ConstructorFieldInitializer node) {
    _recordDataForNode(
      'ConstructorFieldInitializer (expression)',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitConstructorFieldInitializer(node);
  }

  @override
  void visitConstructorName(ConstructorName node) {
    // The token following the `.` is always an identifier.
    super.visitConstructorName(node);
  }

  @override
  void visitContinueStatement(ContinueStatement node) {
    // The token following the `continue` (if there is one) is always a label.
    super.visitContinueStatement(node);
  }

  @override
  void visitDeclaredIdentifier(DeclaredIdentifier node) {
    // There are no completions.
    super.visitDeclaredIdentifier(node);
  }

  @override
  void visitDoStatement(DoStatement node) {
    _recordDataForNode(
      'DoStatement (body)',
      node.body,
      allowedKeywords: statementKeywords,
    );
    _recordDataForNode(
      'DoStatement (condition)',
      node.condition,
      allowedKeywords: expressionKeywords,
    );
    super.visitDoStatement(node);
  }

  @override
  void visitDottedName(DottedName node) {
    // The components are always identifiers.
    super.visitDottedName(node);
  }

  @override
  void visitDoubleLiteral(DoubleLiteral node) {
    // There are no completions.
    super.visitDoubleLiteral(node);
  }

  @override
  void visitEmptyFunctionBody(EmptyFunctionBody node) {
    // There are no completions.
    super.visitEmptyFunctionBody(node);
  }

  @override
  void visitEmptyStatement(EmptyStatement node) {
    // There are no completions.
    super.visitEmptyStatement(node);
  }

  @override
  void visitEnumConstantDeclaration(EnumConstantDeclaration node) {
    // There are no completions.
    super.visitEnumConstantDeclaration(node);
  }

  @override
  void visitEnumDeclaration(EnumDeclaration node) {
    // There are no completions.
    super.visitEnumDeclaration(node);
  }

  @override
  void visitExportDirective(ExportDirective node) {
    var context = 'uri';
    if (node.configurations.isNotEmpty) {
      _recordTokenType(
        'ImportDirective ($context)',
        node.configurations[0],
        allowedKeywords: exportKeywords,
      );
      context = 'configurations';
    }
    if (node.combinators.isNotEmpty) {
      _recordTokenType(
        'ImportDirective ($context)',
        node.combinators[0],
        allowedKeywords: exportKeywords,
      );
    }
    for (var combinator in node.combinators) {
      _recordTokenType(
        'ImportDirective (combinator)',
        combinator,
        allowedKeywords: exportKeywords,
      );
    }
    super.visitExportDirective(node);
  }

  @override
  void visitExpressionFunctionBody(ExpressionFunctionBody node) {
    _recordTokenType(
      'ExpressionFunctionBody (start)',
      node,
      allowedKeywords: functionBodyKeywords,
    );
    _recordDataForNode(
      'ExpressionFunctionBody (expression)',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitExpressionFunctionBody(node);
  }

  @override
  void visitExpressionStatement(ExpressionStatement node) {
    _recordDataForNode(
      'ExpressionStatement (start)',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitExpressionStatement(node);
  }

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

  @override
  void visitExtensionDeclaration(ExtensionDeclaration node) {
    var wasInGenericContext = inGenericContext;
    inGenericContext = inGenericContext || node.typeParameters != null;
    data.recordPercentage(
      'Extensions with type parameters',
      node.typeParameters != null,
    );
    _recordDataForNode('ExtensionDeclaration (onClause)', node.onClause);
    if (node.body case BlockClassBody body) {
      for (var member in body.members) {
        _recordDataForNode(
          'ExtensionDeclaration (member)',
          member,
          allowedKeywords: memberKeywords,
        );
      }
    }
    super.visitExtensionDeclaration(node);
    inGenericContext = wasInGenericContext;
  }

  @override
  void visitExtensionOverride(ExtensionOverride node) {
    // There are no completions.
    super.visitExtensionOverride(node);
  }

  @override
  void visitFieldDeclaration(FieldDeclaration node) {
    // There are no completions.
    super.visitFieldDeclaration(node);
  }

  @override
  void visitFieldFormalParameter(FieldFormalParameter node) {
    // The completions after `this.` are always existing fields.
    super.visitFieldFormalParameter(node);
  }

  @override
  void visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) {
    _recordDataForNode(
      'ForEachPartsWithDeclaration (declaration)',
      node.loopVariable,
    );
    _recordDataForNode(
      'ForEachPartsWithDeclaration (in)',
      node.iterable,
      allowedKeywords: expressionKeywords,
    );
    super.visitForEachPartsWithDeclaration(node);
  }

  @override
  void visitForEachPartsWithIdentifier(ForEachPartsWithIdentifier node) {
    _recordDataForNode(
      'ForEachPartsWithIdentifier (in)',
      node.iterable,
      allowedKeywords: expressionKeywords,
    );
    super.visitForEachPartsWithIdentifier(node);
  }

  @override
  void visitForElement(ForElement node) {
    _recordTokenType('ForElement (parts)', node.forLoopParts);
    _recordDataForNode('ForElement (body)', node.body);
    super.visitForElement(node);
  }

  @override
  void visitFormalParameterDefaultClause(FormalParameterDefaultClause node) {
    _recordDataForNode(
      'DefaultFormalParameter (defaultValue)',
      node.value,
      allowedKeywords: expressionKeywords,
    );
    super.visitFormalParameterDefaultClause(node);
  }

  @override
  void visitFormalParameterList(FormalParameterList node) {
    for (var parameter in node.parameters) {
      _recordDataForNode(
        'FormalParameterList (parameter)',
        parameter,
        allowedKeywords: [Keyword.COVARIANT],
      );
    }
    super.visitFormalParameterList(node);
  }

  @override
  void visitForPartsWithDeclarations(ForPartsWithDeclarations node) {
    _recordDataForNode(
      'ForPartsWithDeclarations (condition)',
      node.condition,
      allowedKeywords: expressionKeywords,
    );
    for (var updater in node.updaters) {
      _recordDataForNode(
        'ForPartsWithDeclarations (updater)',
        updater,
        allowedKeywords: expressionKeywords,
      );
    }
    super.visitForPartsWithDeclarations(node);
  }

  @override
  void visitForPartsWithExpression(ForPartsWithExpression node) {
    _recordDataForNode(
      'ForPartsWithDeclarations (condition)',
      node.condition,
      allowedKeywords: expressionKeywords,
    );
    for (var updater in node.updaters) {
      _recordDataForNode(
        'ForPartsWithDeclarations (updater)',
        updater,
        allowedKeywords: expressionKeywords,
      );
    }
    super.visitForPartsWithExpression(node);
  }

  @override
  void visitForStatement(ForStatement node) {
    _recordTokenType('ForElement (parts)', node.forLoopParts);
    _recordDataForNode(
      'ForElement (body)',
      node.body,
      allowedKeywords: statementKeywords,
    );
    super.visitForStatement(node);
  }

  @override
  void visitFunctionDeclaration(FunctionDeclaration node) {
    // There are no completions.
    super.visitFunctionDeclaration(node);
  }

  @override
  void visitFunctionDeclarationStatement(FunctionDeclarationStatement node) {
    // There are no completions.
    super.visitFunctionDeclarationStatement(node);
  }

  @override
  void visitFunctionExpression(FunctionExpression node) {
    // There are no completions.
    data.recordPercentage(
      'Functions with type parameters',
      node.typeParameters != null,
    );
    super.visitFunctionExpression(node);
  }

  @override
  void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) {
    // There are no completions.
    var contextType = featureComputer.computeContextType(node, node.offset);
    if (contextType != null) {
      var memberType = _returnType(node.element);
      if (memberType != null) {
        _recordTypeRelationships(
          'function expression invocation',
          contextType,
          memberType,
          isContextType: true,
        );
      }
    }
    super.visitFunctionExpressionInvocation(node);
  }

  @override
  void visitFunctionTypeAlias(FunctionTypeAlias node) {
    var wasInGenericContext = inGenericContext;
    inGenericContext = inGenericContext || node.typeParameters != null;
    // There are no completions.
    super.visitFunctionTypeAlias(node);
    inGenericContext = wasInGenericContext;
  }

  @override
  void visitFunctionTypedFormalParameterSuffix(
    FunctionTypedFormalParameterSuffix node,
  ) {
    // There are no completions.
    super.visitFunctionTypedFormalParameterSuffix(node);
  }

  @override
  void visitGenericFunctionType(GenericFunctionType node) {
    var wasInGenericContext = inGenericContext;
    inGenericContext = inGenericContext || node.typeParameters != null;
    // There are no completions.
    super.visitGenericFunctionType(node);
    inGenericContext = wasInGenericContext;
  }

  @override
  void visitGenericTypeAlias(GenericTypeAlias node) {
    var wasInGenericContext = inGenericContext;
    inGenericContext = inGenericContext || node.typeParameters != null;
    _recordDataForNode(
      'GenericTypeAlias (functionType)',
      node.functionType,
      allowedKeywords: [Keyword.FUNCTION],
    );
    super.visitGenericTypeAlias(node);
    inGenericContext = wasInGenericContext;
  }

  @override
  void visitHideCombinator(HideCombinator node) {
    for (var name in node.hiddenNames) {
      _recordDataForNode('HideCombinator (name)', name);
    }
    super.visitHideCombinator(node);
  }

  @override
  void visitIfElement(IfElement node) {
    _recordDataForNode(
      'IfElement (condition)',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    _recordDataForNode('IfElement (then)', node.thenElement);
    _recordDataForNode('IfElement (else)', node.elseElement);
    super.visitIfElement(node);
  }

  @override
  void visitIfStatement(IfStatement node) {
    _recordDataForNode(
      'IfStatement (condition)',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    _recordDataForNode(
      'IfStatement (then)',
      node.thenStatement,
      allowedKeywords: statementKeywords,
    );
    _recordDataForNode(
      'IfStatement (else)',
      node.elseStatement,
      allowedKeywords: statementKeywords,
    );
    super.visitIfStatement(node);
  }

  @override
  void visitImplementsClause(ImplementsClause node) {
    // At the start of each type name.
    for (var namedType in node.interfaces) {
      _recordDataForNode('ImplementsClause (type)', namedType);
    }
    super.visitImplementsClause(node);
  }

  @override
  void visitImportDirective(ImportDirective node) {
    var context = 'uri';
    var deferredKeyword = node.deferredKeyword;
    if (deferredKeyword != null) {
      data.recordTokenType('ImportDirective ($context)', deferredKeyword.type);
      context = 'deferred';
    }
    var asKeyword = node.asKeyword;
    if (asKeyword != null) {
      data.recordTokenType('ImportDirective ($context)', asKeyword.type);
      context = 'prefix';
    }
    if (node.configurations.isNotEmpty) {
      _recordTokenType(
        'ImportDirective ($context)',
        node.configurations[0],
        allowedKeywords: importKeywords,
      );
      context = 'configurations';
    }
    if (node.combinators.isNotEmpty) {
      _recordTokenType(
        'ImportDirective ($context)',
        node.combinators[0],
        allowedKeywords: importKeywords,
      );
    }
    for (var combinator in node.combinators) {
      _recordTokenType(
        'ImportDirective (combinator)',
        combinator,
        allowedKeywords: importKeywords,
      );
    }
    super.visitImportDirective(node);
  }

  @override
  void visitIndexExpression(IndexExpression node) {
    _recordDataForNode(
      'IndexExpression (index)',
      node.index,
      allowedKeywords: expressionKeywords,
    );
    _recordTypeMatch(node.index);
    super.visitIndexExpression(node);
  }

  @override
  void visitInstanceCreationExpression(InstanceCreationExpression node) {
    // There are no completions.
    super.visitInstanceCreationExpression(node);
  }

  @override
  void visitIntegerLiteral(IntegerLiteral node) {
    // There are no completions.
    super.visitIntegerLiteral(node);
  }

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

  @override
  void visitInterpolationString(InterpolationString node) {
    // There are no completions.
    super.visitInterpolationString(node);
  }

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

  @override
  void visitLabel(Label node) {
    // There are no completions.
    super.visitLabel(node);
  }

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

  @override
  void visitLibraryDirective(LibraryDirective node) {
    // There are no completions.
    super.visitLibraryDirective(node);
  }

  @override
  void visitListLiteral(ListLiteral node) {
    for (var element in node.elements) {
      _recordDataForNode(
        'ListLiteral (element)',
        element,
        allowedKeywords: expressionKeywords,
      );
    }
    super.visitListLiteral(node);
  }

  @override
  void visitMapLiteralEntry(MapLiteralEntry node) {
    _recordDataForNode(
      'MapLiteralEntry (value)',
      node.value,
      allowedKeywords: expressionKeywords,
    );
    super.visitMapLiteralEntry(node);
  }

  @override
  void visitMethodDeclaration(MethodDeclaration node) {
    var wasInGenericContext = inGenericContext;
    inGenericContext = inGenericContext || node.typeParameters != null;
    // There are no completions.
    data.recordPercentage(
      'Methods with type parameters',
      node.typeParameters != null,
    );
    var fragment = node.declaredFragment!;
    var element = fragment.element;
    var enclosingElement = element.enclosingElement;
    if (!element.isStatic && enclosingElement is InterfaceElement) {
      var overriddenMembers = enclosingElement.getOverridden(
        Name(element.library.uri, element.name!),
      );
      if (overriddenMembers != null) {
        // Consider limiting this to the most immediate override. If the
        // signature of a method is changed by one of the overrides, then it
        // isn't reasonable to expect the overrides of that member to conform to
        // the signatures of the overridden members from superclasses.
        for (var overridden in overriddenMembers) {
          _recordOverride(element, overridden);
        }
      }
    }
    super.visitMethodDeclaration(node);
    inGenericContext = wasInGenericContext;
  }

  @override
  void visitMethodInvocation(MethodInvocation node) {
    var member = node.methodName.element;
    _recordMemberDepth(node.target?.staticType, member);
    if (node.target is SuperExpression) {
      var enclosingMethod = node.thisOrAncestorOfType<MethodDeclaration>();
      if (enclosingMethod != null) {
        if (enclosingMethod.name.lexeme == node.methodName.name) {
          data.recordTypeMatch('super invocation member', 'same');
        } else {
          data.recordTypeMatch('super invocation member', 'different');
        }
      }
    }
    if (node.target != null) {
      var contextType = featureComputer.computeContextType(node, node.offset);
      if (contextType != null) {
        var memberType = _returnType(member);
        if (memberType != null) {
          _recordTypeRelationships(
            'method invocation',
            contextType,
            memberType,
            isContextType: true,
          );
        }
      }
    }
    super.visitMethodInvocation(node);
  }

  @override
  void visitMixinDeclaration(MixinDeclaration node) {
    var wasInGenericContext = inGenericContext;
    inGenericContext = inGenericContext || node.typeParameters != null;
    data.recordPercentage(
      'Mixins with type parameters',
      node.typeParameters != null,
    );
    var context = 'name';
    if (node.onClause != null) {
      _recordTokenType(
        'MixinDeclaration ($context)',
        node.onClause,
        allowedKeywords: [Keyword.ON],
      );
      context = 'on';
    }
    _recordTokenType(
      'MixinDeclaration ($context)',
      node.implementsClause,
      allowedKeywords: [Keyword.IMPLEMENTS],
    );

    if (node.body case BlockClassBody body) {
      for (var member in body.members) {
        _recordDataForNode(
          'MixinDeclaration (member)',
          member,
          allowedKeywords: memberKeywords,
        );
      }
    }

    super.visitMixinDeclaration(node);
    inGenericContext = wasInGenericContext;
  }

  @override
  void visitMixinOnClause(MixinOnClause node) {
    for (var constraint in node.superclassConstraints) {
      _recordDataForNode('OnClause (type)', constraint);
    }
    super.visitMixinOnClause(node);
  }

  @override
  void visitNamedArgument(NamedArgument node) {
    // Named expressions only occur in argument lists and are handled there.
    super.visitNamedArgument(node);
  }

  @override
  void visitNamedType(NamedType node) {
    // There are no completions.
    super.visitNamedType(node);
  }

  @override
  void visitNativeClause(NativeClause node) {
    // There are no completions.
    super.visitNativeClause(node);
  }

  @override
  void visitNativeFunctionBody(NativeFunctionBody node) {
    // There are no completions.
    super.visitNativeFunctionBody(node);
  }

  @override
  void visitNullLiteral(NullLiteral node) {
    // There are no completions.
    super.visitNullLiteral(node);
  }

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

  @override
  void visitPartDirective(PartDirective node) {
    // There are no completions.
    super.visitPartDirective(node);
  }

  @override
  void visitPartOfDirective(PartOfDirective node) {
    // There are no completions.
    super.visitPartOfDirective(node);
  }

  @override
  void visitPostfixExpression(PostfixExpression node) {
    _recordTypeMatch(node.operand);
    super.visitPostfixExpression(node);
  }

  @override
  void visitPrefixedIdentifier(PrefixedIdentifier node) {
    // There are no completions.
    super.visitPrefixedIdentifier(node);
  }

  @override
  void visitPrefixExpression(PrefixExpression node) {
    _recordDataForNode(
      'PrefixExpression (${node.operator})',
      node.operand,
      allowedKeywords: expressionKeywords,
    );
    _recordTypeMatch(node.operand);
    super.visitPrefixExpression(node);
  }

  @override
  void visitPropertyAccess(PropertyAccess node) {
    var member = node.propertyName.element;
    _recordMemberDepth(node.target?.staticType, member);
    if (node.target is SuperExpression) {
      var enclosingMethod = node.thisOrAncestorOfType<MethodDeclaration>();
      if (enclosingMethod != null) {
        if (enclosingMethod.name.lexeme == node.propertyName.name) {
          data.recordTypeMatch('super property access member', 'same');
        } else {
          data.recordTypeMatch('super property access member', 'different');
        }
      }
    }
    if (member is! SetterElement) {
      var contextType = featureComputer.computeContextType(node, node.offset);
      if (contextType != null) {
        var memberType = _returnType(member);
        if (memberType != null) {
          _recordTypeRelationships(
            'property access',
            contextType,
            memberType,
            isContextType: true,
          );
        }
      }
    }
    super.visitPropertyAccess(node);
  }

  @override
  void visitRedirectingConstructorInvocation(
    RedirectingConstructorInvocation node,
  ) {
    // There are no completions.
    super.visitRedirectingConstructorInvocation(node);
  }

  @override
  void visitRegularFormalParameter(RegularFormalParameter node) {
    // There are no completions.
    super.visitRegularFormalParameter(node);
  }

  @override
  void visitRethrowExpression(RethrowExpression node) {
    // There are no completions.
    super.visitRethrowExpression(node);
  }

  @override
  void visitReturnStatement(ReturnStatement node) {
    _recordDataForNode(
      'ReturnStatement (expression)',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    if (node.expression == null) {
      data.recordTokenType('ReturnStatement (expression)', node.semicolon.type);
    }
    super.visitReturnStatement(node);
  }

  @override
  void visitScriptTag(ScriptTag node) {
    // There are no completions.
    super.visitScriptTag(node);
  }

  @override
  void visitSetOrMapLiteral(SetOrMapLiteral node) {
    for (var element in node.elements) {
      _recordDataForNode(
        'SetOrMapLiteral (element)',
        element,
        allowedKeywords: expressionKeywords,
      );
    }
    super.visitSetOrMapLiteral(node);
  }

  @override
  void visitShowCombinator(ShowCombinator node) {
    for (var name in node.shownNames) {
      _recordDataForNode('ShowCombinator (name)', name);
    }
    super.visitShowCombinator(node);
  }

  @override
  void visitSimpleIdentifier(SimpleIdentifier node) {
    data.recordIdentifierOfLength(node.name.length);
    super.visitSimpleIdentifier(node);
  }

  @override
  void visitSimpleStringLiteral(SimpleStringLiteral node) {
    // There are no completions.
    super.visitSimpleStringLiteral(node);
  }

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

  @override
  void visitStringInterpolation(StringInterpolation node) {
    // There are no completions.
    super.visitStringInterpolation(node);
  }

  @override
  void visitSuperConstructorInvocation(SuperConstructorInvocation node) {
    // There are no completions.
    super.visitSuperConstructorInvocation(node);
  }

  @override
  void visitSuperExpression(SuperExpression node) {
    // There are no completions.
    super.visitSuperExpression(node);
  }

  @override
  void visitSwitchCase(SwitchCase node) {
    _recordDataForNode(
      'SwitchCase (expression)',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    for (var statement in node.statements) {
      _recordDataForNode(
        'SwitchCase (statement)',
        statement,
        allowedKeywords: statementKeywords,
      );
    }
    super.visitSwitchCase(node);
  }

  @override
  void visitSwitchDefault(SwitchDefault node) {
    for (var statement in node.statements) {
      _recordDataForNode(
        'SwitchDefault (statement)',
        statement,
        allowedKeywords: statementKeywords,
      );
    }
    super.visitSwitchDefault(node);
  }

  @override
  void visitSwitchStatement(SwitchStatement node) {
    _recordDataForNode(
      'SwitchStatement (expression)',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitSwitchStatement(node);
  }

  @override
  void visitSymbolLiteral(SymbolLiteral node) {
    // There are no completions.
    super.visitSymbolLiteral(node);
  }

  @override
  void visitThisExpression(ThisExpression node) {
    // There are no completions.
    super.visitThisExpression(node);
  }

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

  @override
  void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
    // There are no completions.
    super.visitTopLevelVariableDeclaration(node);
  }

  @override
  void visitTryStatement(TryStatement node) {
    var context = 'try';
    for (var clause in node.catchClauses) {
      _recordTokenType(
        'TryStatement ($context)',
        clause,
        allowedKeywords: [Keyword.ON],
      );
      context = 'catch';
    }
    var finallyKeyword = node.finallyKeyword;
    if (finallyKeyword != null) {
      data.recordTokenType('TryStatement ($context)', finallyKeyword.type);
    }
    super.visitTryStatement(node);
  }

  @override
  void visitTypeArgumentList(TypeArgumentList node) {
    for (var typeArgument in node.arguments) {
      _recordDataForNode('TypeArgumentList (argument)', typeArgument);
    }
    super.visitTypeArgumentList(node);
  }

  @override
  void visitTypeParameter(TypeParameter node) {
    if (node.bound != null) {
      _recordDataForNode('TypeParameter (bound)', node.bound);
    }
    super.visitTypeParameter(node);
  }

  @override
  void visitTypeParameterList(TypeParameterList node) {
    // There are no completions.
    super.visitTypeParameterList(node);
  }

  @override
  void visitVariableDeclaration(VariableDeclaration node) {
    var keywords = node.parent?.parent is FieldDeclaration
        ? [Keyword.COVARIANT, ...expressionKeywords]
        : expressionKeywords;
    _recordDataForNode(
      'VariableDeclaration (initializer)',
      node.initializer,
      allowedKeywords: keywords,
    );
    super.visitVariableDeclaration(node);
  }

  @override
  void visitVariableDeclarationList(VariableDeclarationList node) {
    // There are no completions.
    super.visitVariableDeclarationList(node);
  }

  @override
  void visitVariableDeclarationStatement(VariableDeclarationStatement node) {
    // There are no completions.
    super.visitVariableDeclarationStatement(node);
  }

  @override
  void visitWhileStatement(WhileStatement node) {
    _recordDataForNode(
      'WhileStatement (condition)',
      node.condition,
      allowedKeywords: expressionKeywords,
    );
    _recordDataForNode(
      'WhileStatement (body)',
      node.body,
      allowedKeywords: statementKeywords,
    );
    super.visitWhileStatement(node);
  }

  @override
  void visitWithClause(WithClause node) {
    for (var namedType in node.mixinTypes) {
      _recordDataForNode('WithClause (type)', namedType);
    }
    super.visitWithClause(node);
  }

  @override
  void visitYieldStatement(YieldStatement node) {
    _recordDataForNode(
      'YieldStatement (expression)',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitYieldStatement(node);
  }

  /// Return the context in which the [node] occurs. The [node] is expected to
  /// be the parent of the argument expression.
  String _argumentListContext(AstNode? node) {
    if (node is ArgumentList) {
      var parent = node.parent;
      if (parent is InstanceCreationExpression) {
        return 'constructor';
      } else if (parent is MethodInvocation) {
        return 'method';
      } else if (parent is FunctionExpressionInvocation) {
        return 'function';
      } else if (parent is SuperConstructorInvocation ||
          parent is RedirectingConstructorInvocation) {
        return 'constructor redirect';
      } else if (parent is Annotation) {
        return 'annotation';
      }
    } else if (node is IndexExpression) {
      return 'index';
    } else if (node is AssignmentExpression ||
        node is BinaryExpression ||
        node is PrefixExpression ||
        node is PostfixExpression) {
      return 'binary/unary';
    }
    return 'unknown';
  }

  /// Return the depth of the given [element]. For example:
  /// 0: imported
  /// 1: prefix
  /// 2: top-level decl
  /// 3: class member
  /// 4+: local function
  int _depth(Element element) {
    if (element.library != enclosingLibrary) {
      return 0;
    }
    var depth = 0;
    Element? currentElement = element;
    while (currentElement != enclosingLibrary) {
      depth++;
      currentElement = currentElement?.enclosingElement;
    }
    return depth;
  }

  /// Return the first child of the [node] that is neither a comment nor an
  /// annotation.
  SyntacticEntity? _firstChild(AstNode node) {
    var children = node.childEntities.toList();
    for (var i = 0; i < children.length; i++) {
      var child = children[i];
      if (child is! Comment && child is! Annotation) {
        return child;
      }
    }
    return null;
  }

  /// Return the element associated with the left-most identifier that is a
  /// child of the [node].
  Element? _leftMostElement(AstNode node) => _leftMostIdentifier(node)?.element;

  /// Return the left-most child of the [node] if it is a simple identifier, or
  /// `null` if the left-most child is not a simple identifier. Comments and
  /// annotations are ignored for this purpose.
  SimpleIdentifier? _leftMostIdentifier(AstNode? node) {
    var currentNode = node;
    while (currentNode != null && currentNode is! SimpleIdentifier) {
      var firstChild = _firstChild(currentNode);
      if (firstChild is AstNode) {
        currentNode = firstChild;
      } else {
        currentNode = null;
      }
    }
    if (currentNode is SimpleIdentifier &&
        !currentNode.inDeclarationContext()) {
      return currentNode;
    }
    return null;
  }

  /// Return the element kind of the element associated with the left-most
  /// identifier that is a child of the [node].
  ElementKind? _leftMostKind(AstNode node) {
    if (node is InstanceCreationExpression) {
      return convertElementToElementKind(node.constructorName.element!);
    }
    var element = _leftMostElement(node);
    if (element == null) {
      return null;
    }
    if (element is InterfaceElement) {
      var parent = node.parent;
      if (parent is Annotation && parent.arguments != null) {
        element = parent.element!;
      }
    }
    return convertElementToElementKind(element);
  }

  /// Return the left-most token that is a child of the [node].
  Token? _leftMostToken(AstNode node) {
    SyntacticEntity? entity = node;
    while (entity is AstNode) {
      entity = _firstChild(entity);
    }
    if (entity is Token) {
      return entity;
    }
    return null;
  }

  /// Return the number of functions between the [reference] and the [function]
  /// in which the referenced parameter is declared.
  int _parameterReferenceDepth(AstNode? reference, Element function) {
    var depth = 0;
    var node = reference;
    while (node != null) {
      if (node is MethodDeclaration) {
        if (node.declaredFragment?.element == function) {
          return depth;
        }
        depth++;
      } else if (node is ConstructorDeclaration) {
        if (node.declaredFragment?.element == function) {
          return depth;
        }
        depth++;
      } else if (node is FunctionExpression) {
        if (node.declaredFragment?.element == function) {
          return depth;
        }
        depth++;
      }
      node = node.parent;
    }
    return -1;
  }

  void _recordContextType(DartType? type) {
    if (type == null) {
      data.incrementCount('has no context type');
    } else {
      data.incrementCount('has context type');
    }
  }

  /// Record information about the given [node] occurring in the given
  /// [context].
  void _recordDataForNode(
    String context,
    AstNode? node, {
    List<Keyword> allowedKeywords = noKeywords,
  }) {
    _recordElementKind(context, node);
    if (inGenericContext) {
      _recordElementKind('$context - generic', node);
    } else {
      _recordElementKind('$context - non-generic', node);
    }
    _recordReferenceDepth(node);
    _recordTokenDistance(node);
    _recordTokenType(context, node, allowedKeywords: allowedKeywords);
    if (node != null) {
      var contextType = featureComputer.computeContextType(node, node.offset);
      _recordContextType(contextType);
      if (contextType != null) {
        var elementType = _returnType(_leftMostElement(node));
        if (elementType != null) {
          _recordTypeRelationships(
            'matches context type',
            contextType,
            elementType,
          );
        }
      }
    }
  }

  /// Record the [distance] from a reference to the declaration. The kind of
  /// distance is described by the [descriptor].
  void _recordDistance(String descriptor, int distance) {
    data.recordDistance(descriptor, distance);
  }

  /// Record the element kind of the element associated with the left-most
  /// identifier that is a child of the [node] in the given [context].
  void _recordElementKind(String context, AstNode? node) {
    if (node != null) {
      var kind = _leftMostKind(node);
      if (kind != null) {
        data.recordElementKind(context, kind);
        if (node is Expression) {
          data.recordElementKind('Expression', kind);
        } else if (node is Statement) {
          data.recordElementKind('Statement', kind);
        }
      }
    }
  }

  /// Record the distance between the static type of the target (the
  /// [targetType]) and the [member] to which the reference was resolved.
  void _recordMemberDepth(DartType? targetType, Element? member) {
    if (member == null) {
      return;
    }
    if (targetType is InterfaceType) {
      var targetClass = targetType.element;
      var extension = member.thisOrAncestorOfType<ExtensionElement>();
      if (extension != null) {
        _recordDistance('member (extension)', 0);
        return;
      }
      // TODO(brianwilkerson): It might be interesting to also know whether the
      //  [element] was found in a class, interface, or mixin.
      var memberClass = member.thisOrAncestorOfType<InterfaceElement>();
      if (memberClass != null) {
        /// Return the distance between the [targetClass] and the [memberClass]
        /// along the superclass chain. This includes all of the implicit
        /// superclasses caused by mixins.
        int getSuperclassDepth() {
          var depth = 0;
          InterfaceElement? currentClass = targetClass;
          while (currentClass != null) {
            if (currentClass == memberClass) {
              return depth;
            }
            for (var mixin in currentClass.mixins.reversed) {
              depth++;
              if (mixin.element == memberClass) {
                return depth;
              }
            }
            depth++;
            currentClass = currentClass.supertype?.element;
          }
          return -1;
        }

        /// Return the depth of the [targetClass] in the class hierarchy. This
        /// includes all of the implicit superclasses caused by mixins.
        int getTargetDepth() {
          var depth = 0;
          InterfaceElement? currentClass = targetClass;
          while (currentClass != null) {
            depth += currentClass.mixins.length + 1;
            currentClass = currentClass.supertype?.element;
          }
          return depth;
        }

        var superclassDepth = getSuperclassDepth();
        var interfaceDepth = featureComputer.inheritanceDistance(
          targetClass,
          memberClass,
        );
        if (superclassDepth >= 0) {
          _recordDistance('member (superclass)', superclassDepth);
        } else if (interfaceDepth >= 0) {
          _recordDistance('member (interface)', interfaceDepth);
        } else {
          // This shouldn't happen, so it's worth investigating the cause when
          // it does.
          _recordDistance('member (not found)', 0);
        }
        if (interfaceDepth >= 0) {
          _recordDistance('member (shortest distance)', interfaceDepth);
          data.recordDistanceByDepth(getTargetDepth(), interfaceDepth);
        }
      }
    }
  }

  void _recordOverride(
    ExecutableElement override,
    ExecutableElement overridden,
  ) {
    var positionalInOverride = <FormalParameterElement>[];
    var namedInOverride = <String, FormalParameterElement>{};
    var positionalInOverridden = <FormalParameterElement>[];
    var namedInOverridden = <String, FormalParameterElement>{};
    for (var param in override.formalParameters) {
      if (param.isPositional) {
        positionalInOverride.add(param);
      } else if (param.name case var name?) {
        namedInOverride[name] = param;
      }
    }
    for (var param in overridden.formalParameters) {
      if (param.isPositional) {
        positionalInOverridden.add(param);
      } else if (param.name case var name?) {
        namedInOverridden[name] = param;
      }
    }

    void recordParameterOverride(
      FormalParameterElement? overrideParameter,
      FormalParameterElement? overriddenParameter,
    ) {
      var overrideType = overrideParameter?.type;
      var overriddenType = overriddenParameter?.type;
      if (overrideType == null ||
          overrideType is DynamicType ||
          overriddenType == null ||
          overriddenType is DynamicType) {
        return;
      }
      _recordTypeRelationships(
        'parameter override',
        overriddenType,
        overrideType,
      );
    }

    var count = math.min(
      positionalInOverride.length,
      positionalInOverridden.length,
    );
    for (var i = 0; i < count; i++) {
      recordParameterOverride(
        positionalInOverride[i],
        positionalInOverridden[i],
      );
    }
    for (var name in namedInOverride.keys) {
      var overrideParameter = namedInOverridden[name];
      var overriddenParameter = namedInOverridden[name];
      recordParameterOverride(overrideParameter, overriddenParameter);
    }
  }

  /// Record the depth of the element associated with the left-most identifier
  /// that is a child of the given [node].
  void _recordReferenceDepth(AstNode? node) {
    var reference = _leftMostIdentifier(node);
    var element = reference?.element;
    if (element is FormalParameterElement) {
      var definingElement = element.enclosingElement!;
      var depth = _parameterReferenceDepth(node, definingElement);
      _recordDistance('function depth of referenced parameter', depth);
    } else if (element != null) {
      // TODO(brianwilkerson): We might want to cross reference the depth of
      //  the declaration with the depth of the reference to see whether there
      //  is a pattern.
      _recordDistance(
        'declaration depth of referenced element',
        _depth(element),
      );
    }
  }

  /// Record the number of tokens between a given identifier and the nearest
  /// previous token with the same lexeme.
  void _recordTokenDistance(AstNode? node) {
    var identifier = _leftMostIdentifier(node);
    if (identifier != null) {
      int distance() {
        var token = identifier.token;
        var lexeme = token.lexeme;
        var distance = 1;
        token = token.previous!;
        while (!token.isEof && distance <= 100) {
          if (token.lexeme == lexeme) {
            return distance;
          }
          distance++;
          token = token.previous!;
        }
        return -1;
      }

      data.recordTokenStream(distance());
    }
  }

  /// Record the token type of the left-most token that is a child of the
  /// [node] in the given [context].
  void _recordTokenType(
    String context,
    AstNode? node, {
    List<Keyword> allowedKeywords = noKeywords,
  }) {
    if (node != null) {
      var token = _leftMostToken(node);
      if (token != null) {
        var type = token.type;
        if (token.isKeyword && token.keyword!.isBuiltInOrPseudo) {
          // These keywords can be used as identifiers, so determine whether it
          // is being used as a keyword or an identifier.
          if (!allowedKeywords.contains(token.keyword)) {
            type = TokenType.IDENTIFIER;
          }
        }
        data.recordTokenType(context, type);
        if (node is Expression) {
          data.recordTokenType('Expression', type);
        } else if (node is Statement) {
          data.recordTokenType('Statement', type);
        }
      }
    }
  }

  /// Record information about how the argument as a whole and the first token
  /// in the expression match the type of the associated parameter.
  void _recordTypeMatch(Expression argument) {
    var parameterType = argument.correspondingParameter?.type;
    if (parameterType == null || parameterType is DynamicType) {
      return;
    }
    if (parameterType is FunctionType) {
      data.recordTypeMatch(
        'function typed parameter',
        argument is FunctionExpression ? 'closure' : 'non-closure',
      );
    }
    var context = _argumentListContext(argument.parent);
    var argumentType = argument.staticType;
    if (argumentType != null) {
      _recordTypeRelationships(
        'argument (all, whole)',
        parameterType,
        argumentType,
      );
      _recordTypeRelationships(
        'argument ($context, whole)',
        parameterType,
        argumentType,
      );
    }
    var identifier = _leftMostIdentifier(argument);
    if (identifier != null) {
      var firstTokenType = identifier.staticType;
      if (firstTokenType == null) {
        var element = identifier.element;
        if (element is InterfaceElement) {
          // This is effectively treating a reference to a class name as having
          // the same type as an instance of the class, which isn't valid, but
          // on the other hand, the spec doesn't define the static type of a
          // class name in this context so anything we do will be wrong in some
          // sense.
          firstTokenType = element.thisType;
        }
      }
      if (firstTokenType != null) {
        _recordTypeRelationships(
          'argument (all, first token)',
          parameterType,
          firstTokenType,
        );
        _recordTypeRelationships(
          'argument ($context, first token)',
          parameterType,
          firstTokenType,
        );
      }
    }
  }

  /// Record information about how the [parameterType] and [argumentType] are
  /// related, using the [descriptor] to differentiate between the counts.
  void _recordTypeRelationships(
    String descriptor,
    DartType parameterType,
    DartType argumentType, {
    bool isContextType = false,
  }) {
    if (argumentType == parameterType) {
      data.recordTypeMatch(descriptor, 'exact');
      data.recordTypeMatch('all', 'exact');
    } else if (typeSystem.isSubtypeOf(argumentType, parameterType)) {
      data.recordTypeMatch(descriptor, 'subtype');
      data.recordTypeMatch('all', 'subtype');
      if (isContextType &&
          argumentType is InterfaceType &&
          parameterType is InterfaceType) {
        int distance;
        if (parameterType.element == typeProvider.futureOrElement) {
          var typeArgument = parameterType.typeArguments[0];
          distance = featureComputer.inheritanceDistance(
            argumentType.element,
            typeProvider.futureElement,
          );
          if (typeArgument is InterfaceType) {
            var argDistance = featureComputer.inheritanceDistance(
              argumentType.element,
              typeArgument.element,
            );
            if (distance < 0 || (argDistance >= 0 && argDistance < distance)) {
              distance = argDistance;
            }
          }
        } else {
          distance = featureComputer.inheritanceDistance(
            argumentType.element,
            parameterType.element,
          );
        }
        data.recordDistance('Subtype of context type ($descriptor)', distance);
        data.recordDistance('Subtype of context type (all)', distance);
      }
    } else if (typeSystem.isSubtypeOf(parameterType, argumentType)) {
      data.recordTypeMatch(descriptor, 'supertype');
      data.recordTypeMatch('all', 'supertype');
    } else {
      data.recordTypeMatch(descriptor, 'unrelated');
      data.recordTypeMatch('all', 'unrelated');
    }
  }

  /// Return the return type of the [element], or `null` if the element doesn't
  /// have a return type.
  DartType? _returnType(Element? element) {
    if (element is ExecutableElement) {
      return element.returnType;
    }
    return null;
  }
}

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

  /// 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].
  /// If [corpus] is true, treat rootPath as a container of packages, creating
  /// a new context collection for each subdirectory.
  Future<void> compute(String rootPath, {required bool verbose}) async {
    var collection = AnalysisContextCollection(
      includedPaths: [rootPath],
      resourceProvider: PhysicalResourceProvider.INSTANCE,
    );
    var collector = RelevanceDataCollector(data);
    for (var context in collection.contexts) {
      await _computeInContext(context.contextRoot, collector, verbose: verbose);
    }
  }

  /// Write a report of the metrics that were computed to the [sink].
  void writeMetrics(StringSink sink) {
    var first = <String, Map<String, int>>{};
    var whole = <String, Map<String, int>>{};
    var rest = <String, Map<String, int>>{};
    for (var entry in data.byTypeMatch.entries) {
      var key = entry.key;
      var firstLabel = ', first token';
      var firstIndex = key.indexOf(firstLabel);
      if (firstIndex > 0) {
        first['  ${key.replaceFirst(firstLabel, '')}'] = entry.value.map(
          (key, value) => MapEntry('  $key', value),
        );
      } else {
        var wholeLabel = ', whole';
        var wholeIndex = key.indexOf(wholeLabel);
        if (wholeIndex > 0) {
          whole['  ${key.replaceFirst(wholeLabel, '')}'] = entry.value.map(
            (key, value) => MapEntry('  $key', value),
          );
        } else {
          rest[key] = entry.value;
        }
      }
    }

    sink.writeln();
    _writeCounts(sink, data.simpleCounts);
    sink.writeln();
    _writePercentageData(sink, data._percentageData);
    sink.writeln();
    _writeSideBySide(
      sink,
      [data.byTokenType, data.byElementKind],
      ['Token Types', 'Element Kinds'],
    );
    sink.writeln();
    sink.writeln('Type relationships');
    _writeSideBySide(sink, [first, whole], ['First Token', 'Whole Expression']);
    _writeContextMap(sink, rest);
    sink.writeln();
    sink.writeln('Structural indicators');
    _writeContextMap(sink, data.byDistance);
    sink.writeln();
    sink.writeln('Distance to member (left) by depth of target class (top)');
    _writeMatrix(sink, data.distanceByDepthMap);
    _writeIdentifierLengths(sink, data.identifierLengths);
    _writeTokenData(sink, data.tokenDistances);
  }

  /// 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,
    RelevanceDataCollector collector, {
    required bool verbose,
  }) 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.');
            if (verbose) {
              print('');
            }
            continue;
          } else if (resolvedUnitResult.diagnostics.errors.isNotEmpty) {
            if (verbose) {
              print('File $filePath skipped due to errors:');
              for (var diagnostic in resolvedUnitResult.diagnostics.errors) {
                print('  ${diagnostic.toString()}');
              }
              print('');
            } else {
              print('File $filePath skipped due to analysis errors.');
            }
            continue;
          }

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

  Iterable<List<String>> _convertColumnsToRows(
    Iterable<List<String>> columns,
  ) sync* {
    var maxRowCount = columns.fold<int>(
      0,
      (previous, column) => math.max(previous, column.length),
    );
    for (var i = 0; i < maxRowCount; i++) {
      var row = <String>[];
      for (var column in columns) {
        if (i < column.length) {
          row.add(column[i]);
        } else {
          row.add('');
        }
      }
      yield row;
    }
  }

  /// 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>[];
    if (map == null) {
      return columns;
    }
    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;
  }

  /// Convert the data in a list of [maps] into a table with one column per map.
  /// The columns will be titled using the given [columnTitles].
  List<List<String>> _createTable(
    List<Map<String, Map<String, int>>> maps,
    List<String> columnTitles,
  ) {
    var uniqueContexts = <String>{};
    for (var map in maps) {
      uniqueContexts.addAll(map.keys);
    }
    var contexts = uniqueContexts.toList()..sort();

    var blankRow = <String>[];
    var table = <List<String>>[];
    table.add(columnTitles);
    for (var context in contexts) {
      var columns = maps.map((map) => _convertMap(context, map[context]));
      table.addAll(_convertColumnsToRows(columns));
      table.add(blankRow);
    }
    return table;
  }

  /// 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 a [contextMap] containing one kind of metric data to the [sink].
  void _writeContextMap(
    StringSink sink,
    Map<String, Map<String, int>> contextMap,
  ) {
    var entries = contextMap.entries.toList()
      ..sort((first, second) => first.key.compareTo(second.key));
    for (var i = 0; i < entries.length; i++) {
      if (i > 0) {
        sink.writeln();
      }
      var context = entries[i].key;
      var data = entries[i].value;
      var lines = _convertMap(context, data);
      for (var line in lines) {
        sink.writeln('  $line');
      }
    }
  }

  /// Write a [contextMap] containing one kind of metric data to the [sink].
  void _writeCounts(StringSink sink, Map<String, int> countsMap) {
    var names = countsMap.keys.toList()..sort();
    for (var name in names) {
      sink.writeln('$name = ${countsMap[name]}');
    }
  }

  /// Write information about the [lengths] of identifiers to the given [sink].
  void _writeIdentifierLengths(StringSink sink, Map<int, int> lengths) {
    sink.writeln();
    var column = _convertMap('identifier lengths', lengths);
    var table = _convertColumnsToRows([column]).toList();
    sink.writeTable(table);
  }

  /// Write the given [matrix] to the [sink]. The keys of the outer map will be
  /// the row titles; the keys of the inner map will be the column titles.
  void _writeMatrix(StringSink sink, Map<int, Map<int, int>> matrix) {
    var maxTargetDepth = 0;
    var maxValueWidth = 0;
    for (var innerMap in matrix.values) {
      for (var entry in innerMap.entries) {
        maxTargetDepth = math.max(maxTargetDepth, entry.key);
        maxValueWidth = math.max(maxValueWidth, entry.value.toString().length);
      }
    }
    String intToString(int value, int width) {
      var digits = value.toString();
      var padding = ' ' * (width - digits.length);
      return '$padding$digits';
    }

    var maxRowHeaderWidth = maxTargetDepth.toString().length;
    var headerRow = [''];
    for (var depth = maxTargetDepth; depth > 0; depth--) {
      headerRow.add(intToString(depth, maxValueWidth));
    }
    var zero = intToString(0, maxValueWidth);
    var table = [headerRow];
    for (var distance = maxTargetDepth - 1; distance >= 0; distance--) {
      var innerMap = matrix[distance] ?? {};
      var row = [intToString(distance, maxRowHeaderWidth)];
      for (var depth = maxTargetDepth; depth > 0; depth--) {
        var value = innerMap[depth];
        row.add(
          value == null
              ? (distance < depth ? zero : '')
              : intToString(value, maxValueWidth),
        );
      }
      table.add(row);
    }
    sink.writeTable(table);
  }

  /// Write a [percentageMap] containing one kind of metric data to the [sink].
  void _writePercentageData(
    StringSink sink,
    Map<String, _PercentageData> percentageMap,
  ) {
    var entries = percentageMap.entries.toList()
      ..sort((first, second) => first.key.compareTo(second.key));
    for (var entry in entries) {
      var name = entry.key;
      var data = entry.value;
      var total = data.total;
      var value = data.positive;
      var percent = total == 0 ? '  0.0' : _formatPercent(value, total);
      sink.writeln('$name = $percent ($value / $total)');
    }
  }

  /// Write the given [maps] to the given [sink], formatting them as side-by-side
  /// columns titled by the given [columnTitles].
  void _writeSideBySide(
    StringSink sink,
    List<Map<String, Map<String, int>>> maps,
    List<String> columnTitles,
  ) {
    var table = _createTable(maps, columnTitles);
    sink.writeTable(table);
  }

  /// Write information about the number of identifiers that occur within a
  /// given distance of the nearest previous occurrence of the same identifier.
  void _writeTokenData(StringSink sink, Map<int, int> distances) {
    var firstColumn = _convertMap(
      'distance to previous matching token',
      distances,
    );
    var secondColumn = <String>[];
    var total = distances.values.fold<int>(
      0,
      (previous, current) => previous + current,
    );
    secondColumn.add('matching tokens within a given distance ($total)');
    var cumulative = 0;
    for (var i = 1; i <= 100; i++) {
      cumulative += distances[i] ?? 0;
      var percent = _formatPercent(cumulative, total);
      secondColumn.add('  $percent%: $i');
    }

    sink.writeln();
    sink.writeln('Token stream analysis');
    var table = _convertColumnsToRows([firstColumn, secondColumn]).toList();
    sink.writeTable(table);
  }
}

/// Information collected to compute a percentage of data points that were
/// positive.
class _PercentageData {
  /// The total number of data points.
  int total = 0;

  /// The total number of positive data points.
  int positive = 0;

  /// Initialize a newly created keeper of percentage data.
  new();

  /// Add a data point to the data being collected. If [wasPositive] is `true`
  /// then the data point is a positive data point.
  void addDataPoint(bool wasPositive) {
    total++;
    if (wasPositive) {
      positive++;
    }
  }
}
