Skip to content
This repository has been archived by the owner on Feb 4, 2025. It is now read-only.

add scope and isStatic to QualifiedName, as well as parsing and a URI format #101

Merged
merged 2 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions pkgs/_analyzer_macros/lib/macro_implementation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.

import 'package:_macro_host/macro_host.dart';
import 'package:analyzer/dart/element/element.dart';
// ignore: implementation_imports
import 'package:analyzer/src/summary2/macro_declarations.dart' as analyzer;
// ignore: implementation_imports
Expand Down Expand Up @@ -187,9 +188,30 @@ extension MacroTargetExtension on macros_api_v1.MacroTarget {
final element = ((this as macros_api_v1.Declaration).identifier
as analyzer.IdentifierImpl)
.element!;
return element.qualifiedName;
}
}

extension QualifiedNameForElement on Element {
QualifiedName get qualifiedName {
final uri = '${library!.definingCompilationUnit.source.uri}';
final enclosingElement = enclosingElement3;
if (enclosingElement == null) {
throw UnsupportedError('Library macro targets are not yet supported');
}
final scope = (enclosingElement is LibraryElement ||
enclosingElement is CompilationUnitElement)
? null
: enclosingElement.displayName;
final isStatic = scope == null
? null
: switch (this) {
ClassMemberElement self => self.isStatic,
_ => throw UnimplementedError(
'Cannot create a QualifiedName for $runtimeType'),
};
return QualifiedName(
uri: '${element.library!.definingCompilationUnit.source.uri}',
name: element.displayName);
uri: uri, name: displayName, scope: scope, isStatic: isStatic);
}
}

Expand Down
5 changes: 3 additions & 2 deletions pkgs/_analyzer_macros/lib/src/type_translation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/dart/element/type_visitor.dart';
import 'package:dart_model/dart_model.dart' as model;

import '../macro_implementation.dart';

final class TypeTranslationContext {
final Map<TypeParameterElement, int> _typeParameterIds = {};

Expand Down Expand Up @@ -78,8 +80,7 @@ final class AnalyzerTypesToMacros extends UnifyingTypeVisitorWithArgument<
InterfaceType type, TypeTranslationContext argument) {
final element = type.element;
return model.StaticTypeDesc.namedTypeDesc(model.NamedTypeDesc(
name: model.QualifiedName(
uri: '${element.librarySource.uri}', name: element.name),
name: element.qualifiedName,
instantiation: [
for (final arg in type.typeArguments)
arg.acceptWithArgument(this, argument),
Expand Down
16 changes: 15 additions & 1 deletion pkgs/_cfe_macros/lib/macro_implementation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,21 @@ extension MacroTargetExtension on macros_api_v1.MacroTarget {
final identifier =
((this as macros_api_v1.Declaration).identifier as cfe.IdentifierImpl)
.resolveIdentifier();
return QualifiedName(uri: '${identifier.uri}', name: identifier.name);
return QualifiedName(
uri: '${identifier.uri}',
name: identifier.name,
scope: switch (identifier.kind) {
macros_api_v1.IdentifierKind.local ||
macros_api_v1.IdentifierKind.topLevelMember =>
null,
macros_api_v1.IdentifierKind.instanceMember =>
throw UnimplementedError(
'We dont have access to the parent scope for instance '
'members in the CFE yet'),
macros_api_v1.IdentifierKind.staticInstanceMember =>
identifier.staticScope!,
},
isStatic: identifier.staticScope != null);
}
}

Expand Down
7 changes: 1 addition & 6 deletions pkgs/_test_macros/lib/templating.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,7 @@ List<Code> expandTemplate(String template) {
throw ArgumentError('Unmatched opening brace: $template');
}
final name = template.substring(start + 2, end);
final parts = name.split('#');
if (parts.length != 2) {
throw ArgumentError('Expected "uri#name" in: $name');
}
result
.add(Code.qualifiedName(QualifiedName(uri: parts[0], name: parts[1])));
result.add(Code.qualifiedName(QualifiedName.parse(name)));
index = end + 2;
}
return result;
Expand Down
9 changes: 7 additions & 2 deletions pkgs/dart_model/lib/src/dart_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ import 'json_buffer/json_buffer_builder.dart';
export 'dart_model.g.dart';

extension QualifiedNameExtension on QualifiedName {
String get asString => '$uri#$name';
String get asString =>
'$uri#${scope == null ? '' : '$scope${isStatic! ? '::' : '.'}'}$name';

bool equals(QualifiedName other) => other.uri == uri && other.name == name;
bool equals(QualifiedName other) =>
other.uri == uri &&
other.name == name &&
other.scope == scope &&
other.isStatic == isStatic;
}

extension ModelExtension on Model {
Expand Down
43 changes: 39 additions & 4 deletions pkgs/dart_model/lib/src/dart_model.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -390,35 +390,70 @@ extension type Properties.fromJson(Map<String, Object?> node)
bool get isStatic => node['isStatic'] as bool;
}

/// A URI combined with a name.
/// A URI combined with a name and scope referring to a declaration. The name and scope are looked up in the export scope of the URI.
extension type QualifiedName.fromJson(Map<String, Object?> node)
implements Object {
static final TypedMapSchema _schema = TypedMapSchema({
'uri': Type.stringPointer,
'scope': Type.stringPointer,
'name': Type.stringPointer,
'isStatic': Type.boolean,
});
QualifiedName({
String? uri,
String? scope,
String? name,
bool? isStatic,
}) : this.fromJson(Scope.createMap(
_schema,
uri,
scope,
name,
isStatic,
));

/// Parses [string] of the form `uri#name`.
/// Parses [string] of the form `uri#[scope.|scope::]name`.
///
/// No scope indicates a top level declaration in the library.
///
/// If the scope and name are separated with a `.` that indicates the
/// instance scope, and a `::` indicates the static scope.
static QualifiedName parse(String string) {
final index = string.indexOf('#');
if (index == -1) throw ArgumentError('Expected `#` in string: $string');
final nameAndScope = string.substring(index + 1);
late final String name;
late final String? scope;
late final bool? isStatic;
if (nameAndScope.contains('::')) {
[scope, name] = nameAndScope.split('::');
isStatic = true;
} else if (nameAndScope.contains('.')) {
[scope, name] = nameAndScope.split('.');
isStatic = false;
} else {
name = nameAndScope;
scope = null;
isStatic = null;
}
return QualifiedName(
uri: string.substring(0, index), name: string.substring(index + 1));
uri: string.substring(0, index),
name: name,
scope: scope,
isStatic: isStatic);
}

/// The URI of the file containing the name.
String get uri => node['uri'] as String;

/// The name.
/// The optional scope to look up the name in.
String? get scope => node['scope'] as String?;

/// The name of the declaration to look up.
String get name => node['name'] as String;

/// Whether the name refers to something in the static scope as opposed to the instance scope of `scope`. Will be `null` if `scope` is `null`.
bool? get isStatic => node['isStatic'] as bool?;
}

/// Query about a corpus of Dart source code. TODO(davidmorgan): this queries about a single class, expand to a union type for different types of queries.
Expand Down
26 changes: 25 additions & 1 deletion pkgs/dart_model/test/model_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,40 @@ void main() {
});
});

group(QualifiedName, () {
group('QualifiedName', () {
test('asString', () {
expect(QualifiedName(uri: 'package:foo/foo.dart', name: 'Foo').asString,
'package:foo/foo.dart#Foo');
expect(
QualifiedName(
uri: 'package:foo/foo.dart',
name: 'bar',
scope: 'Foo',
isStatic: false)
.asString,
'package:foo/foo.dart#Foo.bar');
expect(
QualifiedName(
uri: 'package:foo/foo.dart',
name: 'baz',
scope: 'Foo',
isStatic: true)
.asString,
'package:foo/foo.dart#Foo::baz');
});

test('parse', () {
expect(QualifiedName.parse('package:foo/foo.dart#Foo').uri,
'package:foo/foo.dart');
expect(QualifiedName.parse('package:foo/foo.dart#Foo').name, 'Foo');
final fooBar = QualifiedName.parse('package:foo/foo.dart#Foo.bar');
expect(fooBar.name, 'bar');
expect(fooBar.scope, 'Foo');
expect(fooBar.isStatic, false);
final fooBaz = QualifiedName.parse('package:foo/foo.dart#Foo::baz');
expect(fooBaz.name, 'baz');
expect(fooBaz.scope, 'Foo');
expect(fooBaz.isStatic, true);
});
});
}
Expand Down
12 changes: 10 additions & 2 deletions schemas/dart_model.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -276,15 +276,23 @@
},
"QualifiedName": {
"type": "object",
"description": "A URI combined with a name.",
"description": "A URI combined with a name and scope referring to a declaration. The name and scope are looked up in the export scope of the URI.",
"properties": {
"uri": {
"type": "string",
"description": "The URI of the file containing the name."
},
"scope": {
"type": "string",
"description": "The optional scope to look up the name in."
},
"name": {
"type": "string",
"description": "The name."
"description": "The name of the declaration to look up."
},
"isStatic": {
"type": "boolean",
"description": "Whether the name refers to something in the static scope as opposed to the instance scope of `scope`. Will be `null` if `scope` is `null`."
}
}
},
Expand Down
46 changes: 42 additions & 4 deletions tool/dart_model_generator/lib/definitions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -237,21 +237,59 @@ static Protocol handshakeProtocol = Protocol(
type: 'bool', description: 'Whether the entity is static.'),
]),
Definition.clazz('QualifiedName',
description: 'A URI combined with a name.',
description: 'A URI combined with a name and scope referring to a '
'declaration. The name and scope are looked up in the export '
'scope of the URI.',
createInBuffer: true,
properties: [
Property('uri',
type: 'String',
description: 'The URI of the file containing the name.'),
Property('name', type: 'String', description: 'The name.'),
Property('scope',
type: 'String',
description: 'The optional scope to look up the name in.',
nullable: true),
Property('name',
type: 'String',
description: 'The name of the declaration to look up.'),
Property('isStatic',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this will probably end up as an enum, since we might want to distinguish more types of thing (e.g. getter vs setter vs field), and more might arrive later. Maybe a TODO since it's easy to change for now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it ever matters whether it is a getter/setter/field for the purposes of this object, it is just telling you how to look up a declaration unambiguously, which does require knowing if it should be looked up in the static or instance member scope.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't matter for right now, but I was thinking of a case where you have e.g. a subclass with a setter and superclass with a getter; you need to say setter or getter to disambiguate between the two. It's a bit of an awkward corner :) anyway, we can figure that out later if needed. Thanks.

Copy link
Contributor Author

@jakemac53 jakemac53 Oct 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh for setters, the name should probably be followed by an =. At least that is how most of the other tooling deals with this afaik. cc @johnniwinther @scheglov for comment.

Although in terms of macros, maybe we do want to distinguish here, because we don't really want to include the = when outputting code. So I think the existing macro stuff does not include it.

type: 'bool',
description:
'Whether the name refers to something in the static '
'scope as opposed to the instance scope of `scope`. '
'Will be `null` if `scope` is `null`.',
nullable: true)
],
extraCode: r'''
/// Parses [string] of the form `uri#name`.
/// Parses [string] of the form `uri#[scope.|scope::]name`.
///
/// No scope indicates a top level declaration in the library.
///
/// If the scope and name are separated with a `.` that indicates the
/// instance scope, and a `::` indicates the static scope.
static QualifiedName parse(String string) {
final index = string.indexOf('#');
if (index == -1) throw ArgumentError('Expected `#` in string: $string');
final nameAndScope = string.substring(index + 1);
late final String name;
late final String? scope;
late final bool? isStatic;
if (nameAndScope.contains('::')) {
[scope, name] = nameAndScope.split('::');
isStatic = true;
} else if (nameAndScope.contains('.')) {
[scope, name] = nameAndScope.split('.');
isStatic = false;
} else {
name = nameAndScope;
scope = null;
isStatic = null;
}
return QualifiedName(
uri: string.substring(0, index), name: string.substring(index + 1));
uri: string.substring(0, index),
name: name,
scope: scope,
isStatic: isStatic);
}
'''),
Definition.clazz('Query',
Expand Down