| // 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() { | |
| var declarations = unitResult | |
| .unit | |
| .declarations; // ignore: analyzer_to_be_deprecated_use, unnecessary_ignore | |
| for (var declaration in 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() { | |
| var declarations = unitResult | |
| .unit | |
| .declarations; // ignore: analyzer_to_be_deprecated_use, unnecessary_ignore | |
| for (var declaration in 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); | |
| } | |
| } |