// 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 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 { /// Optional default value for this parameter (not including the ` = `). final Code? defaultValue; /// Any keywords to put on this parameter (before the type). final List keywords; /// Optional name of this parameter (can only be omitted for function types). final String? name; /// Optional type for this parameter. final TypeAnnotationCode? type; /// The type of parameter this is. final ParameterStyle style; @override CodeKind get kind => CodeKind.parameter; @override List get parts => [ if (keywords.isNotEmpty) ...[ ...keywords.joinAsCode(' '), ' ', ], if (type != null) ...[ type!, ' ', ], if (style == ParameterStyle.fieldFormal) 'this.' else if (style == ParameterStyle.superFormal) 'super.', if (name != null) name!, if (defaultValue != null) ...[ ' = ', defaultValue!, ] ]; ParameterCode({ this.defaultValue, this.keywords = const [], this.name, this.style = ParameterStyle.normal, 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 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 typeArguments; @override CodeKind get kind => CodeKind.namedTypeAnnotation; @override List 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 namedParameters; final List optionalPositionalParameters; final List positionalParameters; final TypeAnnotationCode? returnType; final List typeParameters; @override CodeKind get kind => CodeKind.functionTypeAnnotation; @override List 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 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 namedFields; final List positionalFields; @override CodeKind get kind => CodeKind.recordTypeAnnotation; @override List 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 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 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 parts) _makeNonNullable( List parts) { final Iterator 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 get parts => [ name, if (bound != null) ...[ ' extends ', bound!, ] ]; TypeParameterCode({this.bound, required this.name}); } extension Join on List { /// 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 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, }