0f9e6da044
Path exclusions should ideally be defined inside a project's `analysis_options.yaml` file, rather than being added programatically. Plus, there's a bug with the constructor that causes this parameter to be completely ignored anyways, so it's been obsolete and non-functional for a while now. `getExcludedGlobs` in the `_ContextLocator` handles parsing and adding excluded paths from the analysis server already, so we should look into deprecating and removing this parameter. Change-Id: I6c023041c7bb5fa4cb9dedc629afa4ea6ecb63d7 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/511160 Commit-Queue: Kallen Tu <kallentu@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
2471 lines
74 KiB
Dart
2471 lines
74 KiB
Dart
// 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++;
|
|
}
|
|
}
|
|
}
|