| // 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:convert'; | |
| import 'dart:io' as io; | |
| import 'package:_fe_analyzer_shared/src/base/syntactic_entity.dart'; | |
| import 'package:analysis_server/src/protocol_server.dart' show 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, InterfaceElement, LibraryElement; | |
| import 'package:analyzer/dart/element/type_provider.dart'; | |
| import 'package:analyzer/dart/element/type_system.dart'; | |
| import 'package:analyzer/file_system/file_system.dart'; | |
| import 'package:analyzer/file_system/physical_file_system.dart'; | |
| import 'package:analyzer/src/util/file_paths.dart' as file_paths; | |
| import 'package:analyzer/src/utilities/extensions/ast.dart'; | |
| import 'package:analyzer/src/utilities/extensions/diagnostic.dart'; | |
| import 'package:analyzer/src/utilities/extensions/flutter.dart'; | |
| import 'package:analyzer_testing/package_root.dart' as package_root; | |
| import 'package:analyzer_utilities/tools.dart'; | |
| import 'package:args/args.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 provider = PhysicalResourceProvider.INSTANCE; | |
| var packageRoot = provider.pathContext.normalize(package_root.packageRoot); | |
| void writeRelevanceTable(RelevanceData data, {String suffix = ''}) { | |
| var generatedFilePath = provider.pathContext.join( | |
| packageRoot, | |
| 'analysis_server', | |
| 'lib', | |
| 'src', | |
| 'services', | |
| 'completion', | |
| 'dart', | |
| 'relevance_tables$suffix.g.dart', | |
| ); | |
| var generatedFile = provider.getFile(generatedFilePath); | |
| var buffer = StringBuffer(); | |
| var writer = RelevanceTableWriter(buffer); | |
| writer.write(data); | |
| generatedFile.writeAsStringSync(buffer.toString()); | |
| DartFormat.formatFile(io.File(generatedFile.path)); | |
| } | |
| if (result.wasParsed('reduceDir')) { | |
| var data = RelevanceData(); | |
| var dir = provider.getFolder(result['reduceDir'] as String); | |
| var suffix = result.rest.isNotEmpty ? result.rest[0] : ''; | |
| for (var child in dir.getChildren()) { | |
| if (child is File) { | |
| var newData = RelevanceData.fromJson(child.readAsStringSync()); | |
| data.addData(newData); | |
| } | |
| } | |
| writeRelevanceTable(data, suffix: suffix); | |
| return; | |
| } | |
| var rootPath = result.rest[0]; | |
| print('Analyzing root: "$rootPath"'); | |
| var computer = RelevanceMetricsComputer(); | |
| var stopwatch = Stopwatch()..start(); | |
| await computer.compute(rootPath, verbose: result['verbose'] as bool); | |
| if (result.wasParsed('mapFile')) { | |
| var mapFile = provider.getFile(result['mapFile'] as String); | |
| mapFile.writeAsStringSync(computer.data.toJson()); | |
| } else { | |
| writeRelevanceTable(computer.data); | |
| } | |
| stopwatch.stop(); | |
| var duration = Duration(milliseconds: stopwatch.elapsedMilliseconds); | |
| print('Tables generated in $duration'); | |
| } | |
| } | |
| /// Create a parser that can be used to parse the command-line arguments. | |
| ArgParser createArgParser() { | |
| var parser = ArgParser(); | |
| parser.addFlag( | |
| 'help', | |
| abbr: 'h', | |
| help: 'Print this help message.', | |
| negatable: false, | |
| ); | |
| parser.addOption( | |
| 'mapFile', | |
| help: | |
| 'The absolute path of the file to which the relevance data will be ' | |
| 'written. Using this option will prevent the relevance table from ' | |
| 'being written.', | |
| ); | |
| parser.addOption( | |
| 'reduceDir', | |
| help: | |
| 'The absolute path of the directory from which the relevance data ' | |
| 'will be read.', | |
| ); | |
| 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_table_generator.dart [options] packagePath'); | |
| print(''); | |
| print('Generate the tables used to compute the values of certain features.'); | |
| 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.wasParsed('reduceDir')) { | |
| return validateDir(parser, result['reduceDir'] as String); | |
| } else if (result.rest.length != 1) { | |
| printUsage(parser, error: 'No package path specified.'); | |
| return false; | |
| } | |
| if (result.wasParsed('mapFile')) { | |
| var mapFilePath = result['mapFile']; | |
| if (mapFilePath is! String || | |
| !PhysicalResourceProvider.INSTANCE.pathContext.isAbsolute( | |
| mapFilePath, | |
| )) { | |
| printUsage( | |
| parser, | |
| error: 'The path "$mapFilePath" must be an absolute path.', | |
| ); | |
| return false; | |
| } | |
| } | |
| return validateDir(parser, result.rest[0]); | |
| } | |
| /// Return `true` if the [dirPath] is an absolute path to a directory that | |
| /// exists. | |
| bool validateDir(ArgParser parser, String dirPath) { | |
| if (!PhysicalResourceProvider.INSTANCE.pathContext.isAbsolute(dirPath)) { | |
| printUsage(parser, error: 'The path "$dirPath" must be an absolute path.'); | |
| return false; | |
| } | |
| if (!io.Directory(dirPath).existsSync()) { | |
| printUsage(parser, error: 'The directory "$dirPath" does not exist.'); | |
| return false; | |
| } | |
| return true; | |
| } | |
| /// An object that records the data used to compute the tables. | |
| class RelevanceData { | |
| /// A table mapping element kinds and keywords to counts by context. | |
| final Map<String, Map<_Kind, int>> _byKind = {}; | |
| /// Initialize a newly created set of relevance data to be empty. | |
| new(); | |
| /// Initialize a newly created set of relevance data based on the content of | |
| /// the JSON encoded string. | |
| new fromJson(String encoded) { | |
| var map = json.decode(encoded) as Map<String, dynamic>; | |
| for (var contextEntry in map.entries) { | |
| var contextMap = _byKind.putIfAbsent(contextEntry.key, () => {}); | |
| for (var kindEntry | |
| in (contextEntry.value as Map<String, dynamic>).entries) { | |
| _Kind kind; | |
| var key = kindEntry.key; | |
| if (key.startsWith('e')) { | |
| kind = _ElementKind(ElementKind.values.byName(key.substring(1))); | |
| } else if (key.startsWith('k')) { | |
| kind = _Keyword(Keyword.keywords[key.substring(1)]!); | |
| } else { | |
| throw StateError('Invalid initial character in unique key "$key"'); | |
| } | |
| contextMap[kind] = int.parse(kindEntry.value as String); | |
| } | |
| } | |
| } | |
| /// Add the data from the given relevance [data] to this set of relevance | |
| /// data. | |
| void addData(RelevanceData data) { | |
| for (var contextEntry in data._byKind.entries) { | |
| var contextMap = _byKind.putIfAbsent(contextEntry.key, () => {}); | |
| for (var kindEntry in contextEntry.value.entries) { | |
| var kind = kindEntry.key; | |
| contextMap[kind] = (contextMap[kind] ?? 0) + kindEntry.value; | |
| } | |
| } | |
| } | |
| /// Record that an element of the given [kind] was found in the given | |
| /// [context]. | |
| void recordElementKind(String context, ElementKind kind) { | |
| var contextMap = _byKind.putIfAbsent(context, () => {}); | |
| var key = _ElementKind(kind); | |
| contextMap[key] = (contextMap[key] ?? 0) + 1; | |
| } | |
| /// Record that the given [keyword] was found in the given [context]. | |
| void recordKeyword(String context, Keyword keyword) { | |
| var contextMap = _byKind.putIfAbsent(context, () => {}); | |
| var key = _Keyword(keyword); | |
| contextMap[key] = (contextMap[key] ?? 0) + 1; | |
| } | |
| /// Convert this data to a JSON encoded format. | |
| String toJson() { | |
| var map = <String, Map<String, String>>{}; | |
| for (var contextEntry in _byKind.entries) { | |
| var kindMap = <String, String>{}; | |
| for (var kindEntry in contextEntry.value.entries) { | |
| kindMap[kindEntry.key.uniqueKey] = kindEntry.value.toString(); | |
| } | |
| map[contextEntry.key] = kindMap; | |
| } | |
| return json.encode(map); | |
| } | |
| } | |
| /// An object that visits a compilation unit in order to collect the data used | |
| /// to create the relevance tables. | |
| /// | |
| /// Even though this class subclasses [RecursiveAstVisitor], and therefore isn't | |
| /// required to implement every visit method, by convention it does have an | |
| /// implementation of every visit method so that we can record that we have | |
| /// considered the node class and don't need to collect any data at that | |
| /// location. This makes it easier to figure out which node classes need to be | |
| /// considered when updating the tool (because adding new visit methods to | |
| /// [AstVisitor] won't force them to be added to this class). | |
| 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> patternKeywords = [ | |
| Keyword.CONST, | |
| Keyword.FALSE, | |
| Keyword.FINAL, | |
| Keyword.NULL, | |
| Keyword.TRUE, | |
| Keyword.VAR, | |
| ]; | |
| static const List<Keyword> noKeywords = []; | |
| static const List<Keyword> statementKeywords = [Keyword.AWAIT, Keyword.YIELD]; | |
| /// The relevance data being collected. | |
| final RelevanceData data; | |
| /// The compilation unit in which data is currently being collected. | |
| late CompilationUnit unit; | |
| /// A list of the identifier and keyword tokens in the compilation unit being | |
| /// analyzed. | |
| /// | |
| /// Used to find tokens that are not included in the tables but should be. | |
| final List<Token> _identifiersAndKeywords = []; | |
| /// The library containing the compilation unit being visited. | |
| late LibraryElement enclosingLibrary; | |
| /// 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); | |
| /// Initialize this collector prior to visiting the unit in the [result]. | |
| void initializeFrom(ResolvedUnitResult result) { | |
| _identifiersAndKeywords.clear(); | |
| unit = result.unit; | |
| var token = unit.beginToken; | |
| while (!token.isEof) { | |
| if (token.isKeywordOrIdentifier) { | |
| _identifiersAndKeywords.add(token); | |
| } | |
| token = token.next!; | |
| } | |
| } | |
| void recordKeyword(String context, Token keyword) { | |
| // TODO(brianwilkerson): Figure out whether this method is needed. It seems | |
| // like the keyword should already have been removed from the list. If | |
| // that's not the case we should understand why. | |
| data.recordKeyword(context, keyword.keyword!); | |
| _identifiersAndKeywords.remove(keyword); | |
| } | |
| @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 | |
| // ignore: experimental_member_use | |
| void visitAnonymousBlockBody(AnonymousBlockBody node) { | |
| // TODO(brianwilkerson): Implement this if the language feature is accepted. | |
| } | |
| @override | |
| // ignore: experimental_member_use | |
| void visitAnonymousExpressionBody(AnonymousExpressionBody node) { | |
| // TODO(brianwilkerson): Implement this if the language feature is accepted. | |
| } | |
| @override | |
| // ignore: experimental_member_use | |
| void visitAnonymousMethodInvocation(AnonymousMethodInvocation node) { | |
| // TODO(brianwilkerson): Implement this if the language feature is accepted. | |
| } | |
| @override | |
| void visitArgumentList(ArgumentList node) { | |
| var context = _argumentListContext(node); | |
| for (var argument in node.arguments) { | |
| var realArgument = argument.argumentExpression; | |
| var argumentKind = argument is NamedArgument ? 'named' : 'unnamed'; | |
| _recordDataForNode( | |
| 'ArgumentList_${context}_$argumentKind', | |
| realArgument, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| } | |
| super.visitArgumentList(node); | |
| } | |
| @override | |
| void visitAsExpression(AsExpression node) { | |
| // There is only one token that is valid at this point. | |
| _unrecorded(node.asOperator); | |
| _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 visitAssignedVariablePattern(AssignedVariablePattern node) { | |
| _recordElementKind('AssignedVariablePattern_identifier', node); | |
| super.visitAssignedVariablePattern(node); | |
| } | |
| @override | |
| void visitAssignmentExpression(AssignmentExpression node) { | |
| _recordDataForNode( | |
| 'AssignmentExpression_rightHandSide', | |
| node.rightHandSide, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitAssignmentExpression(node); | |
| } | |
| @override | |
| void visitAwaitExpression(AwaitExpression node) { | |
| _recordDataForNode( | |
| 'AwaitExpression_expression', | |
| node.expression, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitAwaitExpression(node); | |
| } | |
| @override | |
| void visitBinaryExpression(BinaryExpression node) { | |
| _recordDataForNode( | |
| 'BinaryExpression_${node.operator}_rightOperand', | |
| node.rightOperand, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| 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 visitBlockClassBody(BlockClassBody node) { | |
| // Data is not recorded here. It is recorded in the parent | |
| // (ClassDeclaration, MixinDeclaration, etc.) in order to allow the tables | |
| // to have different probabilities for the same construct depending on the | |
| // context. | |
| super.visitBlockClassBody(node); | |
| } | |
| @override | |
| void visitBlockFunctionBody(BlockFunctionBody node) { | |
| _recordKeyword( | |
| 'BlockFunctionBody_start', | |
| node, | |
| allowedKeywords: functionBodyKeywords, | |
| ); | |
| super.visitBlockFunctionBody(node); | |
| } | |
| @override | |
| void visitBooleanLiteral(BooleanLiteral node) { | |
| _recordKeyword('BooleanLiteral_start', node); | |
| super.visitBooleanLiteral(node); | |
| } | |
| @override | |
| void visitBreakStatement(BreakStatement node) { | |
| // The token following the `break` (if there is one) is always a label. | |
| if (node.label case var label?) _unrecorded(label.name); | |
| super.visitBreakStatement(node); | |
| } | |
| @override | |
| void visitCascadeExpression(CascadeExpression node) { | |
| for (var cascade in node.cascadeSections) { | |
| _recordDataForNode('CascadeExpression_cascadeSection', cascade); | |
| } | |
| super.visitCascadeExpression(node); | |
| } | |
| @override | |
| void visitCaseClause(CaseClause node) { | |
| // No other completions are valid here. | |
| _unrecorded(node.caseKeyword); | |
| _recordDataForNode( | |
| 'CaseClause_guardedPattern', | |
| node.guardedPattern, | |
| allowedKeywords: patternKeywords, | |
| ); | |
| super.visitCaseClause(node); | |
| } | |
| @override | |
| void visitCastPattern(CastPattern node) { | |
| // There is only one token available at this point. | |
| _unrecorded(node.asToken); | |
| _recordDataForNode('CastPattern_type', node.type); | |
| super.visitCastPattern(node); | |
| } | |
| @override | |
| void visitCatchClause(CatchClause node) { | |
| _recordDataForNode('CatchClause_exceptionType', node.exceptionType); | |
| // No other completions are valid here. | |
| if (node.catchKeyword case var keyword?) _unrecorded(keyword); | |
| super.visitCatchClause(node); | |
| } | |
| @override | |
| void visitCatchClauseParameter(CatchClauseParameter node) { | |
| // There are no completions. | |
| _recordDeclaration(node.name); | |
| super.visitCatchClauseParameter(node); | |
| } | |
| @override | |
| void visitClassDeclaration(ClassDeclaration node) { | |
| // If there are modifiers before `class`, then the first will be recorded | |
| // by the containing node, but the rest will not use the relevance tables | |
| // because there is a fixed order in which they must appear. | |
| _unrecordedBetween( | |
| node.firstTokenAfterCommentAndMetadata, | |
| node.classKeyword, | |
| ); | |
| var context = 'name'; | |
| _recordDeclaration(node.namePart.typeName); | |
| if (node.extendsClause != null) { | |
| _recordKeyword( | |
| 'ClassDeclaration_$context', | |
| node.extendsClause, | |
| allowedKeywords: [Keyword.EXTENDS], | |
| ); | |
| context = 'extends'; | |
| } | |
| if (node.withClause != null) { | |
| _recordKeyword('ClassDeclaration_$context', node.withClause); | |
| context = 'with'; | |
| } | |
| _recordKeyword( | |
| 'ClassDeclaration_$context', | |
| node.implementsClause, | |
| allowedKeywords: [Keyword.IMPLEMENTS], | |
| ); | |
| for (var member in node.body.members) { | |
| _recordDataForNode( | |
| 'ClassDeclaration_member', | |
| member, | |
| allowedKeywords: memberKeywords, | |
| ); | |
| } | |
| super.visitClassDeclaration(node); | |
| } | |
| @override | |
| void visitClassTypeAlias(ClassTypeAlias node) { | |
| _recordDeclaration(node.name); | |
| _recordDataForNode('ClassTypeAlias_superclass', node.superclass); | |
| var context = 'superclass'; | |
| _recordKeyword('ClassTypeAlias_$context', node.withClause); | |
| context = 'with'; | |
| _recordKeyword('ClassTypeAlias_$context', node.implementsClause); | |
| super.visitClassTypeAlias(node); | |
| } | |
| @override | |
| void visitComment(Comment node) { | |
| // There are no completions. | |
| super.visitComment(node); | |
| } | |
| @override | |
| void visitCommentReference(CommentReference node) { | |
| _recordDataForNode('CommentReference_expression', node.expression); | |
| super.visitCommentReference(node); | |
| } | |
| @override | |
| void visitCompilationUnit(CompilationUnit node) { | |
| enclosingLibrary = node.declaredFragment!.element; | |
| typeProvider = enclosingLibrary.typeProvider; | |
| typeSystem = enclosingLibrary.typeSystem; | |
| featureComputer = FeatureComputer(typeSystem, typeProvider); | |
| for (var directive in node.directives) { | |
| _recordKeyword( | |
| 'CompilationUnit_directive', | |
| directive, | |
| allowedKeywords: directiveKeywords, | |
| ); | |
| } | |
| for (var declaration in node.declarations) { | |
| _recordDataForNode( | |
| 'CompilationUnit_declaration', | |
| declaration, | |
| allowedKeywords: declarationKeywords, | |
| ); | |
| } | |
| super.visitCompilationUnit(node); | |
| } | |
| @override | |
| void visitConditionalExpression(ConditionalExpression node) { | |
| _recordDataForNode( | |
| 'ConditionalExpression_thenExpression', | |
| node.thenExpression, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| _recordDataForNode( | |
| 'ConditionalExpression_elseExpression', | |
| node.elseExpression, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitConditionalExpression(node); | |
| } | |
| @override | |
| void visitConfiguration(Configuration node) { | |
| // The list of valid names is short, so we don't use the relevance tables to | |
| // try to sort them by frequency. | |
| for (var token in node.name.tokens) { | |
| _identifiersAndKeywords.remove(token); | |
| } | |
| super.visitConfiguration(node); | |
| } | |
| @override | |
| void visitConstantPattern(ConstantPattern node) { | |
| _recordDataForNode( | |
| 'ConstantPattern_expression', | |
| node.expression, | |
| allowedKeywords: [Keyword.CONST], | |
| ); | |
| super.visitConstantPattern(node); | |
| } | |
| @override | |
| void visitConstructorDeclaration(ConstructorDeclaration node) { | |
| // The type name must always be the same as the enclosing class. | |
| if (node.typeName?.token case var name?) _unrecorded(name); | |
| if (node.name case var name?) _recordDeclaration(name); | |
| var factoryKeyword = node.factoryKeyword; | |
| if (factoryKeyword != null && | |
| factoryKeyword != node.firstTokenAfterCommentAndMetadata) { | |
| _unrecorded(factoryKeyword); | |
| } | |
| _recordDataForNode('ConstructorDeclaration_returnType', node.typeName!); | |
| for (var initializer in node.initializers) { | |
| _recordDataForNode('ConstructorDeclaration_initializer', initializer); | |
| } | |
| var redirectedConstructor = node.redirectedConstructor; | |
| if (redirectedConstructor != null) { | |
| // There is no relevance data for redirections because only constructors | |
| // are allowed after the `=`. | |
| _unrecorded(redirectedConstructor.beginToken); | |
| } | |
| 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 the name of a constructor. | |
| if (node.name?.token case var token?) _unrecorded(token); | |
| super.visitConstructorName(node); | |
| } | |
| @override | |
| void visitConstructorReference(ConstructorReference node) { | |
| // The only valid option is a constructor name. | |
| super.visitConstructorReference(node); | |
| } | |
| @override | |
| void visitConstructorSelector(ConstructorSelector node) { | |
| // The relevance of individual constructor names isn't dependent on any | |
| // information available while building the table. | |
| _unrecorded(node.name.token); | |
| super.visitConstructorSelector(node); | |
| } | |
| @override | |
| void visitContinueStatement(ContinueStatement node) { | |
| // The token following the `continue` (if there is one) is always a label. | |
| if (node.label case var label?) _unrecorded(label.name); | |
| super.visitContinueStatement(node); | |
| } | |
| @override | |
| void visitDeclaredIdentifier(DeclaredIdentifier node) { | |
| // There are no completions. | |
| _recordDeclaration(node.name); | |
| super.visitDeclaredIdentifier(node); | |
| } | |
| @override | |
| void visitDeclaredVariablePattern(DeclaredVariablePattern node) { | |
| // There are no completions. | |
| _recordDeclaration(node.name); | |
| super.visitDeclaredVariablePattern(node); | |
| } | |
| @override | |
| void visitDoStatement(DoStatement node) { | |
| // There's only one valid choice at this location. | |
| _unrecorded(node.whileKeyword); | |
| _recordDataForNode( | |
| 'DoStatement_body', | |
| node.body, | |
| allowedKeywords: statementKeywords, | |
| ); | |
| _recordDataForNode( | |
| 'DoStatement_condition', | |
| node.condition, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitDoStatement(node); | |
| } | |
| @override | |
| void visitDotShorthandConstructorInvocation( | |
| DotShorthandConstructorInvocation node, | |
| ) { | |
| _recordDataForNode( | |
| 'DotShorthandConstructorInvocation_constructorName', | |
| node.constructorName, | |
| ); | |
| super.visitDotShorthandConstructorInvocation(node); | |
| } | |
| @override | |
| void visitDotShorthandInvocation(DotShorthandInvocation node) { | |
| _recordDataForNode('DotShorthandInvocation_memberName', node.memberName); | |
| super.visitDotShorthandInvocation(node); | |
| } | |
| @override | |
| void visitDotShorthandPropertyAccess(DotShorthandPropertyAccess node) { | |
| _recordDataForNode( | |
| 'DotShorthandPropertyAccess_propertyName', | |
| node.propertyName, | |
| ); | |
| super.visitDotShorthandPropertyAccess(node); | |
| } | |
| @override | |
| void visitDoubleLiteral(DoubleLiteral node) { | |
| // There are no completions. | |
| super.visitDoubleLiteral(node); | |
| } | |
| @override | |
| void visitEmptyClassBody(EmptyClassBody node) { | |
| // There are no completions. | |
| super.visitEmptyClassBody(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 visitEnumConstantArguments(EnumConstantArguments node) { | |
| // There are no completions. | |
| super.visitEnumConstantArguments(node); | |
| } | |
| @override | |
| void visitEnumConstantDeclaration(EnumConstantDeclaration node) { | |
| // There are no completions. | |
| _recordDeclaration(node.name); | |
| super.visitEnumConstantDeclaration(node); | |
| } | |
| @override | |
| void visitEnumDeclaration(EnumDeclaration node) { | |
| // If there are modifiers before `enum`, then the first will be recorded | |
| // by the containing node, but the rest will not use the relevance tables | |
| // because there is a fixed order in which they must appear. | |
| _unrecordedBetween( | |
| node.firstTokenAfterCommentAndMetadata, | |
| node.enumKeyword, | |
| ); | |
| _recordDeclaration(node.namePart.typeName); | |
| _recordKeyword( | |
| 'EnumDeclaration_name', | |
| node.implementsClause, | |
| allowedKeywords: [Keyword.IMPLEMENTS], | |
| ); | |
| super.visitEnumDeclaration(node); | |
| } | |
| @override | |
| void visitExportDirective(ExportDirective node) { | |
| var context = 'uri'; | |
| if (node.configurations.isNotEmpty) { | |
| _recordKeyword( | |
| 'ImportDirective_$context', | |
| node.configurations[0], | |
| allowedKeywords: exportKeywords, | |
| ); | |
| context = 'configurations'; | |
| } | |
| if (node.combinators.isNotEmpty) { | |
| _recordKeyword( | |
| 'ImportDirective_$context', | |
| node.combinators[0], | |
| allowedKeywords: exportKeywords, | |
| ); | |
| } | |
| for (var combinator in node.combinators) { | |
| _recordKeyword( | |
| 'ImportDirective_combinator', | |
| combinator, | |
| allowedKeywords: exportKeywords, | |
| ); | |
| } | |
| super.visitExportDirective(node); | |
| } | |
| @override | |
| void visitExpressionFunctionBody(ExpressionFunctionBody node) { | |
| _recordKeyword( | |
| 'ExpressionFunctionBody_start', | |
| node, | |
| allowedKeywords: functionBodyKeywords, | |
| ); | |
| _recordDataForNode( | |
| 'ExpressionFunctionBody_expression', | |
| node.expression, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitExpressionFunctionBody(node); | |
| } | |
| @override | |
| void visitExpressionStatement(ExpressionStatement node) { | |
| _recordDataForNode( | |
| 'ExpressionStatement_expression', | |
| node.expression, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitExpressionStatement(node); | |
| } | |
| @override | |
| void visitExtendsClause(ExtendsClause node) { | |
| _recordDataForNode('ExtendsClause_superclass', node.superclass); | |
| super.visitExtendsClause(node); | |
| } | |
| @override | |
| void visitExtensionDeclaration(ExtensionDeclaration node) { | |
| // If there are modifiers before `extension`, then the first will be | |
| // recorded by the containing node, but the rest will not use the relevance | |
| // tables because there is a fixed order in which they must appear. | |
| _unrecordedBetween( | |
| node.firstTokenAfterCommentAndMetadata, | |
| node.extensionKeyword, | |
| ); | |
| if (node.name case var name?) { | |
| _recordDeclaration(name); | |
| } | |
| _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); | |
| } | |
| @override | |
| void visitExtensionOnClause(ExtensionOnClause node) { | |
| _recordDataForNode('ExtensionOnClause_extendedType', node.extendedType); | |
| super.visitExtensionOnClause(node); | |
| } | |
| @override | |
| void visitExtensionOverride(ExtensionOverride node) { | |
| // There are no completions. | |
| super.visitExtensionOverride(node); | |
| } | |
| @override | |
| void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) { | |
| // If there are modifiers before `extension`, then the first will be | |
| // recorded by the containing node, but the rest will not use the relevance | |
| // tables because there is a fixed order in which they must appear. | |
| _unrecordedBetween( | |
| node.firstTokenAfterCommentAndMetadata, | |
| node.extensionKeyword, | |
| ); | |
| // No other completions are valid after `extension`. | |
| _unrecorded(node.typeKeyword); | |
| _recordDeclaration(node.namePart.typeName); | |
| for (var member in node.body.members) { | |
| _recordDataForNode( | |
| 'ExtensionTypeDeclaration_member', | |
| member, | |
| allowedKeywords: memberKeywords, | |
| ); | |
| } | |
| super.visitExtensionTypeDeclaration(node); | |
| } | |
| @override | |
| void visitFieldDeclaration(FieldDeclaration node) { | |
| _recordDataForNode('FieldDeclaration_fields', node.fields); | |
| super.visitFieldDeclaration(node); | |
| } | |
| @override | |
| void visitFieldFormalParameter(FieldFormalParameter node) { | |
| // No other completions are valid here. | |
| _unrecorded(node.thisKeyword); | |
| // The completions after `this.` are always existing fields. | |
| _unrecorded(node.name); | |
| super.visitFieldFormalParameter(node); | |
| } | |
| @override | |
| void visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) { | |
| // No other completions are valid here. | |
| _unrecorded(node.inKeyword); | |
| _recordDataForNode( | |
| 'ForEachPartsWithDeclaration_loopVariable', | |
| node.loopVariable, | |
| ); | |
| _recordDataForNode( | |
| 'ForEachPartsWithDeclaration_iterable', | |
| node.iterable, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitForEachPartsWithDeclaration(node); | |
| } | |
| @override | |
| void visitForEachPartsWithIdentifier(ForEachPartsWithIdentifier node) { | |
| // No other completions are valid here. | |
| _unrecorded(node.inKeyword); | |
| _recordDataForNode( | |
| 'ForEachPartsWithIdentifier_identifier', | |
| node.identifier, | |
| ); | |
| _recordDataForNode( | |
| 'ForEachPartsWithIdentifier_iterable', | |
| node.iterable, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitForEachPartsWithIdentifier(node); | |
| } | |
| @override | |
| void visitForEachPartsWithPattern(ForEachPartsWithPattern node) { | |
| // No other completions are valid here. | |
| _unrecorded(node.inKeyword); | |
| _recordDataForNode( | |
| 'ForEachPartsWithPattern_pattern', | |
| node.pattern, | |
| allowedKeywords: patternKeywords, | |
| ); | |
| _recordDataForNode( | |
| 'ForEachPartsWithPattern_iterable', | |
| node.iterable, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitForEachPartsWithPattern(node); | |
| } | |
| @override | |
| void visitForElement(ForElement node) { | |
| _recordDataForNode('ForElement_forLoopParts', 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( | |
| 'ForParts_condition', | |
| node.condition, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| for (var updater in node.updaters) { | |
| _recordDataForNode( | |
| 'ForParts_updater', | |
| updater, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| } | |
| super.visitForPartsWithDeclarations(node); | |
| } | |
| @override | |
| void visitForPartsWithExpression(ForPartsWithExpression node) { | |
| _recordDataForNode( | |
| 'ForParts_condition', | |
| node.condition, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| for (var updater in node.updaters) { | |
| _recordDataForNode( | |
| 'ForParts_updater', | |
| updater, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| } | |
| super.visitForPartsWithExpression(node); | |
| } | |
| @override | |
| void visitForPartsWithPattern(ForPartsWithPattern node) { | |
| _recordDataForNode( | |
| 'ForParts_condition', | |
| node.condition, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| for (var updater in node.updaters) { | |
| _recordDataForNode( | |
| 'ForParts_updater', | |
| updater, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| } | |
| super.visitForPartsWithPattern(node); | |
| } | |
| @override | |
| void visitForStatement(ForStatement node) { | |
| _recordDataForNode('ForStatement_forLoopParts', node.forLoopParts); | |
| _recordDataForNode( | |
| 'ForStatement_body', | |
| node.body, | |
| allowedKeywords: statementKeywords, | |
| ); | |
| super.visitForStatement(node); | |
| } | |
| @override | |
| void visitFunctionDeclaration(FunctionDeclaration node) { | |
| _recordDeclaration(node.name); | |
| _recordDataForNode('FunctionDeclaration_returnType', node.returnType); | |
| if (node.propertyKeyword case var keyword?) _unrecorded(keyword); | |
| super.visitFunctionDeclaration(node); | |
| } | |
| @override | |
| void visitFunctionDeclarationStatement(FunctionDeclarationStatement node) { | |
| // There are no completions. | |
| super.visitFunctionDeclarationStatement(node); | |
| } | |
| @override | |
| void visitFunctionExpression(FunctionExpression node) { | |
| // There are no completions. | |
| super.visitFunctionExpression(node); | |
| } | |
| @override | |
| void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { | |
| // There are no completions. | |
| super.visitFunctionExpressionInvocation(node); | |
| } | |
| @override | |
| void visitFunctionReference(FunctionReference node) { | |
| // There are no completions. | |
| super.visitFunctionReference(node); | |
| } | |
| @override | |
| void visitFunctionTypeAlias(FunctionTypeAlias node) { | |
| // There are no completions. | |
| super.visitFunctionTypeAlias(node); | |
| } | |
| @override | |
| void visitFunctionTypedFormalParameterSuffix( | |
| FunctionTypedFormalParameterSuffix node, | |
| ) { | |
| // There are no completions. | |
| super.visitFunctionTypedFormalParameterSuffix(node); | |
| } | |
| @override | |
| void visitGenericFunctionType(GenericFunctionType node) { | |
| // No other completions are valid here. | |
| _unrecorded(node.functionKeyword); | |
| super.visitGenericFunctionType(node); | |
| } | |
| @override | |
| void visitGenericTypeAlias(GenericTypeAlias node) { | |
| _recordDeclaration(node.name); | |
| _recordDataForNode( | |
| 'GenericTypeAlias_type', | |
| node.type, | |
| allowedKeywords: [Keyword.FUNCTION], | |
| ); | |
| super.visitGenericTypeAlias(node); | |
| } | |
| @override | |
| void visitGuardedPattern(GuardedPattern node) { | |
| // There are no completions. | |
| super.visitGuardedPattern(node); | |
| } | |
| @override | |
| void visitHideCombinator(HideCombinator node) { | |
| for (var hiddenName in node.hiddenNames) { | |
| _recordDataForNode('HideCombinator_hiddenName', hiddenName); | |
| } | |
| super.visitHideCombinator(node); | |
| } | |
| @override | |
| void visitIfElement(IfElement node) { | |
| // This is handled by locations where an expression is expected. | |
| if (node.elseKeyword case var keyword?) _unrecorded(keyword); | |
| _recordDataForNode( | |
| 'IfElement_condition', | |
| node.expression, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| _recordDataForNode('IfElement_thenElement', node.thenElement); | |
| _recordDataForNode('IfElement_elseElement', node.elseElement); | |
| super.visitIfElement(node); | |
| } | |
| @override | |
| void visitIfStatement(IfStatement node) { | |
| // This is handled by locations where a statement is expected. | |
| if (node.elseKeyword case var keyword?) _unrecorded(keyword); | |
| _recordDataForNode( | |
| 'IfStatement_condition', | |
| node.expression, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| _recordDataForNode( | |
| 'IfStatement_thenStatement', | |
| node.thenStatement, | |
| allowedKeywords: statementKeywords, | |
| ); | |
| _recordDataForNode( | |
| 'IfStatement_elseStatement', | |
| node.elseStatement, | |
| allowedKeywords: statementKeywords, | |
| ); | |
| super.visitIfStatement(node); | |
| } | |
| @override | |
| void visitImplementsClause(ImplementsClause node) { | |
| // The set of keywords available at this point is small and deterministic. | |
| _unrecorded(node.implementsKeyword); | |
| // At the start of each type name. | |
| for (var namedType in node.interfaces) { | |
| _recordDataForNode('ImplementsClause_interface', namedType); | |
| } | |
| super.visitImplementsClause(node); | |
| } | |
| @override | |
| void visitImplicitCallReference(ImplicitCallReference node) { | |
| // TODO(brianwilkerson): implement visitImplicitCallReference | |
| super.visitImplicitCallReference(node); | |
| } | |
| @override | |
| void visitImportDirective(ImportDirective node) { | |
| // We don't record relevance data here because we make the assumption that | |
| // `as` occurs more often than `deferred` at this location. | |
| if (node.asKeyword case var keyword?) _unrecorded(keyword); | |
| if (node.prefix case var prefix?) _recordDeclaration(prefix.token); | |
| var context = 'uri'; | |
| var deferredKeyword = node.deferredKeyword; | |
| if (deferredKeyword != null) { | |
| recordKeyword('ImportDirective_$context', deferredKeyword); | |
| context = 'deferred'; | |
| } | |
| var asKeyword = node.asKeyword; | |
| if (asKeyword != null) { | |
| recordKeyword('ImportDirective_$context', asKeyword); | |
| context = 'prefix'; | |
| } | |
| if (node.configurations.isNotEmpty) { | |
| _recordKeyword( | |
| 'ImportDirective_$context', | |
| node.configurations[0], | |
| allowedKeywords: importKeywords, | |
| ); | |
| context = 'configurations'; | |
| } | |
| if (node.combinators.isNotEmpty) { | |
| _recordKeyword( | |
| 'ImportDirective_$context', | |
| node.combinators[0], | |
| allowedKeywords: importKeywords, | |
| ); | |
| } | |
| for (var combinator in node.combinators) { | |
| _recordKeyword( | |
| 'ImportDirective_combinator', | |
| combinator, | |
| allowedKeywords: importKeywords, | |
| ); | |
| } | |
| super.visitImportDirective(node); | |
| } | |
| @override | |
| void visitImportPrefixReference(ImportPrefixReference node) { | |
| // There are no completions. | |
| super.visitImportPrefixReference(node); | |
| } | |
| @override | |
| void visitIndexExpression(IndexExpression node) { | |
| _recordDataForNode( | |
| 'IndexExpression_index', | |
| node.index, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitIndexExpression(node); | |
| } | |
| @override | |
| void visitInstanceCreationExpression(InstanceCreationExpression node) { | |
| _recordDataForNode( | |
| 'InstanceCreationExpression_constructorName', | |
| node.constructorName, | |
| ); | |
| super.visitInstanceCreationExpression(node); | |
| } | |
| @override | |
| void visitIntegerLiteral(IntegerLiteral node) { | |
| // There are no completions. | |
| super.visitIntegerLiteral(node); | |
| } | |
| @override | |
| void visitInterpolationExpression(InterpolationExpression node) { | |
| // TODO(brianwilkerson): Consider splitting this based on whether the | |
| // expression is a simple identifier ('$') or a full expression ('${'). | |
| _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) { | |
| _recordMember(node.isOperator); | |
| _recordDataForNode('IsExpression_type', node.type); | |
| super.visitIsExpression(node); | |
| } | |
| @override | |
| void visitLabel(Label node) { | |
| // There are no completions. | |
| _recordDeclaration(node.name); | |
| 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 visitListPattern(ListPattern node) { | |
| for (var element in node.elements) { | |
| _recordDataForNode( | |
| 'ListPattern_element', | |
| element, | |
| allowedKeywords: patternKeywords, | |
| ); | |
| } | |
| super.visitListPattern(node); | |
| } | |
| @override | |
| void visitLogicalAndPattern(LogicalAndPattern node) { | |
| _recordDataForNode( | |
| 'LogicalAndPattern_rightOperand', | |
| node.rightOperand, | |
| allowedKeywords: patternKeywords, | |
| ); | |
| super.visitLogicalAndPattern(node); | |
| } | |
| @override | |
| void visitLogicalOrPattern(LogicalOrPattern node) { | |
| _recordDataForNode( | |
| 'LogicalOrPattern_rightOperand', | |
| node.rightOperand, | |
| allowedKeywords: patternKeywords, | |
| ); | |
| super.visitLogicalOrPattern(node); | |
| } | |
| @override | |
| void visitMapLiteralEntry(MapLiteralEntry node) { | |
| // The information about the key is recorded in `visitSetOrMapLiteral` under | |
| // the key 'SetOrMapLiteral_element'. | |
| _recordDataForNode( | |
| 'MapLiteralEntry_value', | |
| node.value, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitMapLiteralEntry(node); | |
| } | |
| @override | |
| void visitMapPattern(MapPattern node) { | |
| // There are no completions. | |
| super.visitMapPattern(node); | |
| } | |
| @override | |
| void visitMapPatternEntry(MapPatternEntry node) { | |
| _recordDataForNode( | |
| 'MapPatternEntry_key', | |
| node.key, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| _recordDataForNode( | |
| 'MapPatternEntry_value', | |
| node.value, | |
| allowedKeywords: patternKeywords, | |
| ); | |
| super.visitMapPatternEntry(node); | |
| } | |
| @override | |
| void visitMethodDeclaration(MethodDeclaration node) { | |
| if (node.propertyKeyword case var keyword?) _unrecorded(keyword); | |
| if (node.operatorKeyword case var keyword?) _unrecorded(keyword); | |
| _recordDeclaration(node.name); | |
| _recordDataForNode('MethodDeclaration_returnType', node.returnType); | |
| super.visitMethodDeclaration(node); | |
| } | |
| @override | |
| void visitMethodInvocation(MethodInvocation node) { | |
| // There are no completions. | |
| _recordMember(node.methodName.token); | |
| super.visitMethodInvocation(node); | |
| } | |
| @override | |
| void visitMixinDeclaration(MixinDeclaration node) { | |
| // If there are modifiers before `mixin`, then the first will be recorded | |
| // by the containing node, but the rest will not use the relevance tables | |
| // because there is a fixed order in which they must appear. | |
| _unrecordedBetween( | |
| node.firstTokenAfterCommentAndMetadata, | |
| node.mixinKeyword, | |
| ); | |
| _recordDeclaration(node.name); | |
| var context = 'name'; | |
| if (node.onClause != null) { | |
| _recordKeyword( | |
| 'MixinDeclaration_$context', | |
| node.onClause, | |
| allowedKeywords: [Keyword.ON], | |
| ); | |
| context = 'on'; | |
| } | |
| _recordKeyword( | |
| '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); | |
| } | |
| @override | |
| void visitMixinOnClause(MixinOnClause node) { | |
| for (var constraint in node.superclassConstraints) { | |
| _recordDataForNode('OnClause_superclassConstraint', 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. | |
| if (node.importPrefix != null) { | |
| // There is no relevance data for names following a prefix. | |
| _recordMember(node.name); | |
| } | |
| super.visitNamedType(node); | |
| } | |
| @override | |
| void visitNameWithTypeParameters(NameWithTypeParameters node) { | |
| // TODO(brianwilkerson): implement visitNameWithTypeParameters | |
| super.visitNameWithTypeParameters(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 visitNullAssertPattern(NullAssertPattern node) { | |
| // There are no completions. | |
| super.visitNullAssertPattern(node); | |
| } | |
| @override | |
| void visitNullAwareElement(NullAwareElement node) { | |
| _recordDataForNode('NullAwareElement_value', node.value); | |
| super.visitNullAwareElement(node); | |
| } | |
| @override | |
| void visitNullCheckPattern(NullCheckPattern node) { | |
| // There are no completions. | |
| super.visitNullCheckPattern(node); | |
| } | |
| @override | |
| void visitNullLiteral(NullLiteral node) { | |
| // There are no completions. | |
| super.visitNullLiteral(node); | |
| } | |
| @override | |
| void visitObjectPattern(ObjectPattern node) { | |
| // There are no completions. | |
| super.visitObjectPattern(node); | |
| } | |
| @override | |
| void visitParenthesizedExpression(ParenthesizedExpression node) { | |
| _recordDataForNode( | |
| 'ParenthesizedExpression_expression', | |
| node.expression, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitParenthesizedExpression(node); | |
| } | |
| @override | |
| void visitParenthesizedPattern(ParenthesizedPattern node) { | |
| _recordDataForNode( | |
| 'ParenthesizedPattern_pattern', | |
| node.pattern, | |
| allowedKeywords: patternKeywords, | |
| ); | |
| super.visitParenthesizedPattern(node); | |
| } | |
| @override | |
| void visitPartDirective(PartDirective node) { | |
| // There are no completions. | |
| super.visitPartDirective(node); | |
| } | |
| @override | |
| void visitPartOfDirective(PartOfDirective node) { | |
| // No other completions are valid here. | |
| _unrecorded(node.ofKeyword); | |
| super.visitPartOfDirective(node); | |
| } | |
| @override | |
| void visitPatternAssignment(PatternAssignment node) { | |
| _recordDataForNode('PatternAssignment_expression', node.expression); | |
| super.visitPatternAssignment(node); | |
| } | |
| @override | |
| void visitPatternField(PatternField node) { | |
| _recordDataForNode( | |
| 'PatternField_pattern', | |
| node.pattern, | |
| allowedKeywords: patternKeywords, | |
| ); | |
| super.visitPatternField(node); | |
| } | |
| @override | |
| void visitPatternFieldName(PatternFieldName node) { | |
| // The relevance of individual field names isn't dependent on any | |
| // information available while building the table. | |
| if (node.name case var name?) _unrecorded(name); | |
| super.visitPatternFieldName(node); | |
| } | |
| @override | |
| void visitPatternVariableDeclaration(PatternVariableDeclaration node) { | |
| _recordDataForNode( | |
| 'PatternVariableDeclaration_pattern', | |
| node.pattern, | |
| allowedKeywords: patternKeywords, | |
| ); | |
| _recordDataForNode( | |
| 'PatternVariableDeclaration_expression', | |
| node.expression, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitPatternVariableDeclaration(node); | |
| } | |
| @override | |
| void visitPatternVariableDeclarationStatement( | |
| PatternVariableDeclarationStatement node, | |
| ) { | |
| // There are no completions. | |
| super.visitPatternVariableDeclarationStatement(node); | |
| } | |
| @override | |
| void visitPostfixExpression(PostfixExpression node) { | |
| // There are no completions. | |
| super.visitPostfixExpression(node); | |
| } | |
| @override | |
| void visitPrefixedIdentifier(PrefixedIdentifier node) { | |
| // There are no completions. | |
| _recordMember(node.identifier.token); | |
| super.visitPrefixedIdentifier(node); | |
| } | |
| @override | |
| void visitPrefixExpression(PrefixExpression node) { | |
| _recordDataForNode( | |
| 'PrefixExpression_${node.operator}_operand', | |
| node.operand, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitPrefixExpression(node); | |
| } | |
| @override | |
| void visitPrimaryConstructorBody(PrimaryConstructorBody node) { | |
| for (var initializer in node.initializers) { | |
| _recordDataForNode('ConstructorDeclaration_initializer', initializer); | |
| } | |
| super.visitPrimaryConstructorBody(node); | |
| } | |
| @override | |
| void visitPrimaryConstructorDeclaration(PrimaryConstructorDeclaration node) { | |
| // There are no completions. | |
| super.visitPrimaryConstructorDeclaration(node); | |
| } | |
| @override | |
| void visitPrimaryConstructorName(PrimaryConstructorName node) { | |
| // There are no completions. | |
| _recordDeclaration(node.name); | |
| super.visitPrimaryConstructorName(node); | |
| } | |
| @override | |
| void visitPropertyAccess(PropertyAccess node) { | |
| _recordMember(node.propertyName.token); | |
| _recordDataForNode('PropertyAccess_propertyName', node.propertyName); | |
| super.visitPropertyAccess(node); | |
| } | |
| @override | |
| void visitRecordLiteral(RecordLiteral node) { | |
| for (var field in node.fields) { | |
| _recordDataForNode('RecordLiteral_fieldName', field); | |
| if (field is RecordLiteralNamedField) { | |
| _recordDataForNode('RecordListeral_fieldValue', field.fieldExpression); | |
| } | |
| } | |
| super.visitRecordLiteral(node); | |
| } | |
| @override | |
| void visitRecordPattern(RecordPattern node) { | |
| // There are no completions. | |
| super.visitRecordPattern(node); | |
| } | |
| @override | |
| void visitRecordTypeAnnotation(RecordTypeAnnotation node) { | |
| // TODO(brianwilkerson): implement visitRecordTypeAnnotation | |
| super.visitRecordTypeAnnotation(node); | |
| } | |
| @override | |
| void visitRecordTypeAnnotationNamedField( | |
| RecordTypeAnnotationNamedField node, | |
| ) { | |
| _recordDataForNode('RecordType_fieldType', node.type); | |
| _recordDeclaration(node.name); | |
| super.visitRecordTypeAnnotationNamedField(node); | |
| } | |
| @override | |
| void visitRecordTypeAnnotationNamedFields( | |
| RecordTypeAnnotationNamedFields node, | |
| ) { | |
| // There are no completions. | |
| super.visitRecordTypeAnnotationNamedFields(node); | |
| } | |
| @override | |
| void visitRecordTypeAnnotationPositionalField( | |
| RecordTypeAnnotationPositionalField node, | |
| ) { | |
| _recordDataForNode('RecordType_fieldType', node.type); | |
| if (node.name case var name?) _recordDeclaration(name); | |
| super.visitRecordTypeAnnotationPositionalField(node); | |
| } | |
| @override | |
| void visitRedirectingConstructorInvocation( | |
| RedirectingConstructorInvocation node, | |
| ) { | |
| // The name of the constructor being redirected to is currently not | |
| // recorded. Consider adding this to the table so that we could order the | |
| // list of constructors. | |
| if (node.constructorName?.token case var name?) _recordMember(name); | |
| super.visitRedirectingConstructorInvocation(node); | |
| } | |
| @override | |
| void visitRegularFormalParameter(RegularFormalParameter node) { | |
| _recordDataForNode('SimpleFormalParameter_type', node.type); | |
| if (node.name case var name?) { | |
| _recordDeclaration(name); | |
| } | |
| super.visitRegularFormalParameter(node); | |
| } | |
| @override | |
| void visitRelationalPattern(RelationalPattern node) { | |
| _recordDataForNode( | |
| 'RelationalPattern_${node.operator}_operand', | |
| node.operand, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitRelationalPattern(node); | |
| } | |
| @override | |
| void visitRestPatternElement(RestPatternElement node) { | |
| _recordDataForNode( | |
| 'RestPatternElement_pattern', | |
| node.pattern, | |
| allowedKeywords: patternKeywords, | |
| ); | |
| super.visitRestPatternElement(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, | |
| ); | |
| 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_shownName', name); | |
| } | |
| super.visitShowCombinator(node); | |
| } | |
| @override | |
| void visitSimpleIdentifier(SimpleIdentifier node) { | |
| // There are no completions. | |
| 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) { | |
| // The name of the super constructor is currently not recorded. Consider | |
| // adding this to the table so that we could order the list of constructors. | |
| if (node.constructorName?.token case var name?) _recordMember(name); | |
| super.visitSuperConstructorInvocation(node); | |
| } | |
| @override | |
| void visitSuperExpression(SuperExpression node) { | |
| // There are no completions. | |
| super.visitSuperExpression(node); | |
| } | |
| @override | |
| void visitSuperFormalParameter(SuperFormalParameter node) { | |
| // No other completions are valid here. | |
| _unrecorded(node.superKeyword); | |
| _recordDeclaration(node.name); | |
| super.visitSuperFormalParameter(node); | |
| } | |
| @override | |
| void visitSwitchCase(SwitchCase node) { | |
| // No other completions are valid here. | |
| _unrecorded(node.keyword); | |
| _recordDataForNode( | |
| 'SwitchCase_expression', | |
| node.expression, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| for (var statement in node.statements) { | |
| _recordDataForNode( | |
| 'SwitchMember_statement', | |
| statement, | |
| allowedKeywords: statementKeywords, | |
| ); | |
| } | |
| super.visitSwitchCase(node); | |
| } | |
| @override | |
| void visitSwitchDefault(SwitchDefault node) { | |
| // No other completions are valid here. | |
| _unrecorded(node.keyword); | |
| for (var statement in node.statements) { | |
| _recordDataForNode( | |
| 'SwitchMember_statement', | |
| statement, | |
| allowedKeywords: statementKeywords, | |
| ); | |
| } | |
| super.visitSwitchDefault(node); | |
| } | |
| @override | |
| void visitSwitchExpression(SwitchExpression node) { | |
| _recordDataForNode( | |
| 'SwitchExpression_expression', | |
| node.expression, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitSwitchExpression(node); | |
| } | |
| @override | |
| void visitSwitchExpressionCase(SwitchExpressionCase node) { | |
| _recordDataForNode( | |
| 'SwitchExpressionCase_guardedPattern', | |
| node.guardedPattern, | |
| allowedKeywords: patternKeywords, | |
| ); | |
| _recordDataForNode( | |
| 'SwitchExpressionCase_expression', | |
| node.expression, | |
| allowedKeywords: expressionKeywords, | |
| ); | |
| super.visitSwitchExpressionCase(node); | |
| } | |
| @override | |
| void visitSwitchPatternCase(SwitchPatternCase node) { | |
| // No other completions are valid here. | |
| _unrecorded(node.keyword); | |
| _recordDataForNode( | |
| 'SwitchPatternCase_guardedPattern', | |
| node.guardedPattern, | |
| allowedKeywords: patternKeywords, | |
| ); | |
| for (var statement in node.statements) { | |
| _recordDataForNode( | |
| 'SwitchPatternCase_statement', | |
| statement, | |
| allowedKeywords: statementKeywords, | |
| ); | |
| } | |
| super.visitSwitchPatternCase(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 in symbol literals. | |
| for (var component in node.components) { | |
| _unrecorded(component); | |
| } | |
| 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) { | |
| _recordKeyword( | |
| 'TryStatement_$context', | |
| clause, | |
| allowedKeywords: [Keyword.ON], | |
| ); | |
| context = 'catch'; | |
| } | |
| var finallyKeyword = node.finallyKeyword; | |
| if (finallyKeyword != null) { | |
| recordKeyword('TryStatement_$context', finallyKeyword); | |
| } | |
| super.visitTryStatement(node); | |
| } | |
| @override | |
| void visitTypeArgumentList(TypeArgumentList node) { | |
| for (var typeArgument in node.arguments) { | |
| _recordDataForNode('TypeArgumentList_argument', typeArgument); | |
| } | |
| super.visitTypeArgumentList(node); | |
| } | |
| @override | |
| void visitTypeLiteral(TypeLiteral node) { | |
| // TODO(brianwilkerson): Consider recording the kind of declaration that | |
| // produced the type. If it's more common to see classes than mixins, for | |
| // example, then we could use that to adjust the relevance of completions. | |
| super.visitTypeLiteral(node); | |
| } | |
| @override | |
| void visitTypeParameter(TypeParameter node) { | |
| // There is no relevance data because there's only one allowed keyword. | |
| if (node.extendsKeyword case var keyword?) _unrecorded(keyword); | |
| _recordDeclaration(node.name); | |
| 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) { | |
| _recordDeclaration(node.name); | |
| 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) { | |
| if (node.lateKeyword != null) { | |
| if (node.keyword case var keyword?) { | |
| _unrecorded(keyword); | |
| } | |
| } | |
| _recordDataForNode('VariableDeclarationList_type', node.type); | |
| super.visitVariableDeclarationList(node); | |
| } | |
| @override | |
| void visitVariableDeclarationStatement(VariableDeclarationStatement node) { | |
| // There are no completions. | |
| super.visitVariableDeclarationStatement(node); | |
| } | |
| @override | |
| void visitWhenClause(WhenClause node) { | |
| // No other completions are valid here. | |
| _unrecorded(node.whenKeyword); | |
| _recordDataForNode('WhenClause_expression', node.expression); | |
| super.visitWhenClause(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 visitWildcardPattern(WildcardPattern node) { | |
| // There are no completions. | |
| _unrecorded(node.name); | |
| super.visitWildcardPattern(node); | |
| } | |
| @override | |
| void visitWithClause(WithClause node) { | |
| // The set of keywords available at this point is small and deterministic. | |
| _unrecorded(node.withKeyword); | |
| for (var namedType in node.mixinTypes) { | |
| _recordDataForNode('WithClause_mixinType', 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 Annotation) { | |
| return 'annotation'; | |
| } else if (parent is EnumConstantArguments) { | |
| return 'enumConstant'; | |
| } else if (parent is ExtensionOverride) { | |
| return 'extensionOverride'; | |
| } else if (parent is FunctionExpressionInvocation) { | |
| return 'function'; | |
| } else if (parent is InstanceCreationExpression) { | |
| if (parent.staticType.isWidgetType) { | |
| return 'widgetConstructor'; | |
| } | |
| return 'constructor'; | |
| } else if (parent is MethodInvocation) { | |
| return 'method'; | |
| } else if (parent is RedirectingConstructorInvocation) { | |
| return 'constructorRedirect'; | |
| } else if (parent is SuperConstructorInvocation) { | |
| return 'constructorRedirect'; | |
| } | |
| } else if (node is AssignmentExpression || | |
| node is BinaryExpression || | |
| node is PrefixExpression || | |
| node is PostfixExpression) { | |
| return 'operator'; | |
| } else if (node is IndexExpression) { | |
| return 'index'; | |
| } | |
| throw ArgumentError( | |
| 'Unknown parent of ${node.runtimeType}: ${node.parent.runtimeType}', | |
| ); | |
| } | |
| /// 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) { | |
| AstNode? 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 featureComputer.computeElementKind2(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 featureComputer.computeElementKind2(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; | |
| } | |
| /// Record information about the given [node] occurring in the given | |
| /// [context]. | |
| void _recordDataForNode( | |
| String context, | |
| AstNode? node, { | |
| List<Keyword> allowedKeywords = noKeywords, | |
| }) { | |
| // Skip past the null-aware operator if present. This is making the | |
| // assumption that the probability of the next token is not dependent on the | |
| // presence or absence of the null-aware operator. | |
| var token = node?.beginToken; | |
| if (node is MapLiteralEntry && node.keyQuestion == token) { | |
| token = token?.next; | |
| } else if (node is NullAwareElement && node.question == token) { | |
| token = token?.next; | |
| } | |
| _identifiersAndKeywords.remove(token); | |
| _recordElementKind(context, node); | |
| _recordKeyword(context, node, allowedKeywords: allowedKeywords); | |
| // Record data for all of the keywords in a formal parameter. This is | |
| // contrary to the way other strings of keywords are handled, and probably | |
| // needs a good justification (or needs to be changed). | |
| if (node is FormalParameter && token!.isKeyword) { | |
| token = token.next; | |
| do { | |
| _identifiersAndKeywords.remove(token); | |
| _recordElementKind(context, node); | |
| _recordKeyword(context, node, allowedKeywords: allowedKeywords); | |
| token = token?.next; | |
| } while (token != null && token.isKeyword); | |
| } | |
| } | |
| /// There is no information recorded about identifiers that are being | |
| /// declared because code completion bases its suggestions on the name of the | |
| /// context type, but they need to be removed from the list of identifiers and | |
| /// keywords so that they aren't reported as being unrecorded. | |
| /// | |
| /// This is effectively a marker explaining why the identifier isn't recorded | |
| /// that doesn't require a comment at every invocation site. | |
| void _recordDeclaration(Token token) { | |
| _unrecorded(token); | |
| } | |
| /// 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); | |
| } | |
| } | |
| } | |
| } | |
| /// If the left-most token of the [node] is a keyword, then record that it | |
| /// occurred in the given [context]. | |
| void _recordKeyword( | |
| String context, | |
| AstNode? node, { | |
| List<Keyword> allowedKeywords = noKeywords, | |
| }) { | |
| if (node != null) { | |
| var token = _leftMostToken(node); | |
| if (token != null && token.isKeyword) { | |
| _identifiersAndKeywords.remove(token); | |
| var keyword = token.keyword!; | |
| if (keyword == Keyword.NEW) { | |
| // We don't suggest `new`, so we don't care about the frequency with | |
| // which it is used. | |
| return; | |
| } else if (keyword.isBuiltInOrPseudo && | |
| !allowedKeywords.contains(keyword)) { | |
| // These keywords can be used as identifiers, so determine whether | |
| // it is being used as a keyword or an identifier. | |
| return; | |
| } | |
| recordKeyword(context, token); | |
| } | |
| } | |
| } | |
| /// There is no information recorded about the names of members that are being | |
| /// accessed, but they need to be removed from the list of identifiers and | |
| /// keywords so that they aren't reported as being unrecorded. | |
| void _recordMember(Token? token) { | |
| // TODO(brianwilkerson): Consider collecting and using the data at every | |
| // location where this method is invoked. If the data would not be helpful, | |
| // invoke `_unrecorded` instead. | |
| if (token != null) _identifiersAndKeywords.remove(token); | |
| } | |
| /// The given [token] is in a location where no relevance table entry is | |
| /// needed. Remove the token from the list of unrecorded tokens so that it | |
| /// won't produce a false positive. | |
| /// | |
| /// Invocation sites should indicate why there's no relevance table entry. | |
| void _unrecorded(Token token) { | |
| _identifiersAndKeywords.remove(token); | |
| } | |
| /// If the [firstToken] and [lastToken] are not the same, then record that the | |
| /// tokens after [firstToken] up to and including the [lastToken] are not | |
| /// captured in the relevance table. | |
| void _unrecordedBetween(Token firstToken, Token lastToken) { | |
| if (firstToken != lastToken) { | |
| var token = firstToken.next!; | |
| while (token != lastToken) { | |
| _unrecorded(token); | |
| token = token.next!; | |
| } | |
| _unrecorded(token); | |
| } | |
| } | |
| } | |
| /// 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); | |
| } | |
| } | |
| /// 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 couldn't 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; | |
| } | |
| collector.initializeFrom(resolvedUnitResult); | |
| resolvedUnitResult.unit.accept(collector); | |
| var identifiersAndKeywords = collector._identifiersAndKeywords; | |
| if (identifiersAndKeywords.isNotEmpty) { | |
| print('Unrecorded identifiers and keywords in $filePath:'); | |
| for (var token in identifiersAndKeywords) { | |
| print(_tokenInContext(token)); | |
| var ancestors = resolvedUnitResult.unit | |
| .nodeCovering(offset: token.offset) | |
| ?.withAncestors; | |
| if (ancestors == null) { | |
| print('*** Node not found'); | |
| } else { | |
| print( | |
| ancestors | |
| .map((node) => node.runtimeType.toString()) | |
| .join(', '), | |
| ); | |
| } | |
| } | |
| } | |
| } catch (exception, stacktrace) { | |
| print('Exception caught analyzing: "$filePath"'); | |
| print(exception); | |
| print(stacktrace); | |
| } | |
| } | |
| } | |
| } | |
| /// Print the [token] surrounded by markup, together with the 10 tokens before | |
| /// and 10 tokens after the [token]. | |
| String _tokenInContext(Token token) { | |
| var first = token; | |
| for (var i = 0; i < 10; i++) { | |
| first = first.previous!; | |
| } | |
| var last = token; | |
| for (var i = 0; i < 10; i++) { | |
| last = last.next!; | |
| } | |
| var buffer = StringBuffer()..write(' '); | |
| var current = first; | |
| if (!current.isEof) { | |
| buffer.write('...'); | |
| } | |
| while (current != last) { | |
| buffer.write(' '); | |
| if (current == token) buffer.write('[!'); | |
| buffer.write(current.lexeme); | |
| if (current == token) buffer.write('!]'); | |
| current = current.next!; | |
| } | |
| buffer.write(' ${current.lexeme}'); | |
| if (!current.isEof) { | |
| buffer.write(' ...'); | |
| } | |
| return buffer.toString(); | |
| } | |
| } | |
| /// A class used to write relevance data as a set of tables in a generated Dart | |
| /// file. | |
| class RelevanceTableWriter { | |
| final StringSink sink; | |
| new(this.sink); | |
| void write(RelevanceData data) { | |
| writeFileHeader(); | |
| writeElementKindTable(data); | |
| writeKeywordTable(data); | |
| writeFileFooter(); | |
| } | |
| void writeElementKindTable(RelevanceData data) { | |
| sink.writeln(); | |
| sink.write(''' | |
| const defaultElementKindRelevance = { | |
| '''); | |
| var byKind = data._byKind; | |
| var entries = byKind.entries.toList() | |
| ..sort((first, second) => first.key.compareTo(second.key)); | |
| for (var entry in entries) { | |
| var completionLocation = entry.key; | |
| var counts = entry.value; | |
| if (_hasElementKind(counts)) { | |
| var totalCount = _totalCount(counts); | |
| // TODO(brianwilkerson): If two element kinds have the same count they | |
| // ought to have the same probability. This doesn't correctly do that. | |
| var entries = counts.entries.toList() | |
| ..sort((first, second) => first.value.compareTo(second.value)); | |
| sink.write(" '"); | |
| sink.write(completionLocation); | |
| sink.writeln("': {"); | |
| var cumulativeCount = 0; | |
| var lowerBound = 0.0; | |
| for (var entry in entries) { | |
| var kind = entry.key; | |
| cumulativeCount += entry.value; | |
| var upperBound = cumulativeCount / totalCount; | |
| if (kind is _ElementKind) { | |
| sink.write(' ElementKind.'); | |
| sink.write(kind.elementKind.name); | |
| sink.write(': ProbabilityRange(lower: '); | |
| sink.write(lowerBound.toStringAsFixed(3)); | |
| sink.write(', upper: '); | |
| sink.write(upperBound.toStringAsFixed(3)); | |
| sink.writeln('),'); | |
| } | |
| lowerBound = upperBound; | |
| } | |
| sink.writeln(' },'); | |
| } | |
| } | |
| sink.writeln('};'); | |
| } | |
| void writeFileFooter() { | |
| sink.write(''' | |
| /// A table keyed by completion location and element kind whose values are the | |
| /// ranges of the relevance of those element kinds in those locations. | |
| Map<String, Map<ElementKind, ProbabilityRange>> elementKindRelevance = | |
| defaultElementKindRelevance; | |
| /// A table keyed by completion location and keyword whose values are the | |
| /// ranges of the relevance of those keywords in those locations. | |
| Map<String, Map<String, ProbabilityRange>> keywordRelevance = | |
| defaultKeywordRelevance; | |
| '''); | |
| } | |
| void writeFileHeader() { | |
| sink.write(''' | |
| // 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. | |
| // This file has been automatically generated. Please do not edit it manually. | |
| // To regenerate the file, use the script | |
| // "pkg/analysis_server/tool/completion_metrics/relevance_table_generator.dart", | |
| // passing it the location of a corpus of code that is large enough for the | |
| // computed values to be statistically meaningful. | |
| import 'package:analysis_server/src/services/completion/dart/probability_range.dart'; | |
| import 'package:analyzer_plugin/protocol/protocol_common.dart'; | |
| '''); | |
| } | |
| void writeKeywordTable(RelevanceData data) { | |
| sink.writeln(); | |
| sink.write(''' | |
| const defaultKeywordRelevance = { | |
| '''); | |
| var byKind = data._byKind; | |
| var entries = byKind.entries.toList() | |
| ..sort((first, second) => first.key.compareTo(second.key)); | |
| for (var entry in entries) { | |
| var completionLocation = entry.key; | |
| var counts = entry.value; | |
| if (_hasKeyword(counts)) { | |
| var totalCount = _totalCount(counts); | |
| // TODO(brianwilkerson): If two keywords have the same count they ought to | |
| // have the same probability. This doesn't correctly do that. | |
| var entries = counts.entries.toList() | |
| ..sort((first, second) => first.value.compareTo(second.value)); | |
| sink.write(" '"); | |
| sink.write(completionLocation); | |
| sink.writeln("': {"); | |
| var cumulativeCount = 0; | |
| var lowerBound = 0.0; | |
| for (var entry in entries) { | |
| var kind = entry.key; | |
| cumulativeCount += entry.value; | |
| var upperBound = cumulativeCount / totalCount; | |
| if (kind is _Keyword) { | |
| sink.write(" '"); | |
| sink.write(kind.keyword.lexeme); | |
| sink.write("': ProbabilityRange(lower: "); | |
| sink.write(lowerBound.toStringAsFixed(3)); | |
| sink.write(', upper: '); | |
| sink.write(upperBound.toStringAsFixed(3)); | |
| sink.writeln('),'); | |
| } | |
| lowerBound = upperBound; | |
| } | |
| sink.writeln(' },'); | |
| } | |
| } | |
| sink.writeln('};'); | |
| } | |
| /// Return `true` if the table of [counts] contains at least one key that is | |
| /// an element kind. | |
| bool _hasElementKind(Map<_Kind, int> counts) { | |
| for (var kind in counts.keys) { | |
| if (kind is _ElementKind) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /// Return `true` if the table of [counts] contains at least one key that is a | |
| /// keyword. | |
| bool _hasKeyword(Map<_Kind, int> counts) { | |
| for (var kind in counts.keys) { | |
| if (kind is _Keyword) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /// Return the total of the counts in the given table of [counts]. | |
| int _totalCount(Map<_Kind, int> counts) { | |
| return counts.values.fold( | |
| 0, | |
| (previousValue, value) => previousValue + value, | |
| ); | |
| } | |
| } | |
| /// A wrapper for an element kind to allow keywords and element kinds to be used | |
| /// as keys in a single table. | |
| class _ElementKind extends _Kind { | |
| static final Map<ElementKind, _ElementKind> instances = {}; | |
| final ElementKind elementKind; | |
| factory(ElementKind elementKind) => | |
| instances.putIfAbsent(elementKind, () => _ElementKind._(elementKind)); | |
| new _(this.elementKind); | |
| @override | |
| String get uniqueKey => 'e${elementKind.name}'; | |
| } | |
| /// A wrapper for a keyword to allow keywords and element kinds to be used as | |
| /// keys in a single table. | |
| class _Keyword extends _Kind { | |
| static final Map<Keyword, _Keyword> instances = {}; | |
| final Keyword keyword; | |
| factory(Keyword keyword) => | |
| instances.putIfAbsent(keyword, () => _Keyword._(keyword)); | |
| new _(this.keyword); | |
| @override | |
| String get uniqueKey => 'k${keyword.lexeme}'; | |
| } | |
| /// A superclass for [_ElementKind] and [_Keyword] to allow keywords and element | |
| /// kinds to be used as keys in a single table. | |
| abstract class _Kind { | |
| /// Return the unique key used when representing an instance of a subclass in | |
| /// a JSON format. | |
| String get uniqueKey; | |
| } |