blob: 84793ecec2701f361a6f128f2136ca445f0a8a71 [file] [log] [blame]
// Copyright (c) 2021, 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.
part of '../api.dart';
/// The base class representing an arbitrary chunk of Dart code, which may or
/// may not be syntactically or semantically valid yet.
sealed class Code {
/// All the chunks of [Code], raw [String]s, [Identifier]s, or
/// [OmittedTypeAnnotation]s that comprise this [Code] object.
///
/// Note that [OmittedTypeAnnotation] objects can only be provided through
/// the [OmittedTypeAnnotationCode] wrapper instance, but will appear in
/// the [parts] of those, so they must be handled whenever iterating [parts].
final List<Object> parts;
/// Can be used to more efficiently detect the kind of code, avoiding is
/// checks and enabling switch statements.
CodeKind get kind;
Code.fromString(String code) : parts = [code];
Code.fromParts(this.parts) {
for (final part in parts) {
switch (part) {
case Code():
case Identifier():
case String():
break; // OK
default:
throw StateError('Unrecognized code part ${part.runtimeType}');
}
}
}
}
/// An arbitrary chunk of code, which does not have to be syntactically valid
/// on its own. Useful to construct other types of code from several parts.
final class RawCode extends Code {
@override
CodeKind get kind => CodeKind.raw;
RawCode.fromString(super.code) : super.fromString();
RawCode.fromParts(super.parts) : super.fromParts();
}
/// A piece of code representing a syntactically valid declaration.
final class DeclarationCode extends Code {
@override
CodeKind get kind => CodeKind.declaration;
DeclarationCode.fromString(super.code) : super.fromString();
DeclarationCode.fromParts(super.parts) : super.fromParts();
}
/// A piece of code representing a code comment. This may contain identifier
/// references inside of `[]` brackets if the comments are doc comments.
final class CommentCode extends Code {
@override
CodeKind get kind => CodeKind.comment;
CommentCode.fromString(super.code) : super.fromString();
CommentCode.fromParts(super.parts) : super.fromParts();
}
/// A piece of code representing a syntactically valid expression.
final class ExpressionCode extends Code {
@override
CodeKind get kind => CodeKind.expression;
ExpressionCode.fromString(super.code) : super.fromString();
ExpressionCode.fromParts(super.parts) : super.fromParts();
}
/// A piece of code representing a syntactically valid function body.
///
/// This includes any and all code after the parameter list of a function,
/// including modifiers like `async`.
///
/// Both arrow and block function bodies are allowed.
final class FunctionBodyCode extends Code {
@override
CodeKind get kind => CodeKind.functionBody;
FunctionBodyCode.fromString(super.code) : super.fromString();
FunctionBodyCode.fromParts(super.parts) : super.fromParts();
}
/// A piece of code identifying a syntactically valid function or function type
/// parameter.
///
/// There is no distinction here made between named and positional parameters.
///
/// There is also no distinction between function type parameters and normal
/// function parameters, so the [name] is nullable (it is not required for
/// positional function type parameters).
///
/// It is the job of the user to construct and combine these together in a way
/// that creates valid parameter lists.
final class ParameterCode implements Code {
final Code? defaultValue;
final List<String> keywords;
final String? name;
final TypeAnnotationCode? type;
@override
CodeKind get kind => CodeKind.parameter;
@override
List<Object> get parts => [
if (keywords.isNotEmpty) ...[
...keywords.joinAsCode(' '),
' ',
],
if (type != null) ...[
type!,
' ',
],
if (name != null) name!,
if (defaultValue != null) ...[
' = ',
defaultValue!,
]
];
ParameterCode({
this.defaultValue,
this.keywords = const [],
this.name,
this.type,
});
}
/// A piece of code representing a type annotation.
sealed class TypeAnnotationCode implements Code, TypeAnnotation {
@override
TypeAnnotationCode get code => this;
/// Returns a [TypeAnnotationCode] object which is a non-nullable version
/// of this one.
///
/// Returns the current instance if it is already non-nullable.
TypeAnnotationCode get asNonNullable => this;
/// Returns a [TypeAnnotationCode] object which is a non-nullable version
/// of this one.
///
/// Returns the current instance if it is already nullable.
NullableTypeAnnotationCode get asNullable => NullableTypeAnnotationCode(this);
/// Whether or not this type is nullable.
@override
bool get isNullable => false;
}
/// The nullable version of an underlying type annotation.
final class NullableTypeAnnotationCode implements TypeAnnotationCode {
/// The underlying type that is being made nullable.
TypeAnnotationCode underlyingType;
@override
TypeAnnotationCode get code => this;
@override
CodeKind get kind => CodeKind.nullableTypeAnnotation;
@override
List<Object> get parts => [...underlyingType.parts, '?'];
/// Creates a nullable [underlyingType] annotation.
///
/// If [underlyingType] is a NullableTypeAnnotationCode, returns that
/// same type.
NullableTypeAnnotationCode(this.underlyingType);
@override
TypeAnnotationCode get asNonNullable => underlyingType;
@override
NullableTypeAnnotationCode get asNullable => this;
@override
bool get isNullable => true;
}
/// A piece of code representing a reference to a named type.
final class NamedTypeAnnotationCode extends TypeAnnotationCode {
final Identifier name;
final List<TypeAnnotationCode> typeArguments;
@override
CodeKind get kind => CodeKind.namedTypeAnnotation;
@override
List<Object> get parts => [
name,
if (typeArguments.isNotEmpty) ...[
'<',
...typeArguments.joinAsCode(', '),
'>',
],
];
NamedTypeAnnotationCode({required this.name, this.typeArguments = const []});
}
/// A piece of code representing a function type annotation.
final class FunctionTypeAnnotationCode extends TypeAnnotationCode {
final List<ParameterCode> namedParameters;
final List<ParameterCode> optionalPositionalParameters;
final List<ParameterCode> positionalParameters;
final TypeAnnotationCode? returnType;
final List<TypeParameterCode> typeParameters;
@override
CodeKind get kind => CodeKind.functionTypeAnnotation;
@override
List<Object> get parts => [
if (returnType != null) returnType!,
' Function',
if (typeParameters.isNotEmpty) ...[
'<',
...typeParameters.joinAsCode(', '),
'>',
],
'(',
for (ParameterCode positional in positionalParameters) ...[
positional,
', ',
],
if (optionalPositionalParameters.isNotEmpty) ...[
'[',
for (ParameterCode optional in optionalPositionalParameters) ...[
optional,
', ',
],
']',
],
if (namedParameters.isNotEmpty) ...[
'{',
for (ParameterCode named in namedParameters) ...[
named,
', ',
],
'}',
],
')',
];
FunctionTypeAnnotationCode({
this.namedParameters = const [],
this.optionalPositionalParameters = const [],
this.positionalParameters = const [],
this.returnType,
this.typeParameters = const [],
});
}
/// A piece of code identifying a syntactically valid record field declaration.
/// This is only usable in the context of [RecordTypeAnnotationCode] objects.
///
/// There is no distinction here made between named and positional fields.
///
/// The name is not required because it is optional for positional fields.
///
/// It is the job of the user to construct and combine these together in a way
/// that creates valid record type annotations.
final class RecordFieldCode implements Code {
final String? name;
final TypeAnnotationCode type;
@override
CodeKind get kind => CodeKind.recordField;
@override
List<Object> get parts => [
type,
if (name != null) ' ${name!}',
];
RecordFieldCode({
this.name,
required this.type,
});
}
/// A piece of code representing a syntactically valid record type annotation.
final class RecordTypeAnnotationCode extends TypeAnnotationCode {
final List<RecordFieldCode> namedFields;
final List<RecordFieldCode> positionalFields;
@override
CodeKind get kind => CodeKind.recordTypeAnnotation;
@override
List<Object> get parts => [
'(',
if (positionalFields.isNotEmpty)
for (RecordFieldCode positional in positionalFields) ...[
if (positional != positionalFields.first) ', ',
positional,
],
if (namedFields.isNotEmpty) ...[
if (positionalFields.isNotEmpty) ', ',
'{',
for (RecordFieldCode named in namedFields) ...[
if (named != namedFields.first) ', ',
named,
],
'}',
],
')',
];
RecordTypeAnnotationCode({
this.namedFields = const [],
this.positionalFields = const [],
});
}
final class OmittedTypeAnnotationCode extends TypeAnnotationCode {
final OmittedTypeAnnotation typeAnnotation;
OmittedTypeAnnotationCode(this.typeAnnotation);
@override
CodeKind get kind => CodeKind.omittedTypeAnnotation;
@override
List<Object> get parts => [typeAnnotation];
}
/// Raw type annotations are typically used to refer to a local type which you
/// do not have an [Identifier] for (possibly you just created it).
///
/// Whenever possible, use a more specific [TypeAnnotationCode] subtype.
final class RawTypeAnnotationCode extends RawCode
implements TypeAnnotationCode {
@override
CodeKind get kind => CodeKind.rawTypeAnnotation;
/// Returns a [TypeAnnotationCode] object which is a non-nullable version
/// of this one.
///
/// Returns the current instance if it is already non-nullable.
@override
TypeAnnotationCode get asNonNullable => this;
/// Returns a [TypeAnnotationCode] object which is a non-nullable version
/// of this one.
///
/// Returns the current instance if it is already nullable.
@override
NullableTypeAnnotationCode get asNullable => NullableTypeAnnotationCode(this);
RawTypeAnnotationCode._(super.parts) : super.fromParts();
/// Creates a [TypeAnnotationCode] from a raw [String].
///
/// The [code] object must not have trailing whitespace.
static TypeAnnotationCode fromString(String code) => fromParts([code]);
/// Creates a [TypeAnnotationCode] from a raw code [parts].
///
/// Must not end in trailing whitespace.
static TypeAnnotationCode fromParts(List<Object> parts) {
bool wasNullable;
(wasNullable, parts) = _makeNonNullable(parts);
TypeAnnotationCode code = RawTypeAnnotationCode._(parts);
if (wasNullable) code = code.asNullable;
return code;
}
@override
TypeAnnotationCode get code => this;
@override
bool get isNullable => false;
/// Checks if [parts] ends with a ?, and if so then it is removed.
///
/// Returns a record which indicates if [parts] was nullable originally, as
/// well as the potentially new list of parts.
///
/// Throws if [parts] ends with whitespace because we don't allow type
/// annotations to do that.
static (bool wasNullable, List<Object> parts) _makeNonNullable(
List<Object> parts) {
final Iterator<Object> iterator = parts.reversed.iterator;
while (iterator.moveNext()) {
final Object current = iterator.current;
switch (current) {
case String():
if (current.trimRight() != current) {
throw ArgumentError(
'Invalid type annotation, type annotations should not end with '
'whitespace but got `$current`.');
} else if (current.isEmpty) {
continue;
} else if (current.endsWith('?')) {
// It was nullable, trim the `?` and return a copy.
return (
true,
// We are iterating backwards, and need to reverse it after.
[
// Strip the '?'.
current.substring(0, current.length - 1),
for (bool hasNext = iterator.moveNext();
hasNext;
hasNext = iterator.moveNext())
iterator.current,
].reversed.toList(),
);
} else {
return (false, parts);
}
case Identifier():
// Identifiers never contain a `?`.
return (false, parts);
}
}
throw ArgumentError('The empty string is not a valid type annotation.');
}
}
/// A piece of code representing a valid named type parameter.
final class TypeParameterCode implements Code {
final TypeAnnotationCode? bound;
final String name;
@override
CodeKind get kind => CodeKind.typeParameter;
@override
List<Object> get parts => [
name,
if (bound != null) ...[
' extends ',
bound!,
]
];
TypeParameterCode({this.bound, required this.name});
}
extension Join<T extends Object> on List<T> {
/// Joins all the items in this [Join] with [separator], and returns a new
/// list.
///
/// Works on any kind of non-nullable list which accepts String entries, and
/// does not convert the individual items to strings.
List<Object> joinAsCode(String separator) => [
for (int i = 0; i < length - 1; i++) ...[
this[i],
separator,
],
if (isNotEmpty) last,
];
}
enum CodeKind {
comment,
declaration,
expression,
functionBody,
functionTypeAnnotation,
namedTypeAnnotation,
nullableTypeAnnotation,
omittedTypeAnnotation,
parameter,
raw,
rawTypeAnnotation,
recordField,
recordTypeAnnotation,
typeParameter,
}