| // Copyright (c) 2024, 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 '../../ast.dart'; | |
| // ------------------------------------------------------------------------ | |
| // COMPONENT | |
| // ------------------------------------------------------------------------ | |
| /// A way to bundle up libraries in a component. | |
| class Component extends TreeNode { | |
| final CanonicalName root; | |
| /// Problems in this [Component] encoded as json objects. | |
| /// | |
| /// Note that this field can be null, and by convention should be null if the | |
| /// list is empty. | |
| List<String>? problemsAsJson; | |
| final List<Library> libraries; | |
| /// Map from a source file URI to a line-starts table and source code. | |
| /// Given a source file URI and a offset in that file one can translate | |
| /// it to a line:column position in that file. | |
| final Map<Uri, Source> uriToSource; | |
| /// Mapping between string tags and [MetadataRepository] corresponding to | |
| /// those tags. | |
| final Map<String, MetadataRepository<dynamic>> metadata = | |
| <String, MetadataRepository<dynamic>>{}; | |
| /// Reference to the main method in one of the libraries. | |
| Reference? _mainMethodName; | |
| Reference? get mainMethodName => _mainMethodName; | |
| new({ | |
| CanonicalName? nameRoot, | |
| List<Library>? libraries, | |
| Map<Uri, Source>? uriToSource, | |
| }) : root = nameRoot ?? new CanonicalName.root(), | |
| libraries = libraries ?? <Library>[], | |
| uriToSource = uriToSource ?? <Uri, Source>{} { | |
| adoptChildren(); | |
| } | |
| void adoptChildren() { | |
| for (int i = 0; i < libraries.length; ++i) { | |
| // The libraries are owned by this component, and so are their canonical | |
| // names if they exist. | |
| Library library = libraries[i]; | |
| library.parent = this; | |
| CanonicalName? name = library.reference.canonicalName; | |
| if (name != null && name.parent != root) { | |
| root.adoptChild(name); | |
| } | |
| } | |
| } | |
| void computeCanonicalNames() { | |
| for (int i = 0; i < libraries.length; ++i) { | |
| computeCanonicalNamesForLibrary(libraries[i]); | |
| } | |
| } | |
| /// This is an advanced feature. Use of this method should be coordinated | |
| /// with the kernel team. | |
| /// | |
| /// Makes sure all references in named nodes in this component points to said | |
| /// named node. | |
| /// | |
| /// The use case is advanced incremental compilation, where we want to rebuild | |
| /// a single library and make all other libraries use the new library and the | |
| /// content therein *while* having the option to go back to pointing (be | |
| /// "linked") to the old library if the delta is rejected. | |
| /// | |
| /// Please note that calling this is a potentially dangerous thing to do, | |
| /// and that stuff *can* go wrong, and you could end up in a situation where | |
| /// you point to several versions of "the same" library. Examples: | |
| /// * If you only relink part (e.g. a class) if your component you can wind | |
| /// up in an unfortunate situation where if the library (say libA) contains | |
| /// class 'B' and class 'C', you only replace 'B' (with one in library | |
| /// 'libAPrime'), everything pointing to 'B' via parent pointers talks | |
| /// about 'libAPrime', whereas everything pointing to 'C' would still | |
| /// ultimately point to 'libA'. | |
| /// * If you relink to a library that doesn't have exactly the same members | |
| /// as the one you're "linking from" you can wind up in an unfortunate | |
| /// situation, e.g. if the thing you relink two is missing a static method, | |
| /// any links to that static method will still point to the old static | |
| /// method and thus (via parent pointers) to the old library. | |
| /// * (probably more). | |
| void relink() { | |
| for (int i = 0; i < libraries.length; ++i) { | |
| libraries[i].relink(); | |
| } | |
| } | |
| void computeCanonicalNamesForLibrary(Library library) { | |
| library.ensureCanonicalNames(root); | |
| } | |
| void unbindCanonicalNames() { | |
| // TODO(jensj): Get rid of this. | |
| for (int i = 0; i < libraries.length; i++) { | |
| Library lib = libraries[i]; | |
| for (int j = 0; j < lib.classes.length; j++) { | |
| Class c = lib.classes[j]; | |
| c.dirty = true; | |
| } | |
| } | |
| root.unbindAll(); | |
| } | |
| Procedure? get mainMethod => mainMethodName?.asProcedure; | |
| void setMainMethodAndMode(Reference? main, bool overwriteMainIfSet) { | |
| if (_mainMethodName == null || overwriteMainIfSet) { | |
| _mainMethodName = main; | |
| } | |
| } | |
| @override | |
| R accept<R>(TreeVisitor<R> v) => v.visitComponent(this); | |
| @override | |
| R accept1<R, A>(TreeVisitor1<R, A> v, A arg) => v.visitComponent(this, arg); | |
| @override | |
| void visitChildren(Visitor v) { | |
| visitList(libraries, v); | |
| mainMethod?.acceptReference(v); | |
| } | |
| @override | |
| void transformChildren(Transformer v) { | |
| v.transformList(libraries, this); | |
| } | |
| @override | |
| void transformOrRemoveChildren(RemovingTransformer v) { | |
| v.transformLibraryList(libraries, this); | |
| } | |
| @override | |
| Component get enclosingComponent => this; | |
| /// Translates an offset to line and column numbers in the given file. | |
| Location? getLocation(Uri file, int offset, {String? viaForErrorMessage}) { | |
| return uriToSource[file]?.getLocation( | |
| file, | |
| offset, | |
| viaForErrorMessage: viaForErrorMessage, | |
| ); | |
| } | |
| /// Translates line and column numbers to an offset in the given file. | |
| /// | |
| /// Returns offset of the line and column in the file, or -1 if the | |
| /// source is not available or has no lines. | |
| /// Throws [RangeError] if line or calculated offset are out of range. | |
| int getOffset(Uri file, int line, int column) { | |
| return uriToSource[file]?.getOffset(line, column) ?? -1; | |
| } | |
| /// Translates line and column numbers to an offset in the given file. | |
| /// | |
| /// Returns offset of the line and column in the file, or -1 if the | |
| /// source is not available, has no lines, or the line is out of range. | |
| int getOffsetNoThrow(Uri file, int line, int column) { | |
| return uriToSource[file]?.getOffsetNoThrow(line, column) ?? -1; | |
| } | |
| void addMetadataRepository(MetadataRepository repository) { | |
| metadata[repository.tag] = repository; | |
| } | |
| @override | |
| String toString() { | |
| return "Component(${toStringInternal()})"; | |
| } | |
| @override | |
| void toTextInternal(AstPrinter printer) { | |
| // TODO(johnniwinther): Implement this. | |
| } | |
| @override | |
| String leakingDebugToString() => astToText.debugComponentToString(this); | |
| } | |
| /// A tuple with file, line, and column number, for displaying human-readable | |
| /// locations. | |
| class Location { | |
| final Uri file; | |
| final int line; // 1-based. | |
| final int column; // 1-based. | |
| new(this.file, this.line, this.column); | |
| @override | |
| String toString() => '$file:$line:$column'; | |
| } | |
| class Source { | |
| static final Uint8List _emptySource = new Uint8List(0); | |
| final List<int>? lineStarts; | |
| /// A UTF8 encoding of the original source file. | |
| final Uint8List source; | |
| final Uri? importUri; | |
| final Uri? fileUri; | |
| Set<Reference>? constantCoverageConstructors; | |
| String? cachedText; | |
| new(this.lineStarts, this.source, this.importUri, this.fileUri); | |
| new emptySource(this.lineStarts, this.importUri, this.fileUri) | |
| : source = _emptySource; | |
| /// Return the text corresponding to [line] which is a 1-based line | |
| /// number. The returned line contains no line separators. | |
| String? getTextLine(int line) { | |
| List<int>? lineStarts = this.lineStarts; | |
| if (source.isEmpty || lineStarts == null || lineStarts.isEmpty) { | |
| return null; | |
| } | |
| RangeError.checkValueInInterval(line, 1, lineStarts.length, 'line'); | |
| String cachedText = text; | |
| // -1 as line numbers start at 1. | |
| int index = line - 1; | |
| if (index + 1 == lineStarts.length) { | |
| // Last line. | |
| return cachedText.substring(lineStarts[index]); | |
| } else if (index < lineStarts.length) { | |
| // We subtract 1 from the next line for two reasons: | |
| // 1. If the file isn't terminated by a newline, that index is invalid. | |
| // 2. To remove the newline at the end of the line. | |
| int endOfLine = lineStarts[index + 1] - 1; | |
| if (endOfLine > index && cachedText[endOfLine - 1] == "\r") { | |
| --endOfLine; // Windows line endings. | |
| } | |
| return cachedText.substring(lineStarts[index], endOfLine); | |
| } | |
| // This shouldn't happen: should have been caught by the range check above. | |
| throw "Internal error"; | |
| } | |
| String get text => cachedText ??= utf8.decode(source, allowMalformed: true); | |
| /// Translates an offset to 1-based line and column numbers in the given file. | |
| Location getLocation(Uri file, int offset, {String? viaForErrorMessage}) { | |
| List<int>? lineStarts = this.lineStarts; | |
| if (lineStarts == null || lineStarts.isEmpty) { | |
| return new Location(file, TreeNode.noOffset, TreeNode.noOffset); | |
| } | |
| if (viaForErrorMessage != null) { | |
| RangeError.checkValueInInterval( | |
| offset, | |
| 0, | |
| lineStarts.last, | |
| 'offset', | |
| 'Asked for out-of-bounds offset for uri "$file" ' | |
| 'via $viaForErrorMessage', | |
| ); | |
| } else { | |
| RangeError.checkValueInInterval( | |
| offset, | |
| 0, | |
| lineStarts.last, | |
| 'offset', | |
| 'Asked for out-of-bounds offset for uri "$file"', | |
| ); | |
| } | |
| int low = 0, high = lineStarts.length - 1; | |
| while (low < high) { | |
| int mid = high - ((high - low) >> 1); // Get middle, rounding up. | |
| int pivot = lineStarts[mid]; | |
| if (pivot <= offset) { | |
| low = mid; | |
| } else { | |
| high = mid - 1; | |
| } | |
| } | |
| int lineIndex = low; | |
| int lineStart = lineStarts[lineIndex]; | |
| int lineNumber = 1 + lineIndex; | |
| int columnNumber = 1 + offset - lineStart; | |
| return new Location(file, lineNumber, columnNumber); | |
| } | |
| /// Translates 1-based line and column numbers to an offset in the given file | |
| /// | |
| /// Returns offset of the line and column in the file, or -1 if the source | |
| /// has no lines. | |
| /// Throws [RangeError] if line or calculated offset are out of range. | |
| int getOffset(int line, int column) { | |
| List<int>? lineStarts = this.lineStarts; | |
| if (lineStarts == null || lineStarts.isEmpty) { | |
| return -1; | |
| } | |
| RangeError.checkValueInInterval(line, 1, lineStarts.length, 'line'); | |
| int offset = lineStarts[line - 1] + column - 1; | |
| RangeError.checkValueInInterval(offset, 0, lineStarts.last, 'offset'); | |
| return offset; | |
| } | |
| /// Translates 1-based line and column numbers to an offset in the given file | |
| /// | |
| /// Returns offset of the line and column in the file, or -1 if the source | |
| /// has no lines or the input is out of range. | |
| int getOffsetNoThrow(int line, int column) { | |
| List<int>? lineStarts = this.lineStarts; | |
| if (lineStarts == null || lineStarts.isEmpty) { | |
| return -1; | |
| } | |
| if (line < 1 || line > lineStarts.length) { | |
| return -1; | |
| } | |
| int offset = lineStarts[line - 1] + column - 1; | |
| if (offset < 0 || offset > lineStarts.last) { | |
| return -1; | |
| } | |
| return offset; | |
| } | |
| } | |
| abstract class MetadataRepository<T> { | |
| /// Unique string tag associated with this repository. | |
| String get tag; | |
| /// Mutable mapping between nodes and their metadata. | |
| Map<Node, T> get mapping; | |
| /// Write [metadata] object corresponding to the given [Node] into | |
| /// the given [BinarySink]. | |
| /// | |
| /// Metadata is serialized immediately before serializing [node], | |
| /// so implementation of this method can use serialization context of | |
| /// [node]'s parents (such as declared type parameters and variables). | |
| /// In order to use scope declared by the [node] itself, implementation of | |
| /// this method can use [BinarySink.enterScope] and [BinarySink.leaveScope] | |
| /// methods. | |
| /// | |
| /// [metadata] must be an object owned by this repository. | |
| void writeToBinary(T metadata, Node node, BinarySink sink); | |
| /// Construct a metadata object from its binary payload read from the | |
| /// given [BinarySource]. | |
| /// | |
| /// Metadata is deserialized immediately after deserializing [node], | |
| /// so it can use deserialization context of [node]'s parents. | |
| /// In order to use scope declared by the [node] itself, implementation of | |
| /// this method can use [BinarySource.enterScope] and | |
| /// [BinarySource.leaveScope] methods. | |
| T readFromBinary(Node node, BinarySource source); | |
| /// Method to check whether a node can have metadata attached to it | |
| /// or referenced from the metadata payload. | |
| /// | |
| /// Currently due to binary format specifics Catch and MapEntry nodes | |
| /// can't have metadata attached to them. Also, metadata is not saved on | |
| /// Block nodes inside BlockExpressions. | |
| static bool isSupported(Node node) { | |
| return !(node is MapLiteralEntry || | |
| node is Catch || | |
| (node is Block && node.parent is BlockExpression) || | |
| // TODO(johnniwinther): Support [VariableDeclaration]. | |
| node is VariableDeclaration); | |
| } | |
| } |