a3568286ff
vm. This identifies all calls to static methods annotated with `@ResourceIdentifier`, collects the constant arguments passed to the method, and writes the results into a file. The purpose of this feature is to be able to pass the recorded information to packages in a post-compilation step, allowing them to remove or modify assets based on the actual usage in the code prior to bundling in the final application. Tested: pkg/vm/test/transformations/resource_identifier_test.dart Change-Id: I58cb313b66ee23c1d154dcc242547723a1ced359 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/329961 Commit-Queue: Moritz Sümmermann <mosum@google.com> Reviewed-by: Nate Bosch <nbosch@google.com>
233 lines
7.1 KiB
Dart
233 lines
7.1 KiB
Dart
// Copyright (c) 2023, 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 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:collection/collection.dart';
|
|
import 'package:kernel/kernel.dart';
|
|
import 'package:path/path.dart' as path;
|
|
|
|
import 'package:vm/metadata/loading_units.dart';
|
|
import 'package:front_end/src/fasta/kernel/resource_identifier.dart'
|
|
as ResourceIdentifiers;
|
|
|
|
/// Collect calls to methods annotated with `@ResourceIdentifier`.
|
|
///
|
|
/// Identify and collect all calls to static methods annotated in the given
|
|
/// [component]. This requires the deferred loading to be handled already to
|
|
/// also save which loading unit the call is made in. Write the result into a
|
|
/// JSON at [resourcesFile].
|
|
///
|
|
/// The purpose of this feature is to be able to pass the recorded information
|
|
/// to packages in a post-compilation step, allowing them to remove or modify
|
|
/// assets based on the actual usage in the code prior to bundling in the final
|
|
/// application.
|
|
Component transformComponent(Component component, Uri resourcesFile) {
|
|
final tag = LoadingUnitsMetadataRepository.repositoryTag;
|
|
final loadingMetadata =
|
|
component.metadata[tag] as LoadingUnitsMetadataRepository;
|
|
final loadingUnits = loadingMetadata.mapping[component]?.loadingUnits ?? [];
|
|
|
|
final visitor = _ResourceIdentifierVisitor(loadingUnits);
|
|
for (final library in component.libraries) {
|
|
library.visitChildren(visitor);
|
|
}
|
|
|
|
File.fromUri(resourcesFile).writeAsStringSync(_toJson(visitor.identifiers));
|
|
|
|
return component;
|
|
}
|
|
|
|
String _toJson(List<Identifier> identifiers) {
|
|
return JsonEncoder.withIndent(' ').convert({
|
|
'_comment': 'Resources referenced by annotated resource identifers',
|
|
'AppTag': 'TBD',
|
|
'environment': {
|
|
'dart.tool.dart2js': false,
|
|
},
|
|
'identifiers': identifiers,
|
|
});
|
|
}
|
|
|
|
class _ResourceIdentifierVisitor extends RecursiveVisitor<void> {
|
|
final List<Identifier> identifiers = [];
|
|
final List<LoadingUnit> _loadingUnits;
|
|
|
|
_ResourceIdentifierVisitor(this._loadingUnits);
|
|
|
|
@override
|
|
void visitStaticInvocation(StaticInvocation node) {
|
|
final annotations =
|
|
ResourceIdentifiers.findResourceAnnotations(node.target);
|
|
if (annotations.isNotEmpty) {
|
|
_collectCallInformation(node, _firstResourceId(annotations.first));
|
|
annotations.forEach(node.target.annotations.remove);
|
|
}
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
/// In case a method has multiple `ResourceIdentifier` annotations, we just
|
|
/// take the first.
|
|
String _firstResourceId(InstanceConstant instance) {
|
|
final fields = instance.fieldValues;
|
|
final firstField = fields.entries.first;
|
|
return (firstField.value as StringConstant).value;
|
|
}
|
|
|
|
/// Collects all the information needed to transform [node].
|
|
void _collectCallInformation(StaticInvocation node, String resourceId) {
|
|
// Collect the name and definition location of the invocation. This is
|
|
// shared across multiple calls to the same method.
|
|
final identifier = _identifierOf(node, resourceId);
|
|
identifiers.add(identifier);
|
|
|
|
// Collect the call location and loading unit of the call.
|
|
final resourceFile = _resourceFile(node, identifier);
|
|
identifier.files.add(resourceFile);
|
|
|
|
// Collect the (int, bool, double, or String) arguments passed in the call.
|
|
final reference = _reference(node);
|
|
resourceFile.references.add(reference);
|
|
}
|
|
|
|
Identifier _identifierOf(StaticInvocation node, String resourceId) {
|
|
final identifierUri =
|
|
path.relative(node.target.enclosingLibrary.fileUri.toFilePath());
|
|
|
|
return identifiers
|
|
.where((id) => id.name == node.name.text && id.uri == identifierUri)
|
|
.firstOrNull ??
|
|
Identifier(
|
|
name: node.name.text,
|
|
id: resourceId,
|
|
uri: identifierUri,
|
|
nonConstant: !node.isConst,
|
|
files: [],
|
|
);
|
|
}
|
|
|
|
ResourceFile _resourceFile(StaticInvocation node, Identifier identifier) {
|
|
final importUri = node.target.enclosingLibrary.importUri.toString();
|
|
final id = _loadingUnits
|
|
.firstWhereOrNull(
|
|
(element) => element.libraryUris.contains(importUri))
|
|
?.id ??
|
|
-1;
|
|
final resourceFile =
|
|
identifier.files.firstWhereOrNull((element) => element.part == id);
|
|
return resourceFile ?? ResourceFile(part: id, references: []);
|
|
}
|
|
|
|
ResourceReference _reference(StaticInvocation node) {
|
|
// Get rid of the artificial `this` argument for extension methods.
|
|
final int argumentStart;
|
|
if (node.target.isExtensionMember || node.target.isExtensionTypeMember) {
|
|
argumentStart = 1;
|
|
} else {
|
|
argumentStart = 0;
|
|
}
|
|
final arguments = {
|
|
// TODO(mosuem): Support more than just literals here,
|
|
// by adding visitors for enum indices and other const expressions.
|
|
for (var i = argumentStart; i < node.arguments.positional.length; i++)
|
|
if (_evaluateLiteral(node.arguments.positional[i]) case var value?)
|
|
'${i + 1 - argumentStart}': value,
|
|
for (var argument in node.arguments.named)
|
|
if (_evaluateLiteral(argument.value) case var value?)
|
|
argument.name: value,
|
|
};
|
|
|
|
return ResourceReference(
|
|
uri: path.relative(node.location!.file.toFilePath()),
|
|
line: node.location!.line,
|
|
column: node.location!.column,
|
|
arguments: arguments,
|
|
);
|
|
}
|
|
|
|
static Object? _evaluateLiteral(Expression expression) =>
|
|
expression is BasicLiteral ? expression.value : null;
|
|
}
|
|
|
|
//TODO(mosum): Expose these classes externally, as they will have to be used
|
|
//when parsing the generated JSON file.
|
|
class Identifier {
|
|
final String name;
|
|
final String id;
|
|
final String uri;
|
|
final bool nonConstant;
|
|
final List<ResourceFile> files;
|
|
|
|
Identifier({
|
|
required this.name,
|
|
required this.id,
|
|
required this.uri,
|
|
required this.nonConstant,
|
|
required this.files,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'name': name,
|
|
'id': id,
|
|
'uri': uri,
|
|
'nonConstant': nonConstant,
|
|
'files': files,
|
|
};
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return 'Identifier(name: $name, id: $id, uri: $uri, nonConstant: $nonConstant, files: $files)';
|
|
}
|
|
}
|
|
|
|
class ResourceFile {
|
|
final int part;
|
|
final List<ResourceReference> references;
|
|
|
|
ResourceFile({required this.part, required this.references});
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'part': part,
|
|
'references': references,
|
|
};
|
|
}
|
|
|
|
@override
|
|
String toString() => 'ResourceFile(part: $part, references: $references)';
|
|
}
|
|
|
|
class ResourceReference {
|
|
final String uri;
|
|
final int line;
|
|
final int column;
|
|
final Map<String, Object?> arguments;
|
|
|
|
ResourceReference({
|
|
required this.uri,
|
|
required this.line,
|
|
required this.column,
|
|
required this.arguments,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'@': {
|
|
'uri': uri,
|
|
'line': line,
|
|
'column': column,
|
|
},
|
|
...arguments,
|
|
};
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return 'ResourceReference(uri: $uri, line: $line, column: $column, arguments: $arguments)';
|
|
}
|
|
}
|