| // Copyright (c) 2015, 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. | |
| library; | |
| import 'package:kernel/ast.dart' as ir; | |
| import '../common.dart'; | |
| import '../elements/entities.dart'; | |
| import '../js/js.dart' show JavaScriptNodeSourceInformation; | |
| import '../js_model/element_map.dart'; | |
| import '../serialization/serialization.dart'; | |
| import '../universe/call_structure.dart'; | |
| import 'position_information.dart'; | |
| /// Interface for passing source information, for instance for use in source | |
| /// maps, through the backend. | |
| abstract class SourceInformation extends JavaScriptNodeSourceInformation { | |
| const SourceInformation(); | |
| static SourceInformation readFromDataSource(DataSourceReader source) { | |
| int hasSourceInformation = source.readInt(); | |
| if (hasSourceInformation == 1) { | |
| return const SourceMappedMarker(); | |
| } else { | |
| assert(hasSourceInformation == 2); | |
| return PositionSourceInformation.readFromDataSource(source); | |
| } | |
| } | |
| static void writeToDataSink( | |
| DataSinkWriter sink, | |
| SourceInformation sourceInformation, | |
| ) { | |
| if (sourceInformation is SourceMappedMarker) { | |
| sink.writeInt(1); | |
| } else { | |
| sink.writeInt(2); | |
| PositionSourceInformation positionSourceInformation = | |
| sourceInformation as PositionSourceInformation; | |
| positionSourceInformation.writeToDataSinkInternal(sink); | |
| } | |
| } | |
| SourceSpan get sourceSpan; | |
| /// The source location associated with the start of the JS node. | |
| SourceLocation? get startPosition => null; | |
| /// The source location associated with an inner of the JS node. | |
| /// | |
| /// The inner position is for instance `foo()` in `o.foo()`. | |
| SourceLocation? get innerPosition => null; | |
| /// The source location associated with the end of the JS node. | |
| SourceLocation? get endPosition => null; | |
| /// A list containing start, inner, and end positions. | |
| List<SourceLocation> get sourceLocations; | |
| /// A list of inlining context locations. | |
| List<FrameContext>? get inliningContext => null; | |
| /// Return a short textual representation of the source location. | |
| String get shortText; | |
| } | |
| /// Context information about inlined calls. | |
| /// | |
| /// This is associated with SourceInformation objects to be able to emit | |
| /// precise data about inlining that can then be used by deobfuscation tools | |
| /// when reconstructing a source stack from a production stack trace. | |
| class FrameContext { | |
| static const String tag = 'frame-context'; | |
| /// Location of the call that was inlined. | |
| final SourceInformation callInformation; | |
| /// Name of the method that was inlined. | |
| final String inlinedMethodName; | |
| FrameContext(this.callInformation, this.inlinedMethodName); | |
| factory FrameContext.readFromDataSource(DataSourceReader source) { | |
| source.begin(tag); | |
| SourceInformation callInformation = source | |
| .readIndexedNoCache<SourceInformation>( | |
| () => SourceInformation.readFromDataSource(source), | |
| ); | |
| String inlinedMethodName = source.readString(); | |
| source.end(tag); | |
| return FrameContext(callInformation, inlinedMethodName); | |
| } | |
| void writeToDataSink(DataSinkWriter sink) { | |
| sink.begin(tag); | |
| sink.writeIndexed<SourceInformation>( | |
| callInformation, | |
| (SourceInformation sourceInformation) => | |
| SourceInformation.writeToDataSink(sink, sourceInformation), | |
| ); | |
| sink.writeString(inlinedMethodName); | |
| sink.end(tag); | |
| } | |
| @override | |
| String toString() => "(FrameContext: $callInformation, $inlinedMethodName)"; | |
| } | |
| /// Strategy for creating, processing and applying [SourceInformation]. | |
| class SourceInformationStrategy { | |
| const SourceInformationStrategy(); | |
| /// Called when the [JsToElementMap] is available. | |
| /// | |
| /// The [JsToElementMap] is used by some source information strategies to | |
| /// extract member details relevant in the source-map generation process. | |
| void onElementMapAvailable(JsToElementMap elementMap) {} | |
| /// Create a [SourceInformationBuilder] for [member]. | |
| SourceInformationBuilder createBuilderForContext( | |
| covariant MemberEntity member, | |
| ) { | |
| return SourceInformationBuilder(); | |
| } | |
| /// Generate [SourceInformation] marker for non-preamble code. | |
| SourceInformation? buildSourceMappedMarker() => null; | |
| /// Called when compilation has completed. | |
| void onComplete() {} | |
| } | |
| /// Interface for generating [SourceInformation]. | |
| class SourceInformationBuilder { | |
| const SourceInformationBuilder(); | |
| /// Create a [SourceInformationBuilder] for [member] with additional inlining | |
| /// [context]. | |
| SourceInformationBuilder forContext( | |
| covariant MemberEntity member, | |
| SourceInformation? context, | |
| ) => this; | |
| /// Generate [SourceInformation] for the declaration of the [member]. | |
| SourceInformation? buildDeclaration(covariant MemberEntity member) => null; | |
| /// Generate [SourceInformation] for the stub of [callStructure] for [member]. | |
| SourceInformation? buildStub( | |
| covariant FunctionEntity function, | |
| CallStructure callStructure, | |
| ) => null; | |
| /// Generate [SourceInformation] for the generic [node]. | |
| @Deprecated("Use SourceInformationFactory") | |
| SourceInformation? buildGeneric(ir.Node node) => null; | |
| /// Generate [SourceInformation] for an instantiation of a class using [node] | |
| /// for the source position. | |
| SourceInformation? buildCreate(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the return [node]. | |
| SourceInformation? buildReturn(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for an implicit return in [element]. | |
| SourceInformation? buildImplicitReturn(covariant MemberEntity element) => | |
| null; | |
| /// Generate [SourceInformation] for the loop [node]. | |
| SourceInformation? buildLoop(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for a read access like `a.b`. | |
| SourceInformation? buildGet(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for a write access like `a.b = 3`. | |
| SourceInformation? buildSet(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for a call in [node]. | |
| SourceInformation? buildCall(ir.Node receiver, ir.Node call) => null; | |
| /// Generate [SourceInformation] for the if statement in [node]. | |
| SourceInformation? buildIf(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the block statement in [node]. | |
| SourceInformation? buildBlock(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the constructor invocation in [node]. | |
| SourceInformation? buildNew(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the throw in [node]. | |
| SourceInformation? buildThrow(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the assert in [node]. | |
| SourceInformation? buildAssert(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the assignment in [node]. | |
| SourceInformation? buildAssignment(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the variable declaration inserted as | |
| /// first statement of a function. | |
| SourceInformation? buildVariableDeclaration() => null; | |
| /// Generate [SourceInformation] for the await [node]. | |
| SourceInformation? buildAwait(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the yield or yield* [node]. | |
| SourceInformation? buildYield(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for async/await boiler plate code. | |
| SourceInformation? buildAsyncBody() => null; | |
| /// Generate [SourceInformation] for exiting async/await code. | |
| SourceInformation? buildAsyncExit() => null; | |
| /// Generate [SourceInformation] for an invocation of a foreign method. | |
| SourceInformation? buildForeignCode(ir.Node node) => null; | |
| /// Generate [SourceInformation] for a string interpolation of [node]. | |
| SourceInformation? buildStringInterpolation(ir.Node node) => null; | |
| /// Generate [SourceInformation] for the for-in `iterator` access in [node]. | |
| SourceInformation? buildForInIterator(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the for-in `moveNext` call in [node]. | |
| SourceInformation? buildForInMoveNext(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the for-in `current` access in [node]. | |
| SourceInformation? buildForInCurrent(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the for-in variable assignment in [node]. | |
| SourceInformation? buildForInSet(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the operator `[]` access in [node]. | |
| SourceInformation? buildIndex(ir.Node node) => null; | |
| /// Generate [SourceInformation] for the operator `[]=` assignment in [node]. | |
| SourceInformation? buildIndexSet(ir.Node node) => null; | |
| /// Generate [SourceInformation] for the binary operation in [node]. | |
| SourceInformation? buildBinary(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the unary operation in [node]. | |
| SourceInformation? buildUnary(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the try statement in [node]. | |
| SourceInformation? buildTry(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the unary operator in [node]. | |
| SourceInformation? buildCatch(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the is-test in [node]. | |
| SourceInformation? buildIs(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the as-cast in [node]. | |
| SourceInformation? buildAs(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the switch statement [node]. | |
| SourceInformation? buildSwitch(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the switch case in [node]. | |
| SourceInformation? buildSwitchCase(ir.Node node) => null; | |
| /// Generate [SourceInformation] for the list literal in [node]. | |
| SourceInformation? buildListLiteral(ir.TreeNode node) => null; | |
| /// Generate [SourceInformation] for the break/continue in [node]. | |
| SourceInformation? buildGoto(ir.TreeNode node) => null; | |
| } | |
| /// A location in a source file. | |
| abstract class SourceLocation { | |
| static const String tag = 'source-location'; | |
| const SourceLocation(); | |
| /// The absolute URI of the source file of this source location. | |
| // TODO(48820): [sourceUri] is nullable due to `NoSourceLocationMarker`. We | |
| // would not need nullability of we could replace all | |
| // `NoSourceLocationMarker`s with `null`, or rearranged the `SourceLocation` | |
| // class hierarchy. `NoSourceLocationMarker` and `null` have different effects | |
| // on the generated source-map files. | |
| Uri? get sourceUri; | |
| /// The character offset of the this source location into the source file. | |
| int get offset; | |
| /// The 1-based line number of the [offset]. | |
| int get line; | |
| /// The 1-based column number of the [offset] with its line. | |
| int get column; | |
| /// The name associated with this source location, if any. | |
| String? get sourceName; | |
| static SourceLocation readFromDataSource(DataSourceReader source) { | |
| int format = source.readInt(); | |
| if (format == 0) { | |
| return const NoSourceLocationMarker(); | |
| } else { | |
| source.begin(tag); | |
| Uri sourceUri = source.readUri(); | |
| String sourceName = source.readStringOrNull()!; | |
| final locationSource = source.sourceLookup.lookupSource(sourceUri); | |
| final hasLocation = format > 1; | |
| int line = ir.TreeNode.noOffset; | |
| int column = ir.TreeNode.noOffset; | |
| if (hasLocation) { | |
| final lineLower = format - 2; | |
| final lineUpper = source.readInt(); | |
| line = (lineUpper << 6) | lineLower; | |
| column = source.readInt(); | |
| } | |
| source.end(tag); | |
| return _ConcreteSourceLocation(locationSource, sourceName, line, column); | |
| } | |
| } | |
| static void writeToDataSink( | |
| DataSinkWriter sink, | |
| SourceLocation sourceLocation, | |
| ) { | |
| if (sourceLocation is NoSourceLocationMarker) { | |
| sink.writeInt(0); | |
| } else { | |
| final column = sourceLocation.column; | |
| final line = sourceLocation.line; | |
| final hasLocation = line != ir.TreeNode.noOffset; | |
| // There are 2 formats in this case: | |
| // 1) The location has no offset so we only need a URI and name. Don't | |
| // write any line/column info. | |
| // 2) The location has an offset so we use the 'format' to encode the | |
| // bottom bits of the line and write the rest of the line and column | |
| // separately as compact uint30 numbers. | |
| if (!hasLocation) { | |
| sink.writeInt(1); | |
| } else { | |
| sink.writeInt((line & 0x3f) + 2); | |
| } | |
| sink.begin(tag); | |
| sink.writeUri(sourceLocation.sourceUri!); | |
| sink.writeStringOrNull(sourceLocation.sourceName); | |
| if (hasLocation) { | |
| sink.writeInt(line >> 6); | |
| sink.writeInt(column); | |
| } | |
| sink.end(tag); | |
| } | |
| } | |
| @override | |
| int get hashCode { | |
| return sourceUri.hashCode * 17 + | |
| offset.hashCode * 19 + | |
| sourceName.hashCode * 23; | |
| } | |
| @override | |
| bool operator ==(other) { | |
| if (identical(this, other)) return true; | |
| return other is SourceLocation && | |
| sourceUri == other.sourceUri && | |
| offset == other.offset && | |
| sourceName == other.sourceName; | |
| } | |
| String get shortText => '${sourceUri?.pathSegments.last}:[$line,$column]'; | |
| @override | |
| String toString() => '$sourceUri:[$line,$column]'; | |
| } | |
| /// A location in a source file encoded as a kernel [ir.Source] object and a | |
| /// line/column encoded into a single 64 bit int. | |
| class _ConcreteSourceLocation extends SourceLocation { | |
| final ir.Source _source; | |
| final int _lineColumn; | |
| _ConcreteSourceLocation(this._source, this.sourceName, int line, int column) | |
| : _lineColumn = (line << 32) | column; | |
| @override | |
| Uri get sourceUri => _source.fileUri!; | |
| @override | |
| int get offset => _source.getOffset(line, column); | |
| @override | |
| int get line => _lineColumn >>> 32; | |
| @override | |
| int get column => _lineColumn & 0xFFFFFFFF; | |
| @override | |
| final String sourceName; | |
| @override | |
| String get shortText => '${sourceUri.pathSegments.last}:[$line,$column]'; | |
| @override | |
| String toString() => '$sourceUri:[$line,$column]'; | |
| } | |
| /// Compute the source map name for [element]. If [callStructure] is non-null | |
| /// it is used to name the parameter stub for [element]. | |
| // TODO(johnniwinther): Merge this with `computeKernelElementNameForSourceMaps` | |
| // when the old frontend is removed. | |
| String? computeElementNameForSourceMaps( | |
| Entity element, [ | |
| CallStructure? callStructure, | |
| ]) { | |
| if (element is ClassEntity) { | |
| return element.name; | |
| } | |
| if (element is MemberEntity) { | |
| final enclosingClass = element.enclosingClass; | |
| String suffix = computeStubSuffix(callStructure); | |
| if (element is ConstructorEntity || element is ConstructorBodyEntity) { | |
| String className = enclosingClass!.name; | |
| if (element.name == '') { | |
| return className; | |
| } | |
| return '$className.${element.name}$suffix'; | |
| } | |
| if (enclosingClass != null) { | |
| if (enclosingClass.isClosure) { | |
| return computeElementNameForSourceMaps(enclosingClass, callStructure); | |
| } | |
| return '${enclosingClass.name}.${element.name}$suffix'; | |
| } | |
| return '${element.name}$suffix'; | |
| } | |
| // TODO(redemption): Create element names from kernel locals and closures. | |
| return element.name; | |
| } | |
| /// Compute the suffix used for a parameter stub for [callStructure]. | |
| String computeStubSuffix(CallStructure? callStructure) { | |
| if (callStructure == null) return ''; | |
| StringBuffer sb = StringBuffer(); | |
| sb.write(r'[function-entry$'); | |
| sb.write(callStructure.positionalArgumentCount); | |
| if (callStructure.namedArguments.isNotEmpty) { | |
| sb.write(r'$'); | |
| sb.write(callStructure.getOrderedNamedArguments().join(r'$')); | |
| } | |
| sb.write(']'); | |
| return sb.toString(); | |
| } | |
| class NoSourceLocationMarker extends SourceLocation { | |
| const NoSourceLocationMarker(); | |
| @override | |
| Uri? get sourceUri => null; | |
| @override | |
| int get column => 0; | |
| @override | |
| int get line => 0; | |
| @override | |
| int get offset => 0; | |
| @override | |
| String? get sourceName => null; | |
| String get shortName => '<no-location>'; | |
| @override | |
| String toString() => '<no-location>'; | |
| } | |
| /// Information tracked about inlined frames. | |
| /// | |
| /// Dart2js adds an extension to source-map files to track where calls are | |
| /// inlined. This information is used to improve the precision of tools that | |
| /// deobfuscate production stack traces. | |
| class FrameEntry { | |
| /// For push operations, the location of the inlining call, otherwise null. | |
| final SourceLocation? pushLocation; | |
| /// For push operations, the inlined method name, otherwise null. | |
| final String? inlinedMethodName; | |
| /// Whether a pop is the last pop that makes the inlining stack empty. | |
| final bool isEmptyPop; | |
| FrameEntry.push(this.pushLocation, this.inlinedMethodName) | |
| : isEmptyPop = false; | |
| FrameEntry.pop(this.isEmptyPop) | |
| : pushLocation = null, | |
| inlinedMethodName = null; | |
| bool get isPush => pushLocation != null; | |
| bool get isPop => pushLocation == null; | |
| } | |
| class SourceLookup { | |
| final Map<Uri, ir.Source> _map; | |
| SourceLookup(ir.Component component) : _map = component.uriToSource; | |
| ir.Source lookupSource(Uri uri) { | |
| return _map[uri]!; | |
| } | |
| } |