From 61a266f8dc1ecb5ad8af340000de5c829983a74e Mon Sep 17 00:00:00 2001 From: Konstantin Shcheglov Date: Thu, 14 May 2026 12:59:18 -0700 Subject: [PATCH] CQ. Index and search '// [diag.fooBar]' inside analyzer tests. Teach the analysis index and referenced-name computation to recognize `// [diag.foo]` expectation comments embedded in string literals in analyzer tests. Resolve `foo` through the analyzer diagnostic library and record it as a qualified reference to the diagnostic variable. This lets reference search find diagnostics that are used only in test expectation strings, when developing analyzer itself. Change-Id: I91fa020ccd2cfe49fa6e890c8f6796a5b8ca4d1f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/503663 Reviewed-by: Paul Berry Commit-Queue: Konstantin Shcheglov --- .../lib/src/dart/analysis/driver.dart | 2 +- pkg/analyzer/lib/src/dart/analysis/index.dart | 72 ++++++++++++++++++- .../src/dart/analysis/referenced_names.dart | 14 ++++ .../test/src/dart/analysis/index_test.dart | 27 +++++++ .../test/src/dart/analysis/search_test.dart | 28 ++++++++ 5 files changed, 139 insertions(+), 4 deletions(-) diff --git a/pkg/analyzer/lib/src/dart/analysis/driver.dart b/pkg/analyzer/lib/src/dart/analysis/driver.dart index bf64b03dfe2..899c0fbee41 100644 --- a/pkg/analyzer/lib/src/dart/analysis/driver.dart +++ b/pkg/analyzer/lib/src/dart/analysis/driver.dart @@ -109,7 +109,7 @@ testFineAfterLibraryAnalyzerHook; // TODO(scheglov): Clean up the list of implicitly analyzed files. class AnalysisDriver { /// The version of data format, should be incremented on every format change. - static const int DATA_VERSION = 632; + static const int DATA_VERSION = 633; /// The number of exception contexts allowed to write. Once this field is /// zero, we stop writing any new exception contexts in this process. diff --git a/pkg/analyzer/lib/src/dart/analysis/index.dart b/pkg/analyzer/lib/src/dart/analysis/index.dart index 3e5ec311442..d9db30e6926 100644 --- a/pkg/analyzer/lib/src/dart/analysis/index.dart +++ b/pkg/analyzer/lib/src/dart/analysis/index.dart @@ -324,7 +324,7 @@ class _IndexAssembler { /// Index the [unit] and assemble a new [AnalysisDriverUnitIndexBuilder]. AnalysisDriverUnitIndexBuilder assemble(CompilationUnit unit) { - unit.accept(_IndexContributor(this)); + unit.accept(_IndexContributor(this, unit)); // Sort strings and set IDs. List<_StringInfo> stringInfoList = stringMap.values.toList(growable: false); @@ -506,9 +506,20 @@ class _IndexAssembler { /// Visits a resolved AST and adds relationships into the [assembler]. class _IndexContributor extends GeneralizingAstVisitor { - final _IndexAssembler assembler; + static final _expectationPattern = RegExp( + r'//[ \t]*\[diag\.([a-zA-Z0-9_]+)\]', + ); - _IndexContributor(this.assembler); + final _IndexAssembler assembler; + final CompilationUnit unit; + + /// Caches the diagnostic library if the unit being indexed is an analyzer + /// test file. This enables synthetic indexing of expectation comments + /// embedded in string literals (e.g. `// [diag.foo]`). + late final LibraryElementImpl? _analyzerDiagnosticLibrary = + _findAnalyzerDiagnosticLibrary(); + + _IndexContributor(this.assembler, this.unit); /// Record that the name [node] has a relation of the given [kind]. void recordNameRelation( @@ -1172,6 +1183,32 @@ class _IndexContributor extends GeneralizingAstVisitor { recordRelation(element, kind, node, isQualified); } + @override + void visitSimpleStringLiteral(SimpleStringLiteral node) { + // Index analyzer diagnostic expectations inside string literals. + if (_analyzerDiagnosticLibrary case var diagnosticLibrary?) { + var lexeme = node.literal.lexeme; + var matches = _expectationPattern.allMatches(lexeme); + var tokenOffset = node.literal.offset; + for (var match in matches) { + var name = match.group(1)!; + var start = (match.end - 1) - name.length; + var element = diagnosticLibrary.exportNamespace.get2(name); + if (element is GetterElement) { + recordRelationOffset( + element.variable, + IndexRelationKind.IS_REFERENCED_BY, + tokenOffset + start, + name.length, + true, + ); + } + } + } + + super.visitSimpleStringLiteral(node); + } + @override void visitSuperConstructorInvocation(SuperConstructorInvocation node) { var element = node.element; @@ -1317,6 +1354,35 @@ class _IndexContributor extends GeneralizingAstVisitor { ); } + LibraryElementImpl? _findAnalyzerDiagnosticLibrary() { + var unitLibrary = unit.declaredFragment!.element; + + var uriStr = unitLibrary.uri.toString(); + var isAnalyzerTest = + uriStr.startsWith('package:test/') || + uriStr.contains('/pkg/analyzer/test/'); + if (!isAnalyzerTest) { + return null; + } + + if (unitLibrary is LibraryElementImpl) { + var elementFactory = unitLibrary.session.elementFactory; + var diagnosticLibrary = elementFactory.libraryOfUri( + Uri.parse('package:analyzer/src/diagnostic/diagnostic.dart'), + ); + if (diagnosticLibrary != null) { + return diagnosticLibrary; + } + diagnosticLibrary = elementFactory.libraryOfUri( + Uri.parse('package:test/diagnostic.dart'), + ); + if (diagnosticLibrary != null) { + return diagnosticLibrary; + } + } + return null; + } + /// If the given [constructor] is a synthetic constructor created for a /// [ClassTypeAlias], return the actual constructor of a [ClassDeclaration] /// which is invoked. Return `null` if a redirection cycle is detected. diff --git a/pkg/analyzer/lib/src/dart/analysis/referenced_names.dart b/pkg/analyzer/lib/src/dart/analysis/referenced_names.dart index a12139ac5b9..d90eb76a0e2 100644 --- a/pkg/analyzer/lib/src/dart/analysis/referenced_names.dart +++ b/pkg/analyzer/lib/src/dart/analysis/referenced_names.dart @@ -212,6 +212,10 @@ class _LocalNameScope { } class _ReferencedNamesComputer extends GeneralizingAstVisitor { + static final RegExp _analyzerExpectedDiagnosticPattern = RegExp( + r'//[ \t]*\[diag\.([a-zA-Z0-9_]+)\]', + ); + final Set names = {}; final Set importPrefixNames = {}; @@ -375,6 +379,16 @@ class _ReferencedNamesComputer extends GeneralizingAstVisitor { names.add(name); } + @override + void visitSimpleStringLiteral(SimpleStringLiteral node) { + var lexeme = node.literal.lexeme; + var matches = _analyzerExpectedDiagnosticPattern.allMatches(lexeme); + for (var match in matches) { + names.add(match.group(1)!); + } + super.visitSimpleStringLiteral(node); + } + @override void visitSuperFormalParameter(SuperFormalParameter node) { names.add(node.name.lexeme); diff --git a/pkg/analyzer/test/src/dart/analysis/index_test.dart b/pkg/analyzer/test/src/dart/analysis/index_test.dart index 1de81f4ba80..1f4d87d7de6 100644 --- a/pkg/analyzer/test/src/dart/analysis/index_test.dart +++ b/pkg/analyzer/test/src/dart/analysis/index_test.dart @@ -62,6 +62,33 @@ class IndexTest extends PubPackageResolutionTest with _IndexMixin { expect(actual, expected); } + test_analyzer_diagnosticCode() async { + var diagnosticFile = newFile('$testPackageLibPath/diagnostic.dart', r''' +const myDiagnosticCode = 0; +'''); + + var diagnosticLibrary = await libraryElementForFile(diagnosticFile); + var element = diagnosticLibrary.topLevelVariables.firstWhere( + (v) => v.name == 'myDiagnosticCode', + ); + + newFile('$testPackageLibPath/helper.dart', r''' +import 'diagnostic.dart'; +'''); + + await _indexTestUnit(r''' +import 'helper.dart'; + +void f() { + '// [diag.myDiagnosticCode] message'; +} +'''); + + assertElementIndexText(element, r''' +46 4:13 |myDiagnosticCode| IS_REFERENCED_BY qualified +'''); + } + test_ClassElement_emptyBody() async { await _indexTestUnit(r''' class C; diff --git a/pkg/analyzer/test/src/dart/analysis/search_test.dart b/pkg/analyzer/test/src/dart/analysis/search_test.dart index 2f54b223d2f..7e2b1075006 100644 --- a/pkg/analyzer/test/src/dart/analysis/search_test.dart +++ b/pkg/analyzer/test/src/dart/analysis/search_test.dart @@ -1498,6 +1498,34 @@ class C { '''); } + test_searchReferences_analyzer_diagnosticCode() async { + var diagnosticFile = newFile('$testPackageLibPath/diagnostic.dart', r''' +const myDiagnosticCode = 0; +'''); + + var diagnosticLibrary = await libraryElementForFile(diagnosticFile); + var element = diagnosticLibrary.topLevelVariables.firstWhere( + (v) => v.name == 'myDiagnosticCode', + ); + + newFile('$testPackageLibPath/helper.dart', r''' +import 'diagnostic.dart'; +'''); + + await resolveTestCode(r''' +import 'helper.dart'; + +void f() { + '// [diag.myDiagnosticCode]'; +} +'''); + + await assertElementReferencesText(element, r''' + f@28 + 46 4:13 |myDiagnosticCode| REFERENCE qualified +'''); + } + @SkippedTest() // TODO(scheglov): implement augmentation test_searchReferences_class_constructor_declaredInAugmentation() async { newFile('$testPackageLibPath/a.dart', r'''