blob: 157e2bea90ea3eba0560d5f1815859f9d8470ad8 [file] [log] [blame]
// 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:analysis_server/src/utilities/flutter.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
ClassElement,
Element,
ExecutableElement,
ExtensionElement,
LibraryElement,
LocalVariableElement,
ParameterElement,
PropertyAccessorElement;
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/diagnostic/diagnostic.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: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.
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.
Map<String, _PercentageData> percentageData = {};
/// Initialize a newly created set of relevance data to be empty.
RelevanceData();
/// 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;
InheritanceManager3 inheritanceManager = InheritanceManager3();
/// 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].
RelevanceDataCollector(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 &&
Flutter.instance.isWidgetType(parent.staticType);
for (var argument in node.arguments) {
var realArgument = argument;
var argumentKind = 'unnamed';
if (argument is NamedExpression) {
realArgument = argument.expression;
argumentKind = 'named';
}
_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.typeParameters != null;
data.recordPercentage(
'Classes with type parameters', node.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.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.superclass2);
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.declaredElement!.library;
typeProvider = enclosingLibrary.typeProvider;
typeSystem = enclosingLibrary.typeSystem;
inheritanceManager = InheritanceManager3();
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 visitDefaultFormalParameter(DefaultFormalParameter node) {
_recordDataForNode(
'DefaultFormalParameter (defaultValue)', node.defaultValue,
allowedKeywords: expressionKeywords);
super.visitDefaultFormalParameter(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.superclass2);
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 (type)', node.extendedType);
for (var member in node.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 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.staticElement);
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 visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) {
// There are no completions.
super.visitFunctionTypedFormalParameter(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.condition,
allowedKeywords: expressionKeywords);
_recordDataForNode('IfElement (then)', node.thenElement);
_recordDataForNode('IfElement (else)', node.elseElement);
super.visitIfElement(node);
}
@override
void visitIfStatement(IfStatement node) {
_recordDataForNode('IfStatement (condition)', node.condition,
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.interfaces2) {
_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 visitLibraryIdentifier(LibraryIdentifier node) {
// There are no completions.
super.visitLibraryIdentifier(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 element = node.declaredElement!;
if (!element.isStatic && element.enclosingElement is ClassElement) {
var overriddenMembers = inheritanceManager.getOverridden2(
element.enclosingElement as ClassElement,
Name(element.librarySource.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.staticElement;
_recordMemberDepth(node.target?.staticType, member);
if (node.target is SuperExpression) {
var enclosingMethod = node.thisOrAncestorOfType<MethodDeclaration>();
if (enclosingMethod != null) {
if (enclosingMethod.name.name == 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]);
for (var member in node.members) {
_recordDataForNode('MixinDeclaration (member)', member,
allowedKeywords: memberKeywords);
}
super.visitMixinDeclaration(node);
inGenericContext = wasInGenericContext;
}
@override
void visitNamedExpression(NamedExpression node) {
// Named expressions only occur in argument lists and are handled there.
super.visitNamedExpression(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 visitOnClause(OnClause node) {
for (var constraint in node.superclassConstraints2) {
_recordDataForNode('OnClause (type)', constraint);
}
super.visitOnClause(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.staticElement;
_recordMemberDepth(node.target?.staticType, member);
if (node.target is SuperExpression) {
var enclosingMethod = node.thisOrAncestorOfType<MethodDeclaration>();
if (enclosingMethod != null) {
if (enclosingMethod.name.name == node.propertyName.name) {
data.recordTypeMatch('super property access member', 'same');
} else {
data.recordTypeMatch('super property access member', 'different');
}
}
}
if (!(member is PropertyAccessorElement && member.isSetter)) {
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 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 visitSimpleFormalParameter(SimpleFormalParameter node) {
// There are no completions.
super.visitSimpleFormalParameter(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.mixinTypes2) {
_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)?.staticElement;
/// 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.staticElement!);
}
var element = _leftMostElement(node);
if (element == null) {
return null;
}
if (element is ClassElement) {
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.declaredElement == function) {
return depth;
}
depth++;
} else if (node is ConstructorDeclaration) {
if (node.declaredElement == function) {
return depth;
}
depth++;
} else if (node is FunctionExpression) {
if (node.declaredElement == 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<ClassElement>();
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;
ClassElement? 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;
ClassElement? 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 = <ParameterElement>[];
var namedInOverride = <String, ParameterElement>{};
var positionalInOverridden = <ParameterElement>[];
var namedInOverridden = <String, ParameterElement>{};
for (var param in override.parameters) {
if (param.isPositional) {
positionalInOverride.add(param);
} else {
namedInOverride[param.name] = param;
}
}
for (var param in overridden.parameters) {
if (param.isPositional) {
positionalInOverridden.add(param);
} else {
namedInOverridden[param.name] = param;
}
}
void recordParameterOverride(ParameterElement? overrideParameter,
ParameterElement? overriddenParameter) {
var overrideType = overrideParameter?.type;
var overriddenType = overriddenParameter?.type;
if (overrideType == null ||
overrideType.isDynamic ||
overriddenType == null ||
overriddenType.isDynamic) {
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?.staticElement;
if (element is ParameterElement) {
var definingElement = element.enclosingElement!;
var depth = _parameterReferenceDepth(node, definingElement);
_recordDistance('function depth of referenced parameter', depth);
} else if (element is LocalVariableElement) {
// TODO(brianwilkerson) This ignores the fact that nested functions can
// reference variables declared in enclosing functions. Consider
// additionally measuring the number of function boundaries that are
// crossed and then reporting the distance with a label such as
// 'local variable ($boundaryCount)'.
var distance = node == null
? -1
: featureComputer.localVariableDistance(node, element);
_recordDistance('distance to local variable', distance);
} 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.staticParameterElement?.type;
if (parameterType == null || parameterType.isDynamic) {
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.staticElement;
if (element is ClassElement) {
// 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.
RelevanceMetricsComputer();
/// 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 {
final collection = AnalysisContextCollection(
includedPaths: [rootPath],
resourceProvider: PhysicalResourceProvider.INSTANCE,
);
final 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.
final collection = AnalysisContextCollection(
includedPaths: root.includedPaths.toList(),
excludedPaths: root.excludedPaths.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 (hasError(resolvedUnitResult)) {
if (verbose) {
print('File $filePath skipped due to errors:');
for (var error in resolvedUnitResult.errors
.where((e) => e.severity == Severity.error)) {
print(' ${error.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);
}
/// Return `true` if the [result] contains an error.
static bool hasError(ResolvedUnitResult result) {
for (var error in result.errors) {
if (error.severity == Severity.error) {
return true;
}
}
return false;
}
}
class Timer {
Stopwatch stopwatch = Stopwatch();
int count = 0;
Timer();
double get averageTime => count == 0 ? 0 : totalTime / count;
int get totalTime => stopwatch.elapsedMilliseconds;
void start() {
stopwatch.start();
}
void stop() {
stopwatch.stop();
count++;
}
}
/// 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.
_PercentageData();
/// 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++;
}
}
}