blob: d6f79162b9fadab21cd3211c32b03738f9f0b218 [file] [edit]
// 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 'package:analysis_server_client/protocol.dart';
import 'package:analysis_server_client/server.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/utilities/extensions/string.dart';
import 'package:analyzer_testing/package_root.dart' as pkg_root;
import 'package:analyzer_utilities/tools.dart';
import 'package:path/path.dart';
void main() async {
await GeneratedContent.generateAll(pkg_root.packageRoot, await allTargets);
}
Future<List<GeneratedContent>> get allTargets async {
var elementUnit = await _getElementUnit();
return <GeneratedContent>[
GeneratedFile('analyzer/lib/src/dart/element/element.dart', (_) async {
var generator = _ElementGenerator(elementUnit);
return await generator.generate();
}),
];
}
String get _analyzerPath => normalize(join(pkg_root.packageRoot, 'analyzer'));
String get _elementPath {
return normalize(
join(_analyzerPath, 'lib', 'src', 'dart', 'element', 'element.dart'),
);
}
void writeEnum(StringBuffer buffer, String enumName, Set<String> values) {
buffer.writeln('@generated');
buffer.writeln('enum $enumName {');
for (var value in values) {
buffer.writeln(' $value,');
}
buffer.writeln('}');
}
Future<String> _formatSortCode(String path, String code) async {
var server = Server();
await server.start();
server.listenToOutput();
await server.send('analysis.setAnalysisRoots', {
'included': [path],
'excluded': [],
});
Future<void> updateContent() async {
await server.send('analysis.updateContent', {
'files': {
path: {'type': 'add', 'content': code},
},
});
}
await updateContent();
var formatResponse = await server.send('edit.format', {
'file': path,
'selectionOffset': 0,
'selectionLength': code.length,
});
var formatResult = EditFormatResult.fromJson(
ResponseDecoder(null),
'result',
formatResponse,
);
code = SourceEdit.applySequence(code, formatResult.edits);
await updateContent();
var sortResponse = await server.send('edit.sortMembers', {'file': path});
var sortResult = EditSortMembersResult.fromJson(
ResponseDecoder(null),
'result',
sortResponse,
);
code = SourceEdit.applySequence(code, sortResult.edit.edits);
await server.kill();
return code;
}
Future<ResolvedUnitResult> _getElementUnit() async {
var collection = AnalysisContextCollection(includedPaths: [_analyzerPath]);
var analysisContext = collection.contextFor(_analyzerPath);
var analysisSession = analysisContext.currentSession;
var unitResult = await analysisSession.getResolvedUnit(_elementPath);
return unitResult as ResolvedUnitResult;
}
enum _ElementFlagSource { none, firstFragment, stored, computed }
class _ElementGenerator {
final ResolvedUnitResult unitResult;
final Set<String> elementStorageFlags = {};
final Set<String> fragmentStorageFlags = {};
final List<_Replacement> replacements = [];
_ElementGenerator(this.unitResult);
Future<String> generate() async {
_replaceGeneratedFlags();
_replaceStorageEnums();
replacements.sort((a, b) => b.offset - a.offset);
var newCode = unitResult.content;
for (var replacement in replacements) {
newCode =
newCode.substring(0, replacement.offset) +
replacement.text +
newCode.substring(replacement.end);
}
return _formatSortCode(_elementPath, newCode);
}
void _replaceGeneratedFlags() {
for (var declaration in unitResult.unit.declarations) {
if (declaration is! ClassDeclarationImpl) {
continue;
}
var classElement = declaration.declaredFragment!.element;
var generateFlags = classElement.asGenerateElementFlags;
if (generateFlags == null) {
continue;
}
var className = classElement.name!;
var isElement = className.endsWith('ElementImpl');
var isFragment = className.endsWith('FragmentImpl');
if (!isElement && !isFragment) {
throw StateError('$className is not an element or fragment class.');
}
for (var child in classElement.children) {
if (child.metadata.hasGenerated) {
if (child is! PropertyAccessorElement) {
throw StateError(
'Only getters and setters can be marked as @generated, '
'but ${classElement.name}.${child.name} is a ${child.kind.name}.',
);
}
}
}
var existingGetters = <String, _ExistingGeneratedGetter>{};
var body = declaration.body as BlockClassBodyImpl;
for (var member in body.members) {
if (member is MethodDeclarationImpl && member.isGenerated) {
replacements.add(
_Replacement(offset: member.offset, end: member.end, text: ''),
);
if (member.isGetter) {
var getterElement = member.declaredFragment!.element;
var getterName = getterElement.name!;
existingGetters[getterName] = _ExistingGeneratedGetter(
documentationComment: getterElement.documentationComment,
annotations: member.metadata
.map((annotation) {
return unitResult.content.substring(
annotation.offset,
annotation.end,
);
})
.where((source) => source != '@generated')
.toList(),
);
}
}
}
var storagePrefix = className.storagePrefix;
var buffer = StringBuffer();
for (var flag in generateFlags.flags) {
var existing = existingGetters[flag.name];
var documentationComment = existing?.documentationComment;
var annotations = existing?.annotations ?? const [];
if (isFragment) {
if (flag.fragment) {
var storageFlag = '${storagePrefix}_${flag.name}';
var storageFlagCode = '_FragmentStorageFlag.$storageFlag';
fragmentStorageFlags.add(storageFlag);
// getter
buffer.writeln();
buffer.writeDocumentation(documentationComment);
buffer.writeAnnotations(['@generated', ...annotations]);
buffer.writeln('bool get ${flag.name} {');
buffer.writeln(' return hasFlag($storageFlagCode);');
buffer.writeln('}');
// setter
buffer.writeln();
buffer.writeln('@generated');
buffer.writeln('set ${flag.name}(bool value) {');
buffer.writeln(' setFlag($storageFlagCode, value);');
buffer.writeln('}');
}
} else {
switch (flag.elementSource) {
case _ElementFlagSource.none:
case _ElementFlagSource.computed:
break;
case _ElementFlagSource.firstFragment:
buffer.writeln();
buffer.writeDocumentation(documentationComment);
buffer.writeAnnotations(['@generated', ...annotations]);
buffer.writeln('bool get ${flag.name} {');
buffer.writeln(' return _firstFragment.${flag.name};');
buffer.writeln('}');
case _ElementFlagSource.stored:
var storageFlag = '${storagePrefix}_${flag.name}';
var storageFlagCode = '_ElementStorageFlag.$storageFlag';
elementStorageFlags.add(storageFlag);
// getter
buffer.writeln();
buffer.writeDocumentation(documentationComment);
buffer.writeAnnotations(['@generated', ...annotations]);
buffer.writeln('bool get ${flag.name} {');
buffer.writeln(' return hasFlag($storageFlagCode);');
buffer.writeln('}');
// setter
buffer.writeln();
buffer.writeln('@generated');
buffer.writeln('set ${flag.name}(bool value) {');
buffer.writeln(' setFlag($storageFlagCode, value);');
buffer.writeln('}');
}
}
}
var flagsForTesting = generateFlags.flags.where((flag) {
return isFragment
? flag.fragment
: flag.elementSource != _ElementFlagSource.none;
}).toList();
if (flagsForTesting.isNotEmpty) {
buffer.writeln();
buffer.writeln('@generated');
if (className != 'ElementImpl' && className != 'FragmentImpl') {
buffer.writeln('@override');
}
buffer.writeln('@visibleForTesting');
buffer.writeln('@trackedInternal');
buffer.writeln('Map<String, bool> get flagsForTesting {');
buffer.writeln(' return {');
if (className != 'ElementImpl' && className != 'FragmentImpl') {
buffer.writeln(' ...super.flagsForTesting,');
}
for (var flag in flagsForTesting) {
buffer.writeln(" '${flag.name}': ${flag.name},");
}
buffer.writeln(' };');
buffer.writeln('}');
}
if (buffer.isNotEmpty) {
var body = declaration.body as BlockClassBodyImpl;
var offset = body.rightBracket.offset;
replacements.add(
_Replacement(offset: offset, end: offset, text: '\n$buffer'),
);
}
}
}
void _replaceStorageEnums() {
for (var declaration in unitResult.unit.declarations) {
if (declaration is EnumDeclarationImpl) {
var name = declaration.declaredFragment!.element.name!;
if (name == '_ElementStorageFlag' || name == '_FragmentStorageFlag') {
replacements.add(
_Replacement(
offset: declaration.offset,
end: declaration.end,
text: '',
),
);
}
}
}
var buffer = StringBuffer();
buffer.writeln();
writeEnum(buffer, '_ElementStorageFlag', elementStorageFlags);
buffer.writeln();
writeEnum(buffer, '_FragmentStorageFlag', fragmentStorageFlags);
var endOfFile = unitResult.content.length;
replacements.add(
_Replacement(offset: endOfFile, end: endOfFile, text: buffer.toString()),
);
}
}
class _ExistingGeneratedGetter {
final String? documentationComment;
final List<String> annotations;
_ExistingGeneratedGetter({
required this.documentationComment,
required this.annotations,
});
}
class _GenerateElementFlag {
final String name;
final bool fragment;
final _ElementFlagSource elementSource;
_GenerateElementFlag({
required this.name,
required this.fragment,
required this.elementSource,
});
}
class _GenerateElementFlags {
final ClassElement element;
final List<_GenerateElementFlag> flags;
_GenerateElementFlags(this.element, this.flags);
}
class _Replacement {
final int offset;
final int end;
final String text;
_Replacement({required this.offset, required this.end, required this.text});
}
extension on StringBuffer {
void writeAnnotations(List<String> annotations) {
for (var annotation in annotations) {
writeln(annotation);
}
}
void writeDocumentation(String? documentationComment) {
if (documentationComment != null) {
writeln(documentationComment);
}
}
}
extension _ClassElementExtension on ClassElement {
_GenerateElementFlags? get asGenerateElementFlags {
var generateObject = metadata.annotations
.map((annotation) {
var generateObject = annotation.computeConstantValue();
var generateObjectType = generateObject?.type;
if (generateObjectType?.element?.name != 'GenerateElementFlags') {
return null;
}
return generateObject;
})
.nonNulls
.firstOrNull;
if (generateObject == null) {
return null;
}
var flagsField = generateObject.getField('flags')!;
var flags = flagsField.toListValue()!.map((flag) {
var variable = flag.variable!;
var elementField = flag.getField('element')!;
var elementSourceName = elementField.variable!.name!;
return _GenerateElementFlag(
name: variable.name!,
fragment: flag.getField('fragment')!.toBoolValue()!,
elementSource: _ElementFlagSource.values.byName(elementSourceName),
);
}).toList();
for (var i = 0; i < flags.length - 1; i++) {
if (flags[i].name.compareTo(flags[i + 1].name) > 0) {
throw StateError(
'Flags for $name are not sorted: '
'"${flags[i].name}" appears before "${flags[i + 1].name}".',
);
}
}
return _GenerateElementFlags(this, flags);
}
}
extension _ElementAnnotationExtension on ElementAnnotation {
bool get isGenerated {
var value = computeConstantValue();
return value?.type?.element?.name == '_Generated';
}
}
extension _MetadataExtension on Metadata {
bool get hasGenerated {
return annotations.any((annotation) => annotation.isGenerated);
}
}
extension _MethodDeclarationImplExtension on MethodDeclarationImpl {
bool get isGenerated {
var element = declaredFragment!.element;
return element.metadata.hasGenerated;
}
}
extension _StringExtension on String {
String get storagePrefix {
var name = removeSuffixOrSelf('Impl');
return name.substring(0, 1).toLowerCase() + name.substring(1);
}
}