Macro. Build macro augmentation using the existing method.

The method is _buildAugmentationImport(), used for any other augmentation.

To do this, we create a FileState instance that corresponds to the
augmentation library. This way, we keep its content there, so that
later we will able resolve it.

For now, this allows reusing existing support for imports, and write
a macro that references a type identifier from a library, that later
is generated into a library import, and a prefixed referenced in code.

Change-Id: I5affeaa07253f2464b1da255c6218c3d46a3d887
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/326882
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Konstantin Shcheglov 2023-09-20 15:50:53 +00:00
parent c562719814
commit 784b9d70e6
9 changed files with 230 additions and 83 deletions

View file

@ -35,7 +35,7 @@ void buildTests({required String packagePath}) {
if (file_paths.isDart(pathContext, file.path)) {
test(file.path, () {
final content = file.readAsStringSync();
if (content.contains('utf8.encode')) {
if (content.contains('utf8.encode(')) {
fail('Should not use `utf8.encode` before SDK 3.1');
}
});

View file

@ -88,7 +88,7 @@ import 'package:analyzer/src/utilities/uri_cache.dart';
/// TODO(scheglov) Clean up the list of implicitly analyzed files.
class AnalysisDriver implements AnalysisDriverGeneric {
/// The version of data format, should be incremented on every format change.
static const int DATA_VERSION = 306;
static const int DATA_VERSION = 307;
/// The number of exception contexts allowed to write. Once this field is
/// zero, we stop writing any new exception contexts in this process.

View file

@ -2,6 +2,7 @@
// 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:typed_data';
import 'package:_fe_analyzer_shared/src/scanner/string_canonicalizer.dart';
@ -39,6 +40,7 @@ import 'package:analyzer/src/utilities/uri_cache.dart';
import 'package:analyzer/src/workspace/workspace.dart';
import 'package:collection/collection.dart';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:pub_semver/pub_semver.dart';
@ -502,7 +504,14 @@ class FileState {
FileStateRefreshResult refresh() {
_invalidateCurrentUnresolvedData();
final rawFileState = _fsState.fileContentStrategy.get(path);
final FileContent rawFileState;
if (_fsState._macroFileContent case final macroFileContent?) {
_fsState._macroFileContent = null;
rawFileState = macroFileContent;
} else {
rawFileState = _fsState.fileContentStrategy.get(path);
}
final contentChanged =
_fileContent?.contentHash != rawFileState.contentHash;
_fileContent = rawFileState;
@ -1144,6 +1153,10 @@ class FileSystemState {
final FileSystemTestData? testData;
/// We set a value for this field when we are about to refresh the current
/// macro [FileState]. During the refresh, this will is reset back to `null`.
FileContent? _macroFileContent;
FileSystemState(
this._logger,
this._byteStore,
@ -1263,6 +1276,10 @@ class FileSystemState {
return _pathToFile[path];
}
FileState? getExistingFromUri(Uri uri) {
return _uriToFile[uri];
}
/// Return the [FileState] for the given absolute [path]. The returned file
/// has the last known state since if was last refreshed.
/// TODO(scheglov) Merge with [getFileForPath2].
@ -1657,6 +1674,12 @@ class LibraryFileKind extends LibraryOrAugmentationFileKind {
List<PartState>? _parts;
/// The synthetic augmentation import added to [augmentationImports] for
/// the macro application result of this library. It is filled only if the
/// library uses any macros, during linking or after loading the summary
/// bundle.
AugmentationImportWithFile? _macroImport;
LibraryCycle? _libraryCycle;
LibraryFileKind({
@ -1749,6 +1772,60 @@ class LibraryFileKind extends LibraryOrAugmentationFileKind {
}).toFixedList();
}
AugmentationImportWithFile? addOrUpdateMacro(String code) {
final pathContext = file._fsState.pathContext;
final libraryFileName = pathContext.basename(file.path);
final macroFileName =
pathContext.setExtension(libraryFileName, '.macro.dart');
final augmentationContent = '''
library augment '$libraryFileName';
$code
''';
final contentBytes = utf8.encoder.convert(augmentationContent);
final hashBytes = md5.convert(contentBytes).bytes;
final hashStr = hex.encode(hashBytes);
file._fsState._macroFileContent = StoredFileContent(
content: augmentationContent,
contentHash: hashStr,
exists: true,
);
final macroRelativeUri = uriCache.parse(macroFileName);
final macroUri = uriCache.resolveRelative(file.uri, macroRelativeUri);
final macroFileResolution = file._fsState.getFileForUri(macroUri);
if (macroFileResolution is! UriResolutionFile) return null;
final macroFile = macroFileResolution.file;
final import = AugmentationImportWithFile(
container: this,
unlinked: UnlinkedAugmentationImportDirective(
importKeywordOffset: -1,
augmentKeywordOffset: -1,
uri: macroFileName,
),
uri: DirectiveUriWithFile(
relativeUriStr: macroFileName,
relativeUri: macroRelativeUri,
file: macroFile,
),
);
_macroImport = import;
final lastImport = augmentationImports.lastOrNull;
if (lastImport != null && lastImport.unlinked.uri == macroFileName) {
augmentationImports[augmentationImports.length - 1] = import;
// TODO(scheglov) refresh file
} else {
_augmentationImports = [...augmentationImports, import].toFixedList();
}
return import;
}
@override
void collectTransitive(Set<FileState> files) {
super.collectTransitive(files);
@ -1786,6 +1863,7 @@ class LibraryFileKind extends LibraryOrAugmentationFileKind {
void internal_setLibraryCycle(LibraryCycle? cycle) {
_libraryCycle = cycle;
_disposeMacroAugmentation();
}
@override
@ -1793,6 +1871,26 @@ class LibraryFileKind extends LibraryOrAugmentationFileKind {
_libraryCycle?.invalidate();
_libraryCycle = null;
}
@override
String toString() {
return 'LibraryFileKind($file)';
}
/// When the library cycle that contains this library is invalidated, the
/// macros might potentially generate different code, or no code at all. So,
/// we discard the existing macro augmentation library, it will be rebuilt
/// during linking.
void _disposeMacroAugmentation() {
if (_macroImport case final macroImport?) {
_macroImport = null;
_augmentationImports = augmentationImports.withoutLast.toFixedList();
// Discard the file.
final macroFile = macroImport.importedFile;
file._fsState._pathToFile.remove(macroFile.path);
file._fsState._uriToFile.remove(macroFile.uri);
}
}
}
/// Information about a single `import` directive.

View file

@ -20,7 +20,6 @@ import 'package:analyzer/src/summary2/constructor_initializer_resolver.dart';
import 'package:analyzer/src/summary2/default_value_resolver.dart';
import 'package:analyzer/src/summary2/element_builder.dart';
import 'package:analyzer/src/summary2/export.dart';
import 'package:analyzer/src/summary2/informative_data.dart';
import 'package:analyzer/src/summary2/link.dart';
import 'package:analyzer/src/summary2/macro_application.dart';
import 'package:analyzer/src/summary2/metadata_resolver.dart';
@ -489,79 +488,28 @@ class LibraryBuilder {
return;
}
final libraryFileName = uri.pathSegments.lastOrNull;
if (libraryFileName == null) {
return;
}
if (!libraryFileName.endsWith('.dart')) {
return;
}
final augmentationFileName =
'${libraryFileName.substring(0, '.dart'.length - 1)}.macro.dart';
final augmentationUri = uri.resolve(augmentationFileName);
final augmentationUriStr = '$augmentationUri';
final augmentationSource = _sourceFactory.forUri2(augmentationUri)!;
final parseResult = parseString(
content: augmentationCode,
featureSet: element.featureSet,
throwIfDiagnostics: false,
);
final unitNode = parseResult.unit as ast.CompilationUnitImpl;
final unitElement = CompilationUnitElementImpl(
source: augmentationSource,
librarySource: augmentationSource,
lineInfo: parseResult.lineInfo,
);
unitElement.uri = augmentationUriStr;
final augmentationReference =
reference.getChild('@augmentation').getChild(augmentationUriStr);
_bindReference(augmentationReference, unitElement);
final augmentation = LibraryAugmentationElementImpl(
augmentationTarget: element,
nameOffset: -1,
);
augmentation.definingCompilationUnit = unitElement;
augmentation.macroGenerated = MacroGenerationAugmentationLibrary(
code: augmentationCode,
informativeBytes: writeUnitInformative(unitNode),
);
final directiveUri = DirectiveUriWithAugmentationImpl(
relativeUriString: augmentationUriStr,
relativeUri: augmentationUri,
source: augmentationSource,
augmentation: augmentation,
);
final macroState = kind.addOrUpdateMacro(augmentationCode);
if (macroState == null) return;
final macroImport = _buildAugmentationImport(element, macroState);
macroImport.isSynthetic = true;
element.augmentationImports = [
...element.augmentationImports,
AugmentationImportElementImpl(
importKeywordOffset: -1,
uri: directiveUri,
),
];
macroImport,
].toFixedList();
final macroLinkingUnit = units.last;
ElementBuilder(
libraryBuilder: this,
container: augmentation,
unitReference: augmentationReference,
unitElement: unitElement,
).buildDeclarationElements(unitNode);
container: macroLinkingUnit.container,
unitReference: macroLinkingUnit.reference,
unitElement: macroLinkingUnit.element,
).buildDeclarationElements(macroLinkingUnit.node);
units.add(
LinkingUnit(
reference: augmentationReference,
node: unitNode,
element: unitElement,
container: element,
),
macroImport.importedAugmentation!.macroGenerated =
MacroGenerationAugmentationLibrary(
code: macroState.importedFile.content,
informativeBytes: macroState.importedFile.unlinked2.informativeBytes,
);
}

View file

@ -91,6 +91,8 @@ class LibraryMacroApplier {
required this.libraryBuilder,
});
bool get hasTargets => _targets.isNotEmpty;
Linker get _linker => libraryBuilder.linker;
/// Fill [_targets]s with macro applications.
@ -347,6 +349,17 @@ class LibraryMacroApplier {
}
macro.ResolvedIdentifier _resolveIdentifier(macro.Identifier identifier) {
if (identifier is IdentifierImplFromElement) {
// TODO(scheglov) other elements
final element = identifier.element as ClassElementImpl;
return macro.ResolvedIdentifier(
// TODO(scheglov) other kinds
kind: macro.IdentifierKind.topLevelMember,
name: element.name,
uri: element.source.uri,
staticScope: null,
);
}
throw UnimplementedError();
}

View file

@ -16,6 +16,11 @@ extension IterableExtension<E> on Iterable<E> {
}
extension ListExtension<E> on List<E> {
Iterable<E> get withoutLast {
final length = this.length;
return length > 0 ? take(length - 1) : Iterable.empty();
}
void addIfNotNull(E? element) {
if (element != null) {
add(element);

View file

@ -658,6 +658,15 @@ class _ElementWriter {
}
void _writeLibraryOrAugmentationElement(LibraryOrAugmentationElementImpl e) {
if (e is LibraryAugmentationElementImpl) {
if (e.macroGenerated case final macroGenerated?) {
_sink.writelnWithIndent('macroGeneratedCode');
_sink.writeln('---');
_sink.write(macroGenerated.code);
_sink.writeln('---');
}
}
_writeDocumentation(e);
_writeMetadata(e);
_writeSinceSdkVersion(e);
@ -671,15 +680,6 @@ class _ElementWriter {
_writeElements('exports', e.libraryExports, _writeExportElement);
if (e is LibraryAugmentationElementImpl) {
if (e.macroGenerated case final macroGenerated?) {
_sink.writelnWithIndent('macroGeneratedCode');
_sink.writeln('---');
_sink.writeln(macroGenerated.code);
_sink.writeln('---');
}
}
_sink.writelnWithIndent('definingUnit');
_sink.withIndent(() {
_writeUnitElement(e.definingCompilationUnit);

View file

@ -1364,11 +1364,13 @@ library
package:test/test.macro.dart
macroGeneratedCode
---
library augment 'test.dart';
class MyClass {}
---
definingUnit
classes
class MyClass @6
class MyClass @36
constructors
synthetic @-1
''');
@ -1412,11 +1414,13 @@ library
package:test/test.macro.dart
macroGeneratedCode
---
library augment 'test.dart';
class MyClass {}
---
definingUnit
classes
class MyClass @6
class MyClass @36
constructors
synthetic @-1
''');
@ -1459,11 +1463,13 @@ library
package:test/test.macro.dart
macroGeneratedCode
---
library augment 'test.dart';
class MyClass {}
---
definingUnit
classes
class MyClass @6
class MyClass @36
constructors
synthetic @-1
''');
@ -1506,13 +1512,83 @@ library
package:test/test.macro.dart
macroGeneratedCode
---
library augment 'test.dart';
class MyClass {}
---
definingUnit
classes
class MyClass @6
class MyClass @36
constructors
synthetic @-1
''');
}
test_imports_class() async {
newFile('$testPackageLibPath/a.dart', r'''
import 'dart:async';
import 'package:_fe_analyzer_shared/src/macros/api.dart';
macro class MyMacro implements ClassTypesMacro {
const MyMacro();
FutureOr<void> buildTypesForClass(clazz, ClassTypeBuilder builder) async {
final identifier = await builder.resolveIdentifier(
Uri.parse('dart:math'),
'Random',
);
builder.declareType(
'MyClass',
DeclarationCode.fromParts([
'class MyClass {\n void foo(',
identifier,
' _) {}\n}',
]),
);
}
}
''');
var library = await buildLibrary(r'''
import 'a.dart';
@MyMacro()
class A {}
''');
configuration
..withConstructors = false
..withMetadata = false;
checkElementText(library, r'''
library
imports
package:test/a.dart
definingUnit
classes
class A @35
augmentationImports
package:test/test.macro.dart
macroGeneratedCode
---
library augment 'test.dart';
import 'dart:math' as prefix0;
class MyClass {
void foo(prefix0.Random _) {}
}
---
imports
dart:math as prefix0 @52
definingUnit
classes
class MyClass @68
methods
foo @85
parameters
requiredPositional _ @104
type: Random
returnType: void
''');
}
}

View file

@ -51,6 +51,13 @@ class ListExtensionTest {
[4, 2, 0, 5, 3, 1],
);
}
test_withoutLast() {
expect([0, 1, 2].withoutLast, [0, 1]);
expect([0, 1].withoutLast, [0]);
expect([0].withoutLast, <int>[]);
expect(<int>[].withoutLast, <int>[]);
}
}
@reflectiveTest