From eddba24b96bfe8dc08b4124529276706c1e82237 Mon Sep 17 00:00:00 2001 From: Paul Berry Date: Wed, 10 Dec 2025 20:14:19 -0800 Subject: [PATCH] [messages] Create a utility to migrate to literate diagnostic reporting API. Adds the file `use_literate_api_in_analyzer.dart`. This script reads the analyzer source files and identifies any places where the old diagnostic reporting API is being used, e.g.: _diagnosticReporter.atNode( node, diag.deprecatedExtend, arguments: [element.name!], ); And refactors the code to use the new "literate" diagnostic reporting API, e.g.: _diagnosticReporter.report( diag.deprecatedExtend.withArguments(typeName: element.name!).at(node), ); The idea is to automate the majority of the migration from the old to the new diagnostic reporting API by taking care of the most straightforward cases. More complex cases are skipped; they will have to be migrated manually. In particular, any call site that contains one of the following things will be left alone: - A diagnostic code that isn't a direct reference to a diagnostic code constant (e.g., in the above example, `diag.deprecatedExtend` is ok because it refers directly to the `deprecatedExtend` constant). - An argument list that isn't a list literal, or is a list literal containing flow control or spreads (e.g., in the above example, `[element.name!]` is ok because it's a simple list literal with no flow control or spreads). - A comment somewhere inside the invocation. These are left to human translation so that the meaning of the comment can be preserved. The script also skips translation of any diagnostic codes that use the placeholder parameter names `p0`, `p1`, `p2`, etc. The rationale is that if we start using the placeholder names now, then in the future when we want to assign more reasonable parameter names, refactoring will be more difficult. In future CLs, I plan to give better names to some of these placeholder parameters, and then re-run the script to allow more migration to occur. Change-Id: I6a6a696404b42ca67e5208cb4e80de17e485551c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/467385 Commit-Queue: Paul Berry Reviewed-by: Konstantin Shcheglov --- pkg/analyzer_utilities/lib/messages.dart | 4 + pkg/analyzer_utilities/pubspec.yaml | 1 + .../use_literate_api_in_analyzer.dart | 345 ++++++++++++++++++ 3 files changed, 350 insertions(+) create mode 100644 pkg/analyzer_utilities/tool/messages/use_literate_api_in_analyzer.dart diff --git a/pkg/analyzer_utilities/lib/messages.dart b/pkg/analyzer_utilities/lib/messages.dart index cfa126c69ca..5ade3ff4a89 100644 --- a/pkg/analyzer_utilities/lib/messages.dart +++ b/pkg/analyzer_utilities/lib/messages.dart @@ -438,6 +438,9 @@ class DiagnosticTables { final Map> activeMessagesByPackage = {}; + final Map diagnosticsByAnalyzerUniqueName = + {}; + DiagnosticTables._(List messages) { var frontEndCodeDuplicateChecker = _DuplicateChecker( kind: 'Front end code', @@ -468,6 +471,7 @@ class DiagnosticTables { .snakeCaseName] ??= []) .add(message); + diagnosticsByAnalyzerUniqueName[analyzerCode.snakeCaseName] = message; var package = message.package; var type = message.type; if (!package.permittedTypes.contains(type)) { diff --git a/pkg/analyzer_utilities/pubspec.yaml b/pkg/analyzer_utilities/pubspec.yaml index 67d33616b09..336134a7a3b 100644 --- a/pkg/analyzer_utilities/pubspec.yaml +++ b/pkg/analyzer_utilities/pubspec.yaml @@ -21,5 +21,6 @@ dependencies: # Use 'any' constraints here; we get our versions from the DEPS file. dev_dependencies: + analyzer_plugin: any lints: any test_reflective_loader: any diff --git a/pkg/analyzer_utilities/tool/messages/use_literate_api_in_analyzer.dart b/pkg/analyzer_utilities/tool/messages/use_literate_api_in_analyzer.dart new file mode 100644 index 00000000000..1bdcd562f20 --- /dev/null +++ b/pkg/analyzer_utilities/tool/messages/use_literate_api_in_analyzer.dart @@ -0,0 +1,345 @@ +// Copyright (c) 2025, 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'; + +import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/ast/syntactic_entity.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/constant/value.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:analyzer/source/line_info.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:analyzer/src/dart/ast/ast.dart'; +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart'; +import 'package:analyzer_testing/package_root.dart'; +import 'package:analyzer_utilities/analyzer_messages.dart'; +import 'package:analyzer_utilities/messages.dart'; +import 'package:collection/collection.dart'; +import 'package:path/path.dart'; + +void main() async { + var provider = PhysicalResourceProvider.INSTANCE; + var collection = AnalysisContextCollection( + includedPaths: [join(packageRoot, 'analyzer')], + resourceProvider: provider, + ); + // Use `.single` to make sure that `collection` just contains a single + // context. This ensures that the code below will see all the files in the + // packages. + var context = collection.contexts.single; + var rejectStats = _RejectStats(); + var changeBuilder = ChangeBuilder(session: context.currentSession); + for (var libraryFile in context.contextRoot.analyzedFiles()) { + if (!libraryFile.endsWith('.dart')) continue; + var fileResult = context.currentSession.getFile(libraryFile) as FileResult; + if (fileResult.isLibrary) { + var resolvedLibraryResult = + (await context.currentSession.getResolvedLibrary(libraryFile)) + as ResolvedLibraryResult; + for (var unit in resolvedLibraryResult.units) { + var visitor = _Visitor( + rejectStats: rejectStats, + fileContents: unit.content, + path: unit.path, + lineInfo: unit.lineInfo, + ); + unit.unit.accept(visitor); + var changes = visitor.changes; + if (changes.isNotEmpty) { + var s = changes.length == 1 ? '' : 's'; + print('Found ${changes.length} change$s in ${unit.path}'); + await changeBuilder.addDartFileEdit(unit.path, (builder) { + for (var change in changes) { + change(builder); + } + // Attempting to format the whole file can result in conflicts + // because any added imports will be applied after formatting. So + // just format the portion of the file starting at the first + // declaration. + var offset = unit.unit.declarations.first.offset; + builder.format(SourceRange(offset, unit.content.length - offset)); + }); + } + } + } + } + for (var edit in changeBuilder.sourceChange.edits) { + var filePath = edit.file; + var content = File(filePath).readAsStringSync(); + var newContent = SourceEdit.applySequence(content, edit.edits); + File(filePath).writeAsStringSync(newContent); + } + rejectStats.dump(); +} + +class _RejectStats { + List dueToMissingDiagnosticCode = []; + List dueToInvalidLocationArgs = []; + List dueToUnrecognizedArgument = []; + List dueToComplexDiagnostic = []; + Map> dueToPlaceholderParameterNames = {}; + List dueToComplexArguments = []; + List dueToComments = []; + + void dump() { + print( + 'Rejects due to missing diagnostic code: ${dueToMissingDiagnosticCode.length}', + ); + print( + 'Rejects due to invalid location args: ${dueToInvalidLocationArgs.length}', + ); + print( + 'Rejects due to unrecognized argument: ' + '${dueToUnrecognizedArgument.length}', + ); + print( + 'Rejects due to complex diagnostic: ${dueToComplexDiagnostic.length}', + ); + print('Rejects due to complex arguments: ${dueToComplexArguments.length}'); + print('Rejects due to comments: ${dueToComments.length}'); + print( + 'Rejects due to placeholder parameter names: ' + '${dueToPlaceholderParameterNames.values.map((v) => v.length).sum}', + ); + for (var entry in dueToPlaceholderParameterNames.entries.sortedBy( + (entry) => -entry.value.length, + )) { + print(' ${entry.key}: ${entry.value.length}'); + } + } +} + +class _Visitor extends RecursiveAstVisitor { + static final _placeholderParameterNameRegExp = RegExp(r'^p[0-9]+$'); + final _RejectStats rejectStats; + final String fileContents; + final String path; + final LineInfo lineInfo; + + final List changes = []; + + _Visitor({ + required this.rejectStats, + required this.fileContents, + required this.path, + required this.lineInfo, + }); + + @override + void visitMethodInvocation(MethodInvocation node) { + if (_tryFixingMethodInvocation(node) case var change?) { + changes.add(change); + } else { + super.visitMethodInvocation(node); + } + } + + bool _containsComments(AstNode node) { + var token = node.beginToken; + var endToken = node.endToken; + while (token != endToken) { + token = token.next!; + if (token.precedingComments != null) return true; + } + return false; + } + + MessageWithAnalyzerCode? _decodeDiagnostic(Expression expr) { + if (expr is! PrefixedIdentifier) return null; + if (expr.element case GetterElement(:var variable)) { + if (variable.isConst) { + var value = variable.computeConstantValue(); + if (value == null) return null; + var uniqueName = value + .superAwareGetField('uniqueName') + ?.toStringValue(); + return diagnosticTables.diagnosticsByAnalyzerUniqueName[uniqueName] ?? + (throw 'Diagnostic not found: $uniqueName'); + } + } + return null; + } + + String _text(SyntacticEntity entity) { + return fileContents.substring(entity.offset, entity.end); + } + + bool _translateArguments( + MessageWithAnalyzerCode diagnostic, + Expression? arguments, + StringBuffer replacementText, + ) { + List argumentList = []; + if (arguments != null) { + if (arguments is! ListLiteral) return false; + for (var element in arguments.elements) { + if (element is! Expression) return false; + argumentList.add(_text(element)); + } + } + if (diagnostic.parameters.length != argumentList.length) { + return false; + } + if (argumentList.isNotEmpty) { + replacementText.write('.withArguments('); + for (var (i, key) in diagnostic.parameters.keys.indexed) { + replacementText.write(key); + replacementText.write(': '); + replacementText.write(argumentList[i]); + replacementText.write(', '); + } + replacementText.write(')'); + } + return true; + } + + void Function(DartFileEditBuilder)? _tryFixingMethodInvocation( + MethodInvocation node, + ) { + late var characterLocation = lineInfo.getLocation(node.offset); + late var location = + '$path:${characterLocation.lineNumber}:' + '${characterLocation.columnNumber}'; + if (node.methodName.element case MethodElement( + enclosingElement: ClassElement(name: 'DiagnosticReporter'), + :var name, + )) { + String? Function(List, Map) + translateLocationArgs; + bool diagnosticCodeArgumentIsNamed; + switch (name) { + case 'atEntity': + case 'atNode': + case 'atToken': + translateLocationArgs = (positionalArgs, namedArgs) { + if (positionalArgs.isNotEmpty) { + return '.at(${_text(positionalArgs.removeAt(0))})'; + } else { + return null; + } + }; + diagnosticCodeArgumentIsNamed = false; + case 'atOffset': + translateLocationArgs = (positionalArgs, namedArgs) { + var offset = namedArgs.remove('offset'); + var length = namedArgs.remove('length'); + if (offset != null && length != null) { + return '.atOffset(offset: ${_text(offset)}, length: ${_text(length)})'; + } else { + return null; + } + }; + diagnosticCodeArgumentIsNamed = true; + case 'atSourceSpan': + translateLocationArgs = (positionalArgs, namedArgs) { + if (positionalArgs.isNotEmpty) { + return '.atSourceSpan(${_text(positionalArgs.removeAt(0))})'; + } else { + return null; + } + }; + diagnosticCodeArgumentIsNamed = false; + default: + return null; + } + var positionalArgs = []; + var namedArgs = {}; + for (var arg in node.argumentList.arguments) { + if (arg case NamedExpression(:var name, :var expression)) { + namedArgs[name.label.name] = expression; + } else { + positionalArgs.add(arg); + } + } + var locationText = translateLocationArgs(positionalArgs, namedArgs); + if (locationText == null) { + rejectStats.dueToInvalidLocationArgs.add(location); + return null; + } + Expression diagnosticCodeArg; + if (diagnosticCodeArgumentIsNamed) { + if (namedArgs.remove('diagnosticCode') case var expr?) { + diagnosticCodeArg = expr; + } else { + rejectStats.dueToMissingDiagnosticCode.add(location); + return null; + } + } else { + if (positionalArgs.isEmpty) { + rejectStats.dueToMissingDiagnosticCode.add(location); + return null; + } + diagnosticCodeArg = positionalArgs.removeLast(); + } + var diagnostic = _decodeDiagnostic(diagnosticCodeArg); + if (diagnostic == null) { + rejectStats.dueToComplexDiagnostic.add(location); + return null; + } + if (diagnostic.parameters.keys.any( + (k) => _placeholderParameterNameRegExp.matchAsPrefix(k) != null, + )) { + (rejectStats.dueToPlaceholderParameterNames[diagnostic.constantName] ??= + []) + .add(location); + return null; + } + if (positionalArgs.isNotEmpty || + namedArgs.keys.any( + (k) => !const {'arguments', 'contextMessages'}.contains(k), + )) { + rejectStats.dueToUnrecognizedArgument.add(location); + return null; + } + var replacementText = StringBuffer('report('); + replacementText.write(_text(diagnosticCodeArg)); + if (!_translateArguments( + diagnostic, + namedArgs['arguments'], + replacementText, + )) { + rejectStats.dueToComplexArguments.add(location); + return null; + } + if (namedArgs['contextMessages'] case var expr?) { + replacementText.write('.withContextMessages(${_text(expr)})'); + } + replacementText.write(locationText); + replacementText.write(')'); + if (_containsComments(node)) { + rejectStats.dueToComments.add(location); + return null; + } + return (builder) { + var offset = node.methodName.offset; + builder.addSimpleReplacement( + SourceRange(offset, node.end - offset), + replacementText.toString(), + ); + builder.importLibrary( + Uri.parse('package:analyzer/src/error/listener.dart'), + ); + }; + } else { + return null; + } + } +} + +extension on DartObject { + DartObject? superAwareGetField(String field) { + if (getField(field) case var value?) { + return value; + } else if (getField('(super)') case var value?) { + return value.superAwareGetField(field); + } else { + return null; + } + } +}