blob: 20dc11c06372a1bb11b582081db148cba51e4bcb [file] [edit]
// Copyright (c) 2025, 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:collection';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/scope.dart';
// ignore: implementation_imports
import 'package:analyzer/src/dart/element/scope.dart';
import 'package:path/path.dart' as p;
import 'src/index.dart';
import 'src/markdown.dart';
import 'src/utils.dart';
import 'workspace.dart';
class Api {
final List<Package> packages = [];
final Resolver resolver = Resolver();
final Map<Element, Item> elementItemMap = {};
late final Index index;
Api();
LibraryItemContainer addLibrary(
LibraryElement element,
String packageName,
String libraryPath,
) {
var package = packages.firstWhere(
(p) => p.name == packageName,
orElse: () {
var package = Package(packageName);
packages.add(package);
return package;
},
);
var library = LibraryItemContainer(package, element, libraryPath);
package.libraries.add(library);
return library;
}
void finish() {
// populate the element item map
for (var package in packages) {
for (var library in package.libraries) {
_buildElementItemMap(library);
}
}
// handle situations where elements are exported from multiple libraries
_processDuplicates();
// perform calculations; super, children, ...
elementItemMap.values.forEach(_calculateFor);
index = Index();
for (var package in packages) {
var packageIndex = index.add(package.name, 'package');
for (var library in package.libraries) {
var libraryIndex = packageIndex.add(
library.name,
'library',
ref: resolve(library.element),
docs: docSummary(library),
);
for (var child in library.allChildrenSorted) {
if (child is Items) {
var childIndex = libraryIndex.add(
child.name,
child.type.displayName,
ref: resolve(child.element),
docs: docSummary(child),
);
for (var c in child.allChildrenSorted) {
childIndex.add(
c.name,
c.type.displayName,
id: c.anchorId,
docs: docSummary(c),
);
}
} else {
libraryIndex.add(
child.name,
child.type.displayName,
id: child.anchorId,
docs: docSummary(child),
);
}
}
}
}
}
void _buildElementItemMap(Item item) {
if (elementItemMap[item.element] == null) {
elementItemMap[item.element] = item;
}
if (item is Items) {
item.allChildren.forEach(_buildElementItemMap);
}
}
/// Indicate that the given item is the cannonical location from which to
/// reference its associated Element.
void _makeCanonical(Item item) {
elementItemMap[item.element] = item;
if (item is Items) {
item.allChildren.forEach(_makeCanonical);
}
}
void _calculateFor(Item item) {
if (item.element is InterfaceElement) {
var element = item.element as InterfaceElement;
var superItem = itemForElement(element.supertype?.element);
superItem?._addRelationship(item, RelationshipKind.$subclasses);
for (var $interface in element.interfaces) {
var interfaceItem = itemForElement($interface.element);
interfaceItem?._addRelationship(item, RelationshipKind.$implements);
}
for (var $mixin in element.mixins) {
var interfaceItem = itemForElement($mixin.element);
interfaceItem?._addRelationship(item, RelationshipKind.$mixes);
}
} else if (item.element is ExtensionElement) {
var element = item.element as ExtensionElement;
var extendedElement = element.extendedType.element;
var extendedItem = itemForElement(extendedElement);
extendedItem?._addRelationship(item, RelationshipKind.$extended);
}
}
Item? itemForElement(Element? element) {
return elementItemMap[element];
}
void addResolution(Item item, WorkspaceFile file) {
resolver.addResolution(item, file);
}
String? resolve(Element? element, {WorkspaceFile? from}) {
return resolver.resolve(element, from: from);
}
String hrefOrSpan(String text, Element element, {WorkspaceFile? from}) {
return resolver.hrefOrSpan(text, element, from: from);
}
void _processDuplicates() {
// todo: decide whether elements should be de-duped globally or just
// de-duped per package
// todo: instead of having one unique element definition location globally,
// have a main one, but allow per-lib scopes to have an override
// Note that we only care about de-duping for library members.
// todo: we want to the container members to resolve to the same place as
// their parents
final topLevelElementsToItems = <Element, List<Item>>{};
// look for dups
for (var package in packages) {
for (var library in package.libraries) {
for (var item in library.allChildren) {
var element = item.element;
topLevelElementsToItems.putIfAbsent(element, () => []).add(item);
}
}
}
// todo: create a relationship between an item and the library's its defined
// in
// todo: do we have multiple Item's for each unique Element?
// todo: where are Item's created, and do we de-dupe wrt Elements?
// var libraryElement = item.element.library;
// if (item.element is! LibraryElement && libraryElement != null) {
// item._addRelationship(item, RelationshipKind.$definedIn);
// }
for (var entry in topLevelElementsToItems.entries.where(
(entry) => entry.value.length > 1,
)) {
_updateForDuplicates(entry.key, entry.value);
}
resolver.initElementItemMap(elementItemMap);
}
void _updateForDuplicates(Element element, List<Item> items) {
// Use cases:
// definied in a public lib; exported from a 2nd public lib
// dups for [promiseToFuture<T>(...)] defined in dart:js_util:
// promiseToFuture from dart:js_util
// promiseToFuture from dart:html
// defined in a private lib, exported (independently) from two public libs
// dups for [abstract class BytesBuilder] defined in dart:_internal:
// BytesBuilder from dart:io
// BytesBuilder from dart:typed_data
// defined in a private library
// exported from a public library
// exported from other public libraries from the first public library
// ex., flutter:widgets symbols (defined in private libs), exported as part
// of flutter:material
// todo: we want to sort these dups via the length of their export chain
// 0: defined in a public lib
// 1: defined in a private lib, exported from a public one
// 2: exported from a public lib
// 3: ...
// then, choose a 'best' Item
// replace all the worst Items with forward refs to that bext one
// have all Items equal to the best item add local package overrides to
// their local ref
var exports = items.map((item) {
var parentLib = item.libraryParent;
var len = parentLib!._calcElementExportLength(element);
return _ElementExports(element, item, parentLib, len);
}).toList()..sort();
print(
'dups for [$element] defined in '
'${element.library?.librarySource.uri}:',
);
for (var export in exports) {
print(
' ${export.element.name} from '
'${export.libraryContainer.name}: ${export.exportLength}',
);
}
// for non-canonical items:
// - convert to export
// - make sure the element map point to the cannonical versions
var canonical = exports.first;
_makeCanonical(canonical.item);
print('[$element] => ${elementItemMap[element]!.debugPath}');
for (var export in exports) {
if (export.exportLength > canonical.exportLength) {
export.item._convertToExport(
exportedFrom: canonical.libraryContainer,
cannonicalItem: canonical.item,
);
}
}
}
}
class _ElementExports implements Comparable<_ElementExports> {
final Element element;
final Item item;
final LibraryItemContainer libraryContainer;
final int exportLength;
_ElementExports(
this.element,
this.item,
this.libraryContainer,
this.exportLength,
);
@override
int compareTo(_ElementExports other) {
var diff = exportLength - other.exportLength;
if (diff != 0) return diff;
return libraryContainer.name.compareTo(other.libraryContainer.name);
}
}
class Package implements Comparable<Package> {
final String name;
final List<LibraryItemContainer> libraries = [];
String? version;
String? description;
Package(this.name);
bool get includeInUrls => name.contains(':');
@override
int compareTo(Package other) {
return name.compareTo(other.name);
}
}
/// A tuple of a relationship kind (subclass, implementor, ...) and a set of
/// items.
typedef RelationshipMap = Map<RelationshipKind, List<Item>>;
class Item {
Items? parent;
final Element element;
late final GroupType type = GroupType.typeFor(element);
final RelationshipMap relationships = SplayTreeMap();
String? nameOverride;
Scope? _scopeCache;
Item(this.parent, this.element);
String get name {
if (nameOverride != null) return nameOverride!;
var result = element.displayName;
if (result.isEmpty) {
// Assume this is a ctor.
return element.enclosingElement!.name!;
} else {
return result;
}
}
LibraryItemContainer? get libraryParent {
Item? item = this;
while (item != null) {
if (item is LibraryItemContainer) return item;
item = item.parent;
}
return null;
}
Scope get scope => _scopeCache ??= _calcScope(element)!;
String get anchorId => stringToAnchorId(name);
late String? docs = _calculateDocs();
bool get hasDocs => docs != null;
ClassElement get asClass => element as ClassElement;
MixinElement get asMixin => element as MixinElement;
ExtensionElement get asExtension => element as ExtensionElement;
// ignore: experimental_member_use
ExtensionTypeElement get asExtensionType => element as ExtensionTypeElement;
EnumElement get asEnum => element as EnumElement;
FieldElement get asField => element as FieldElement;
TopLevelVariableElement get asTopLevelVariableElement =>
element as TopLevelVariableElement;
PropertyAccessorElement get asAccessor => element as PropertyAccessorElement;
MethodElement get asMethod => element as MethodElement;
FunctionElement get asFunction => element as FunctionElement;
ConstructorElement get asConstructor => element as ConstructorElement;
TypeAliasElement get asTypeAlias => element as TypeAliasElement;
bool get isStatic {
if (element is ClassMemberElement) {
return (element as ClassMemberElement).isStatic;
} else if (element is ExecutableElement) {
return (element as ExecutableElement).isStatic;
}
throw StateError('element not a class member: $element');
}
String get debugPath => parent == null ? name : '${parent!.debugPath}/$name';
void _addRelationship(Item item, RelationshipKind kind) {
relationships.putIfAbsent(kind, () => []).add(item);
}
String? _calculateDocs() {
var result = element.documentationComment;
if (result == null) {
var enclosing = element.enclosingElement;
if (enclosing is InterfaceElement) {
if (element.kind == ElementKind.METHOD) {
result = _lookupMethodDocs(element as MethodElement);
} else if (element.kind == ElementKind.GETTER) {
result = _lookupGetterDocs(element as PropertyAccessorElement);
} else if (element.kind == ElementKind.SETTER) {
result = _lookupSetterDocs(element as PropertyAccessorElement);
}
}
}
return result == null ? null : stripDartdoc(result);
}
String? _lookupMethodDocs(MethodElement element) {
var enclosing = element.enclosingElement as InterfaceElement;
var superMethod = enclosing.thisType.lookUpMethod2(
element.name,
element.library,
inherited: true,
);
return superMethod == null
? null
: (superMethod.documentationComment ?? _lookupMethodDocs(superMethod));
}
String? _lookupGetterDocs(PropertyAccessorElement element) {
var enclosing = element.enclosingElement as InterfaceElement;
var accessor = enclosing.thisType.lookUpGetter2(
element.name,
element.library,
inherited: true,
);
return accessor == null
? null
: (accessor.documentationComment ?? _lookupGetterDocs(accessor));
}
String? _lookupSetterDocs(PropertyAccessorElement element) {
var enclosing = element.enclosingElement as InterfaceElement;
var accessor = enclosing.thisType.lookUpSetter2(
element.name,
element.library,
inherited: true,
);
return accessor == null
? null
: (accessor.documentationComment ?? _lookupSetterDocs(accessor));
}
MarkdownLinkResolver markdownLinkResolver(Resolver resolver) {
return (String ref) {
var result = resolver.resolveDocReference(
ref,
context: this,
fromFile: resolver.fileFor(element),
);
// todo: log this?
// if (result == null) {
// print(' unresolved: [$ref] from ${element.displayName}');
// }
return result;
};
}
List<ElementAnnotation> get annotations {
return element.metadata.where((annotation) {
var meta = annotation.element;
if (meta == null) return false;
// `meta` is a PropertyAccessorElement or a ConstructorElement.
// Filter pragma annotations - these are directions to tools, not first
// class properties of the source code.
if (meta is PropertyAccessorElement) {
if (meta.name == 'pragma') return false;
} else if (meta is ConstructorElement) {
if (meta.enclosingElement.name == 'pragma') return false;
}
return true;
}).toList();
}
static Scope? _calcScope(Element element) {
// todo: more of these scopes could be cached / looked up by Item
if (element is CompilationUnitElement) {
return _calcScope(element.enclosingElement);
}
if (element is LibraryElement) {
return element.scope;
} else if (element is ExtensionElement) {
return ExtensionScope(_calcScope(element.enclosingElement)!, element);
} else if (element is InterfaceElement) {
return InterfaceScope(_calcScope(element.enclosingElement)!, element);
} else if (element is MethodElement) {
return FormalParameterScope(
_calcScope(element.enclosingElement)!,
element.parameters,
);
} else if (element is FunctionElement) {
return FormalParameterScope(
_calcScope(element.enclosingElement)!,
element.parameters,
);
} else if (element is ConstructorElement) {
return ConstructorInitializerScope(
_calcScope(element.enclosingElement)!,
element,
);
} else if (element is PropertyAccessorElement) {
return LocalScope(_calcScope(element.enclosingElement)!)..add(element);
} else if (element is FieldElement) {
return LocalScope(_calcScope(element.enclosingElement)!)..add(element);
} else if (element is TypeAliasElement) {
return _calcScope(element.enclosingElement);
} else {
print('scope not found for ${element.runtimeType}');
return null;
}
}
/// Indicate that this Item should not be considered to be defined in the
/// current library, but documented as an export.
///
/// The item should be a direct child of a library.
void _convertToExport({
required LibraryItemContainer exportedFrom,
required Item cannonicalItem,
}) {
var library = parent as LibraryItemContainer;
// todo: there are more dangling references here to clean up
// remove from children
library.groups[type]!.items.remove(this);
if (library.groups[type]!.items.isEmpty) {
library.groups.remove(type);
}
// add to an exports list
library.exports.add(ExportedItem(cannonicalItem, exportedFrom));
}
}
class ExportedItem {
final Item item;
final LibraryItemContainer exportedFrom;
ExportedItem(this.item, this.exportedFrom);
}
abstract class Items extends Item {
final Map<GroupType, Group> groups = SplayTreeMap();
Items(super.parent, super.element);
T addChild<T extends Item>(T item) {
var groupType = GroupType.typeFor(item.element);
if (groupType != GroupType.skip) {
groups.putIfAbsent(groupType, () => Group(groupType)).items.add(item);
}
return item;
}
Iterable<Item> get allChildren {
return groups.entries.expand((entry) => entry.value.items);
}
List<Item> get allChildrenSorted {
var items = allChildren.toList();
items.sort((a, b) => adjustedLexicalCompare(a.name, b.name));
return items;
}
void sort() {
for (var entry in groups.entries) {
// have enums retain their declaration order
if (entry.key == GroupType.enumValue) continue;
entry.value.sort();
}
}
static Map<GroupType, List<Item>> itemsByGroup(List<Item> items) {
var result = SplayTreeMap<GroupType, List<Item>>();
for (var item in items) {
result.putIfAbsent(item.type, () => []).add(item);
}
for (var entry in result.entries) {
entry.value.sort((a, b) => adjustedLexicalCompare(a.name, b.name));
}
return result;
}
}
class InterfaceElementItems extends Items {
InterfaceElementItems(super.parent, InterfaceElement super.element);
@override
InterfaceElement get element => super.element as InterfaceElement;
}
class ExtensionElementItems extends Items {
ExtensionElementItems(super.parent, super.element);
@override
ExtensionElement get element => super.element as ExtensionElement;
}
class LibraryItemContainer extends Items
implements Comparable<LibraryItemContainer> {
final Package? package;
/// The list of items that are part of this library's public API, but that
/// are considered exports (primarily defined elsewhere).
final List<ExportedItem> exports = [];
LibraryItemContainer(this.package, LibraryElement element, String name)
: super(null, element) {
nameOverride = name;
var exportNamespace = element.exportNamespace;
var elements = exportNamespace.definedNames.values
.where((element) => element.isPublic)
.toList();
elements.sort((a, b) => adjustedLexicalCompare(a.name ?? '', b.name ?? ''));
for (var e in elements) {
if (e is InterfaceElement) {
var interfaceElement = e;
var interfaceElementChildren = addChild(
InterfaceElementItems(this, interfaceElement),
);
for (var child in interfaceElement.children.where((c) => c.isPublic)) {
if (child.isSynthetic) continue;
interfaceElementChildren.addChild(
Item(interfaceElementChildren, child),
);
}
interfaceElementChildren.sort();
} else if (e is ExtensionElement) {
var extensionElement = e;
var extensionElementChildren = addChild(
ExtensionElementItems(this, extensionElement),
);
for (var child in extensionElement.children.where((c) => c.isPublic)) {
if (child.isSynthetic) continue;
extensionElementChildren.addChild(
Item(extensionElementChildren, child),
);
}
extensionElementChildren.sort();
} else {
addChild(Item(this, e));
}
}
}
String get urlName => package != null && package!.includeInUrls
? '${package!.name}/$name'
: name;
@override
LibraryElement get element => super.element as LibraryElement;
Map<LibraryItemContainer, List<Item>> get exportsByLibrary {
var results = SplayTreeMap<LibraryItemContainer, List<Item>>();
for (var export in exports) {
results.putIfAbsent(export.exportedFrom, () => []).add(export.item);
}
return results;
}
/// For a given element defined in this library or exported from it, calculate
/// how many library exports exist betwen this library and the library where
/// the element is actually defined.
///
/// For example, if the element is defined in this library, the export length
/// would be 0.
///
/// For an element defined in a private library exported from this library,
/// the export length would be 1. Higer numbers indicate a longer export path.
int _calcElementExportLength(Element e) {
// todo: implement this using directive export info
if (e.library == element) return 0;
return 1;
}
@override
int compareTo(LibraryItemContainer other) {
return name.compareTo(other.name);
}
}
class Group implements Comparable<Group> {
final GroupType type;
final List<Item> items = [];
Group(this.type);
String get name => type.title;
String get anchorId => stringToAnchorId('_$name');
bool get containerType => type.containerType;
void sort() {
items.sort((a, b) => adjustedLexicalCompare(a.name, b.name));
}
@override
int compareTo(Group other) {
return type.index - other.type.index;
}
}
enum GroupType implements Comparable<GroupType> {
// TODO: why is there not an ElementKind.MIXIN?
// class members
constructor('Constructors', 'constructor', {ElementKind.CONSTRUCTOR}),
enumValue('Values', 'enum value', {}),
field('Fields', 'field', {ElementKind.FIELD}),
accessor('Accessors', 'accessor', {ElementKind.GETTER, ElementKind.SETTER}),
method('Methods', 'method', {ElementKind.METHOD}),
// library members
topLevelVariable('Top Level Variables', 'top level variable', {
ElementKind.TOP_LEVEL_VARIABLE,
}),
function('Functions', 'function', {ElementKind.FUNCTION}),
functionTypeAlias('Function Type Aliases', 'function type alias', {
ElementKind.FUNCTION_TYPE_ALIAS,
}),
typeAlias('Type Aliases', 'type alias', {ElementKind.TYPE_ALIAS}),
// container items
$enum('Enums', 'enum', {ElementKind.ENUM}, containerType: true),
$mixin('Mixins', 'mixin', {}, containerType: true),
$class('Classes', 'class', {ElementKind.CLASS}, containerType: true),
$extension('Extensions', 'extension', {
ElementKind.EXTENSION,
}, containerType: true),
$extensionType('Extension Types', 'extension type', {
ElementKind.EXTENSION_TYPE,
}, containerType: true),
// todo: implement
// $record('Records', 'record', {ElementKind.RECORD}, containerType: true),
// catch-all
skip('Skip', 'skip', {
ElementKind.DYNAMIC,
ElementKind.NEVER,
ElementKind.TYPE_PARAMETER,
}),
other('Other', 'other', {});
final String title;
final Set<Object> elementKinds;
final bool containerType;
final String displayName;
const GroupType(
this.title,
this.displayName,
this.elementKinds, {
this.containerType = false,
});
@override
int compareTo(GroupType other) {
return index - other.index;
}
/// Return the [GroupType] for the given [element].
static GroupType typeFor(Element element) {
final kind = element.kind;
if (element is FieldElement && element.isEnumConstant) {
return GroupType.enumValue;
}
if (element is MixinElement) return GroupType.$mixin;
for (var val in GroupType.values) {
if (val.elementKinds.contains(kind)) {
return val;
}
}
print('*** ${element.kind} ***');
return GroupType.other;
}
}
// todo: remove files if we remove an Item
class Resolver {
// libraries and classes to files
final Map<Item, WorkspaceFile> itemToFileMap = {};
late final Map<Element, Item> elementToItemMap;
void addResolution(Item item, WorkspaceFile file) {
itemToFileMap[item] = file;
}
void initElementItemMap(Map<Element, Item> elementItemMap) {
elementToItemMap = elementItemMap;
}
String? resolve(Element? element, {WorkspaceFile? from}) {
var item = elementToItemMap[element];
var target = itemToFileMap[item];
if (target == null) return null;
if (from != null) {
// Replaced for performance reasons.
return target.path.pathRelative(fromDir: p.dirname(from.path));
// return p.relative(target.path, from: p.dirname(from.path));
} else {
return target.path;
}
}
// todo: html encode 'text'
String hrefOrSpan(String text, Element element, {WorkspaceFile? from}) {
var ref = resolve(element, from: from);
if (ref != null) {
return '<a href="$ref">$text</a>';
} else {
return '<span>$text</span>';
}
}
/// Returns a Uri if the reference is resolved.
String? resolveDocReference(
String reference, {
required Item context,
WorkspaceFile? fromFile,
}) {
// TODO: handle dotted references (Foo.bar, ...)
var scope = context.scope;
var element = scope.lookup(reference).getter;
return resolve(element, from: fromFile);
}
WorkspaceFile? fileFor(Element element) {
// See if this element cooresponds directly to a file.
var item = elementToItemMap[element];
var file = itemToFileMap[item];
if (file != null) return file;
// Otherwise check its library or compilation element.
if (element.enclosingElement is CompilationUnitElement) {
var enclosingItem = elementToItemMap[element.library];
file = itemToFileMap[enclosingItem];
} else {
var enclosingItem = elementToItemMap[element.enclosingElement];
file = itemToFileMap[enclosingItem];
}
return file;
}
}
enum RelationshipKind implements Comparable<RelationshipKind> {
$subclasses('Subclassed by'),
$implements('Implemented by'),
$mixes('Mixed into'),
$extended('Extended by');
final String title;
const RelationshipKind(this.title);
@override
int compareTo(RelationshipKind other) {
return index - other.index;
}
}
/// A StringBuffer like class that allows you to write out Element references,
/// dart format the written text, and emit the result as html with the element
/// references converted to html links.
class LinkedText {
final Resolver resolver;
final WorkspaceFile fromFile;
final List<_ElementSpan> _elementSpans = [];
final StringBuffer _buf = StringBuffer();
int _charIndex = 0;
LinkedText(this.resolver, this.fromFile);
void write(String str) {
for (var i = 0; i < str.length; i++) {
if (str[i].trim().isNotEmpty) {
_charIndex++;
}
}
_buf.write(str);
}
void writeElement(String str, Element element) {
_elementSpans.add(_ElementSpan(element, _charIndex, str));
_charIndex += str.length;
_buf.write(str);
}
String emitHtml(String Function(String) formatHelper, [String suffix = '']) {
var fmt = formatHelper(_buf.toString());
var chunks = _chunks(fmt, _elementSpans, _spanStarts(fmt)).toList();
var html = chunks.map((chunk) {
if (chunk is String) {
return htmlEscape(chunk);
} else {
var span = chunk as _ElementSpan;
var uri = resolver.resolve(span.element, from: fromFile);
if (uri == null) {
return htmlEscape(span.text);
} else {
return '<a href="$uri">${htmlEscape(span.text)}</a>';
}
}
}).join();
return '<pre class="declaration"><code>$html$suffix</code></pre>';
}
List<int> _spanStarts(String fmt) {
var result = <int>[];
var spans = _elementSpans.iterator;
if (!spans.moveNext()) return result;
var index = 0;
int i;
for (i = 0; i < fmt.length; i++) {
if (fmt[i].trim().isEmpty) continue;
if (index == spans.current.start) {
result.add(i);
if (!spans.moveNext()) return result;
}
index++;
}
return result;
}
// A List of Strings and _ElementSpans.
Iterable<dynamic> _chunks(
String fmt,
List<_ElementSpan> spans,
List<int> spanStarts,
) sync* {
var fmtIndex = 0;
for (var i = 0; i < spans.length; i++) {
var span = spans[i];
var spanStart = spanStarts[i];
if (fmtIndex < spanStart) {
yield fmt.substring(fmtIndex, spanStart);
fmtIndex = spanStart;
}
yield span;
fmtIndex = spanStart + span.text.length;
}
if (fmtIndex < fmt.length) {
yield fmt.substring(fmtIndex);
}
}
@override
String toString() => _buf.toString();
}
class _ElementSpan {
final Element element;
final int start;
final String text;
_ElementSpan(this.element, this.start, this.text);
@override
String toString() => '[$text]';
}