blob: c0175a48d7ba303bb66658d92766a51fd5878347 [file] [log] [blame]
// Copyright (c) 2020, 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:kernel/ast.dart';
import 'package:kernel/clone.dart' show CloneVisitorNotMembers;
import 'package:kernel/core_types.dart' show CoreTypes;
import 'package:kernel/library_index.dart' show LibraryIndex;
import 'package:kernel/type_algebra.dart' show Substitution;
import 'utils.dart';
/// Tracks used getters and setters of generated protobuf message
/// classes and prunes metadata declarations.
class ProtobufHandler {
static const String protobufLibraryUri = 'package:protobuf/protobuf.dart';
static const String metadataFieldName = '_i';
// All of those methods have the dart field name as second positional
// parameter.
// Method names are defined in:
// https://github.com/dart-lang/protobuf/blob/master/protobuf/lib/src/protobuf/builder_info.dart
// The code is generated by:
// https://github.com/dart-lang/protobuf/blob/master/protoc_plugin/lib/protobuf_field.dart.
static const Set<String> fieldAddingMethods = const <String>{
'a',
'aOM',
'aOS',
'aQM',
'pPS',
'aQS',
'aInt64',
'aOB',
'e',
'p',
'pc',
'm',
};
final CoreTypes coreTypes;
final Class _generatedMessageClass;
final Class _tagNumberClass;
final Field _tagNumberField;
final Class _builderInfoClass;
final Procedure _builderInfoAddMethod;
// Type of BuilderInfo.add<Null>().
late FunctionType _typeOfBuilderInfoAddOfNull;
final _messageClasses = <Class, _MessageClass>{};
final _invalidatedClasses = <_MessageClass>{};
/// Creates [ProtobufHandler] instance for [component].
/// Returns null if protobuf library is not used.
static ProtobufHandler? forComponent(
Component component, CoreTypes coreTypes) {
final libraryIndex = LibraryIndex(component, [protobufLibraryUri]);
if (!libraryIndex.containsLibrary(protobufLibraryUri)) {
return null;
}
return ProtobufHandler._internal(libraryIndex, coreTypes);
}
ProtobufHandler._internal(LibraryIndex libraryIndex, this.coreTypes)
: _generatedMessageClass =
libraryIndex.getClass(protobufLibraryUri, 'GeneratedMessage'),
_tagNumberClass =
libraryIndex.getClass(protobufLibraryUri, 'TagNumber'),
_tagNumberField =
libraryIndex.getField(protobufLibraryUri, 'TagNumber', 'tagNumber'),
_builderInfoClass =
libraryIndex.getClass(protobufLibraryUri, 'BuilderInfo'),
_builderInfoAddMethod = libraryIndex.getProcedure(
protobufLibraryUri, 'BuilderInfo', 'add') {
final functionType = _builderInfoAddMethod.getterType as FunctionType;
_typeOfBuilderInfoAddOfNull = Substitution.fromPairs(
functionType.typeParameters, const <DartType>[NullType()])
.substituteType(functionType.withoutTypeParameters) as FunctionType;
}
bool usesAnnotationClass(Class cls) => cls == _tagNumberClass;
/// This method is called from summary collector when analysis discovered
/// that [member] is called and needs to construct a summary for its body.
///
/// At this point protobuf handler can
/// - modify static field initializer of metadata field;
/// - track used members of the generated message classes.
void beforeSummaryCreation(Member member) {
// Only interested in members of subclasses of GeneratedMessage class.
final cls = member.enclosingClass;
if (cls == null || cls.superclass != _generatedMessageClass) {
return;
}
final messageClass = (_messageClasses[cls] ??= _MessageClass());
if (member is Field && member.name.text == metadataFieldName) {
// Update contents of static field initializer of metadata field (_i).
// according to the used tag numbers.
assert(member.isStatic);
if (messageClass._metadataField == null) {
messageClass._metadataField = member;
++Statistics.protobufMessagesUsed;
} else {
assert(messageClass._metadataField == member);
}
_updateMetadataField(messageClass);
return;
}
if (member is Procedure && !member.isStatic) {
// Track usage of accessors of protobuf fields: extract tag number
// from annotations and add tag number to the set of used tags.
// This may also add message class to the set of invalidated classes,
// so their metadata field initializers will be revisited.
for (var annotation in member.annotations) {
final constant = (annotation as ConstantExpression).constant;
if (constant is InstanceConstant &&
constant.classReference == _tagNumberClass.reference) {
if (messageClass._usedTags.add((constant
.fieldValues[_tagNumberField.getterReference] as IntConstant)
.value)) {
_invalidatedClasses.add(messageClass);
}
}
}
}
}
List<Field> getInvalidatedFields() {
final fields = <Field>[];
for (var cls in _invalidatedClasses) {
final field = cls._metadataField;
if (field != null) {
fields.add(field);
}
}
_invalidatedClasses.clear();
return fields;
}
/// Updates initializer of metadata field of [cls] message class.
void _updateMetadataField(_MessageClass cls) {
++Statistics.protobufMetadataInitializersUpdated;
Statistics.protobufMetadataFieldsPruned -= cls.numberOfFieldsPruned;
final field = cls._metadataField!;
Expression? originalInitializer = cls._originalInitializer;
if (originalInitializer == null) {
cls._originalInitializer = originalInitializer = field.initializer!;
}
final cloner = CloneVisitorNotMembers();
field.initializer = cloner.clone(originalInitializer)..parent = field;
final transformer = _MetadataTransformer(this, cls);
field.initializer!.accept(transformer);
_invalidatedClasses.remove(cls);
cls.numberOfFieldsPruned = transformer.numberOfFieldsPruned;
Statistics.protobufMetadataFieldsPruned += cls.numberOfFieldsPruned;
}
bool _isUnusedMetadata(_MessageClass cls, InstanceInvocation node) {
if (node.interfaceTarget.enclosingClass == _builderInfoClass &&
fieldAddingMethods.contains(node.name.text)) {
final tagNumber = (node.arguments.positional[0] as IntLiteral).value;
return !cls._usedTags.contains(tagNumber);
}
return false;
}
}
class _MessageClass {
Field? _metadataField;
Expression? _originalInitializer;
final _usedTags = <int>{};
int numberOfFieldsPruned = 0;
}
class _MetadataTransformer extends Transformer {
final ProtobufHandler ph;
final _MessageClass cls;
int numberOfFieldsPruned = 0;
_MetadataTransformer(this.ph, this.cls);
@override
TreeNode visitInstanceInvocation(InstanceInvocation node) {
if (!ph._isUnusedMetadata(cls, node)) {
super.visitInstanceInvocation(node);
return node;
}
// Replace the field metadata method with a dummy call to
// `BuilderInfo.add`. This is to preserve the index calculations when
// removing a field.
// Change the tag-number to 0. Otherwise the decoder will get confused.
++numberOfFieldsPruned;
return InstanceInvocation(
InstanceAccessKind.Instance,
node.receiver,
ph._builderInfoAddMethod.name,
Arguments(
<Expression>[
IntLiteral(0), // tagNumber
NullLiteral(), // name
NullLiteral(), // fieldType
NullLiteral(), // defaultOrMaker
NullLiteral(), // subBuilder
NullLiteral(), // valueOf
NullLiteral(), // enumValues
],
types: <DartType>[const NullType()],
),
interfaceTarget: ph._builderInfoAddMethod,
functionType: ph._typeOfBuilderInfoAddOfNull)
..fileOffset = node.fileOffset;
}
}