├── .github ├── dependabot.yaml └── workflows │ ├── no-response.yml │ ├── publish.yaml │ └── test-package.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── lib ├── builder.dart ├── parser.dart ├── printer.dart ├── refactor.dart ├── source_maps.dart └── src │ ├── source_map_span.dart │ ├── utils.dart │ └── vlq.dart ├── pubspec.yaml └── test ├── builder_test.dart ├── common.dart ├── end2end_test.dart ├── parser_test.dart ├── printer_test.dart ├── refactor_test.dart ├── utils_test.dart └── vlq_test.dart /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: github-actions 6 | directory: / 7 | schedule: 8 | interval: monthly 9 | labels: 10 | - autosubmit 11 | groups: 12 | github-actions: 13 | patterns: 14 | - "*" 15 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | # A workflow to close issues where the author hasn't responded to a request for 2 | # more information; see https://github.com/actions/stale. 3 | 4 | name: No Response 5 | 6 | # Run as a daily cron. 7 | on: 8 | schedule: 9 | # Every day at 8am 10 | - cron: '0 8 * * *' 11 | 12 | # All permissions not specified are set to 'none'. 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | 17 | jobs: 18 | no-response: 19 | runs-on: ubuntu-latest 20 | if: ${{ github.repository_owner == 'dart-lang' }} 21 | steps: 22 | - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e 23 | with: 24 | # Don't automatically mark inactive issues+PRs as stale. 25 | days-before-stale: -1 26 | # Close needs-info issues and PRs after 14 days of inactivity. 27 | days-before-close: 14 28 | stale-issue-label: "needs-info" 29 | close-issue-message: > 30 | Without additional information we're not able to resolve this issue. 31 | Feel free to add more info or respond to any questions above and we 32 | can reopen the case. Thanks for your contribution! 33 | stale-pr-label: "needs-info" 34 | close-pr-message: > 35 | Without additional information we're not able to resolve this PR. 36 | Feel free to add more info or respond to any questions above. 37 | Thanks for your contribution! 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # A CI configuration to auto-publish pub packages. 2 | 3 | name: Publish 4 | 5 | on: 6 | pull_request: 7 | branches: [ master ] 8 | push: 9 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 10 | 11 | jobs: 12 | publish: 13 | if: ${{ github.repository_owner == 'dart-lang' }} 14 | uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main 15 | permissions: 16 | id-token: write # Required for authentication using OIDC 17 | pull-requests: write # Required for writing the pull request note 18 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | # Run on PRs and pushes to the default branch. 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | 15 | jobs: 16 | # Check code formatting and static analysis on a single OS (linux) 17 | # against Dart dev. 18 | analyze: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | sdk: [dev] 24 | steps: 25 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 26 | - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 27 | with: 28 | sdk: ${{ matrix.sdk }} 29 | - id: install 30 | name: Install dependencies 31 | run: dart pub get 32 | - name: Check formatting 33 | run: dart format --output=none --set-exit-if-changed . 34 | if: always() && steps.install.outcome == 'success' 35 | - name: Analyze code 36 | run: dart analyze --fatal-infos 37 | if: always() && steps.install.outcome == 'success' 38 | 39 | # Run tests on a matrix consisting of two dimensions: 40 | # 1. OS: ubuntu-latest, (macos-latest, windows-latest) 41 | # 2. release channel: dev 42 | test: 43 | needs: analyze 44 | runs-on: ${{ matrix.os }} 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | # Add macos-latest and/or windows-latest if relevant for this package. 49 | os: [ubuntu-latest] 50 | sdk: [3.3.0, dev] 51 | steps: 52 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 53 | - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 54 | with: 55 | sdk: ${{ matrix.sdk }} 56 | - id: install 57 | name: Install dependencies 58 | run: dart pub get 59 | - name: Run VM tests 60 | run: dart test --platform vm 61 | if: always() && steps.install.outcome == 'success' 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | .packages 3 | .pub/ 4 | pubspec.lock 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.10.13-wip 2 | 3 | - Require Dart 3.3 4 | 5 | ## 0.10.12 6 | 7 | * Add additional types at API boundaries. 8 | 9 | ## 0.10.11 10 | 11 | * Populate the pubspec `repository` field. 12 | * Update the source map documentation link in the readme. 13 | 14 | ## 0.10.10 15 | 16 | * Stable release for null safety. 17 | 18 | ## 0.10.9 19 | 20 | * Fix a number of document comment issues. 21 | * Allow parsing source map files with a missing `names` field. 22 | 23 | ## 0.10.8 24 | 25 | * Preserve source-map extensions in `SingleMapping`. Extensions are keys in the 26 | json map that start with `"x_"`. 27 | 28 | ## 0.10.7 29 | 30 | * Set max SDK version to `<3.0.0`, and adjust other dependencies. 31 | 32 | ## 0.10.6 33 | 34 | * Require version 2.0.0 of the Dart SDK. 35 | 36 | ## 0.10.5 37 | 38 | * Add a `SingleMapping.files` field which provides access to `SourceFile`s 39 | representing the `"sourcesContent"` fields in the source map. 40 | 41 | * Add an `includeSourceContents` flag to `SingleMapping.toJson()` which 42 | indicates whether to include source file contents in the source map. 43 | 44 | ## 0.10.4 45 | * Implement `highlight` in `SourceMapFileSpan`. 46 | * Require version `^1.3.0` of `source_span`. 47 | 48 | ## 0.10.3 49 | * Add `addMapping` and `containsMapping` members to `MappingBundle`. 50 | 51 | ## 0.10.2 52 | * Support for extended source map format. 53 | * Polish `MappingBundle.spanFor` handling of URIs that have a suffix that 54 | exactly match a source map in the MappingBundle. 55 | 56 | ## 0.10.1+5 57 | * Fix strong mode warning in test. 58 | 59 | ## 0.10.1+4 60 | 61 | * Extend `MappingBundle.spanFor` to accept requests for output files that 62 | don't have source maps. 63 | 64 | ## 0.10.1+3 65 | 66 | * Add `MappingBundle` class that handles extended source map format that 67 | supports source maps for multiple output files in a single mapper. 68 | Extend `Mapping.spanFor` API to accept a uri parameter that is optional 69 | for normal source maps but required for MappingBundle source maps. 70 | 71 | ## 0.10.1+2 72 | 73 | * Fix more strong mode warnings. 74 | 75 | ## 0.10.1+1 76 | 77 | * Fix all strong mode warnings. 78 | 79 | ## 0.10.1 80 | 81 | * Add a `mapUrl` named argument to `parse` and `parseJson`. This argument is 82 | used to resolve source URLs for source spans. 83 | 84 | ## 0.10.0+2 85 | 86 | * Fix analyzer error (FileSpan has a new field since `source_span` 1.1.1) 87 | 88 | ## 0.10.0+1 89 | 90 | * Remove an unnecessary warning printed when the "file" field is missing from a 91 | Json formatted source map. This field is optional and its absence is not 92 | unusual. 93 | 94 | ## 0.10.0 95 | 96 | * Remove the `Span`, `Location` and `SourceFile` classes. Use the 97 | corresponding `source_span` classes instead. 98 | 99 | ## 0.9.4 100 | 101 | * Update `SpanFormatException` with `source` and `offset`. 102 | 103 | * All methods that take `Span`s, `Location`s, and `SourceFile`s as inputs now 104 | also accept the corresponding `source_span` classes as well. Using the old 105 | classes is now deprecated and will be unsupported in version 0.10.0. 106 | 107 | ## 0.9.3 108 | 109 | * Support writing SingleMapping objects to source map version 3 format. 110 | * Support the `sourceRoot` field in the SingleMapping class. 111 | * Support updating the `targetUrl` field in the SingleMapping class. 112 | 113 | ## 0.9.2+2 114 | 115 | * Fix a bug in `FixedSpan.getLocationMessage`. 116 | 117 | ## 0.9.2+1 118 | 119 | * Minor readability improvements to `FixedSpan.getLocationMessage` and 120 | `SpanException.toString`. 121 | 122 | ## 0.9.2 123 | 124 | * Add `SpanException` and `SpanFormatException` classes. 125 | 126 | ## 0.9.1 127 | 128 | * Support unmapped areas in source maps. 129 | 130 | * Increase the readability of location messages. 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repo has moved to https://github.com/dart-lang/tools/tree/main/pkgs/source_maps 3 | 4 | [![Dart CI](https://github.com/dart-lang/source_maps/actions/workflows/test-package.yml/badge.svg)](https://github.com/dart-lang/source_maps/actions/workflows/test-package.yml) 5 | [![pub package](https://img.shields.io/pub/v/source_maps.svg)](https://pub.dev/packages/source_maps) 6 | [![package publisher](https://img.shields.io/pub/publisher/source_maps.svg)](https://pub.dev/packages/source_maps/publisher) 7 | 8 | This project implements a Dart pub package to work with source maps. 9 | 10 | ## Docs and usage 11 | 12 | The implementation is based on the [source map version 3 spec][spec] which was 13 | originated from the [Closure Compiler][closure] and has been implemented in 14 | Chrome and Firefox. 15 | 16 | In this package we provide: 17 | 18 | * Data types defining file locations and spans: these are not part of the 19 | original source map specification. These data types are great for tracking 20 | source locations on source maps, but they can also be used by tools to 21 | reporting useful error messages that include on source locations. 22 | * A builder that creates a source map programmatically and produces the encoded 23 | source map format. 24 | * A parser that reads the source map format and provides APIs to read the 25 | mapping information. 26 | 27 | [closure]: https://github.com/google/closure-compiler/wiki/Source-Maps 28 | [spec]: https://docs.google.com/a/google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit 29 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:dart_flutter_team_lints/analysis_options.yaml 2 | -------------------------------------------------------------------------------- /lib/builder.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Contains a builder object useful for creating source maps programatically. 6 | library source_maps.builder; 7 | 8 | // TODO(sigmund): add a builder for multi-section mappings. 9 | 10 | import 'dart:convert'; 11 | 12 | import 'package:source_span/source_span.dart'; 13 | 14 | import 'parser.dart'; 15 | import 'src/source_map_span.dart'; 16 | 17 | /// Builds a source map given a set of mappings. 18 | class SourceMapBuilder { 19 | final List _entries = []; 20 | 21 | /// Adds an entry mapping the [targetOffset] to [source]. 22 | void addFromOffset(SourceLocation source, SourceFile targetFile, 23 | int targetOffset, String identifier) { 24 | ArgumentError.checkNotNull(targetFile, 'targetFile'); 25 | _entries.add(Entry(source, targetFile.location(targetOffset), identifier)); 26 | } 27 | 28 | /// Adds an entry mapping [target] to [source]. 29 | /// 30 | /// If [isIdentifier] is true or if [target] is a [SourceMapSpan] with 31 | /// `isIdentifier` set to true, this entry is considered to represent an 32 | /// identifier whose value will be stored in the source map. [isIdentifier] 33 | /// takes precedence over [target]'s `isIdentifier` value. 34 | void addSpan(SourceSpan source, SourceSpan target, {bool? isIdentifier}) { 35 | isIdentifier ??= source is SourceMapSpan ? source.isIdentifier : false; 36 | 37 | var name = isIdentifier ? source.text : null; 38 | _entries.add(Entry(source.start, target.start, name)); 39 | } 40 | 41 | /// Adds an entry mapping [target] to [source]. 42 | void addLocation( 43 | SourceLocation source, SourceLocation target, String? identifier) { 44 | _entries.add(Entry(source, target, identifier)); 45 | } 46 | 47 | /// Encodes all mappings added to this builder as a json map. 48 | Map build(String fileUrl) { 49 | return SingleMapping.fromEntries(_entries, fileUrl).toJson(); 50 | } 51 | 52 | /// Encodes all mappings added to this builder as a json string. 53 | String toJson(String fileUrl) => jsonEncode(build(fileUrl)); 54 | } 55 | 56 | /// An entry in the source map builder. 57 | class Entry implements Comparable { 58 | /// Span denoting the original location in the input source file 59 | final SourceLocation source; 60 | 61 | /// Span indicating the corresponding location in the target file. 62 | final SourceLocation target; 63 | 64 | /// An identifier name, when this location is the start of an identifier. 65 | final String? identifierName; 66 | 67 | /// Creates a new [Entry] mapping [target] to [source]. 68 | Entry(this.source, this.target, this.identifierName); 69 | 70 | /// Implements [Comparable] to ensure that entries are ordered by their 71 | /// location in the target file. We sort primarily by the target offset 72 | /// because source map files are encoded by printing each mapping in order as 73 | /// they appear in the target file. 74 | @override 75 | int compareTo(Entry other) { 76 | var res = target.compareTo(other.target); 77 | if (res != 0) return res; 78 | res = source.sourceUrl 79 | .toString() 80 | .compareTo(other.source.sourceUrl.toString()); 81 | if (res != 0) return res; 82 | return source.compareTo(other.source); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/parser.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Contains the top-level function to parse source maps version 3. 6 | library source_maps.parser; 7 | 8 | import 'dart:convert'; 9 | 10 | import 'package:source_span/source_span.dart'; 11 | 12 | import 'builder.dart' as builder; 13 | import 'src/source_map_span.dart'; 14 | import 'src/utils.dart'; 15 | import 'src/vlq.dart'; 16 | 17 | /// Parses a source map directly from a json string. 18 | /// 19 | /// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of 20 | /// the source map file itself. If it's passed, any URLs in the source 21 | /// map will be interpreted as relative to this URL when generating spans. 22 | // TODO(sigmund): evaluate whether other maps should have the json parsed, or 23 | // the string represenation. 24 | // TODO(tjblasi): Ignore the first line of [jsonMap] if the JSON safety string 25 | // `)]}'` begins the string representation of the map. 26 | Mapping parse(String jsonMap, 27 | {Map? otherMaps, /*String|Uri*/ Object? mapUrl}) => 28 | parseJson(jsonDecode(jsonMap) as Map, otherMaps: otherMaps, mapUrl: mapUrl); 29 | 30 | /// Parses a source map or source map bundle directly from a json string. 31 | /// 32 | /// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of 33 | /// the source map file itself. If it's passed, any URLs in the source 34 | /// map will be interpreted as relative to this URL when generating spans. 35 | Mapping parseExtended(String jsonMap, 36 | {Map? otherMaps, /*String|Uri*/ Object? mapUrl}) => 37 | parseJsonExtended(jsonDecode(jsonMap), 38 | otherMaps: otherMaps, mapUrl: mapUrl); 39 | 40 | /// Parses a source map or source map bundle. 41 | /// 42 | /// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of 43 | /// the source map file itself. If it's passed, any URLs in the source 44 | /// map will be interpreted as relative to this URL when generating spans. 45 | Mapping parseJsonExtended(/*List|Map*/ Object? json, 46 | {Map? otherMaps, /*String|Uri*/ Object? mapUrl}) { 47 | if (json is List) { 48 | return MappingBundle.fromJson(json, mapUrl: mapUrl); 49 | } 50 | return parseJson(json as Map); 51 | } 52 | 53 | /// Parses a source map. 54 | /// 55 | /// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of 56 | /// the source map file itself. If it's passed, any URLs in the source 57 | /// map will be interpreted as relative to this URL when generating spans. 58 | Mapping parseJson(Map map, 59 | {Map? otherMaps, /*String|Uri*/ Object? mapUrl}) { 60 | if (map['version'] != 3) { 61 | throw ArgumentError('unexpected source map version: ${map["version"]}. ' 62 | 'Only version 3 is supported.'); 63 | } 64 | 65 | if (map.containsKey('sections')) { 66 | if (map.containsKey('mappings') || 67 | map.containsKey('sources') || 68 | map.containsKey('names')) { 69 | throw const FormatException('map containing "sections" ' 70 | 'cannot contain "mappings", "sources", or "names".'); 71 | } 72 | return MultiSectionMapping.fromJson(map['sections'] as List, otherMaps, 73 | mapUrl: mapUrl); 74 | } 75 | return SingleMapping.fromJson(map.cast(), mapUrl: mapUrl); 76 | } 77 | 78 | /// A mapping parsed out of a source map. 79 | abstract class Mapping { 80 | /// Returns the span associated with [line] and [column]. 81 | /// 82 | /// [uri] is the optional location of the output file to find the span for 83 | /// to disambiguate cases where a mapping may have different mappings for 84 | /// different output files. 85 | SourceMapSpan? spanFor(int line, int column, 86 | {Map? files, String? uri}); 87 | 88 | /// Returns the span associated with [location]. 89 | SourceMapSpan? spanForLocation(SourceLocation location, 90 | {Map? files}) { 91 | return spanFor(location.line, location.column, 92 | uri: location.sourceUrl?.toString(), files: files); 93 | } 94 | } 95 | 96 | /// A meta-level map containing sections. 97 | class MultiSectionMapping extends Mapping { 98 | /// For each section, the start line offset. 99 | final List _lineStart = []; 100 | 101 | /// For each section, the start column offset. 102 | final List _columnStart = []; 103 | 104 | /// For each section, the actual source map information, which is not adjusted 105 | /// for offsets. 106 | final List _maps = []; 107 | 108 | /// Creates a section mapping from json. 109 | MultiSectionMapping.fromJson(List sections, Map? otherMaps, 110 | {/*String|Uri*/ Object? mapUrl}) { 111 | for (var section in sections.cast()) { 112 | var offset = section['offset'] as Map?; 113 | if (offset == null) throw const FormatException('section missing offset'); 114 | 115 | var line = offset['line'] as int?; 116 | if (line == null) throw const FormatException('offset missing line'); 117 | 118 | var column = offset['column'] as int?; 119 | if (column == null) throw const FormatException('offset missing column'); 120 | 121 | _lineStart.add(line); 122 | _columnStart.add(column); 123 | 124 | var url = section['url'] as String?; 125 | var map = section['map'] as Map?; 126 | 127 | if (url != null && map != null) { 128 | throw const FormatException( 129 | "section can't use both url and map entries"); 130 | } else if (url != null) { 131 | var other = otherMaps?[url]; 132 | if (otherMaps == null || other == null) { 133 | throw FormatException( 134 | 'section contains refers to $url, but no map was ' 135 | 'given for it. Make sure a map is passed in "otherMaps"'); 136 | } 137 | _maps.add(parseJson(other, otherMaps: otherMaps, mapUrl: url)); 138 | } else if (map != null) { 139 | _maps.add(parseJson(map, otherMaps: otherMaps, mapUrl: mapUrl)); 140 | } else { 141 | throw const FormatException('section missing url or map'); 142 | } 143 | } 144 | if (_lineStart.isEmpty) { 145 | throw const FormatException('expected at least one section'); 146 | } 147 | } 148 | 149 | int _indexFor(int line, int column) { 150 | for (var i = 0; i < _lineStart.length; i++) { 151 | if (line < _lineStart[i]) return i - 1; 152 | if (line == _lineStart[i] && column < _columnStart[i]) return i - 1; 153 | } 154 | return _lineStart.length - 1; 155 | } 156 | 157 | @override 158 | SourceMapSpan? spanFor(int line, int column, 159 | {Map? files, String? uri}) { 160 | // TODO(jacobr): perhaps verify that targetUrl matches the actual uri 161 | // or at least ends in the same file name. 162 | var index = _indexFor(line, column); 163 | return _maps[index].spanFor( 164 | line - _lineStart[index], column - _columnStart[index], 165 | files: files); 166 | } 167 | 168 | @override 169 | String toString() { 170 | var buff = StringBuffer('$runtimeType : ['); 171 | for (var i = 0; i < _lineStart.length; i++) { 172 | buff 173 | ..write('(') 174 | ..write(_lineStart[i]) 175 | ..write(',') 176 | ..write(_columnStart[i]) 177 | ..write(':') 178 | ..write(_maps[i]) 179 | ..write(')'); 180 | } 181 | buff.write(']'); 182 | return buff.toString(); 183 | } 184 | } 185 | 186 | class MappingBundle extends Mapping { 187 | final Map _mappings = {}; 188 | 189 | MappingBundle(); 190 | 191 | MappingBundle.fromJson(List json, {/*String|Uri*/ Object? mapUrl}) { 192 | for (var map in json) { 193 | addMapping(parseJson(map as Map, mapUrl: mapUrl) as SingleMapping); 194 | } 195 | } 196 | 197 | void addMapping(SingleMapping mapping) { 198 | // TODO(jacobr): verify that targetUrl is valid uri instead of a windows 199 | // path. 200 | // TODO: Remove type arg https://github.com/dart-lang/sdk/issues/42227 201 | var targetUrl = ArgumentError.checkNotNull( 202 | mapping.targetUrl, 'mapping.targetUrl'); 203 | _mappings[targetUrl] = mapping; 204 | } 205 | 206 | /// Encodes the Mapping mappings as a json map. 207 | List toJson() => _mappings.values.map((v) => v.toJson()).toList(); 208 | 209 | @override 210 | String toString() { 211 | var buff = StringBuffer(); 212 | for (var map in _mappings.values) { 213 | buff.write(map.toString()); 214 | } 215 | return buff.toString(); 216 | } 217 | 218 | bool containsMapping(String url) => _mappings.containsKey(url); 219 | 220 | @override 221 | SourceMapSpan? spanFor(int line, int column, 222 | {Map? files, String? uri}) { 223 | // TODO: Remove type arg https://github.com/dart-lang/sdk/issues/42227 224 | uri = ArgumentError.checkNotNull(uri, 'uri'); 225 | 226 | // Find the longest suffix of the uri that matches the sourcemap 227 | // where the suffix starts after a path segment boundary. 228 | // We consider ":" and "/" as path segment boundaries so that 229 | // "package:" uris can be handled with minimal special casing. Having a 230 | // few false positive path segment boundaries is not a significant issue 231 | // as we prefer the longest matching prefix. 232 | // Using package:path `path.split` to find path segment boundaries would 233 | // not generate all of the path segment boundaries we want for "package:" 234 | // urls as "package:package_name" would be one path segment when we want 235 | // "package" and "package_name" to be sepearate path segments. 236 | 237 | var onBoundary = true; 238 | var separatorCodeUnits = ['/'.codeUnitAt(0), ':'.codeUnitAt(0)]; 239 | for (var i = 0; i < uri.length; ++i) { 240 | if (onBoundary) { 241 | var candidate = uri.substring(i); 242 | var candidateMapping = _mappings[candidate]; 243 | if (candidateMapping != null) { 244 | return candidateMapping.spanFor(line, column, 245 | files: files, uri: candidate); 246 | } 247 | } 248 | onBoundary = separatorCodeUnits.contains(uri.codeUnitAt(i)); 249 | } 250 | 251 | // Note: when there is no source map for an uri, this behaves like an 252 | // identity function, returning the requested location as the result. 253 | 254 | // Create a mock offset for the output location. We compute it in terms 255 | // of the input line and column to minimize the chances that two different 256 | // line and column locations are mapped to the same offset. 257 | var offset = line * 1000000 + column; 258 | var location = SourceLocation(offset, 259 | line: line, column: column, sourceUrl: Uri.parse(uri)); 260 | return SourceMapSpan(location, location, ''); 261 | } 262 | } 263 | 264 | /// A map containing direct source mappings. 265 | class SingleMapping extends Mapping { 266 | /// Source urls used in the mapping, indexed by id. 267 | final List urls; 268 | 269 | /// Source names used in the mapping, indexed by id. 270 | final List names; 271 | 272 | /// The [SourceFile]s to which the entries in [lines] refer. 273 | /// 274 | /// This is in the same order as [urls]. If this was constructed using 275 | /// [SingleMapping.fromEntries], this contains files from any [FileLocation]s 276 | /// used to build the mapping. If it was parsed from JSON, it contains files 277 | /// for any sources whose contents were provided via the `"sourcesContent"` 278 | /// field. 279 | /// 280 | /// Files whose contents aren't available are `null`. 281 | final List files; 282 | 283 | /// Entries indicating the beginning of each span. 284 | final List lines; 285 | 286 | /// Url of the target file. 287 | String? targetUrl; 288 | 289 | /// Source root prepended to all entries in [urls]. 290 | String? sourceRoot; 291 | 292 | final Uri? _mapUrl; 293 | 294 | final Map extensions; 295 | 296 | SingleMapping._(this.targetUrl, this.files, this.urls, this.names, this.lines) 297 | : _mapUrl = null, 298 | extensions = {}; 299 | 300 | factory SingleMapping.fromEntries(Iterable entries, 301 | [String? fileUrl]) { 302 | // The entries needs to be sorted by the target offsets. 303 | var sourceEntries = entries.toList()..sort(); 304 | var lines = []; 305 | 306 | // Indices associated with file urls that will be part of the source map. We 307 | // rely on map order so that `urls.keys[urls[u]] == u` 308 | var urls = {}; 309 | 310 | // Indices associated with identifiers that will be part of the source map. 311 | // We rely on map order so that `names.keys[names[n]] == n` 312 | var names = {}; 313 | 314 | /// The file for each URL, indexed by [urls]' values. 315 | var files = {}; 316 | 317 | int? lineNum; 318 | late List targetEntries; 319 | for (var sourceEntry in sourceEntries) { 320 | if (lineNum == null || sourceEntry.target.line > lineNum) { 321 | lineNum = sourceEntry.target.line; 322 | targetEntries = []; 323 | lines.add(TargetLineEntry(lineNum, targetEntries)); 324 | } 325 | 326 | var sourceUrl = sourceEntry.source.sourceUrl; 327 | var urlId = urls.putIfAbsent( 328 | sourceUrl == null ? '' : sourceUrl.toString(), () => urls.length); 329 | 330 | if (sourceEntry.source is FileLocation) { 331 | files.putIfAbsent( 332 | urlId, () => (sourceEntry.source as FileLocation).file); 333 | } 334 | 335 | var sourceEntryIdentifierName = sourceEntry.identifierName; 336 | var srcNameId = sourceEntryIdentifierName == null 337 | ? null 338 | : names.putIfAbsent(sourceEntryIdentifierName, () => names.length); 339 | targetEntries.add(TargetEntry(sourceEntry.target.column, urlId, 340 | sourceEntry.source.line, sourceEntry.source.column, srcNameId)); 341 | } 342 | return SingleMapping._(fileUrl, urls.values.map((i) => files[i]).toList(), 343 | urls.keys.toList(), names.keys.toList(), lines); 344 | } 345 | 346 | SingleMapping.fromJson(Map map, {Object? mapUrl}) 347 | : targetUrl = map['file'] as String?, 348 | urls = List.from(map['sources'] as List), 349 | names = List.from((map['names'] as List?) ?? []), 350 | files = List.filled((map['sources'] as List).length, null), 351 | sourceRoot = map['sourceRoot'] as String?, 352 | lines = [], 353 | _mapUrl = mapUrl is String ? Uri.parse(mapUrl) : (mapUrl as Uri?), 354 | extensions = {} { 355 | var sourcesContent = map['sourcesContent'] == null 356 | ? const [] 357 | : List.from(map['sourcesContent'] as List); 358 | for (var i = 0; i < urls.length && i < sourcesContent.length; i++) { 359 | var source = sourcesContent[i]; 360 | if (source == null) continue; 361 | files[i] = SourceFile.fromString(source, url: urls[i]); 362 | } 363 | 364 | var line = 0; 365 | var column = 0; 366 | var srcUrlId = 0; 367 | var srcLine = 0; 368 | var srcColumn = 0; 369 | var srcNameId = 0; 370 | var tokenizer = _MappingTokenizer(map['mappings'] as String); 371 | var entries = []; 372 | 373 | while (tokenizer.hasTokens) { 374 | if (tokenizer.nextKind.isNewLine) { 375 | if (entries.isNotEmpty) { 376 | lines.add(TargetLineEntry(line, entries)); 377 | entries = []; 378 | } 379 | line++; 380 | column = 0; 381 | tokenizer._consumeNewLine(); 382 | continue; 383 | } 384 | 385 | // Decode the next entry, using the previous encountered values to 386 | // decode the relative values. 387 | // 388 | // We expect 1, 4, or 5 values. If present, values are expected in the 389 | // following order: 390 | // 0: the starting column in the current line of the generated file 391 | // 1: the id of the original source file 392 | // 2: the starting line in the original source 393 | // 3: the starting column in the original source 394 | // 4: the id of the original symbol name 395 | // The values are relative to the previous encountered values. 396 | if (tokenizer.nextKind.isNewSegment) throw _segmentError(0, line); 397 | column += tokenizer._consumeValue(); 398 | if (!tokenizer.nextKind.isValue) { 399 | entries.add(TargetEntry(column)); 400 | } else { 401 | srcUrlId += tokenizer._consumeValue(); 402 | if (srcUrlId >= urls.length) { 403 | throw StateError( 404 | 'Invalid source url id. $targetUrl, $line, $srcUrlId'); 405 | } 406 | if (!tokenizer.nextKind.isValue) throw _segmentError(2, line); 407 | srcLine += tokenizer._consumeValue(); 408 | if (!tokenizer.nextKind.isValue) throw _segmentError(3, line); 409 | srcColumn += tokenizer._consumeValue(); 410 | if (!tokenizer.nextKind.isValue) { 411 | entries.add(TargetEntry(column, srcUrlId, srcLine, srcColumn)); 412 | } else { 413 | srcNameId += tokenizer._consumeValue(); 414 | if (srcNameId >= names.length) { 415 | throw StateError('Invalid name id: $targetUrl, $line, $srcNameId'); 416 | } 417 | entries.add( 418 | TargetEntry(column, srcUrlId, srcLine, srcColumn, srcNameId)); 419 | } 420 | } 421 | if (tokenizer.nextKind.isNewSegment) tokenizer._consumeNewSegment(); 422 | } 423 | if (entries.isNotEmpty) { 424 | lines.add(TargetLineEntry(line, entries)); 425 | } 426 | 427 | map.forEach((name, value) { 428 | if (name.startsWith('x_')) extensions[name] = value; 429 | }); 430 | } 431 | 432 | /// Encodes the Mapping mappings as a json map. 433 | /// 434 | /// If [includeSourceContents] is `true`, this includes the source file 435 | /// contents from [files] in the map if possible. 436 | Map toJson({bool includeSourceContents = false}) { 437 | var buff = StringBuffer(); 438 | var line = 0; 439 | var column = 0; 440 | var srcLine = 0; 441 | var srcColumn = 0; 442 | var srcUrlId = 0; 443 | var srcNameId = 0; 444 | var first = true; 445 | 446 | for (var entry in lines) { 447 | var nextLine = entry.line; 448 | if (nextLine > line) { 449 | for (var i = line; i < nextLine; ++i) { 450 | buff.write(';'); 451 | } 452 | line = nextLine; 453 | column = 0; 454 | first = true; 455 | } 456 | 457 | for (var segment in entry.entries) { 458 | if (!first) buff.write(','); 459 | first = false; 460 | column = _append(buff, column, segment.column); 461 | 462 | // Encoding can be just the column offset if there is no source 463 | // information. 464 | var newUrlId = segment.sourceUrlId; 465 | if (newUrlId == null) continue; 466 | srcUrlId = _append(buff, srcUrlId, newUrlId); 467 | srcLine = _append(buff, srcLine, segment.sourceLine!); 468 | srcColumn = _append(buff, srcColumn, segment.sourceColumn!); 469 | 470 | if (segment.sourceNameId == null) continue; 471 | srcNameId = _append(buff, srcNameId, segment.sourceNameId!); 472 | } 473 | } 474 | 475 | var result = { 476 | 'version': 3, 477 | 'sourceRoot': sourceRoot ?? '', 478 | 'sources': urls, 479 | 'names': names, 480 | 'mappings': buff.toString(), 481 | }; 482 | if (targetUrl != null) result['file'] = targetUrl!; 483 | 484 | if (includeSourceContents) { 485 | result['sourcesContent'] = files.map((file) => file?.getText(0)).toList(); 486 | } 487 | extensions.forEach((name, value) => result[name] = value); 488 | 489 | return result; 490 | } 491 | 492 | /// Appends to [buff] a VLQ encoding of [newValue] using the difference 493 | /// between [oldValue] and [newValue] 494 | static int _append(StringBuffer buff, int oldValue, int newValue) { 495 | buff.writeAll(encodeVlq(newValue - oldValue)); 496 | return newValue; 497 | } 498 | 499 | StateError _segmentError(int seen, int line) => 500 | StateError('Invalid entry in sourcemap, expected 1, 4, or 5' 501 | ' values, but got $seen.\ntargeturl: $targetUrl, line: $line'); 502 | 503 | /// Returns [TargetLineEntry] which includes the location in the target [line] 504 | /// number. In particular, the resulting entry is the last entry whose line 505 | /// number is lower or equal to [line]. 506 | TargetLineEntry? _findLine(int line) { 507 | var index = binarySearch(lines, (e) => e.line > line); 508 | return (index <= 0) ? null : lines[index - 1]; 509 | } 510 | 511 | /// Returns [TargetEntry] which includes the location denoted by 512 | /// [line], [column]. If [lineEntry] corresponds to [line], then this will be 513 | /// the last entry whose column is lower or equal than [column]. If 514 | /// [lineEntry] corresponds to a line prior to [line], then the result will be 515 | /// the very last entry on that line. 516 | TargetEntry? _findColumn(int line, int column, TargetLineEntry? lineEntry) { 517 | if (lineEntry == null || lineEntry.entries.isEmpty) return null; 518 | if (lineEntry.line != line) return lineEntry.entries.last; 519 | var entries = lineEntry.entries; 520 | var index = binarySearch(entries, (e) => e.column > column); 521 | return (index <= 0) ? null : entries[index - 1]; 522 | } 523 | 524 | @override 525 | SourceMapSpan? spanFor(int line, int column, 526 | {Map? files, String? uri}) { 527 | var entry = _findColumn(line, column, _findLine(line)); 528 | if (entry == null) return null; 529 | 530 | var sourceUrlId = entry.sourceUrlId; 531 | if (sourceUrlId == null) return null; 532 | 533 | var url = urls[sourceUrlId]; 534 | if (sourceRoot != null) { 535 | url = '$sourceRoot$url'; 536 | } 537 | 538 | var sourceNameId = entry.sourceNameId; 539 | var file = files?[url]; 540 | if (file != null) { 541 | var start = file.getOffset(entry.sourceLine!, entry.sourceColumn); 542 | if (sourceNameId != null) { 543 | var text = names[sourceNameId]; 544 | return SourceMapFileSpan(file.span(start, start + text.length), 545 | isIdentifier: true); 546 | } else { 547 | return SourceMapFileSpan(file.location(start).pointSpan()); 548 | } 549 | } else { 550 | var start = SourceLocation(0, 551 | sourceUrl: _mapUrl?.resolve(url) ?? url, 552 | line: entry.sourceLine, 553 | column: entry.sourceColumn); 554 | 555 | // Offset and other context is not available. 556 | if (sourceNameId != null) { 557 | return SourceMapSpan.identifier(start, names[sourceNameId]); 558 | } else { 559 | return SourceMapSpan(start, start, ''); 560 | } 561 | } 562 | } 563 | 564 | @override 565 | String toString() { 566 | return (StringBuffer('$runtimeType : [') 567 | ..write('targetUrl: ') 568 | ..write(targetUrl) 569 | ..write(', sourceRoot: ') 570 | ..write(sourceRoot) 571 | ..write(', urls: ') 572 | ..write(urls) 573 | ..write(', names: ') 574 | ..write(names) 575 | ..write(', lines: ') 576 | ..write(lines) 577 | ..write(']')) 578 | .toString(); 579 | } 580 | 581 | String get debugString { 582 | var buff = StringBuffer(); 583 | for (var lineEntry in lines) { 584 | var line = lineEntry.line; 585 | for (var entry in lineEntry.entries) { 586 | buff 587 | ..write(targetUrl) 588 | ..write(': ') 589 | ..write(line) 590 | ..write(':') 591 | ..write(entry.column); 592 | var sourceUrlId = entry.sourceUrlId; 593 | if (sourceUrlId != null) { 594 | buff 595 | ..write(' --> ') 596 | ..write(sourceRoot) 597 | ..write(urls[sourceUrlId]) 598 | ..write(': ') 599 | ..write(entry.sourceLine) 600 | ..write(':') 601 | ..write(entry.sourceColumn); 602 | } 603 | var sourceNameId = entry.sourceNameId; 604 | if (sourceNameId != null) { 605 | buff 606 | ..write(' (') 607 | ..write(names[sourceNameId]) 608 | ..write(')'); 609 | } 610 | buff.write('\n'); 611 | } 612 | } 613 | return buff.toString(); 614 | } 615 | } 616 | 617 | /// A line entry read from a source map. 618 | class TargetLineEntry { 619 | final int line; 620 | List entries; 621 | TargetLineEntry(this.line, this.entries); 622 | 623 | @override 624 | String toString() => '$runtimeType: $line $entries'; 625 | } 626 | 627 | /// A target segment entry read from a source map 628 | class TargetEntry { 629 | final int column; 630 | final int? sourceUrlId; 631 | final int? sourceLine; 632 | final int? sourceColumn; 633 | final int? sourceNameId; 634 | 635 | TargetEntry(this.column, 636 | [this.sourceUrlId, 637 | this.sourceLine, 638 | this.sourceColumn, 639 | this.sourceNameId]); 640 | 641 | @override 642 | String toString() => '$runtimeType: ' 643 | '($column, $sourceUrlId, $sourceLine, $sourceColumn, $sourceNameId)'; 644 | } 645 | 646 | /// A character iterator over a string that can peek one character ahead. 647 | class _MappingTokenizer implements Iterator { 648 | final String _internal; 649 | final int _length; 650 | int index = -1; 651 | _MappingTokenizer(String internal) 652 | : _internal = internal, 653 | _length = internal.length; 654 | 655 | // Iterator API is used by decodeVlq to consume VLQ entries. 656 | @override 657 | bool moveNext() => ++index < _length; 658 | 659 | @override 660 | String get current => (index >= 0 && index < _length) 661 | ? _internal[index] 662 | : throw RangeError.index(index, _internal); 663 | 664 | bool get hasTokens => index < _length - 1 && _length > 0; 665 | 666 | _TokenKind get nextKind { 667 | if (!hasTokens) return _TokenKind.eof; 668 | var next = _internal[index + 1]; 669 | if (next == ';') return _TokenKind.line; 670 | if (next == ',') return _TokenKind.segment; 671 | return _TokenKind.value; 672 | } 673 | 674 | int _consumeValue() => decodeVlq(this); 675 | void _consumeNewLine() { 676 | ++index; 677 | } 678 | 679 | void _consumeNewSegment() { 680 | ++index; 681 | } 682 | 683 | // Print the state of the iterator, with colors indicating the current 684 | // position. 685 | @override 686 | String toString() { 687 | var buff = StringBuffer(); 688 | for (var i = 0; i < index; i++) { 689 | buff.write(_internal[i]); 690 | } 691 | buff.write(''); 692 | try { 693 | buff.write(current); 694 | // TODO: Determine whether this try / catch can be removed. 695 | // ignore: avoid_catching_errors 696 | } on RangeError catch (_) {} 697 | buff.write(''); 698 | for (var i = index + 1; i < _internal.length; i++) { 699 | buff.write(_internal[i]); 700 | } 701 | buff.write(' ($index)'); 702 | return buff.toString(); 703 | } 704 | } 705 | 706 | class _TokenKind { 707 | static const _TokenKind line = _TokenKind(isNewLine: true); 708 | static const _TokenKind segment = _TokenKind(isNewSegment: true); 709 | static const _TokenKind eof = _TokenKind(isEof: true); 710 | static const _TokenKind value = _TokenKind(); 711 | final bool isNewLine; 712 | final bool isNewSegment; 713 | final bool isEof; 714 | bool get isValue => !isNewLine && !isNewSegment && !isEof; 715 | 716 | const _TokenKind( 717 | {this.isNewLine = false, this.isNewSegment = false, this.isEof = false}); 718 | } 719 | -------------------------------------------------------------------------------- /lib/printer.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Contains a code printer that generates code by recording the source maps. 6 | library source_maps.printer; 7 | 8 | import 'package:source_span/source_span.dart'; 9 | 10 | import 'builder.dart'; 11 | import 'src/source_map_span.dart'; 12 | import 'src/utils.dart'; 13 | 14 | /// A simple printer that keeps track of offset locations and records source 15 | /// maps locations. 16 | class Printer { 17 | final String filename; 18 | final StringBuffer _buff = StringBuffer(); 19 | final SourceMapBuilder _maps = SourceMapBuilder(); 20 | String get text => _buff.toString(); 21 | String get map => _maps.toJson(filename); 22 | 23 | /// Current source location mapping. 24 | SourceLocation? _loc; 25 | 26 | /// Current line in the buffer; 27 | int _line = 0; 28 | 29 | /// Current column in the buffer. 30 | int _column = 0; 31 | 32 | Printer(this.filename); 33 | 34 | /// Add [str] contents to the output, tracking new lines to track correct 35 | /// positions for span locations. When [projectMarks] is true, this method 36 | /// adds a source map location on each new line, projecting that every new 37 | /// line in the target file (printed here) corresponds to a new line in the 38 | /// source file. 39 | void add(String str, {bool projectMarks = false}) { 40 | var chars = str.runes.toList(); 41 | var length = chars.length; 42 | for (var i = 0; i < length; i++) { 43 | var c = chars[i]; 44 | if (c == lineFeed || 45 | (c == carriageReturn && 46 | (i + 1 == length || chars[i + 1] != lineFeed))) { 47 | // Return not followed by line-feed is treated as a new line. 48 | _line++; 49 | _column = 0; 50 | { 51 | // **Warning**: Any calls to `mark` will change the value of `_loc`, 52 | // so this local variable is no longer up to date after that point. 53 | // 54 | // This is why it has been put inside its own block to limit the 55 | // scope in which it is available. 56 | var loc = _loc; 57 | if (projectMarks && loc != null) { 58 | if (loc is FileLocation) { 59 | var file = loc.file; 60 | mark(file.location(file.getOffset(loc.line + 1))); 61 | } else { 62 | mark(SourceLocation(0, 63 | sourceUrl: loc.sourceUrl, line: loc.line + 1, column: 0)); 64 | } 65 | } 66 | } 67 | } else { 68 | _column++; 69 | } 70 | } 71 | _buff.write(str); 72 | } 73 | 74 | /// Append a [total] number of spaces in the target file. Typically used for 75 | /// formatting indentation. 76 | void addSpaces(int total) { 77 | for (var i = 0; i < total; i++) { 78 | _buff.write(' '); 79 | } 80 | _column += total; 81 | } 82 | 83 | /// Marks that the current point in the target file corresponds to the [mark] 84 | /// in the source file, which can be either a [SourceLocation] or a 85 | /// [SourceSpan]. When the mark is a [SourceMapSpan] with `isIdentifier` set, 86 | /// this also records the name of the identifier in the source map 87 | /// information. 88 | void mark(Object mark) { 89 | late final SourceLocation loc; 90 | String? identifier; 91 | if (mark is SourceLocation) { 92 | loc = mark; 93 | } else if (mark is SourceSpan) { 94 | loc = mark.start; 95 | if (mark is SourceMapSpan && mark.isIdentifier) identifier = mark.text; 96 | } 97 | _maps.addLocation(loc, 98 | SourceLocation(_buff.length, line: _line, column: _column), identifier); 99 | _loc = loc; 100 | } 101 | } 102 | 103 | /// A more advanced printer that keeps track of offset locations to record 104 | /// source maps, but additionally allows nesting of different kind of items, 105 | /// including [NestedPrinter]s, and it let's you automatically indent text. 106 | /// 107 | /// This class is especially useful when doing code generation, where different 108 | /// pieces of the code are generated independently on separate printers, and are 109 | /// finally put together in the end. 110 | class NestedPrinter implements NestedItem { 111 | /// Items recoded by this printer, which can be [String] literals, 112 | /// [NestedItem]s, and source map information like [SourceLocation] and 113 | /// [SourceSpan]. 114 | final List _items = []; 115 | 116 | /// Internal buffer to merge consecutive strings added to this printer. 117 | StringBuffer? _buff; 118 | 119 | /// Current indentation, which can be updated from outside this class. 120 | int indent; 121 | 122 | /// [Printer] used during the last call to [build], if any. 123 | Printer? printer; 124 | 125 | /// Returns the text produced after calling [build]. 126 | String? get text => printer?.text; 127 | 128 | /// Returns the source-map information produced after calling [build]. 129 | String? get map => printer?.map; 130 | 131 | /// Item used to indicate that the following item is copied from the original 132 | /// source code, and hence we should preserve source-maps on every new line. 133 | static final _original = Object(); 134 | 135 | NestedPrinter([this.indent = 0]); 136 | 137 | /// Adds [object] to this printer. [object] can be a [String], 138 | /// [NestedPrinter], or anything implementing [NestedItem]. If [object] is a 139 | /// [String], the value is appended directly, without doing any formatting 140 | /// changes. If you wish to add a line of code with automatic indentation, use 141 | /// [addLine] instead. [NestedPrinter]s and [NestedItem]s are not processed 142 | /// until [build] gets called later on. We ensure that [build] emits every 143 | /// object in the order that they were added to this printer. 144 | /// 145 | /// The [location] and [span] parameters indicate the corresponding source map 146 | /// location of [object] in the original input. Only one, [location] or 147 | /// [span], should be provided at a time. 148 | /// 149 | /// Indicate [isOriginal] when [object] is copied directly from the user code. 150 | /// Setting [isOriginal] will make this printer propagate source map locations 151 | /// on every line-break. 152 | void add(Object object, 153 | {SourceLocation? location, SourceSpan? span, bool isOriginal = false}) { 154 | if (object is! String || location != null || span != null || isOriginal) { 155 | _flush(); 156 | assert(location == null || span == null); 157 | if (location != null) _items.add(location); 158 | if (span != null) _items.add(span); 159 | if (isOriginal) _items.add(_original); 160 | } 161 | 162 | if (object is String) { 163 | _appendString(object); 164 | } else { 165 | _items.add(object); 166 | } 167 | } 168 | 169 | /// Append `2 * indent` spaces to this printer. 170 | void insertIndent() => _indent(indent); 171 | 172 | /// Add a [line], autoindenting to the current value of [indent]. Note, 173 | /// indentation is not inferred from the contents added to this printer. If a 174 | /// line starts or ends an indentation block, you need to also update [indent] 175 | /// accordingly. Also, indentation is not adapted for nested printers. If 176 | /// you add a [NestedPrinter] to this printer, its indentation is set 177 | /// separately and will not include any the indentation set here. 178 | /// 179 | /// The [location] and [span] parameters indicate the corresponding source map 180 | /// location of [line] in the original input. Only one, [location] or 181 | /// [span], should be provided at a time. 182 | void addLine(String? line, {SourceLocation? location, SourceSpan? span}) { 183 | if (location != null || span != null) { 184 | _flush(); 185 | assert(location == null || span == null); 186 | if (location != null) _items.add(location); 187 | if (span != null) _items.add(span); 188 | } 189 | if (line == null) return; 190 | if (line != '') { 191 | // We don't indent empty lines. 192 | _indent(indent); 193 | _appendString(line); 194 | } 195 | _appendString('\n'); 196 | } 197 | 198 | /// Appends a string merging it with any previous strings, if possible. 199 | void _appendString(String s) { 200 | var buf = _buff ??= StringBuffer(); 201 | buf.write(s); 202 | } 203 | 204 | /// Adds all of the current [_buff] contents as a string item. 205 | void _flush() { 206 | if (_buff != null) { 207 | _items.add(_buff.toString()); 208 | _buff = null; 209 | } 210 | } 211 | 212 | void _indent(int indent) { 213 | for (var i = 0; i < indent; i++) { 214 | _appendString(' '); 215 | } 216 | } 217 | 218 | /// Returns a string representation of all the contents appended to this 219 | /// printer, including source map location tokens. 220 | @override 221 | String toString() { 222 | _flush(); 223 | return (StringBuffer()..writeAll(_items)).toString(); 224 | } 225 | 226 | /// Builds the output of this printer and source map information. After 227 | /// calling this function, you can use [text] and [map] to retrieve the 228 | /// geenrated code and source map information, respectively. 229 | void build(String filename) { 230 | writeTo(printer = Printer(filename)); 231 | } 232 | 233 | /// Implements the [NestedItem] interface. 234 | @override 235 | void writeTo(Printer printer) { 236 | _flush(); 237 | var propagate = false; 238 | for (var item in _items) { 239 | if (item is NestedItem) { 240 | item.writeTo(printer); 241 | } else if (item is String) { 242 | printer.add(item, projectMarks: propagate); 243 | propagate = false; 244 | } else if (item is SourceLocation || item is SourceSpan) { 245 | printer.mark(item); 246 | } else if (item == _original) { 247 | // we insert booleans when we are about to quote text that was copied 248 | // from the original source. In such case, we will propagate marks on 249 | // every new-line. 250 | propagate = true; 251 | } else { 252 | throw UnsupportedError('Unknown item type: $item'); 253 | } 254 | } 255 | } 256 | } 257 | 258 | /// An item added to a [NestedPrinter]. 259 | abstract class NestedItem { 260 | /// Write the contents of this item into [printer]. 261 | void writeTo(Printer printer); 262 | } 263 | -------------------------------------------------------------------------------- /lib/refactor.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Tools to help implement refactoring like transformations to Dart code. 6 | /// 7 | /// [TextEditTransaction] supports making a series of changes to a text buffer. 8 | /// [guessIndent] helps to guess the appropriate indentiation for the new code. 9 | library source_maps.refactor; 10 | 11 | import 'package:source_span/source_span.dart'; 12 | 13 | import 'printer.dart'; 14 | import 'src/utils.dart'; 15 | 16 | /// Editable text transaction. 17 | /// 18 | /// Applies a series of edits using original location 19 | /// information, and composes them into the edited string. 20 | class TextEditTransaction { 21 | final SourceFile? file; 22 | final String original; 23 | final _edits = <_TextEdit>[]; 24 | 25 | /// Creates a new transaction. 26 | TextEditTransaction(this.original, this.file); 27 | 28 | bool get hasEdits => _edits.isNotEmpty; 29 | 30 | /// Edit the original text, replacing text on the range [begin] and [end] 31 | /// with the [replacement]. [replacement] can be either a string or a 32 | /// [NestedPrinter]. 33 | void edit(int begin, int end, Object replacement) { 34 | _edits.add(_TextEdit(begin, end, replacement)); 35 | } 36 | 37 | /// Create a source map [SourceLocation] for [offset], if [file] is not 38 | /// `null`. 39 | SourceLocation? _loc(int offset) => file?.location(offset); 40 | 41 | /// Applies all pending [edit]s and returns a [NestedPrinter] containing the 42 | /// rewritten string and source map information. [file]`.location` is given to 43 | /// the underlying printer to indicate the name of the generated file that 44 | /// will contains the source map information. 45 | /// 46 | /// Throws [UnsupportedError] if the edits were overlapping. If no edits were 47 | /// made, the printer simply contains the original string. 48 | NestedPrinter commit() { 49 | var printer = NestedPrinter(); 50 | if (_edits.isEmpty) { 51 | return printer..add(original, location: _loc(0), isOriginal: true); 52 | } 53 | 54 | // Sort edits by start location. 55 | _edits.sort(); 56 | 57 | var consumed = 0; 58 | for (var edit in _edits) { 59 | if (consumed > edit.begin) { 60 | var sb = StringBuffer(); 61 | sb 62 | ..write(file?.location(edit.begin).toolString) 63 | ..write(': overlapping edits. Insert at offset ') 64 | ..write(edit.begin) 65 | ..write(' but have consumed ') 66 | ..write(consumed) 67 | ..write(' input characters. List of edits:'); 68 | for (var e in _edits) { 69 | sb 70 | ..write('\n ') 71 | ..write(e); 72 | } 73 | throw UnsupportedError(sb.toString()); 74 | } 75 | 76 | // Add characters from the original string between this edit and the last 77 | // one, if any. 78 | var betweenEdits = original.substring(consumed, edit.begin); 79 | printer 80 | ..add(betweenEdits, location: _loc(consumed), isOriginal: true) 81 | ..add(edit.replace, location: _loc(edit.begin)); 82 | consumed = edit.end; 83 | } 84 | 85 | // Add any text from the end of the original string that was not replaced. 86 | printer.add(original.substring(consumed), 87 | location: _loc(consumed), isOriginal: true); 88 | return printer; 89 | } 90 | } 91 | 92 | class _TextEdit implements Comparable<_TextEdit> { 93 | final int begin; 94 | final int end; 95 | 96 | /// The replacement used by the edit, can be a string or a [NestedPrinter]. 97 | final Object replace; 98 | 99 | _TextEdit(this.begin, this.end, this.replace); 100 | 101 | int get length => end - begin; 102 | 103 | @override 104 | String toString() => '(Edit @ $begin,$end: "$replace")'; 105 | 106 | @override 107 | int compareTo(_TextEdit other) { 108 | var diff = begin - other.begin; 109 | if (diff != 0) return diff; 110 | return end - other.end; 111 | } 112 | } 113 | 114 | /// Returns all whitespace characters at the start of [charOffset]'s line. 115 | String guessIndent(String code, int charOffset) { 116 | // Find the beginning of the line 117 | var lineStart = 0; 118 | for (var i = charOffset - 1; i >= 0; i--) { 119 | var c = code.codeUnitAt(i); 120 | if (c == lineFeed || c == carriageReturn) { 121 | lineStart = i + 1; 122 | break; 123 | } 124 | } 125 | 126 | // Grab all the whitespace 127 | var whitespaceEnd = code.length; 128 | for (var i = lineStart; i < code.length; i++) { 129 | var c = code.codeUnitAt(i); 130 | if (c != _space && c != _tab) { 131 | whitespaceEnd = i; 132 | break; 133 | } 134 | } 135 | 136 | return code.substring(lineStart, whitespaceEnd); 137 | } 138 | 139 | const int _tab = 9; 140 | const int _space = 32; 141 | -------------------------------------------------------------------------------- /lib/source_maps.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Library to create and parse source maps. 6 | /// 7 | /// Create a source map using [SourceMapBuilder]. For example: 8 | /// 9 | /// ```dart 10 | /// var json = (new SourceMapBuilder() 11 | /// ..add(inputSpan1, outputSpan1) 12 | /// ..add(inputSpan2, outputSpan2) 13 | /// ..add(inputSpan3, outputSpan3) 14 | /// .toJson(outputFile); 15 | /// ``` 16 | /// 17 | /// Use the source_span package's [SourceSpan] and [SourceFile] classes to 18 | /// specify span locations. 19 | /// 20 | /// Parse a source map using [parse], and call `spanFor` on the returned mapping 21 | /// object. For example: 22 | /// 23 | /// ```dart 24 | /// var mapping = parse(json); 25 | /// mapping.spanFor(outputSpan1.line, outputSpan1.column) 26 | /// ``` 27 | library source_maps; 28 | 29 | import 'package:source_span/source_span.dart'; 30 | 31 | import 'builder.dart'; 32 | import 'parser.dart'; 33 | 34 | export 'builder.dart'; 35 | export 'parser.dart'; 36 | export 'printer.dart'; 37 | export 'refactor.dart'; 38 | export 'src/source_map_span.dart'; 39 | -------------------------------------------------------------------------------- /lib/src/source_map_span.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:source_span/source_span.dart'; 6 | 7 | /// A [SourceSpan] for spans coming from or being written to source maps. 8 | /// 9 | /// These spans have an extra piece of metadata: whether or not they represent 10 | /// an identifier (see [isIdentifier]). 11 | class SourceMapSpan extends SourceSpanBase { 12 | /// Whether this span represents an identifier. 13 | /// 14 | /// If this is `true`, [text] is the value of the identifier. 15 | final bool isIdentifier; 16 | 17 | SourceMapSpan(super.start, super.end, super.text, 18 | {this.isIdentifier = false}); 19 | 20 | /// Creates a [SourceMapSpan] for an identifier with value [text] starting at 21 | /// [start]. 22 | /// 23 | /// The [end] location is determined by adding [text] to [start]. 24 | SourceMapSpan.identifier(SourceLocation start, String text) 25 | : this( 26 | start, 27 | SourceLocation(start.offset + text.length, 28 | sourceUrl: start.sourceUrl, 29 | line: start.line, 30 | column: start.column + text.length), 31 | text, 32 | isIdentifier: true); 33 | } 34 | 35 | /// A wrapper aruond a [FileSpan] that implements [SourceMapSpan]. 36 | class SourceMapFileSpan implements SourceMapSpan, FileSpan { 37 | final FileSpan _inner; 38 | @override 39 | final bool isIdentifier; 40 | 41 | @override 42 | SourceFile get file => _inner.file; 43 | @override 44 | FileLocation get start => _inner.start; 45 | @override 46 | FileLocation get end => _inner.end; 47 | @override 48 | String get text => _inner.text; 49 | @override 50 | String get context => _inner.context; 51 | @override 52 | Uri? get sourceUrl => _inner.sourceUrl; 53 | @override 54 | int get length => _inner.length; 55 | 56 | SourceMapFileSpan(this._inner, {this.isIdentifier = false}); 57 | 58 | @override 59 | int compareTo(SourceSpan other) => _inner.compareTo(other); 60 | @override 61 | String highlight({Object? color}) => _inner.highlight(color: color); 62 | @override 63 | SourceSpan union(SourceSpan other) => _inner.union(other); 64 | @override 65 | FileSpan expand(FileSpan other) => _inner.expand(other); 66 | @override 67 | String message(String message, {Object? color}) => 68 | _inner.message(message, color: color); 69 | @override 70 | String toString() => 71 | _inner.toString().replaceAll('FileSpan', 'SourceMapFileSpan'); 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Utilities that shouldn't be in this package. 6 | library source_maps.utils; 7 | 8 | /// Find the first entry in a sorted [list] that matches a monotonic predicate. 9 | /// Given a result `n`, that all items before `n` will not match, `n` matches, 10 | /// and all items after `n` match too. The result is -1 when there are no 11 | /// items, 0 when all items match, and list.length when none does. 12 | // TODO(sigmund): remove this function after dartbug.com/5624 is fixed. 13 | int binarySearch(List list, bool Function(T) matches) { 14 | if (list.isEmpty) return -1; 15 | if (matches(list.first)) return 0; 16 | if (!matches(list.last)) return list.length; 17 | 18 | var min = 0; 19 | var max = list.length - 1; 20 | while (min < max) { 21 | var half = min + ((max - min) ~/ 2); 22 | if (matches(list[half])) { 23 | max = half; 24 | } else { 25 | min = half + 1; 26 | } 27 | } 28 | return max; 29 | } 30 | 31 | const int lineFeed = 10; 32 | const int carriageReturn = 13; 33 | -------------------------------------------------------------------------------- /lib/src/vlq.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Utilities to encode and decode VLQ values used in source maps. 6 | /// 7 | /// Sourcemaps are encoded with variable length numbers as base64 encoded 8 | /// strings with the least significant digit coming first. Each base64 digit 9 | /// encodes a 5-bit value (0-31) and a continuation bit. Signed values can be 10 | /// represented by using the least significant bit of the value as the sign bit. 11 | /// 12 | /// For more details see the source map [version 3 documentation](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?usp=sharing). 13 | library source_maps.src.vlq; 14 | 15 | import 'dart:math'; 16 | 17 | const int vlqBaseShift = 5; 18 | 19 | const int vlqBaseMask = (1 << 5) - 1; 20 | 21 | const int vlqContinuationBit = 1 << 5; 22 | 23 | const int vlqContinuationMask = 1 << 5; 24 | 25 | const String base64Digits = 26 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 27 | 28 | final Map _digits = () { 29 | var map = {}; 30 | for (var i = 0; i < 64; i++) { 31 | map[base64Digits[i]] = i; 32 | } 33 | return map; 34 | }(); 35 | 36 | final int maxInt32 = (pow(2, 31) as int) - 1; 37 | final int minInt32 = -(pow(2, 31) as int); 38 | 39 | /// Creates the VLQ encoding of [value] as a sequence of characters 40 | Iterable encodeVlq(int value) { 41 | if (value < minInt32 || value > maxInt32) { 42 | throw ArgumentError('expected 32 bit int, got: $value'); 43 | } 44 | var res = []; 45 | var signBit = 0; 46 | if (value < 0) { 47 | signBit = 1; 48 | value = -value; 49 | } 50 | value = (value << 1) | signBit; 51 | do { 52 | var digit = value & vlqBaseMask; 53 | value >>= vlqBaseShift; 54 | if (value > 0) { 55 | digit |= vlqContinuationBit; 56 | } 57 | res.add(base64Digits[digit]); 58 | } while (value > 0); 59 | return res; 60 | } 61 | 62 | /// Decodes a value written as a sequence of VLQ characters. The first input 63 | /// character will be `chars.current` after calling `chars.moveNext` once. The 64 | /// iterator is advanced until a stop character is found (a character without 65 | /// the [vlqContinuationBit]). 66 | int decodeVlq(Iterator chars) { 67 | var result = 0; 68 | var stop = false; 69 | var shift = 0; 70 | while (!stop) { 71 | if (!chars.moveNext()) throw StateError('incomplete VLQ value'); 72 | var char = chars.current; 73 | var digit = _digits[char]; 74 | if (digit == null) { 75 | throw FormatException('invalid character in VLQ encoding: $char'); 76 | } 77 | stop = (digit & vlqContinuationBit) == 0; 78 | digit &= vlqBaseMask; 79 | result += digit << shift; 80 | shift += vlqBaseShift; 81 | } 82 | 83 | // Result uses the least significant bit as a sign bit. We convert it into a 84 | // two-complement value. For example, 85 | // 2 (10 binary) becomes 1 86 | // 3 (11 binary) becomes -1 87 | // 4 (100 binary) becomes 2 88 | // 5 (101 binary) becomes -2 89 | // 6 (110 binary) becomes 3 90 | // 7 (111 binary) becomes -3 91 | var negate = (result & 1) == 1; 92 | result = result >> 1; 93 | result = negate ? -result : result; 94 | 95 | // TODO(sigmund): can we detect this earlier? 96 | if (result < minInt32 || result > maxInt32) { 97 | throw FormatException( 98 | 'expected an encoded 32 bit int, but we got: $result'); 99 | } 100 | return result; 101 | } 102 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: source_maps 2 | version: 0.10.13-wip 3 | description: A library to programmatically manipulate source map files. 4 | repository: https://github.com/dart-lang/source_maps 5 | 6 | environment: 7 | sdk: ^3.3.0 8 | 9 | dependencies: 10 | source_span: ^1.8.0 11 | 12 | dev_dependencies: 13 | dart_flutter_team_lints: ^2.0.0 14 | term_glyph: ^1.2.0 15 | test: ^1.16.0 16 | -------------------------------------------------------------------------------- /test/builder_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | 7 | import 'package:source_maps/source_maps.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | import 'common.dart'; 11 | 12 | void main() { 13 | test('builder - with span', () { 14 | var map = (SourceMapBuilder() 15 | ..addSpan(inputVar1, outputVar1) 16 | ..addSpan(inputFunction, outputFunction) 17 | ..addSpan(inputVar2, outputVar2) 18 | ..addSpan(inputExpr, outputExpr)) 19 | .build(output.url.toString()); 20 | expect(map, equals(expectedMap)); 21 | }); 22 | 23 | test('builder - with location', () { 24 | var str = (SourceMapBuilder() 25 | ..addLocation(inputVar1.start, outputVar1.start, 'longVar1') 26 | ..addLocation(inputFunction.start, outputFunction.start, 'longName') 27 | ..addLocation(inputVar2.start, outputVar2.start, 'longVar2') 28 | ..addLocation(inputExpr.start, outputExpr.start, null)) 29 | .toJson(output.url.toString()); 30 | expect(str, jsonEncode(expectedMap)); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/common.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Common input/output used by builder, parser and end2end tests 6 | library test.common; 7 | 8 | import 'package:source_maps/source_maps.dart'; 9 | import 'package:source_span/source_span.dart'; 10 | import 'package:test/test.dart'; 11 | 12 | /// Content of the source file 13 | const String inputContent = ''' 14 | /** this is a comment. */ 15 | int longVar1 = 3; 16 | 17 | // this is a comment too 18 | int longName(int longVar2) { 19 | return longVar1 + longVar2; 20 | } 21 | '''; 22 | final input = SourceFile.fromString(inputContent, url: 'input.dart'); 23 | 24 | /// A span in the input file 25 | SourceMapSpan ispan(int start, int end, [bool isIdentifier = false]) => 26 | SourceMapFileSpan(input.span(start, end), isIdentifier: isIdentifier); 27 | 28 | SourceMapSpan inputVar1 = ispan(30, 38, true); 29 | SourceMapSpan inputFunction = ispan(74, 82, true); 30 | SourceMapSpan inputVar2 = ispan(87, 95, true); 31 | 32 | SourceMapSpan inputVar1NoSymbol = ispan(30, 38); 33 | SourceMapSpan inputFunctionNoSymbol = ispan(74, 82); 34 | SourceMapSpan inputVar2NoSymbol = ispan(87, 95); 35 | 36 | SourceMapSpan inputExpr = ispan(108, 127); 37 | 38 | /// Content of the target file 39 | const String outputContent = ''' 40 | var x = 3; 41 | f(y) => x + y; 42 | '''; 43 | final output = SourceFile.fromString(outputContent, url: 'output.dart'); 44 | 45 | /// A span in the output file 46 | SourceMapSpan ospan(int start, int end, [bool isIdentifier = false]) => 47 | SourceMapFileSpan(output.span(start, end), isIdentifier: isIdentifier); 48 | 49 | SourceMapSpan outputVar1 = ospan(4, 5, true); 50 | SourceMapSpan outputFunction = ospan(11, 12, true); 51 | SourceMapSpan outputVar2 = ospan(13, 14, true); 52 | SourceMapSpan outputVar1NoSymbol = ospan(4, 5); 53 | SourceMapSpan outputFunctionNoSymbol = ospan(11, 12); 54 | SourceMapSpan outputVar2NoSymbol = ospan(13, 14); 55 | SourceMapSpan outputExpr = ospan(19, 24); 56 | 57 | /// Expected output mapping when recording the following four mappings: 58 | /// inputVar1 <= outputVar1 59 | /// inputFunction <= outputFunction 60 | /// inputVar2 <= outputVar2 61 | /// inputExpr <= outputExpr 62 | /// 63 | /// This mapping is stored in the tests so we can independently test the builder 64 | /// and parser algorithms without relying entirely on end2end tests. 65 | const Map expectedMap = { 66 | 'version': 3, 67 | 'sourceRoot': '', 68 | 'sources': ['input.dart'], 69 | 'names': ['longVar1', 'longName', 'longVar2'], 70 | 'mappings': 'IACIA;AAGAC,EAAaC,MACR', 71 | 'file': 'output.dart' 72 | }; 73 | 74 | void check(SourceSpan outputSpan, Mapping mapping, SourceMapSpan inputSpan, 75 | bool realOffsets) { 76 | var line = outputSpan.start.line; 77 | var column = outputSpan.start.column; 78 | var files = realOffsets ? {'input.dart': input} : null; 79 | var span = mapping.spanFor(line, column, files: files)!; 80 | var span2 = mapping.spanForLocation(outputSpan.start, files: files)!; 81 | 82 | // Both mapping APIs are equivalent. 83 | expect(span.start.offset, span2.start.offset); 84 | expect(span.start.line, span2.start.line); 85 | expect(span.start.column, span2.start.column); 86 | expect(span.end.offset, span2.end.offset); 87 | expect(span.end.line, span2.end.line); 88 | expect(span.end.column, span2.end.column); 89 | 90 | // Mapping matches our input location (modulo using real offsets) 91 | expect(span.start.line, inputSpan.start.line); 92 | expect(span.start.column, inputSpan.start.column); 93 | expect(span.sourceUrl, inputSpan.sourceUrl); 94 | expect(span.start.offset, realOffsets ? inputSpan.start.offset : 0); 95 | 96 | // Mapping includes the identifier, if any 97 | if (inputSpan.isIdentifier) { 98 | expect(span.end.line, inputSpan.end.line); 99 | expect(span.end.column, inputSpan.end.column); 100 | expect(span.end.offset, span.start.offset + inputSpan.text.length); 101 | if (realOffsets) expect(span.end.offset, inputSpan.end.offset); 102 | } else { 103 | expect(span.end.offset, span.start.offset); 104 | expect(span.end.line, span.start.line); 105 | expect(span.end.column, span.start.column); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/end2end_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:source_maps/source_maps.dart'; 6 | import 'package:source_span/source_span.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | import 'common.dart'; 10 | 11 | void main() { 12 | test('end-to-end setup', () { 13 | expect(inputVar1.text, 'longVar1'); 14 | expect(inputFunction.text, 'longName'); 15 | expect(inputVar2.text, 'longVar2'); 16 | expect(inputVar1NoSymbol.text, 'longVar1'); 17 | expect(inputFunctionNoSymbol.text, 'longName'); 18 | expect(inputVar2NoSymbol.text, 'longVar2'); 19 | expect(inputExpr.text, 'longVar1 + longVar2'); 20 | 21 | expect(outputVar1.text, 'x'); 22 | expect(outputFunction.text, 'f'); 23 | expect(outputVar2.text, 'y'); 24 | expect(outputVar1NoSymbol.text, 'x'); 25 | expect(outputFunctionNoSymbol.text, 'f'); 26 | expect(outputVar2NoSymbol.text, 'y'); 27 | expect(outputExpr.text, 'x + y'); 28 | }); 29 | 30 | test('build + parse', () { 31 | var map = (SourceMapBuilder() 32 | ..addSpan(inputVar1, outputVar1) 33 | ..addSpan(inputFunction, outputFunction) 34 | ..addSpan(inputVar2, outputVar2) 35 | ..addSpan(inputExpr, outputExpr)) 36 | .build(output.url.toString()); 37 | var mapping = parseJson(map); 38 | check(outputVar1, mapping, inputVar1, false); 39 | check(outputVar2, mapping, inputVar2, false); 40 | check(outputFunction, mapping, inputFunction, false); 41 | check(outputExpr, mapping, inputExpr, false); 42 | }); 43 | 44 | test('build + parse - no symbols', () { 45 | var map = (SourceMapBuilder() 46 | ..addSpan(inputVar1NoSymbol, outputVar1NoSymbol) 47 | ..addSpan(inputFunctionNoSymbol, outputFunctionNoSymbol) 48 | ..addSpan(inputVar2NoSymbol, outputVar2NoSymbol) 49 | ..addSpan(inputExpr, outputExpr)) 50 | .build(output.url.toString()); 51 | var mapping = parseJson(map); 52 | check(outputVar1NoSymbol, mapping, inputVar1NoSymbol, false); 53 | check(outputVar2NoSymbol, mapping, inputVar2NoSymbol, false); 54 | check(outputFunctionNoSymbol, mapping, inputFunctionNoSymbol, false); 55 | check(outputExpr, mapping, inputExpr, false); 56 | }); 57 | 58 | test('build + parse, repeated entries', () { 59 | var map = (SourceMapBuilder() 60 | ..addSpan(inputVar1, outputVar1) 61 | ..addSpan(inputVar1, outputVar1) 62 | ..addSpan(inputFunction, outputFunction) 63 | ..addSpan(inputFunction, outputFunction) 64 | ..addSpan(inputVar2, outputVar2) 65 | ..addSpan(inputVar2, outputVar2) 66 | ..addSpan(inputExpr, outputExpr) 67 | ..addSpan(inputExpr, outputExpr)) 68 | .build(output.url.toString()); 69 | var mapping = parseJson(map); 70 | check(outputVar1, mapping, inputVar1, false); 71 | check(outputVar2, mapping, inputVar2, false); 72 | check(outputFunction, mapping, inputFunction, false); 73 | check(outputExpr, mapping, inputExpr, false); 74 | }); 75 | 76 | test('build + parse - no symbols, repeated entries', () { 77 | var map = (SourceMapBuilder() 78 | ..addSpan(inputVar1NoSymbol, outputVar1NoSymbol) 79 | ..addSpan(inputVar1NoSymbol, outputVar1NoSymbol) 80 | ..addSpan(inputFunctionNoSymbol, outputFunctionNoSymbol) 81 | ..addSpan(inputFunctionNoSymbol, outputFunctionNoSymbol) 82 | ..addSpan(inputVar2NoSymbol, outputVar2NoSymbol) 83 | ..addSpan(inputVar2NoSymbol, outputVar2NoSymbol) 84 | ..addSpan(inputExpr, outputExpr)) 85 | .build(output.url.toString()); 86 | var mapping = parseJson(map); 87 | check(outputVar1NoSymbol, mapping, inputVar1NoSymbol, false); 88 | check(outputVar2NoSymbol, mapping, inputVar2NoSymbol, false); 89 | check(outputFunctionNoSymbol, mapping, inputFunctionNoSymbol, false); 90 | check(outputExpr, mapping, inputExpr, false); 91 | }); 92 | 93 | test('build + parse with file', () { 94 | var json = (SourceMapBuilder() 95 | ..addSpan(inputVar1, outputVar1) 96 | ..addSpan(inputFunction, outputFunction) 97 | ..addSpan(inputVar2, outputVar2) 98 | ..addSpan(inputExpr, outputExpr)) 99 | .toJson(output.url.toString()); 100 | var mapping = parse(json); 101 | check(outputVar1, mapping, inputVar1, true); 102 | check(outputVar2, mapping, inputVar2, true); 103 | check(outputFunction, mapping, inputFunction, true); 104 | check(outputExpr, mapping, inputExpr, true); 105 | }); 106 | 107 | test('printer projecting marks + parse', () { 108 | var out = inputContent.replaceAll('long', '_s'); 109 | var file = SourceFile.fromString(out, url: 'output2.dart'); 110 | var printer = Printer('output2.dart'); 111 | printer.mark(ispan(0, 0)); 112 | 113 | var segments = inputContent.split('long'); 114 | expect(segments.length, 6); 115 | printer.add(segments[0], projectMarks: true); 116 | printer.mark(inputVar1); 117 | printer.add('_s'); 118 | printer.add(segments[1], projectMarks: true); 119 | printer.mark(inputFunction); 120 | printer.add('_s'); 121 | printer.add(segments[2], projectMarks: true); 122 | printer.mark(inputVar2); 123 | printer.add('_s'); 124 | printer.add(segments[3], projectMarks: true); 125 | printer.mark(inputExpr); 126 | printer.add('_s'); 127 | printer.add(segments[4], projectMarks: true); 128 | printer.add('_s'); 129 | printer.add(segments[5], projectMarks: true); 130 | 131 | expect(printer.text, out); 132 | 133 | var mapping = parse(printer.map); 134 | void checkHelper(SourceMapSpan inputSpan, int adjustment) { 135 | var start = inputSpan.start.offset - adjustment; 136 | var end = (inputSpan.end.offset - adjustment) - 2; 137 | var span = SourceMapFileSpan(file.span(start, end), 138 | isIdentifier: inputSpan.isIdentifier); 139 | check(span, mapping, inputSpan, true); 140 | } 141 | 142 | checkHelper(inputVar1, 0); 143 | checkHelper(inputFunction, 2); 144 | checkHelper(inputVar2, 4); 145 | checkHelper(inputExpr, 6); 146 | 147 | // We projected correctly lines that have no mappings 148 | check(file.span(66, 66), mapping, ispan(45, 45), true); 149 | check(file.span(63, 64), mapping, ispan(45, 45), true); 150 | check(file.span(68, 68), mapping, ispan(70, 70), true); 151 | check(file.span(71, 71), mapping, ispan(70, 70), true); 152 | 153 | // Start of the last line 154 | var oOffset = out.length - 2; 155 | var iOffset = inputContent.length - 2; 156 | check(file.span(oOffset, oOffset), mapping, ispan(iOffset, iOffset), true); 157 | check(file.span(oOffset + 1, oOffset + 1), mapping, ispan(iOffset, iOffset), 158 | true); 159 | }); 160 | } 161 | -------------------------------------------------------------------------------- /test/parser_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | // ignore_for_file: inference_failure_on_collection_literal 6 | // ignore_for_file: inference_failure_on_instance_creation 7 | 8 | import 'dart:convert'; 9 | 10 | import 'package:source_maps/source_maps.dart'; 11 | import 'package:source_span/source_span.dart'; 12 | import 'package:test/test.dart'; 13 | 14 | import 'common.dart'; 15 | 16 | const Map _mapWithNoSourceLocation = { 17 | 'version': 3, 18 | 'sourceRoot': '', 19 | 'sources': ['input.dart'], 20 | 'names': [], 21 | 'mappings': 'A', 22 | 'file': 'output.dart' 23 | }; 24 | 25 | const Map _mapWithSourceLocation = { 26 | 'version': 3, 27 | 'sourceRoot': '', 28 | 'sources': ['input.dart'], 29 | 'names': [], 30 | 'mappings': 'AAAA', 31 | 'file': 'output.dart' 32 | }; 33 | 34 | const Map _mapWithSourceLocationAndMissingNames = { 35 | 'version': 3, 36 | 'sourceRoot': '', 37 | 'sources': ['input.dart'], 38 | 'mappings': 'AAAA', 39 | 'file': 'output.dart' 40 | }; 41 | 42 | const Map _mapWithSourceLocationAndName = { 43 | 'version': 3, 44 | 'sourceRoot': '', 45 | 'sources': ['input.dart'], 46 | 'names': ['var'], 47 | 'mappings': 'AAAAA', 48 | 'file': 'output.dart' 49 | }; 50 | 51 | const Map _mapWithSourceLocationAndName1 = { 52 | 'version': 3, 53 | 'sourceRoot': 'pkg/', 54 | 'sources': ['input1.dart'], 55 | 'names': ['var1'], 56 | 'mappings': 'AAAAA', 57 | 'file': 'output.dart' 58 | }; 59 | 60 | const Map _mapWithSourceLocationAndName2 = { 61 | 'version': 3, 62 | 'sourceRoot': 'pkg/', 63 | 'sources': ['input2.dart'], 64 | 'names': ['var2'], 65 | 'mappings': 'AAAAA', 66 | 'file': 'output2.dart' 67 | }; 68 | 69 | const Map _mapWithSourceLocationAndName3 = { 70 | 'version': 3, 71 | 'sourceRoot': 'pkg/', 72 | 'sources': ['input3.dart'], 73 | 'names': ['var3'], 74 | 'mappings': 'AAAAA', 75 | 'file': '3/output.dart' 76 | }; 77 | 78 | const _sourceMapBundle = [ 79 | _mapWithSourceLocationAndName1, 80 | _mapWithSourceLocationAndName2, 81 | _mapWithSourceLocationAndName3, 82 | ]; 83 | 84 | void main() { 85 | test('parse', () { 86 | var mapping = parseJson(expectedMap); 87 | check(outputVar1, mapping, inputVar1, false); 88 | check(outputVar2, mapping, inputVar2, false); 89 | check(outputFunction, mapping, inputFunction, false); 90 | check(outputExpr, mapping, inputExpr, false); 91 | }); 92 | 93 | test('parse + json', () { 94 | var mapping = parse(jsonEncode(expectedMap)); 95 | check(outputVar1, mapping, inputVar1, false); 96 | check(outputVar2, mapping, inputVar2, false); 97 | check(outputFunction, mapping, inputFunction, false); 98 | check(outputExpr, mapping, inputExpr, false); 99 | }); 100 | 101 | test('parse with file', () { 102 | var mapping = parseJson(expectedMap); 103 | check(outputVar1, mapping, inputVar1, true); 104 | check(outputVar2, mapping, inputVar2, true); 105 | check(outputFunction, mapping, inputFunction, true); 106 | check(outputExpr, mapping, inputExpr, true); 107 | }); 108 | 109 | test('parse with no source location', () { 110 | var map = parse(jsonEncode(_mapWithNoSourceLocation)) as SingleMapping; 111 | expect(map.lines.length, 1); 112 | expect(map.lines.first.entries.length, 1); 113 | var entry = map.lines.first.entries.first; 114 | 115 | expect(entry.column, 0); 116 | expect(entry.sourceUrlId, null); 117 | expect(entry.sourceColumn, null); 118 | expect(entry.sourceLine, null); 119 | expect(entry.sourceNameId, null); 120 | }); 121 | 122 | test('parse with source location and no name', () { 123 | var map = parse(jsonEncode(_mapWithSourceLocation)) as SingleMapping; 124 | expect(map.lines.length, 1); 125 | expect(map.lines.first.entries.length, 1); 126 | var entry = map.lines.first.entries.first; 127 | 128 | expect(entry.column, 0); 129 | expect(entry.sourceUrlId, 0); 130 | expect(entry.sourceColumn, 0); 131 | expect(entry.sourceLine, 0); 132 | expect(entry.sourceNameId, null); 133 | }); 134 | 135 | test('parse with source location and missing names entry', () { 136 | var map = parse(jsonEncode(_mapWithSourceLocationAndMissingNames)) 137 | as SingleMapping; 138 | expect(map.lines.length, 1); 139 | expect(map.lines.first.entries.length, 1); 140 | var entry = map.lines.first.entries.first; 141 | 142 | expect(entry.column, 0); 143 | expect(entry.sourceUrlId, 0); 144 | expect(entry.sourceColumn, 0); 145 | expect(entry.sourceLine, 0); 146 | expect(entry.sourceNameId, null); 147 | }); 148 | 149 | test('parse with source location and name', () { 150 | var map = parse(jsonEncode(_mapWithSourceLocationAndName)) as SingleMapping; 151 | expect(map.lines.length, 1); 152 | expect(map.lines.first.entries.length, 1); 153 | var entry = map.lines.first.entries.first; 154 | 155 | expect(entry.sourceUrlId, 0); 156 | expect(entry.sourceUrlId, 0); 157 | expect(entry.sourceColumn, 0); 158 | expect(entry.sourceLine, 0); 159 | expect(entry.sourceNameId, 0); 160 | }); 161 | 162 | test('parse with source root', () { 163 | var inputMap = Map.from(_mapWithSourceLocation); 164 | inputMap['sourceRoot'] = '/pkg/'; 165 | var mapping = parseJson(inputMap) as SingleMapping; 166 | expect(mapping.spanFor(0, 0)?.sourceUrl, Uri.parse('/pkg/input.dart')); 167 | expect( 168 | mapping 169 | .spanForLocation( 170 | SourceLocation(0, sourceUrl: Uri.parse('ignored.dart'))) 171 | ?.sourceUrl, 172 | Uri.parse('/pkg/input.dart')); 173 | 174 | var newSourceRoot = '/new/'; 175 | 176 | mapping.sourceRoot = newSourceRoot; 177 | inputMap['sourceRoot'] = newSourceRoot; 178 | 179 | expect(mapping.toJson(), equals(inputMap)); 180 | }); 181 | 182 | test('parse with map URL', () { 183 | var inputMap = Map.from(_mapWithSourceLocation); 184 | inputMap['sourceRoot'] = 'pkg/'; 185 | var mapping = parseJson(inputMap, mapUrl: 'file:///path/to/map'); 186 | expect(mapping.spanFor(0, 0)?.sourceUrl, 187 | Uri.parse('file:///path/to/pkg/input.dart')); 188 | }); 189 | 190 | group('parse with bundle', () { 191 | var mapping = 192 | parseJsonExtended(_sourceMapBundle, mapUrl: 'file:///path/to/map'); 193 | 194 | test('simple', () { 195 | expect( 196 | mapping 197 | .spanForLocation(SourceLocation(0, 198 | sourceUrl: Uri.file('/path/to/output.dart'))) 199 | ?.sourceUrl, 200 | Uri.parse('file:///path/to/pkg/input1.dart')); 201 | expect( 202 | mapping 203 | .spanForLocation(SourceLocation(0, 204 | sourceUrl: Uri.file('/path/to/output2.dart'))) 205 | ?.sourceUrl, 206 | Uri.parse('file:///path/to/pkg/input2.dart')); 207 | expect( 208 | mapping 209 | .spanForLocation(SourceLocation(0, 210 | sourceUrl: Uri.file('/path/to/3/output.dart'))) 211 | ?.sourceUrl, 212 | Uri.parse('file:///path/to/pkg/input3.dart')); 213 | 214 | expect( 215 | mapping.spanFor(0, 0, uri: 'file:///path/to/output.dart')?.sourceUrl, 216 | Uri.parse('file:///path/to/pkg/input1.dart')); 217 | expect( 218 | mapping.spanFor(0, 0, uri: 'file:///path/to/output2.dart')?.sourceUrl, 219 | Uri.parse('file:///path/to/pkg/input2.dart')); 220 | expect( 221 | mapping 222 | .spanFor(0, 0, uri: 'file:///path/to/3/output.dart') 223 | ?.sourceUrl, 224 | Uri.parse('file:///path/to/pkg/input3.dart')); 225 | }); 226 | 227 | test('package uris', () { 228 | expect( 229 | mapping 230 | .spanForLocation(SourceLocation(0, 231 | sourceUrl: Uri.parse('package:1/output.dart'))) 232 | ?.sourceUrl, 233 | Uri.parse('file:///path/to/pkg/input1.dart')); 234 | expect( 235 | mapping 236 | .spanForLocation(SourceLocation(0, 237 | sourceUrl: Uri.parse('package:2/output2.dart'))) 238 | ?.sourceUrl, 239 | Uri.parse('file:///path/to/pkg/input2.dart')); 240 | expect( 241 | mapping 242 | .spanForLocation(SourceLocation(0, 243 | sourceUrl: Uri.parse('package:3/output.dart'))) 244 | ?.sourceUrl, 245 | Uri.parse('file:///path/to/pkg/input3.dart')); 246 | 247 | expect(mapping.spanFor(0, 0, uri: 'package:1/output.dart')?.sourceUrl, 248 | Uri.parse('file:///path/to/pkg/input1.dart')); 249 | expect(mapping.spanFor(0, 0, uri: 'package:2/output2.dart')?.sourceUrl, 250 | Uri.parse('file:///path/to/pkg/input2.dart')); 251 | expect(mapping.spanFor(0, 0, uri: 'package:3/output.dart')?.sourceUrl, 252 | Uri.parse('file:///path/to/pkg/input3.dart')); 253 | }); 254 | 255 | test('unmapped path', () { 256 | var span = mapping.spanFor(0, 0, uri: 'unmapped_output.dart')!; 257 | expect(span.sourceUrl, Uri.parse('unmapped_output.dart')); 258 | expect(span.start.line, equals(0)); 259 | expect(span.start.column, equals(0)); 260 | 261 | span = mapping.spanFor(10, 5, uri: 'unmapped_output.dart')!; 262 | expect(span.sourceUrl, Uri.parse('unmapped_output.dart')); 263 | expect(span.start.line, equals(10)); 264 | expect(span.start.column, equals(5)); 265 | }); 266 | 267 | test('missing path', () { 268 | expect(() => mapping.spanFor(0, 0), throwsA(anything)); 269 | }); 270 | 271 | test('incomplete paths', () { 272 | expect(mapping.spanFor(0, 0, uri: 'output.dart')?.sourceUrl, 273 | Uri.parse('file:///path/to/pkg/input1.dart')); 274 | expect(mapping.spanFor(0, 0, uri: 'output2.dart')?.sourceUrl, 275 | Uri.parse('file:///path/to/pkg/input2.dart')); 276 | expect(mapping.spanFor(0, 0, uri: '3/output.dart')?.sourceUrl, 277 | Uri.parse('file:///path/to/pkg/input3.dart')); 278 | }); 279 | 280 | test('parseExtended', () { 281 | var mapping = parseExtended(jsonEncode(_sourceMapBundle), 282 | mapUrl: 'file:///path/to/map'); 283 | 284 | expect(mapping.spanFor(0, 0, uri: 'output.dart')?.sourceUrl, 285 | Uri.parse('file:///path/to/pkg/input1.dart')); 286 | expect(mapping.spanFor(0, 0, uri: 'output2.dart')?.sourceUrl, 287 | Uri.parse('file:///path/to/pkg/input2.dart')); 288 | expect(mapping.spanFor(0, 0, uri: '3/output.dart')?.sourceUrl, 289 | Uri.parse('file:///path/to/pkg/input3.dart')); 290 | }); 291 | 292 | test('build bundle incrementally', () { 293 | var mapping = MappingBundle(); 294 | 295 | mapping.addMapping(parseJson(_mapWithSourceLocationAndName1, 296 | mapUrl: 'file:///path/to/map') as SingleMapping); 297 | expect(mapping.spanFor(0, 0, uri: 'output.dart')?.sourceUrl, 298 | Uri.parse('file:///path/to/pkg/input1.dart')); 299 | 300 | expect(mapping.containsMapping('output2.dart'), isFalse); 301 | mapping.addMapping(parseJson(_mapWithSourceLocationAndName2, 302 | mapUrl: 'file:///path/to/map') as SingleMapping); 303 | expect(mapping.containsMapping('output2.dart'), isTrue); 304 | expect(mapping.spanFor(0, 0, uri: 'output2.dart')?.sourceUrl, 305 | Uri.parse('file:///path/to/pkg/input2.dart')); 306 | 307 | expect(mapping.containsMapping('3/output.dart'), isFalse); 308 | mapping.addMapping(parseJson(_mapWithSourceLocationAndName3, 309 | mapUrl: 'file:///path/to/map') as SingleMapping); 310 | expect(mapping.containsMapping('3/output.dart'), isTrue); 311 | expect(mapping.spanFor(0, 0, uri: '3/output.dart')?.sourceUrl, 312 | Uri.parse('file:///path/to/pkg/input3.dart')); 313 | }); 314 | 315 | // Test that the source map can handle cases where the uri passed in is 316 | // not from the expected host but it is still unambiguous which source 317 | // map should be used. 318 | test('different paths', () { 319 | expect( 320 | mapping 321 | .spanForLocation(SourceLocation(0, 322 | sourceUrl: Uri.parse('http://localhost/output.dart'))) 323 | ?.sourceUrl, 324 | Uri.parse('file:///path/to/pkg/input1.dart')); 325 | expect( 326 | mapping 327 | .spanForLocation(SourceLocation(0, 328 | sourceUrl: Uri.parse('http://localhost/output2.dart'))) 329 | ?.sourceUrl, 330 | Uri.parse('file:///path/to/pkg/input2.dart')); 331 | expect( 332 | mapping 333 | .spanForLocation(SourceLocation(0, 334 | sourceUrl: Uri.parse('http://localhost/3/output.dart'))) 335 | ?.sourceUrl, 336 | Uri.parse('file:///path/to/pkg/input3.dart')); 337 | 338 | expect( 339 | mapping.spanFor(0, 0, uri: 'http://localhost/output.dart')?.sourceUrl, 340 | Uri.parse('file:///path/to/pkg/input1.dart')); 341 | expect( 342 | mapping 343 | .spanFor(0, 0, uri: 'http://localhost/output2.dart') 344 | ?.sourceUrl, 345 | Uri.parse('file:///path/to/pkg/input2.dart')); 346 | expect( 347 | mapping 348 | .spanFor(0, 0, uri: 'http://localhost/3/output.dart') 349 | ?.sourceUrl, 350 | Uri.parse('file:///path/to/pkg/input3.dart')); 351 | }); 352 | }); 353 | 354 | test('parse and re-emit', () { 355 | for (var expected in [ 356 | expectedMap, 357 | _mapWithNoSourceLocation, 358 | _mapWithSourceLocation, 359 | _mapWithSourceLocationAndName 360 | ]) { 361 | var mapping = parseJson(expected) as SingleMapping; 362 | expect(mapping.toJson(), equals(expected)); 363 | 364 | mapping = parseJsonExtended(expected) as SingleMapping; 365 | expect(mapping.toJson(), equals(expected)); 366 | } 367 | 368 | var mapping = parseJsonExtended(_sourceMapBundle) as MappingBundle; 369 | expect(mapping.toJson(), equals(_sourceMapBundle)); 370 | }); 371 | 372 | test('parse extensions', () { 373 | var map = Map.from(expectedMap); 374 | map['x_foo'] = 'a'; 375 | map['x_bar'] = [3]; 376 | var mapping = parseJson(map) as SingleMapping; 377 | expect(mapping.toJson(), equals(map)); 378 | expect(mapping.extensions['x_foo'], equals('a')); 379 | expect((mapping.extensions['x_bar'] as List).first, equals(3)); 380 | }); 381 | 382 | group('source files', () { 383 | group('from fromEntries()', () { 384 | test('are null for non-FileLocations', () { 385 | var mapping = SingleMapping.fromEntries([ 386 | Entry(SourceLocation(10, line: 1, column: 8), outputVar1.start, null) 387 | ]); 388 | expect(mapping.files, equals([null])); 389 | }); 390 | 391 | test("use a file location's file", () { 392 | var mapping = SingleMapping.fromEntries( 393 | [Entry(inputVar1.start, outputVar1.start, null)]); 394 | expect(mapping.files, equals([input])); 395 | }); 396 | }); 397 | 398 | group('from parse()', () { 399 | group('are null', () { 400 | test('with no sourcesContent field', () { 401 | var mapping = parseJson(expectedMap) as SingleMapping; 402 | expect(mapping.files, equals([null])); 403 | }); 404 | 405 | test('with null sourcesContent values', () { 406 | var map = Map.from(expectedMap); 407 | map['sourcesContent'] = [null]; 408 | var mapping = parseJson(map) as SingleMapping; 409 | expect(mapping.files, equals([null])); 410 | }); 411 | 412 | test('with a too-short sourcesContent', () { 413 | var map = Map.from(expectedMap); 414 | map['sourcesContent'] = []; 415 | var mapping = parseJson(map) as SingleMapping; 416 | expect(mapping.files, equals([null])); 417 | }); 418 | }); 419 | 420 | test('are parsed from sourcesContent', () { 421 | var map = Map.from(expectedMap); 422 | map['sourcesContent'] = ['hello, world!']; 423 | var mapping = parseJson(map) as SingleMapping; 424 | 425 | var file = mapping.files[0]!; 426 | expect(file.url, equals(Uri.parse('input.dart'))); 427 | expect(file.getText(0), equals('hello, world!')); 428 | }); 429 | }); 430 | }); 431 | } 432 | -------------------------------------------------------------------------------- /test/printer_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | 7 | import 'package:source_maps/source_maps.dart'; 8 | import 'package:source_span/source_span.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | import 'common.dart'; 12 | 13 | void main() { 14 | test('printer', () { 15 | var printer = Printer('output.dart'); 16 | printer 17 | ..add('var ') 18 | ..mark(inputVar1) 19 | ..add('x = 3;\n') 20 | ..mark(inputFunction) 21 | ..add('f(') 22 | ..mark(inputVar2) 23 | ..add('y) => ') 24 | ..mark(inputExpr) 25 | ..add('x + y;\n'); 26 | expect(printer.text, outputContent); 27 | expect(printer.map, jsonEncode(expectedMap)); 28 | }); 29 | 30 | test('printer projecting marks', () { 31 | var out = inputContent.replaceAll('long', '_s'); 32 | var printer = Printer('output2.dart'); 33 | 34 | var segments = inputContent.split('long'); 35 | expect(segments.length, 6); 36 | printer 37 | ..mark(ispan(0, 0)) 38 | ..add(segments[0], projectMarks: true) 39 | ..mark(inputVar1) 40 | ..add('_s') 41 | ..add(segments[1], projectMarks: true) 42 | ..mark(inputFunction) 43 | ..add('_s') 44 | ..add(segments[2], projectMarks: true) 45 | ..mark(inputVar2) 46 | ..add('_s') 47 | ..add(segments[3], projectMarks: true) 48 | ..mark(inputExpr) 49 | ..add('_s') 50 | ..add(segments[4], projectMarks: true) 51 | ..add('_s') 52 | ..add(segments[5], projectMarks: true); 53 | 54 | expect(printer.text, out); 55 | // 8 new lines in the source map: 56 | expect(printer.map.split(';').length, 8); 57 | 58 | SourceMapSpan asFixed(SourceMapSpan s) => 59 | SourceMapSpan(s.start, s.end, s.text, isIdentifier: s.isIdentifier); 60 | 61 | // The result is the same if we use fixed positions 62 | var printer2 = Printer('output2.dart'); 63 | printer2 64 | ..mark(SourceLocation(0, sourceUrl: 'input.dart').pointSpan()) 65 | ..add(segments[0], projectMarks: true) 66 | ..mark(asFixed(inputVar1)) 67 | ..add('_s') 68 | ..add(segments[1], projectMarks: true) 69 | ..mark(asFixed(inputFunction)) 70 | ..add('_s') 71 | ..add(segments[2], projectMarks: true) 72 | ..mark(asFixed(inputVar2)) 73 | ..add('_s') 74 | ..add(segments[3], projectMarks: true) 75 | ..mark(asFixed(inputExpr)) 76 | ..add('_s') 77 | ..add(segments[4], projectMarks: true) 78 | ..add('_s') 79 | ..add(segments[5], projectMarks: true); 80 | 81 | expect(printer2.text, out); 82 | expect(printer2.map, printer.map); 83 | }); 84 | 85 | group('nested printer', () { 86 | test('simple use', () { 87 | var printer = NestedPrinter(); 88 | printer 89 | ..add('var ') 90 | ..add('x = 3;\n', span: inputVar1) 91 | ..add('f(', span: inputFunction) 92 | ..add('y) => ', span: inputVar2) 93 | ..add('x + y;\n', span: inputExpr) 94 | ..build('output.dart'); 95 | expect(printer.text, outputContent); 96 | expect(printer.map, jsonEncode(expectedMap)); 97 | }); 98 | 99 | test('nested use', () { 100 | var printer = NestedPrinter(); 101 | printer 102 | ..add('var ') 103 | ..add(NestedPrinter()..add('x = 3;\n', span: inputVar1)) 104 | ..add('f(', span: inputFunction) 105 | ..add(NestedPrinter()..add('y) => ', span: inputVar2)) 106 | ..add('x + y;\n', span: inputExpr) 107 | ..build('output.dart'); 108 | expect(printer.text, outputContent); 109 | expect(printer.map, jsonEncode(expectedMap)); 110 | }); 111 | 112 | test('add indentation', () { 113 | var out = inputContent.replaceAll('long', '_s'); 114 | var lines = inputContent.trim().split('\n'); 115 | expect(lines.length, 7); 116 | var printer = NestedPrinter(); 117 | for (var i = 0; i < lines.length; i++) { 118 | if (i == 5) printer.indent++; 119 | printer.addLine(lines[i].replaceAll('long', '_s').trim()); 120 | if (i == 5) printer.indent--; 121 | } 122 | printer.build('output.dart'); 123 | expect(printer.text, out); 124 | }); 125 | }); 126 | } 127 | -------------------------------------------------------------------------------- /test/refactor_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:source_maps/parser.dart' show Mapping, parse; 6 | import 'package:source_maps/refactor.dart'; 7 | import 'package:source_span/source_span.dart'; 8 | import 'package:term_glyph/term_glyph.dart' as term_glyph; 9 | import 'package:test/test.dart'; 10 | 11 | void main() { 12 | setUpAll(() { 13 | term_glyph.ascii = true; 14 | }); 15 | 16 | group('conflict detection', () { 17 | var original = '0123456789abcdefghij'; 18 | var file = SourceFile.fromString(original); 19 | 20 | test('no conflict, in order', () { 21 | var txn = TextEditTransaction(original, file); 22 | txn.edit(2, 4, '.'); 23 | txn.edit(5, 5, '|'); 24 | txn.edit(6, 6, '-'); 25 | txn.edit(6, 7, '_'); 26 | expect((txn.commit()..build('')).text, '01.4|5-_789abcdefghij'); 27 | }); 28 | 29 | test('no conflict, out of order', () { 30 | var txn = TextEditTransaction(original, file); 31 | txn.edit(2, 4, '.'); 32 | txn.edit(5, 5, '|'); 33 | 34 | // Regresion test for issue #404: there is no conflict/overlap for edits 35 | // that don't remove any of the original code. 36 | txn.edit(6, 7, '_'); 37 | txn.edit(6, 6, '-'); 38 | expect((txn.commit()..build('')).text, '01.4|5-_789abcdefghij'); 39 | }); 40 | 41 | test('conflict', () { 42 | var txn = TextEditTransaction(original, file); 43 | txn.edit(2, 4, '.'); 44 | txn.edit(3, 3, '-'); 45 | expect( 46 | () => txn.commit(), 47 | throwsA( 48 | predicate((e) => e.toString().contains('overlapping edits')))); 49 | }); 50 | }); 51 | 52 | test('generated source maps', () { 53 | var original = 54 | '0123456789\n0*23456789\n01*3456789\nabcdefghij\nabcd*fghij\n'; 55 | var file = SourceFile.fromString(original); 56 | var txn = TextEditTransaction(original, file); 57 | txn.edit(27, 29, '__\n '); 58 | txn.edit(34, 35, '___'); 59 | var printer = (txn.commit()..build('')); 60 | var output = printer.text; 61 | var map = parse(printer.map!); 62 | expect(output, 63 | '0123456789\n0*23456789\n01*34__\n 789\na___cdefghij\nabcd*fghij\n'); 64 | 65 | // Line 1 and 2 are unmodified: mapping any column returns the beginning 66 | // of the corresponding line: 67 | expect( 68 | _span(1, 1, map, file), 69 | 'line 1, column 1: \n' 70 | ' ,\n' 71 | '1 | 0123456789\n' 72 | ' | ^\n' 73 | " '"); 74 | expect( 75 | _span(1, 5, map, file), 76 | 'line 1, column 1: \n' 77 | ' ,\n' 78 | '1 | 0123456789\n' 79 | ' | ^\n' 80 | " '"); 81 | expect( 82 | _span(2, 1, map, file), 83 | 'line 2, column 1: \n' 84 | ' ,\n' 85 | '2 | 0*23456789\n' 86 | ' | ^\n' 87 | " '"); 88 | expect( 89 | _span(2, 8, map, file), 90 | 'line 2, column 1: \n' 91 | ' ,\n' 92 | '2 | 0*23456789\n' 93 | ' | ^\n' 94 | " '"); 95 | 96 | // Line 3 is modified part way: mappings before the edits have the right 97 | // mapping, after the edits the mapping is null. 98 | expect( 99 | _span(3, 1, map, file), 100 | 'line 3, column 1: \n' 101 | ' ,\n' 102 | '3 | 01*3456789\n' 103 | ' | ^\n' 104 | " '"); 105 | expect( 106 | _span(3, 5, map, file), 107 | 'line 3, column 1: \n' 108 | ' ,\n' 109 | '3 | 01*3456789\n' 110 | ' | ^\n' 111 | " '"); 112 | 113 | // Start of edits map to beginning of the edit secion: 114 | expect( 115 | _span(3, 6, map, file), 116 | 'line 3, column 6: \n' 117 | ' ,\n' 118 | '3 | 01*3456789\n' 119 | ' | ^\n' 120 | " '"); 121 | expect( 122 | _span(3, 7, map, file), 123 | 'line 3, column 6: \n' 124 | ' ,\n' 125 | '3 | 01*3456789\n' 126 | ' | ^\n' 127 | " '"); 128 | 129 | // Lines added have no mapping (they should inherit the last mapping), 130 | // but the end of the edit region continues were we left off: 131 | expect(_span(4, 1, map, file), isNull); 132 | expect( 133 | _span(4, 5, map, file), 134 | 'line 3, column 8: \n' 135 | ' ,\n' 136 | '3 | 01*3456789\n' 137 | ' | ^\n' 138 | " '"); 139 | 140 | // Subsequent lines are still mapped correctly: 141 | // a (in a___cd...) 142 | expect( 143 | _span(5, 1, map, file), 144 | 'line 4, column 1: \n' 145 | ' ,\n' 146 | '4 | abcdefghij\n' 147 | ' | ^\n' 148 | " '"); 149 | // _ (in a___cd...) 150 | expect( 151 | _span(5, 2, map, file), 152 | 'line 4, column 2: \n' 153 | ' ,\n' 154 | '4 | abcdefghij\n' 155 | ' | ^\n' 156 | " '"); 157 | // _ (in a___cd...) 158 | expect( 159 | _span(5, 3, map, file), 160 | 'line 4, column 2: \n' 161 | ' ,\n' 162 | '4 | abcdefghij\n' 163 | ' | ^\n' 164 | " '"); 165 | // _ (in a___cd...) 166 | expect( 167 | _span(5, 4, map, file), 168 | 'line 4, column 2: \n' 169 | ' ,\n' 170 | '4 | abcdefghij\n' 171 | ' | ^\n' 172 | " '"); 173 | // c (in a___cd...) 174 | expect( 175 | _span(5, 5, map, file), 176 | 'line 4, column 3: \n' 177 | ' ,\n' 178 | '4 | abcdefghij\n' 179 | ' | ^\n' 180 | " '"); 181 | expect( 182 | _span(6, 1, map, file), 183 | 'line 5, column 1: \n' 184 | ' ,\n' 185 | '5 | abcd*fghij\n' 186 | ' | ^\n' 187 | " '"); 188 | expect( 189 | _span(6, 8, map, file), 190 | 'line 5, column 1: \n' 191 | ' ,\n' 192 | '5 | abcd*fghij\n' 193 | ' | ^\n' 194 | " '"); 195 | }); 196 | } 197 | 198 | String? _span(int line, int column, Mapping map, SourceFile file) => 199 | map.spanFor(line - 1, column - 1, files: {'': file})?.message('').trim(); 200 | -------------------------------------------------------------------------------- /test/utils_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Tests for the binary search utility algorithm. 6 | library test.utils_test; 7 | 8 | import 'package:source_maps/src/utils.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void main() { 12 | group('binary search', () { 13 | test('empty', () { 14 | expect(binarySearch([], (x) => true), -1); 15 | }); 16 | 17 | test('single element', () { 18 | expect(binarySearch([1], (x) => true), 0); 19 | expect(binarySearch([1], (x) => false), 1); 20 | }); 21 | 22 | test('no matches', () { 23 | var list = [1, 2, 3, 4, 5, 6, 7]; 24 | expect(binarySearch(list, (x) => false), list.length); 25 | }); 26 | 27 | test('all match', () { 28 | var list = [1, 2, 3, 4, 5, 6, 7]; 29 | expect(binarySearch(list, (x) => true), 0); 30 | }); 31 | 32 | test('compare with linear search', () { 33 | for (var size = 0; size < 100; size++) { 34 | var list = []; 35 | for (var i = 0; i < size; i++) { 36 | list.add(i); 37 | } 38 | for (var pos = 0; pos <= size; pos++) { 39 | expect(binarySearch(list, (x) => x >= pos), 40 | _linearSearch(list, (x) => x >= pos)); 41 | } 42 | } 43 | }); 44 | }); 45 | } 46 | 47 | int _linearSearch(List list, bool Function(T) predicate) { 48 | if (list.isEmpty) return -1; 49 | for (var i = 0; i < list.length; i++) { 50 | if (predicate(list[i])) return i; 51 | } 52 | return list.length; 53 | } 54 | -------------------------------------------------------------------------------- /test/vlq_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:math'; 6 | 7 | import 'package:source_maps/src/vlq.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | void main() { 11 | test('encode and decode - simple values', () { 12 | expect(encodeVlq(1).join(''), 'C'); 13 | expect(encodeVlq(2).join(''), 'E'); 14 | expect(encodeVlq(3).join(''), 'G'); 15 | expect(encodeVlq(100).join(''), 'oG'); 16 | expect(decodeVlq('C'.split('').iterator), 1); 17 | expect(decodeVlq('E'.split('').iterator), 2); 18 | expect(decodeVlq('G'.split('').iterator), 3); 19 | expect(decodeVlq('oG'.split('').iterator), 100); 20 | }); 21 | 22 | test('encode and decode', () { 23 | for (var i = -10000; i < 10000; i++) { 24 | _checkEncodeDecode(i); 25 | } 26 | }); 27 | 28 | test('only 32-bit ints allowed', () { 29 | var maxInt = (pow(2, 31) as int) - 1; 30 | var minInt = -(pow(2, 31) as int); 31 | _checkEncodeDecode(maxInt - 1); 32 | _checkEncodeDecode(minInt + 1); 33 | _checkEncodeDecode(maxInt); 34 | _checkEncodeDecode(minInt); 35 | 36 | expect(encodeVlq(minInt).join(''), 'hgggggE'); 37 | expect(decodeVlq('hgggggE'.split('').iterator), minInt); 38 | 39 | expect(() => encodeVlq(maxInt + 1), throwsA(anything)); 40 | expect(() => encodeVlq(maxInt + 2), throwsA(anything)); 41 | expect(() => encodeVlq(minInt - 1), throwsA(anything)); 42 | expect(() => encodeVlq(minInt - 2), throwsA(anything)); 43 | 44 | // if we allowed more than 32 bits, these would be the expected encodings 45 | // for the large numbers above. 46 | expect(() => decodeVlq('ggggggE'.split('').iterator), throwsA(anything)); 47 | expect(() => decodeVlq('igggggE'.split('').iterator), throwsA(anything)); 48 | expect(() => decodeVlq('jgggggE'.split('').iterator), throwsA(anything)); 49 | expect(() => decodeVlq('lgggggE'.split('').iterator), throwsA(anything)); 50 | }, 51 | // This test uses integers so large they overflow in JS. 52 | testOn: 'dart-vm'); 53 | } 54 | 55 | void _checkEncodeDecode(int value) { 56 | var encoded = encodeVlq(value); 57 | expect(decodeVlq(encoded.iterator), value); 58 | expect(decodeVlq(encoded.join('').split('').iterator), value); 59 | } 60 | --------------------------------------------------------------------------------