| // Copyright (c) 2018, 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 'package:source_maps/source_maps.dart' hide Printer; | |
| import 'package:source_span/source_span.dart' show SourceLocation; | |
| import 'js_ast.dart'; | |
| /// A source map marker signaling that there is no new source information from | |
| /// a node and the previous source mapping can continue through it. | |
| /// | |
| /// Conceptually this works exactly the same as `foo.sourceInformation = null` | |
| /// but allows the AST emitter to signal there is no new information in a way | |
| /// that null aware assignments don't override the lack of sourcemap information | |
| /// later. | |
| class _ContinueSourceMap { | |
| const _ContinueSourceMap(); | |
| } | |
| const continueSourceMap = _ContinueSourceMap(); | |
| class NodeEnd { | |
| final SourceLocation end; | |
| NodeEnd(this.end); | |
| @override | |
| String toString() => '#<NodeEnd $end>'; | |
| } | |
| class NodeSpan { | |
| final SourceLocation start, end; | |
| NodeSpan(this.start, this.end); | |
| @override | |
| String toString() => '#<NodeSpan $start to $end>'; | |
| } | |
| class HoverComment { | |
| final SourceLocation start, end; | |
| final Expression expression; | |
| HoverComment(this.expression, this.start, this.end); | |
| @override | |
| String toString() => '#<HoverComment `$expression` @ $start to $end>'; | |
| } | |
| class SourceMapPrintingContext extends SimpleJavaScriptPrintingContext { | |
| /// Current line in the buffer. | |
| int _line = 0; | |
| /// Current column in the buffer. | |
| int _column = 0; | |
| /// The source_maps builder we write JavaScript code to. | |
| final sourceMap = SourceMapBuilder(); | |
| /// The last marked line in the buffer. | |
| int _previousDartOffset = -1; | |
| SourceLocation? _pendingDartOffset; | |
| SourceLocation? _pendingJSLocation; | |
| @override | |
| void emit(String code) { | |
| var chars = code.runes.toList(); | |
| var length = chars.length; | |
| for (var i = 0; i < length; i++) { | |
| var c = chars[i]; | |
| if (c == _LF || (c == _CR && (i + 1 == length || chars[i + 1] != _LF))) { | |
| // Return not followed by line-feed is treated as a new line. | |
| _line++; | |
| _column = 0; | |
| } else { | |
| _column++; | |
| } | |
| } | |
| super.emit(code); | |
| } | |
| @override | |
| void enterNode(Node node) { | |
| var srcInfo = node.sourceInformation; | |
| if (srcInfo == null || srcInfo == continueSourceMap) return; | |
| SourceLocation? dartStart; | |
| if (srcInfo is SourceLocation) { | |
| dartStart = srcInfo; | |
| } else if (srcInfo is NodeSpan) { | |
| dartStart = srcInfo.start; | |
| } else if (srcInfo is HoverComment) { | |
| emit('/*'); | |
| _mark(srcInfo.start); | |
| srcInfo.expression.accept(printer); | |
| _mark(srcInfo.end); | |
| emit('*/'); | |
| } else if (srcInfo is! NodeEnd) { | |
| throw StateError( | |
| 'wrong kind of source map data: `$srcInfo` <${srcInfo.runtimeType}>', | |
| ); | |
| } | |
| if (dartStart != null) { | |
| if (node is Expression) { | |
| _markExpressionStart(dartStart); | |
| } else { | |
| _mark(dartStart); | |
| } | |
| } | |
| } | |
| @override | |
| void exitNode(Node node) { | |
| if (node is! Expression) _flushPendingMarks(); | |
| var srcInfo = node.sourceInformation; | |
| if (srcInfo == null || srcInfo == continueSourceMap) return; | |
| SourceLocation? dartEnd; | |
| if (srcInfo is NodeSpan) { | |
| dartEnd = srcInfo.end; | |
| } else if (srcInfo is NodeEnd) { | |
| dartEnd = srcInfo.end; | |
| } else if (srcInfo is! SourceLocation && srcInfo is! HoverComment) { | |
| throw StateError( | |
| 'wrong kind of source map data: `$srcInfo` <${srcInfo.runtimeType}>', | |
| ); | |
| } | |
| if (dartEnd != null) { | |
| if (node is Fun || node is Method || node is NamedFunction) { | |
| // Mark the exit point of a function. V8 steps to the end of a function | |
| // at its exit, so this provides a mapping for that location. | |
| var column = _column - 1; | |
| if (column >= 0) { | |
| // Adjust the column, because any ending brace or semicolon is already | |
| // in the output. | |
| var jsEnd = SourceLocation( | |
| buffer.length - 1, | |
| line: _line, | |
| column: column, | |
| ); | |
| _mark(dartEnd, jsEnd); | |
| } | |
| } else { | |
| _mark(dartEnd); | |
| } | |
| } | |
| } | |
| @override | |
| void expressionBranch() { | |
| // Only one branch of a conditional expression will be executed but the | |
| // expressions could be mapped to the same source location. To ensure | |
| // mappings are produced for both branches any pending locations should be | |
| // flushed when switching output from one branch to the other. | |
| _flushPendingMarks(); | |
| } | |
| @override | |
| void expressionJoin() { | |
| // To ensure mappings get output for all branches when nesting conditional | |
| // expressions they should be flushed after code flow joins after branching. | |
| _flushPendingMarks(); | |
| } | |
| /// Marks that we entered a Dart node at this offset. | |
| /// | |
| /// Multiple nested Dart expressions will appear to start at the same Dart | |
| /// location, but they can have different JavaScript locations. For example: | |
| /// | |
| /// get foo => _foo.bar && _baz; | |
| /// // ^^^^^^^^^^^^^^^^ binary-op, JS code `dart.test(...) && ...` | |
| /// // ^^^^^^^^ property access, JS code `this[_foo].bar` | |
| /// // ^^^^ identifier, JS code `this[_foo]` | |
| /// | |
| /// To ensure hover works, we must not include `dart.test` at the start of | |
| /// `_foo`. Also to ensure `_foo` and `_foo.bar` works, we must mark the end | |
| /// of `_foo`. | |
| void _markExpressionStart(SourceLocation dartOffset) { | |
| if (_pendingDartOffset != dartOffset) _flushPendingMarks(); | |
| _pendingDartOffset = dartOffset; | |
| _pendingJSLocation = _getJSLocation(); | |
| } | |
| /// Mark any pending marks from [_markExpressionStart]. | |
| /// | |
| /// This ensures we get the innermost JS offset for a given Dart offset. | |
| void _flushPendingMarks() { | |
| var pending = _pendingDartOffset; | |
| if (pending != null) { | |
| _markInternal(pending, _pendingJSLocation!); | |
| _pendingDartOffset = null; | |
| _pendingJSLocation = null; | |
| } | |
| } | |
| void _mark(SourceLocation dartLocation, [SourceLocation? jsLocation]) { | |
| _flushPendingMarks(); | |
| jsLocation ??= _getJSLocation(); | |
| _markInternal(dartLocation, jsLocation); | |
| } | |
| void _markInternal(SourceLocation dartLocation, SourceLocation jsLocation) { | |
| // Don't mark the same JS location to two different Dart locations. | |
| if (_previousDartOffset == dartLocation.offset) return; | |
| _previousDartOffset = dartLocation.offset; | |
| sourceMap.addLocation(dartLocation, jsLocation, null); | |
| } | |
| SourceLocation _getJSLocation() => | |
| SourceLocation(buffer.length, line: _line, column: _column); | |
| } | |
| const int _LF = 10; | |
| const int _CR = 13; |