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 <paulberry@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
This commit is contained in:
Konstantin Shcheglov
2026-05-14 12:59:18 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent a667cde7c1
commit 61a266f8dc
5 changed files with 139 additions and 4 deletions
@@ -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.
+69 -3
View File
@@ -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.
@@ -212,6 +212,10 @@ class _LocalNameScope {
}
class _ReferencedNamesComputer extends GeneralizingAstVisitor<void> {
static final RegExp _analyzerExpectedDiagnosticPattern = RegExp(
r'//[ \t]*\[diag\.([a-zA-Z0-9_]+)\]',
);
final Set<String> names = <String>{};
final Set<String> importPrefixNames = <String>{};
@@ -375,6 +379,16 @@ class _ReferencedNamesComputer extends GeneralizingAstVisitor<void> {
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);
@@ -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;
@@ -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'''
<testLibraryFragment> f@28
46 4:13 |myDiagnosticCode| REFERENCE qualified
''');
}
@SkippedTest() // TODO(scheglov): implement augmentation
test_searchReferences_class_constructor_declaredInAugmentation() async {
newFile('$testPackageLibPath/a.dart', r'''