Merge package:source_maps into the tools monorepo
diff --git a/pkgs/source_maps/.github/dependabot.yaml b/pkgs/source_maps/.github/dependabot.yaml
new file mode 100644
index 0000000..bf6b38a
--- /dev/null
+++ b/pkgs/source_maps/.github/dependabot.yaml
@@ -0,0 +1,14 @@
+# Dependabot configuration file.
+version: 2
+
+updates:
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: monthly
+ labels:
+ - autosubmit
+ groups:
+ github-actions:
+ patterns:
+ - "*"
diff --git a/pkgs/source_maps/.github/workflows/no-response.yml b/pkgs/source_maps/.github/workflows/no-response.yml
new file mode 100644
index 0000000..ab1ac49
--- /dev/null
+++ b/pkgs/source_maps/.github/workflows/no-response.yml
@@ -0,0 +1,37 @@
+# A workflow to close issues where the author hasn't responded to a request for
+# more information; see https://github.com/actions/stale.
+
+name: No Response
+
+# Run as a daily cron.
+on:
+ schedule:
+ # Every day at 8am
+ - cron: '0 8 * * *'
+
+# All permissions not specified are set to 'none'.
+permissions:
+ issues: write
+ pull-requests: write
+
+jobs:
+ no-response:
+ runs-on: ubuntu-latest
+ if: ${{ github.repository_owner == 'dart-lang' }}
+ steps:
+ - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e
+ with:
+ # Don't automatically mark inactive issues+PRs as stale.
+ days-before-stale: -1
+ # Close needs-info issues and PRs after 14 days of inactivity.
+ days-before-close: 14
+ stale-issue-label: "needs-info"
+ close-issue-message: >
+ Without additional information we're not able to resolve this issue.
+ Feel free to add more info or respond to any questions above and we
+ can reopen the case. Thanks for your contribution!
+ stale-pr-label: "needs-info"
+ close-pr-message: >
+ Without additional information we're not able to resolve this PR.
+ Feel free to add more info or respond to any questions above.
+ Thanks for your contribution!
diff --git a/pkgs/source_maps/.github/workflows/publish.yaml b/pkgs/source_maps/.github/workflows/publish.yaml
new file mode 100644
index 0000000..27157a0
--- /dev/null
+++ b/pkgs/source_maps/.github/workflows/publish.yaml
@@ -0,0 +1,17 @@
+# A CI configuration to auto-publish pub packages.
+
+name: Publish
+
+on:
+ pull_request:
+ branches: [ master ]
+ push:
+ tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ]
+
+jobs:
+ publish:
+ if: ${{ github.repository_owner == 'dart-lang' }}
+ uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main
+ permissions:
+ id-token: write # Required for authentication using OIDC
+ pull-requests: write # Required for writing the pull request note
diff --git a/pkgs/source_maps/.github/workflows/test-package.yml b/pkgs/source_maps/.github/workflows/test-package.yml
new file mode 100644
index 0000000..e6f7c86
--- /dev/null
+++ b/pkgs/source_maps/.github/workflows/test-package.yml
@@ -0,0 +1,61 @@
+name: Dart CI
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.3.0, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
diff --git a/pkgs/source_maps/.gitignore b/pkgs/source_maps/.gitignore
new file mode 100644
index 0000000..f73b2f9
--- /dev/null
+++ b/pkgs/source_maps/.gitignore
@@ -0,0 +1,4 @@
+.dart_tool/
+.packages
+.pub/
+pubspec.lock
diff --git a/pkgs/source_maps/CHANGELOG.md b/pkgs/source_maps/CHANGELOG.md
new file mode 100644
index 0000000..9c8e7b3
--- /dev/null
+++ b/pkgs/source_maps/CHANGELOG.md
@@ -0,0 +1,130 @@
+## 0.10.13-wip
+
+- Require Dart 3.3
+
+## 0.10.12
+
+* Add additional types at API boundaries.
+
+## 0.10.11
+
+* Populate the pubspec `repository` field.
+* Update the source map documentation link in the readme.
+
+## 0.10.10
+
+* Stable release for null safety.
+
+## 0.10.9
+
+* Fix a number of document comment issues.
+* Allow parsing source map files with a missing `names` field.
+
+## 0.10.8
+
+* Preserve source-map extensions in `SingleMapping`. Extensions are keys in the
+ json map that start with `"x_"`.
+
+## 0.10.7
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 0.10.6
+
+* Require version 2.0.0 of the Dart SDK.
+
+## 0.10.5
+
+* Add a `SingleMapping.files` field which provides access to `SourceFile`s
+ representing the `"sourcesContent"` fields in the source map.
+
+* Add an `includeSourceContents` flag to `SingleMapping.toJson()` which
+ indicates whether to include source file contents in the source map.
+
+## 0.10.4
+* Implement `highlight` in `SourceMapFileSpan`.
+* Require version `^1.3.0` of `source_span`.
+
+## 0.10.3
+ * Add `addMapping` and `containsMapping` members to `MappingBundle`.
+
+## 0.10.2
+ * Support for extended source map format.
+ * Polish `MappingBundle.spanFor` handling of URIs that have a suffix that
+ exactly match a source map in the MappingBundle.
+
+## 0.10.1+5
+ * Fix strong mode warning in test.
+
+## 0.10.1+4
+
+* Extend `MappingBundle.spanFor` to accept requests for output files that
+ don't have source maps.
+
+## 0.10.1+3
+
+* Add `MappingBundle` class that handles extended source map format that
+ supports source maps for multiple output files in a single mapper.
+ Extend `Mapping.spanFor` API to accept a uri parameter that is optional
+ for normal source maps but required for MappingBundle source maps.
+
+## 0.10.1+2
+
+* Fix more strong mode warnings.
+
+## 0.10.1+1
+
+* Fix all strong mode warnings.
+
+## 0.10.1
+
+* Add a `mapUrl` named argument to `parse` and `parseJson`. This argument is
+ used to resolve source URLs for source spans.
+
+## 0.10.0+2
+
+* Fix analyzer error (FileSpan has a new field since `source_span` 1.1.1)
+
+## 0.10.0+1
+
+* Remove an unnecessary warning printed when the "file" field is missing from a
+ Json formatted source map. This field is optional and its absence is not
+ unusual.
+
+## 0.10.0
+
+* Remove the `Span`, `Location` and `SourceFile` classes. Use the
+ corresponding `source_span` classes instead.
+
+## 0.9.4
+
+* Update `SpanFormatException` with `source` and `offset`.
+
+* All methods that take `Span`s, `Location`s, and `SourceFile`s as inputs now
+ also accept the corresponding `source_span` classes as well. Using the old
+ classes is now deprecated and will be unsupported in version 0.10.0.
+
+## 0.9.3
+
+* Support writing SingleMapping objects to source map version 3 format.
+* Support the `sourceRoot` field in the SingleMapping class.
+* Support updating the `targetUrl` field in the SingleMapping class.
+
+## 0.9.2+2
+
+* Fix a bug in `FixedSpan.getLocationMessage`.
+
+## 0.9.2+1
+
+* Minor readability improvements to `FixedSpan.getLocationMessage` and
+ `SpanException.toString`.
+
+## 0.9.2
+
+* Add `SpanException` and `SpanFormatException` classes.
+
+## 0.9.1
+
+* Support unmapped areas in source maps.
+
+* Increase the readability of location messages.
diff --git a/pkgs/source_maps/LICENSE b/pkgs/source_maps/LICENSE
new file mode 100644
index 0000000..162572a
--- /dev/null
+++ b/pkgs/source_maps/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/source_maps/README.md b/pkgs/source_maps/README.md
new file mode 100644
index 0000000..30da7f8
--- /dev/null
+++ b/pkgs/source_maps/README.md
@@ -0,0 +1,25 @@
+[](https://github.com/dart-lang/source_maps/actions/workflows/test-package.yml)
+[](https://pub.dev/packages/source_maps)
+[](https://pub.dev/packages/source_maps/publisher)
+
+This project implements a Dart pub package to work with source maps.
+
+## Docs and usage
+
+The implementation is based on the [source map version 3 spec][spec] which was
+originated from the [Closure Compiler][closure] and has been implemented in
+Chrome and Firefox.
+
+In this package we provide:
+
+ * Data types defining file locations and spans: these are not part of the
+ original source map specification. These data types are great for tracking
+ source locations on source maps, but they can also be used by tools to
+ reporting useful error messages that include on source locations.
+ * A builder that creates a source map programmatically and produces the encoded
+ source map format.
+ * A parser that reads the source map format and provides APIs to read the
+ mapping information.
+
+[closure]: https://github.com/google/closure-compiler/wiki/Source-Maps
+[spec]: https://docs.google.com/a/google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
diff --git a/pkgs/source_maps/analysis_options.yaml b/pkgs/source_maps/analysis_options.yaml
new file mode 100644
index 0000000..d978f81
--- /dev/null
+++ b/pkgs/source_maps/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/source_maps/lib/builder.dart b/pkgs/source_maps/lib/builder.dart
new file mode 100644
index 0000000..54ba743
--- /dev/null
+++ b/pkgs/source_maps/lib/builder.dart
@@ -0,0 +1,84 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Contains a builder object useful for creating source maps programatically.
+library source_maps.builder;
+
+// TODO(sigmund): add a builder for multi-section mappings.
+
+import 'dart:convert';
+
+import 'package:source_span/source_span.dart';
+
+import 'parser.dart';
+import 'src/source_map_span.dart';
+
+/// Builds a source map given a set of mappings.
+class SourceMapBuilder {
+ final List<Entry> _entries = <Entry>[];
+
+ /// Adds an entry mapping the [targetOffset] to [source].
+ void addFromOffset(SourceLocation source, SourceFile targetFile,
+ int targetOffset, String identifier) {
+ ArgumentError.checkNotNull(targetFile, 'targetFile');
+ _entries.add(Entry(source, targetFile.location(targetOffset), identifier));
+ }
+
+ /// Adds an entry mapping [target] to [source].
+ ///
+ /// If [isIdentifier] is true or if [target] is a [SourceMapSpan] with
+ /// `isIdentifier` set to true, this entry is considered to represent an
+ /// identifier whose value will be stored in the source map. [isIdentifier]
+ /// takes precedence over [target]'s `isIdentifier` value.
+ void addSpan(SourceSpan source, SourceSpan target, {bool? isIdentifier}) {
+ isIdentifier ??= source is SourceMapSpan ? source.isIdentifier : false;
+
+ var name = isIdentifier ? source.text : null;
+ _entries.add(Entry(source.start, target.start, name));
+ }
+
+ /// Adds an entry mapping [target] to [source].
+ void addLocation(
+ SourceLocation source, SourceLocation target, String? identifier) {
+ _entries.add(Entry(source, target, identifier));
+ }
+
+ /// Encodes all mappings added to this builder as a json map.
+ Map<String, dynamic> build(String fileUrl) {
+ return SingleMapping.fromEntries(_entries, fileUrl).toJson();
+ }
+
+ /// Encodes all mappings added to this builder as a json string.
+ String toJson(String fileUrl) => jsonEncode(build(fileUrl));
+}
+
+/// An entry in the source map builder.
+class Entry implements Comparable<Entry> {
+ /// Span denoting the original location in the input source file
+ final SourceLocation source;
+
+ /// Span indicating the corresponding location in the target file.
+ final SourceLocation target;
+
+ /// An identifier name, when this location is the start of an identifier.
+ final String? identifierName;
+
+ /// Creates a new [Entry] mapping [target] to [source].
+ Entry(this.source, this.target, this.identifierName);
+
+ /// Implements [Comparable] to ensure that entries are ordered by their
+ /// location in the target file. We sort primarily by the target offset
+ /// because source map files are encoded by printing each mapping in order as
+ /// they appear in the target file.
+ @override
+ int compareTo(Entry other) {
+ var res = target.compareTo(other.target);
+ if (res != 0) return res;
+ res = source.sourceUrl
+ .toString()
+ .compareTo(other.source.sourceUrl.toString());
+ if (res != 0) return res;
+ return source.compareTo(other.source);
+ }
+}
diff --git a/pkgs/source_maps/lib/parser.dart b/pkgs/source_maps/lib/parser.dart
new file mode 100644
index 0000000..b699ac7
--- /dev/null
+++ b/pkgs/source_maps/lib/parser.dart
@@ -0,0 +1,718 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Contains the top-level function to parse source maps version 3.
+library source_maps.parser;
+
+import 'dart:convert';
+
+import 'package:source_span/source_span.dart';
+
+import 'builder.dart' as builder;
+import 'src/source_map_span.dart';
+import 'src/utils.dart';
+import 'src/vlq.dart';
+
+/// Parses a source map directly from a json string.
+///
+/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
+/// the source map file itself. If it's passed, any URLs in the source
+/// map will be interpreted as relative to this URL when generating spans.
+// TODO(sigmund): evaluate whether other maps should have the json parsed, or
+// the string represenation.
+// TODO(tjblasi): Ignore the first line of [jsonMap] if the JSON safety string
+// `)]}'` begins the string representation of the map.
+Mapping parse(String jsonMap,
+ {Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) =>
+ parseJson(jsonDecode(jsonMap) as Map, otherMaps: otherMaps, mapUrl: mapUrl);
+
+/// Parses a source map or source map bundle directly from a json string.
+///
+/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
+/// the source map file itself. If it's passed, any URLs in the source
+/// map will be interpreted as relative to this URL when generating spans.
+Mapping parseExtended(String jsonMap,
+ {Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) =>
+ parseJsonExtended(jsonDecode(jsonMap),
+ otherMaps: otherMaps, mapUrl: mapUrl);
+
+/// Parses a source map or source map bundle.
+///
+/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
+/// the source map file itself. If it's passed, any URLs in the source
+/// map will be interpreted as relative to this URL when generating spans.
+Mapping parseJsonExtended(/*List|Map*/ Object? json,
+ {Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) {
+ if (json is List) {
+ return MappingBundle.fromJson(json, mapUrl: mapUrl);
+ }
+ return parseJson(json as Map);
+}
+
+/// Parses a source map.
+///
+/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
+/// the source map file itself. If it's passed, any URLs in the source
+/// map will be interpreted as relative to this URL when generating spans.
+Mapping parseJson(Map map,
+ {Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) {
+ if (map['version'] != 3) {
+ throw ArgumentError('unexpected source map version: ${map["version"]}. '
+ 'Only version 3 is supported.');
+ }
+
+ if (map.containsKey('sections')) {
+ if (map.containsKey('mappings') ||
+ map.containsKey('sources') ||
+ map.containsKey('names')) {
+ throw const FormatException('map containing "sections" '
+ 'cannot contain "mappings", "sources", or "names".');
+ }
+ return MultiSectionMapping.fromJson(map['sections'] as List, otherMaps,
+ mapUrl: mapUrl);
+ }
+ return SingleMapping.fromJson(map.cast<String, dynamic>(), mapUrl: mapUrl);
+}
+
+/// A mapping parsed out of a source map.
+abstract class Mapping {
+ /// Returns the span associated with [line] and [column].
+ ///
+ /// [uri] is the optional location of the output file to find the span for
+ /// to disambiguate cases where a mapping may have different mappings for
+ /// different output files.
+ SourceMapSpan? spanFor(int line, int column,
+ {Map<String, SourceFile>? files, String? uri});
+
+ /// Returns the span associated with [location].
+ SourceMapSpan? spanForLocation(SourceLocation location,
+ {Map<String, SourceFile>? files}) {
+ return spanFor(location.line, location.column,
+ uri: location.sourceUrl?.toString(), files: files);
+ }
+}
+
+/// A meta-level map containing sections.
+class MultiSectionMapping extends Mapping {
+ /// For each section, the start line offset.
+ final List<int> _lineStart = <int>[];
+
+ /// For each section, the start column offset.
+ final List<int> _columnStart = <int>[];
+
+ /// For each section, the actual source map information, which is not adjusted
+ /// for offsets.
+ final List<Mapping> _maps = <Mapping>[];
+
+ /// Creates a section mapping from json.
+ MultiSectionMapping.fromJson(List sections, Map<String, Map>? otherMaps,
+ {/*String|Uri*/ Object? mapUrl}) {
+ for (var section in sections.cast<Map>()) {
+ var offset = section['offset'] as Map?;
+ if (offset == null) throw const FormatException('section missing offset');
+
+ var line = offset['line'] as int?;
+ if (line == null) throw const FormatException('offset missing line');
+
+ var column = offset['column'] as int?;
+ if (column == null) throw const FormatException('offset missing column');
+
+ _lineStart.add(line);
+ _columnStart.add(column);
+
+ var url = section['url'] as String?;
+ var map = section['map'] as Map?;
+
+ if (url != null && map != null) {
+ throw const FormatException(
+ "section can't use both url and map entries");
+ } else if (url != null) {
+ var other = otherMaps?[url];
+ if (otherMaps == null || other == null) {
+ throw FormatException(
+ 'section contains refers to $url, but no map was '
+ 'given for it. Make sure a map is passed in "otherMaps"');
+ }
+ _maps.add(parseJson(other, otherMaps: otherMaps, mapUrl: url));
+ } else if (map != null) {
+ _maps.add(parseJson(map, otherMaps: otherMaps, mapUrl: mapUrl));
+ } else {
+ throw const FormatException('section missing url or map');
+ }
+ }
+ if (_lineStart.isEmpty) {
+ throw const FormatException('expected at least one section');
+ }
+ }
+
+ int _indexFor(int line, int column) {
+ for (var i = 0; i < _lineStart.length; i++) {
+ if (line < _lineStart[i]) return i - 1;
+ if (line == _lineStart[i] && column < _columnStart[i]) return i - 1;
+ }
+ return _lineStart.length - 1;
+ }
+
+ @override
+ SourceMapSpan? spanFor(int line, int column,
+ {Map<String, SourceFile>? files, String? uri}) {
+ // TODO(jacobr): perhaps verify that targetUrl matches the actual uri
+ // or at least ends in the same file name.
+ var index = _indexFor(line, column);
+ return _maps[index].spanFor(
+ line - _lineStart[index], column - _columnStart[index],
+ files: files);
+ }
+
+ @override
+ String toString() {
+ var buff = StringBuffer('$runtimeType : [');
+ for (var i = 0; i < _lineStart.length; i++) {
+ buff
+ ..write('(')
+ ..write(_lineStart[i])
+ ..write(',')
+ ..write(_columnStart[i])
+ ..write(':')
+ ..write(_maps[i])
+ ..write(')');
+ }
+ buff.write(']');
+ return buff.toString();
+ }
+}
+
+class MappingBundle extends Mapping {
+ final Map<String, SingleMapping> _mappings = {};
+
+ MappingBundle();
+
+ MappingBundle.fromJson(List json, {/*String|Uri*/ Object? mapUrl}) {
+ for (var map in json) {
+ addMapping(parseJson(map as Map, mapUrl: mapUrl) as SingleMapping);
+ }
+ }
+
+ void addMapping(SingleMapping mapping) {
+ // TODO(jacobr): verify that targetUrl is valid uri instead of a windows
+ // path.
+ // TODO: Remove type arg https://github.com/dart-lang/sdk/issues/42227
+ var targetUrl = ArgumentError.checkNotNull<String>(
+ mapping.targetUrl, 'mapping.targetUrl');
+ _mappings[targetUrl] = mapping;
+ }
+
+ /// Encodes the Mapping mappings as a json map.
+ List toJson() => _mappings.values.map((v) => v.toJson()).toList();
+
+ @override
+ String toString() {
+ var buff = StringBuffer();
+ for (var map in _mappings.values) {
+ buff.write(map.toString());
+ }
+ return buff.toString();
+ }
+
+ bool containsMapping(String url) => _mappings.containsKey(url);
+
+ @override
+ SourceMapSpan? spanFor(int line, int column,
+ {Map<String, SourceFile>? files, String? uri}) {
+ // TODO: Remove type arg https://github.com/dart-lang/sdk/issues/42227
+ uri = ArgumentError.checkNotNull<String>(uri, 'uri');
+
+ // Find the longest suffix of the uri that matches the sourcemap
+ // where the suffix starts after a path segment boundary.
+ // We consider ":" and "/" as path segment boundaries so that
+ // "package:" uris can be handled with minimal special casing. Having a
+ // few false positive path segment boundaries is not a significant issue
+ // as we prefer the longest matching prefix.
+ // Using package:path `path.split` to find path segment boundaries would
+ // not generate all of the path segment boundaries we want for "package:"
+ // urls as "package:package_name" would be one path segment when we want
+ // "package" and "package_name" to be sepearate path segments.
+
+ var onBoundary = true;
+ var separatorCodeUnits = ['/'.codeUnitAt(0), ':'.codeUnitAt(0)];
+ for (var i = 0; i < uri.length; ++i) {
+ if (onBoundary) {
+ var candidate = uri.substring(i);
+ var candidateMapping = _mappings[candidate];
+ if (candidateMapping != null) {
+ return candidateMapping.spanFor(line, column,
+ files: files, uri: candidate);
+ }
+ }
+ onBoundary = separatorCodeUnits.contains(uri.codeUnitAt(i));
+ }
+
+ // Note: when there is no source map for an uri, this behaves like an
+ // identity function, returning the requested location as the result.
+
+ // Create a mock offset for the output location. We compute it in terms
+ // of the input line and column to minimize the chances that two different
+ // line and column locations are mapped to the same offset.
+ var offset = line * 1000000 + column;
+ var location = SourceLocation(offset,
+ line: line, column: column, sourceUrl: Uri.parse(uri));
+ return SourceMapSpan(location, location, '');
+ }
+}
+
+/// A map containing direct source mappings.
+class SingleMapping extends Mapping {
+ /// Source urls used in the mapping, indexed by id.
+ final List<String> urls;
+
+ /// Source names used in the mapping, indexed by id.
+ final List<String> names;
+
+ /// The [SourceFile]s to which the entries in [lines] refer.
+ ///
+ /// This is in the same order as [urls]. If this was constructed using
+ /// [SingleMapping.fromEntries], this contains files from any [FileLocation]s
+ /// used to build the mapping. If it was parsed from JSON, it contains files
+ /// for any sources whose contents were provided via the `"sourcesContent"`
+ /// field.
+ ///
+ /// Files whose contents aren't available are `null`.
+ final List<SourceFile?> files;
+
+ /// Entries indicating the beginning of each span.
+ final List<TargetLineEntry> lines;
+
+ /// Url of the target file.
+ String? targetUrl;
+
+ /// Source root prepended to all entries in [urls].
+ String? sourceRoot;
+
+ final Uri? _mapUrl;
+
+ final Map<String, dynamic> extensions;
+
+ SingleMapping._(this.targetUrl, this.files, this.urls, this.names, this.lines)
+ : _mapUrl = null,
+ extensions = {};
+
+ factory SingleMapping.fromEntries(Iterable<builder.Entry> entries,
+ [String? fileUrl]) {
+ // The entries needs to be sorted by the target offsets.
+ var sourceEntries = entries.toList()..sort();
+ var lines = <TargetLineEntry>[];
+
+ // Indices associated with file urls that will be part of the source map. We
+ // rely on map order so that `urls.keys[urls[u]] == u`
+ var urls = <String, int>{};
+
+ // Indices associated with identifiers that will be part of the source map.
+ // We rely on map order so that `names.keys[names[n]] == n`
+ var names = <String, int>{};
+
+ /// The file for each URL, indexed by [urls]' values.
+ var files = <int, SourceFile>{};
+
+ int? lineNum;
+ late List<TargetEntry> targetEntries;
+ for (var sourceEntry in sourceEntries) {
+ if (lineNum == null || sourceEntry.target.line > lineNum) {
+ lineNum = sourceEntry.target.line;
+ targetEntries = <TargetEntry>[];
+ lines.add(TargetLineEntry(lineNum, targetEntries));
+ }
+
+ var sourceUrl = sourceEntry.source.sourceUrl;
+ var urlId = urls.putIfAbsent(
+ sourceUrl == null ? '' : sourceUrl.toString(), () => urls.length);
+
+ if (sourceEntry.source is FileLocation) {
+ files.putIfAbsent(
+ urlId, () => (sourceEntry.source as FileLocation).file);
+ }
+
+ var sourceEntryIdentifierName = sourceEntry.identifierName;
+ var srcNameId = sourceEntryIdentifierName == null
+ ? null
+ : names.putIfAbsent(sourceEntryIdentifierName, () => names.length);
+ targetEntries.add(TargetEntry(sourceEntry.target.column, urlId,
+ sourceEntry.source.line, sourceEntry.source.column, srcNameId));
+ }
+ return SingleMapping._(fileUrl, urls.values.map((i) => files[i]).toList(),
+ urls.keys.toList(), names.keys.toList(), lines);
+ }
+
+ SingleMapping.fromJson(Map<String, dynamic> map, {Object? mapUrl})
+ : targetUrl = map['file'] as String?,
+ urls = List<String>.from(map['sources'] as List),
+ names = List<String>.from((map['names'] as List?) ?? []),
+ files = List.filled((map['sources'] as List).length, null),
+ sourceRoot = map['sourceRoot'] as String?,
+ lines = <TargetLineEntry>[],
+ _mapUrl = mapUrl is String ? Uri.parse(mapUrl) : (mapUrl as Uri?),
+ extensions = {} {
+ var sourcesContent = map['sourcesContent'] == null
+ ? const <String?>[]
+ : List<String?>.from(map['sourcesContent'] as List);
+ for (var i = 0; i < urls.length && i < sourcesContent.length; i++) {
+ var source = sourcesContent[i];
+ if (source == null) continue;
+ files[i] = SourceFile.fromString(source, url: urls[i]);
+ }
+
+ var line = 0;
+ var column = 0;
+ var srcUrlId = 0;
+ var srcLine = 0;
+ var srcColumn = 0;
+ var srcNameId = 0;
+ var tokenizer = _MappingTokenizer(map['mappings'] as String);
+ var entries = <TargetEntry>[];
+
+ while (tokenizer.hasTokens) {
+ if (tokenizer.nextKind.isNewLine) {
+ if (entries.isNotEmpty) {
+ lines.add(TargetLineEntry(line, entries));
+ entries = <TargetEntry>[];
+ }
+ line++;
+ column = 0;
+ tokenizer._consumeNewLine();
+ continue;
+ }
+
+ // Decode the next entry, using the previous encountered values to
+ // decode the relative values.
+ //
+ // We expect 1, 4, or 5 values. If present, values are expected in the
+ // following order:
+ // 0: the starting column in the current line of the generated file
+ // 1: the id of the original source file
+ // 2: the starting line in the original source
+ // 3: the starting column in the original source
+ // 4: the id of the original symbol name
+ // The values are relative to the previous encountered values.
+ if (tokenizer.nextKind.isNewSegment) throw _segmentError(0, line);
+ column += tokenizer._consumeValue();
+ if (!tokenizer.nextKind.isValue) {
+ entries.add(TargetEntry(column));
+ } else {
+ srcUrlId += tokenizer._consumeValue();
+ if (srcUrlId >= urls.length) {
+ throw StateError(
+ 'Invalid source url id. $targetUrl, $line, $srcUrlId');
+ }
+ if (!tokenizer.nextKind.isValue) throw _segmentError(2, line);
+ srcLine += tokenizer._consumeValue();
+ if (!tokenizer.nextKind.isValue) throw _segmentError(3, line);
+ srcColumn += tokenizer._consumeValue();
+ if (!tokenizer.nextKind.isValue) {
+ entries.add(TargetEntry(column, srcUrlId, srcLine, srcColumn));
+ } else {
+ srcNameId += tokenizer._consumeValue();
+ if (srcNameId >= names.length) {
+ throw StateError('Invalid name id: $targetUrl, $line, $srcNameId');
+ }
+ entries.add(
+ TargetEntry(column, srcUrlId, srcLine, srcColumn, srcNameId));
+ }
+ }
+ if (tokenizer.nextKind.isNewSegment) tokenizer._consumeNewSegment();
+ }
+ if (entries.isNotEmpty) {
+ lines.add(TargetLineEntry(line, entries));
+ }
+
+ map.forEach((name, value) {
+ if (name.startsWith('x_')) extensions[name] = value;
+ });
+ }
+
+ /// Encodes the Mapping mappings as a json map.
+ ///
+ /// If [includeSourceContents] is `true`, this includes the source file
+ /// contents from [files] in the map if possible.
+ Map<String, dynamic> toJson({bool includeSourceContents = false}) {
+ var buff = StringBuffer();
+ var line = 0;
+ var column = 0;
+ var srcLine = 0;
+ var srcColumn = 0;
+ var srcUrlId = 0;
+ var srcNameId = 0;
+ var first = true;
+
+ for (var entry in lines) {
+ var nextLine = entry.line;
+ if (nextLine > line) {
+ for (var i = line; i < nextLine; ++i) {
+ buff.write(';');
+ }
+ line = nextLine;
+ column = 0;
+ first = true;
+ }
+
+ for (var segment in entry.entries) {
+ if (!first) buff.write(',');
+ first = false;
+ column = _append(buff, column, segment.column);
+
+ // Encoding can be just the column offset if there is no source
+ // information.
+ var newUrlId = segment.sourceUrlId;
+ if (newUrlId == null) continue;
+ srcUrlId = _append(buff, srcUrlId, newUrlId);
+ srcLine = _append(buff, srcLine, segment.sourceLine!);
+ srcColumn = _append(buff, srcColumn, segment.sourceColumn!);
+
+ if (segment.sourceNameId == null) continue;
+ srcNameId = _append(buff, srcNameId, segment.sourceNameId!);
+ }
+ }
+
+ var result = <String, dynamic>{
+ 'version': 3,
+ 'sourceRoot': sourceRoot ?? '',
+ 'sources': urls,
+ 'names': names,
+ 'mappings': buff.toString(),
+ };
+ if (targetUrl != null) result['file'] = targetUrl!;
+
+ if (includeSourceContents) {
+ result['sourcesContent'] = files.map((file) => file?.getText(0)).toList();
+ }
+ extensions.forEach((name, value) => result[name] = value);
+
+ return result;
+ }
+
+ /// Appends to [buff] a VLQ encoding of [newValue] using the difference
+ /// between [oldValue] and [newValue]
+ static int _append(StringBuffer buff, int oldValue, int newValue) {
+ buff.writeAll(encodeVlq(newValue - oldValue));
+ return newValue;
+ }
+
+ StateError _segmentError(int seen, int line) =>
+ StateError('Invalid entry in sourcemap, expected 1, 4, or 5'
+ ' values, but got $seen.\ntargeturl: $targetUrl, line: $line');
+
+ /// Returns [TargetLineEntry] which includes the location in the target [line]
+ /// number. In particular, the resulting entry is the last entry whose line
+ /// number is lower or equal to [line].
+ TargetLineEntry? _findLine(int line) {
+ var index = binarySearch(lines, (e) => e.line > line);
+ return (index <= 0) ? null : lines[index - 1];
+ }
+
+ /// Returns [TargetEntry] which includes the location denoted by
+ /// [line], [column]. If [lineEntry] corresponds to [line], then this will be
+ /// the last entry whose column is lower or equal than [column]. If
+ /// [lineEntry] corresponds to a line prior to [line], then the result will be
+ /// the very last entry on that line.
+ TargetEntry? _findColumn(int line, int column, TargetLineEntry? lineEntry) {
+ if (lineEntry == null || lineEntry.entries.isEmpty) return null;
+ if (lineEntry.line != line) return lineEntry.entries.last;
+ var entries = lineEntry.entries;
+ var index = binarySearch(entries, (e) => e.column > column);
+ return (index <= 0) ? null : entries[index - 1];
+ }
+
+ @override
+ SourceMapSpan? spanFor(int line, int column,
+ {Map<String, SourceFile>? files, String? uri}) {
+ var entry = _findColumn(line, column, _findLine(line));
+ if (entry == null) return null;
+
+ var sourceUrlId = entry.sourceUrlId;
+ if (sourceUrlId == null) return null;
+
+ var url = urls[sourceUrlId];
+ if (sourceRoot != null) {
+ url = '$sourceRoot$url';
+ }
+
+ var sourceNameId = entry.sourceNameId;
+ var file = files?[url];
+ if (file != null) {
+ var start = file.getOffset(entry.sourceLine!, entry.sourceColumn);
+ if (sourceNameId != null) {
+ var text = names[sourceNameId];
+ return SourceMapFileSpan(file.span(start, start + text.length),
+ isIdentifier: true);
+ } else {
+ return SourceMapFileSpan(file.location(start).pointSpan());
+ }
+ } else {
+ var start = SourceLocation(0,
+ sourceUrl: _mapUrl?.resolve(url) ?? url,
+ line: entry.sourceLine,
+ column: entry.sourceColumn);
+
+ // Offset and other context is not available.
+ if (sourceNameId != null) {
+ return SourceMapSpan.identifier(start, names[sourceNameId]);
+ } else {
+ return SourceMapSpan(start, start, '');
+ }
+ }
+ }
+
+ @override
+ String toString() {
+ return (StringBuffer('$runtimeType : [')
+ ..write('targetUrl: ')
+ ..write(targetUrl)
+ ..write(', sourceRoot: ')
+ ..write(sourceRoot)
+ ..write(', urls: ')
+ ..write(urls)
+ ..write(', names: ')
+ ..write(names)
+ ..write(', lines: ')
+ ..write(lines)
+ ..write(']'))
+ .toString();
+ }
+
+ String get debugString {
+ var buff = StringBuffer();
+ for (var lineEntry in lines) {
+ var line = lineEntry.line;
+ for (var entry in lineEntry.entries) {
+ buff
+ ..write(targetUrl)
+ ..write(': ')
+ ..write(line)
+ ..write(':')
+ ..write(entry.column);
+ var sourceUrlId = entry.sourceUrlId;
+ if (sourceUrlId != null) {
+ buff
+ ..write(' --> ')
+ ..write(sourceRoot)
+ ..write(urls[sourceUrlId])
+ ..write(': ')
+ ..write(entry.sourceLine)
+ ..write(':')
+ ..write(entry.sourceColumn);
+ }
+ var sourceNameId = entry.sourceNameId;
+ if (sourceNameId != null) {
+ buff
+ ..write(' (')
+ ..write(names[sourceNameId])
+ ..write(')');
+ }
+ buff.write('\n');
+ }
+ }
+ return buff.toString();
+ }
+}
+
+/// A line entry read from a source map.
+class TargetLineEntry {
+ final int line;
+ List<TargetEntry> entries;
+ TargetLineEntry(this.line, this.entries);
+
+ @override
+ String toString() => '$runtimeType: $line $entries';
+}
+
+/// A target segment entry read from a source map
+class TargetEntry {
+ final int column;
+ final int? sourceUrlId;
+ final int? sourceLine;
+ final int? sourceColumn;
+ final int? sourceNameId;
+
+ TargetEntry(this.column,
+ [this.sourceUrlId,
+ this.sourceLine,
+ this.sourceColumn,
+ this.sourceNameId]);
+
+ @override
+ String toString() => '$runtimeType: '
+ '($column, $sourceUrlId, $sourceLine, $sourceColumn, $sourceNameId)';
+}
+
+/// A character iterator over a string that can peek one character ahead.
+class _MappingTokenizer implements Iterator<String> {
+ final String _internal;
+ final int _length;
+ int index = -1;
+ _MappingTokenizer(String internal)
+ : _internal = internal,
+ _length = internal.length;
+
+ // Iterator API is used by decodeVlq to consume VLQ entries.
+ @override
+ bool moveNext() => ++index < _length;
+
+ @override
+ String get current => (index >= 0 && index < _length)
+ ? _internal[index]
+ : throw RangeError.index(index, _internal);
+
+ bool get hasTokens => index < _length - 1 && _length > 0;
+
+ _TokenKind get nextKind {
+ if (!hasTokens) return _TokenKind.eof;
+ var next = _internal[index + 1];
+ if (next == ';') return _TokenKind.line;
+ if (next == ',') return _TokenKind.segment;
+ return _TokenKind.value;
+ }
+
+ int _consumeValue() => decodeVlq(this);
+ void _consumeNewLine() {
+ ++index;
+ }
+
+ void _consumeNewSegment() {
+ ++index;
+ }
+
+ // Print the state of the iterator, with colors indicating the current
+ // position.
+ @override
+ String toString() {
+ var buff = StringBuffer();
+ for (var i = 0; i < index; i++) {
+ buff.write(_internal[i]);
+ }
+ buff.write('[31m');
+ try {
+ buff.write(current);
+ // TODO: Determine whether this try / catch can be removed.
+ // ignore: avoid_catching_errors
+ } on RangeError catch (_) {}
+ buff.write('[0m');
+ for (var i = index + 1; i < _internal.length; i++) {
+ buff.write(_internal[i]);
+ }
+ buff.write(' ($index)');
+ return buff.toString();
+ }
+}
+
+class _TokenKind {
+ static const _TokenKind line = _TokenKind(isNewLine: true);
+ static const _TokenKind segment = _TokenKind(isNewSegment: true);
+ static const _TokenKind eof = _TokenKind(isEof: true);
+ static const _TokenKind value = _TokenKind();
+ final bool isNewLine;
+ final bool isNewSegment;
+ final bool isEof;
+ bool get isValue => !isNewLine && !isNewSegment && !isEof;
+
+ const _TokenKind(
+ {this.isNewLine = false, this.isNewSegment = false, this.isEof = false});
+}
diff --git a/pkgs/source_maps/lib/printer.dart b/pkgs/source_maps/lib/printer.dart
new file mode 100644
index 0000000..17733cd
--- /dev/null
+++ b/pkgs/source_maps/lib/printer.dart
@@ -0,0 +1,262 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Contains a code printer that generates code by recording the source maps.
+library source_maps.printer;
+
+import 'package:source_span/source_span.dart';
+
+import 'builder.dart';
+import 'src/source_map_span.dart';
+import 'src/utils.dart';
+
+/// A simple printer that keeps track of offset locations and records source
+/// maps locations.
+class Printer {
+ final String filename;
+ final StringBuffer _buff = StringBuffer();
+ final SourceMapBuilder _maps = SourceMapBuilder();
+ String get text => _buff.toString();
+ String get map => _maps.toJson(filename);
+
+ /// Current source location mapping.
+ SourceLocation? _loc;
+
+ /// Current line in the buffer;
+ int _line = 0;
+
+ /// Current column in the buffer.
+ int _column = 0;
+
+ Printer(this.filename);
+
+ /// Add [str] contents to the output, tracking new lines to track correct
+ /// positions for span locations. When [projectMarks] is true, this method
+ /// adds a source map location on each new line, projecting that every new
+ /// line in the target file (printed here) corresponds to a new line in the
+ /// source file.
+ void add(String str, {bool projectMarks = false}) {
+ var chars = str.runes.toList();
+ var length = chars.length;
+ for (var i = 0; i < length; i++) {
+ var c = chars[i];
+ if (c == lineFeed ||
+ (c == carriageReturn &&
+ (i + 1 == length || chars[i + 1] != lineFeed))) {
+ // Return not followed by line-feed is treated as a new line.
+ _line++;
+ _column = 0;
+ {
+ // **Warning**: Any calls to `mark` will change the value of `_loc`,
+ // so this local variable is no longer up to date after that point.
+ //
+ // This is why it has been put inside its own block to limit the
+ // scope in which it is available.
+ var loc = _loc;
+ if (projectMarks && loc != null) {
+ if (loc is FileLocation) {
+ var file = loc.file;
+ mark(file.location(file.getOffset(loc.line + 1)));
+ } else {
+ mark(SourceLocation(0,
+ sourceUrl: loc.sourceUrl, line: loc.line + 1, column: 0));
+ }
+ }
+ }
+ } else {
+ _column++;
+ }
+ }
+ _buff.write(str);
+ }
+
+ /// Append a [total] number of spaces in the target file. Typically used for
+ /// formatting indentation.
+ void addSpaces(int total) {
+ for (var i = 0; i < total; i++) {
+ _buff.write(' ');
+ }
+ _column += total;
+ }
+
+ /// Marks that the current point in the target file corresponds to the [mark]
+ /// in the source file, which can be either a [SourceLocation] or a
+ /// [SourceSpan]. When the mark is a [SourceMapSpan] with `isIdentifier` set,
+ /// this also records the name of the identifier in the source map
+ /// information.
+ void mark(Object mark) {
+ late final SourceLocation loc;
+ String? identifier;
+ if (mark is SourceLocation) {
+ loc = mark;
+ } else if (mark is SourceSpan) {
+ loc = mark.start;
+ if (mark is SourceMapSpan && mark.isIdentifier) identifier = mark.text;
+ }
+ _maps.addLocation(loc,
+ SourceLocation(_buff.length, line: _line, column: _column), identifier);
+ _loc = loc;
+ }
+}
+
+/// A more advanced printer that keeps track of offset locations to record
+/// source maps, but additionally allows nesting of different kind of items,
+/// including [NestedPrinter]s, and it let's you automatically indent text.
+///
+/// This class is especially useful when doing code generation, where different
+/// pieces of the code are generated independently on separate printers, and are
+/// finally put together in the end.
+class NestedPrinter implements NestedItem {
+ /// Items recoded by this printer, which can be [String] literals,
+ /// [NestedItem]s, and source map information like [SourceLocation] and
+ /// [SourceSpan].
+ final List<Object> _items = [];
+
+ /// Internal buffer to merge consecutive strings added to this printer.
+ StringBuffer? _buff;
+
+ /// Current indentation, which can be updated from outside this class.
+ int indent;
+
+ /// [Printer] used during the last call to [build], if any.
+ Printer? printer;
+
+ /// Returns the text produced after calling [build].
+ String? get text => printer?.text;
+
+ /// Returns the source-map information produced after calling [build].
+ String? get map => printer?.map;
+
+ /// Item used to indicate that the following item is copied from the original
+ /// source code, and hence we should preserve source-maps on every new line.
+ static final _original = Object();
+
+ NestedPrinter([this.indent = 0]);
+
+ /// Adds [object] to this printer. [object] can be a [String],
+ /// [NestedPrinter], or anything implementing [NestedItem]. If [object] is a
+ /// [String], the value is appended directly, without doing any formatting
+ /// changes. If you wish to add a line of code with automatic indentation, use
+ /// [addLine] instead. [NestedPrinter]s and [NestedItem]s are not processed
+ /// until [build] gets called later on. We ensure that [build] emits every
+ /// object in the order that they were added to this printer.
+ ///
+ /// The [location] and [span] parameters indicate the corresponding source map
+ /// location of [object] in the original input. Only one, [location] or
+ /// [span], should be provided at a time.
+ ///
+ /// Indicate [isOriginal] when [object] is copied directly from the user code.
+ /// Setting [isOriginal] will make this printer propagate source map locations
+ /// on every line-break.
+ void add(Object object,
+ {SourceLocation? location, SourceSpan? span, bool isOriginal = false}) {
+ if (object is! String || location != null || span != null || isOriginal) {
+ _flush();
+ assert(location == null || span == null);
+ if (location != null) _items.add(location);
+ if (span != null) _items.add(span);
+ if (isOriginal) _items.add(_original);
+ }
+
+ if (object is String) {
+ _appendString(object);
+ } else {
+ _items.add(object);
+ }
+ }
+
+ /// Append `2 * indent` spaces to this printer.
+ void insertIndent() => _indent(indent);
+
+ /// Add a [line], autoindenting to the current value of [indent]. Note,
+ /// indentation is not inferred from the contents added to this printer. If a
+ /// line starts or ends an indentation block, you need to also update [indent]
+ /// accordingly. Also, indentation is not adapted for nested printers. If
+ /// you add a [NestedPrinter] to this printer, its indentation is set
+ /// separately and will not include any the indentation set here.
+ ///
+ /// The [location] and [span] parameters indicate the corresponding source map
+ /// location of [line] in the original input. Only one, [location] or
+ /// [span], should be provided at a time.
+ void addLine(String? line, {SourceLocation? location, SourceSpan? span}) {
+ if (location != null || span != null) {
+ _flush();
+ assert(location == null || span == null);
+ if (location != null) _items.add(location);
+ if (span != null) _items.add(span);
+ }
+ if (line == null) return;
+ if (line != '') {
+ // We don't indent empty lines.
+ _indent(indent);
+ _appendString(line);
+ }
+ _appendString('\n');
+ }
+
+ /// Appends a string merging it with any previous strings, if possible.
+ void _appendString(String s) {
+ var buf = _buff ??= StringBuffer();
+ buf.write(s);
+ }
+
+ /// Adds all of the current [_buff] contents as a string item.
+ void _flush() {
+ if (_buff != null) {
+ _items.add(_buff.toString());
+ _buff = null;
+ }
+ }
+
+ void _indent(int indent) {
+ for (var i = 0; i < indent; i++) {
+ _appendString(' ');
+ }
+ }
+
+ /// Returns a string representation of all the contents appended to this
+ /// printer, including source map location tokens.
+ @override
+ String toString() {
+ _flush();
+ return (StringBuffer()..writeAll(_items)).toString();
+ }
+
+ /// Builds the output of this printer and source map information. After
+ /// calling this function, you can use [text] and [map] to retrieve the
+ /// geenrated code and source map information, respectively.
+ void build(String filename) {
+ writeTo(printer = Printer(filename));
+ }
+
+ /// Implements the [NestedItem] interface.
+ @override
+ void writeTo(Printer printer) {
+ _flush();
+ var propagate = false;
+ for (var item in _items) {
+ if (item is NestedItem) {
+ item.writeTo(printer);
+ } else if (item is String) {
+ printer.add(item, projectMarks: propagate);
+ propagate = false;
+ } else if (item is SourceLocation || item is SourceSpan) {
+ printer.mark(item);
+ } else if (item == _original) {
+ // we insert booleans when we are about to quote text that was copied
+ // from the original source. In such case, we will propagate marks on
+ // every new-line.
+ propagate = true;
+ } else {
+ throw UnsupportedError('Unknown item type: $item');
+ }
+ }
+ }
+}
+
+/// An item added to a [NestedPrinter].
+abstract class NestedItem {
+ /// Write the contents of this item into [printer].
+ void writeTo(Printer printer);
+}
diff --git a/pkgs/source_maps/lib/refactor.dart b/pkgs/source_maps/lib/refactor.dart
new file mode 100644
index 0000000..98e0c93
--- /dev/null
+++ b/pkgs/source_maps/lib/refactor.dart
@@ -0,0 +1,140 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Tools to help implement refactoring like transformations to Dart code.
+///
+/// [TextEditTransaction] supports making a series of changes to a text buffer.
+/// [guessIndent] helps to guess the appropriate indentiation for the new code.
+library source_maps.refactor;
+
+import 'package:source_span/source_span.dart';
+
+import 'printer.dart';
+import 'src/utils.dart';
+
+/// Editable text transaction.
+///
+/// Applies a series of edits using original location
+/// information, and composes them into the edited string.
+class TextEditTransaction {
+ final SourceFile? file;
+ final String original;
+ final _edits = <_TextEdit>[];
+
+ /// Creates a new transaction.
+ TextEditTransaction(this.original, this.file);
+
+ bool get hasEdits => _edits.isNotEmpty;
+
+ /// Edit the original text, replacing text on the range [begin] and [end]
+ /// with the [replacement]. [replacement] can be either a string or a
+ /// [NestedPrinter].
+ void edit(int begin, int end, Object replacement) {
+ _edits.add(_TextEdit(begin, end, replacement));
+ }
+
+ /// Create a source map [SourceLocation] for [offset], if [file] is not
+ /// `null`.
+ SourceLocation? _loc(int offset) => file?.location(offset);
+
+ /// Applies all pending [edit]s and returns a [NestedPrinter] containing the
+ /// rewritten string and source map information. [file]`.location` is given to
+ /// the underlying printer to indicate the name of the generated file that
+ /// will contains the source map information.
+ ///
+ /// Throws [UnsupportedError] if the edits were overlapping. If no edits were
+ /// made, the printer simply contains the original string.
+ NestedPrinter commit() {
+ var printer = NestedPrinter();
+ if (_edits.isEmpty) {
+ return printer..add(original, location: _loc(0), isOriginal: true);
+ }
+
+ // Sort edits by start location.
+ _edits.sort();
+
+ var consumed = 0;
+ for (var edit in _edits) {
+ if (consumed > edit.begin) {
+ var sb = StringBuffer();
+ sb
+ ..write(file?.location(edit.begin).toolString)
+ ..write(': overlapping edits. Insert at offset ')
+ ..write(edit.begin)
+ ..write(' but have consumed ')
+ ..write(consumed)
+ ..write(' input characters. List of edits:');
+ for (var e in _edits) {
+ sb
+ ..write('\n ')
+ ..write(e);
+ }
+ throw UnsupportedError(sb.toString());
+ }
+
+ // Add characters from the original string between this edit and the last
+ // one, if any.
+ var betweenEdits = original.substring(consumed, edit.begin);
+ printer
+ ..add(betweenEdits, location: _loc(consumed), isOriginal: true)
+ ..add(edit.replace, location: _loc(edit.begin));
+ consumed = edit.end;
+ }
+
+ // Add any text from the end of the original string that was not replaced.
+ printer.add(original.substring(consumed),
+ location: _loc(consumed), isOriginal: true);
+ return printer;
+ }
+}
+
+class _TextEdit implements Comparable<_TextEdit> {
+ final int begin;
+ final int end;
+
+ /// The replacement used by the edit, can be a string or a [NestedPrinter].
+ final Object replace;
+
+ _TextEdit(this.begin, this.end, this.replace);
+
+ int get length => end - begin;
+
+ @override
+ String toString() => '(Edit @ $begin,$end: "$replace")';
+
+ @override
+ int compareTo(_TextEdit other) {
+ var diff = begin - other.begin;
+ if (diff != 0) return diff;
+ return end - other.end;
+ }
+}
+
+/// Returns all whitespace characters at the start of [charOffset]'s line.
+String guessIndent(String code, int charOffset) {
+ // Find the beginning of the line
+ var lineStart = 0;
+ for (var i = charOffset - 1; i >= 0; i--) {
+ var c = code.codeUnitAt(i);
+ if (c == lineFeed || c == carriageReturn) {
+ lineStart = i + 1;
+ break;
+ }
+ }
+
+ // Grab all the whitespace
+ var whitespaceEnd = code.length;
+ for (var i = lineStart; i < code.length; i++) {
+ var c = code.codeUnitAt(i);
+ if (c != _space && c != _tab) {
+ whitespaceEnd = i;
+ break;
+ }
+ }
+
+ return code.substring(lineStart, whitespaceEnd);
+}
+
+const int _tab = 9;
+const int _space = 32;
diff --git a/pkgs/source_maps/lib/source_maps.dart b/pkgs/source_maps/lib/source_maps.dart
new file mode 100644
index 0000000..58f805a
--- /dev/null
+++ b/pkgs/source_maps/lib/source_maps.dart
@@ -0,0 +1,38 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Library to create and parse source maps.
+///
+/// Create a source map using [SourceMapBuilder]. For example:
+///
+/// ```dart
+/// var json = (new SourceMapBuilder()
+/// ..add(inputSpan1, outputSpan1)
+/// ..add(inputSpan2, outputSpan2)
+/// ..add(inputSpan3, outputSpan3)
+/// .toJson(outputFile);
+/// ```
+///
+/// Use the source_span package's [SourceSpan] and [SourceFile] classes to
+/// specify span locations.
+///
+/// Parse a source map using [parse], and call `spanFor` on the returned mapping
+/// object. For example:
+///
+/// ```dart
+/// var mapping = parse(json);
+/// mapping.spanFor(outputSpan1.line, outputSpan1.column)
+/// ```
+library source_maps;
+
+import 'package:source_span/source_span.dart';
+
+import 'builder.dart';
+import 'parser.dart';
+
+export 'builder.dart';
+export 'parser.dart';
+export 'printer.dart';
+export 'refactor.dart';
+export 'src/source_map_span.dart';
diff --git a/pkgs/source_maps/lib/src/source_map_span.dart b/pkgs/source_maps/lib/src/source_map_span.dart
new file mode 100644
index 0000000..aad8a32
--- /dev/null
+++ b/pkgs/source_maps/lib/src/source_map_span.dart
@@ -0,0 +1,72 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_span/source_span.dart';
+
+/// A [SourceSpan] for spans coming from or being written to source maps.
+///
+/// These spans have an extra piece of metadata: whether or not they represent
+/// an identifier (see [isIdentifier]).
+class SourceMapSpan extends SourceSpanBase {
+ /// Whether this span represents an identifier.
+ ///
+ /// If this is `true`, [text] is the value of the identifier.
+ final bool isIdentifier;
+
+ SourceMapSpan(super.start, super.end, super.text,
+ {this.isIdentifier = false});
+
+ /// Creates a [SourceMapSpan] for an identifier with value [text] starting at
+ /// [start].
+ ///
+ /// The [end] location is determined by adding [text] to [start].
+ SourceMapSpan.identifier(SourceLocation start, String text)
+ : this(
+ start,
+ SourceLocation(start.offset + text.length,
+ sourceUrl: start.sourceUrl,
+ line: start.line,
+ column: start.column + text.length),
+ text,
+ isIdentifier: true);
+}
+
+/// A wrapper aruond a [FileSpan] that implements [SourceMapSpan].
+class SourceMapFileSpan implements SourceMapSpan, FileSpan {
+ final FileSpan _inner;
+ @override
+ final bool isIdentifier;
+
+ @override
+ SourceFile get file => _inner.file;
+ @override
+ FileLocation get start => _inner.start;
+ @override
+ FileLocation get end => _inner.end;
+ @override
+ String get text => _inner.text;
+ @override
+ String get context => _inner.context;
+ @override
+ Uri? get sourceUrl => _inner.sourceUrl;
+ @override
+ int get length => _inner.length;
+
+ SourceMapFileSpan(this._inner, {this.isIdentifier = false});
+
+ @override
+ int compareTo(SourceSpan other) => _inner.compareTo(other);
+ @override
+ String highlight({Object? color}) => _inner.highlight(color: color);
+ @override
+ SourceSpan union(SourceSpan other) => _inner.union(other);
+ @override
+ FileSpan expand(FileSpan other) => _inner.expand(other);
+ @override
+ String message(String message, {Object? color}) =>
+ _inner.message(message, color: color);
+ @override
+ String toString() =>
+ _inner.toString().replaceAll('FileSpan', 'SourceMapFileSpan');
+}
diff --git a/pkgs/source_maps/lib/src/utils.dart b/pkgs/source_maps/lib/src/utils.dart
new file mode 100644
index 0000000..f70531e
--- /dev/null
+++ b/pkgs/source_maps/lib/src/utils.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Utilities that shouldn't be in this package.
+library source_maps.utils;
+
+/// Find the first entry in a sorted [list] that matches a monotonic predicate.
+/// Given a result `n`, that all items before `n` will not match, `n` matches,
+/// and all items after `n` match too. The result is -1 when there are no
+/// items, 0 when all items match, and list.length when none does.
+// TODO(sigmund): remove this function after dartbug.com/5624 is fixed.
+int binarySearch<T>(List<T> list, bool Function(T) matches) {
+ if (list.isEmpty) return -1;
+ if (matches(list.first)) return 0;
+ if (!matches(list.last)) return list.length;
+
+ var min = 0;
+ var max = list.length - 1;
+ while (min < max) {
+ var half = min + ((max - min) ~/ 2);
+ if (matches(list[half])) {
+ max = half;
+ } else {
+ min = half + 1;
+ }
+ }
+ return max;
+}
+
+const int lineFeed = 10;
+const int carriageReturn = 13;
diff --git a/pkgs/source_maps/lib/src/vlq.dart b/pkgs/source_maps/lib/src/vlq.dart
new file mode 100644
index 0000000..61b4768
--- /dev/null
+++ b/pkgs/source_maps/lib/src/vlq.dart
@@ -0,0 +1,101 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Utilities to encode and decode VLQ values used in source maps.
+///
+/// Sourcemaps are encoded with variable length numbers as base64 encoded
+/// strings with the least significant digit coming first. Each base64 digit
+/// encodes a 5-bit value (0-31) and a continuation bit. Signed values can be
+/// represented by using the least significant bit of the value as the sign bit.
+///
+/// For more details see the source map [version 3 documentation](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?usp=sharing).
+library source_maps.src.vlq;
+
+import 'dart:math';
+
+const int vlqBaseShift = 5;
+
+const int vlqBaseMask = (1 << 5) - 1;
+
+const int vlqContinuationBit = 1 << 5;
+
+const int vlqContinuationMask = 1 << 5;
+
+const String base64Digits =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+
+final Map<String, int> _digits = () {
+ var map = <String, int>{};
+ for (var i = 0; i < 64; i++) {
+ map[base64Digits[i]] = i;
+ }
+ return map;
+}();
+
+final int maxInt32 = (pow(2, 31) as int) - 1;
+final int minInt32 = -(pow(2, 31) as int);
+
+/// Creates the VLQ encoding of [value] as a sequence of characters
+Iterable<String> encodeVlq(int value) {
+ if (value < minInt32 || value > maxInt32) {
+ throw ArgumentError('expected 32 bit int, got: $value');
+ }
+ var res = <String>[];
+ var signBit = 0;
+ if (value < 0) {
+ signBit = 1;
+ value = -value;
+ }
+ value = (value << 1) | signBit;
+ do {
+ var digit = value & vlqBaseMask;
+ value >>= vlqBaseShift;
+ if (value > 0) {
+ digit |= vlqContinuationBit;
+ }
+ res.add(base64Digits[digit]);
+ } while (value > 0);
+ return res;
+}
+
+/// Decodes a value written as a sequence of VLQ characters. The first input
+/// character will be `chars.current` after calling `chars.moveNext` once. The
+/// iterator is advanced until a stop character is found (a character without
+/// the [vlqContinuationBit]).
+int decodeVlq(Iterator<String> chars) {
+ var result = 0;
+ var stop = false;
+ var shift = 0;
+ while (!stop) {
+ if (!chars.moveNext()) throw StateError('incomplete VLQ value');
+ var char = chars.current;
+ var digit = _digits[char];
+ if (digit == null) {
+ throw FormatException('invalid character in VLQ encoding: $char');
+ }
+ stop = (digit & vlqContinuationBit) == 0;
+ digit &= vlqBaseMask;
+ result += digit << shift;
+ shift += vlqBaseShift;
+ }
+
+ // Result uses the least significant bit as a sign bit. We convert it into a
+ // two-complement value. For example,
+ // 2 (10 binary) becomes 1
+ // 3 (11 binary) becomes -1
+ // 4 (100 binary) becomes 2
+ // 5 (101 binary) becomes -2
+ // 6 (110 binary) becomes 3
+ // 7 (111 binary) becomes -3
+ var negate = (result & 1) == 1;
+ result = result >> 1;
+ result = negate ? -result : result;
+
+ // TODO(sigmund): can we detect this earlier?
+ if (result < minInt32 || result > maxInt32) {
+ throw FormatException(
+ 'expected an encoded 32 bit int, but we got: $result');
+ }
+ return result;
+}
diff --git a/pkgs/source_maps/pubspec.yaml b/pkgs/source_maps/pubspec.yaml
new file mode 100644
index 0000000..fcc9925
--- /dev/null
+++ b/pkgs/source_maps/pubspec.yaml
@@ -0,0 +1,15 @@
+name: source_maps
+version: 0.10.13-wip
+description: A library to programmatically manipulate source map files.
+repository: https://github.com/dart-lang/source_maps
+
+environment:
+ sdk: ^3.3.0
+
+dependencies:
+ source_span: ^1.8.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^2.0.0
+ term_glyph: ^1.2.0
+ test: ^1.16.0
diff --git a/pkgs/source_maps/test/builder_test.dart b/pkgs/source_maps/test/builder_test.dart
new file mode 100644
index 0000000..4f773e7
--- /dev/null
+++ b/pkgs/source_maps/test/builder_test.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:source_maps/source_maps.dart';
+import 'package:test/test.dart';
+
+import 'common.dart';
+
+void main() {
+ test('builder - with span', () {
+ var map = (SourceMapBuilder()
+ ..addSpan(inputVar1, outputVar1)
+ ..addSpan(inputFunction, outputFunction)
+ ..addSpan(inputVar2, outputVar2)
+ ..addSpan(inputExpr, outputExpr))
+ .build(output.url.toString());
+ expect(map, equals(expectedMap));
+ });
+
+ test('builder - with location', () {
+ var str = (SourceMapBuilder()
+ ..addLocation(inputVar1.start, outputVar1.start, 'longVar1')
+ ..addLocation(inputFunction.start, outputFunction.start, 'longName')
+ ..addLocation(inputVar2.start, outputVar2.start, 'longVar2')
+ ..addLocation(inputExpr.start, outputExpr.start, null))
+ .toJson(output.url.toString());
+ expect(str, jsonEncode(expectedMap));
+ });
+}
diff --git a/pkgs/source_maps/test/common.dart b/pkgs/source_maps/test/common.dart
new file mode 100644
index 0000000..f6139de
--- /dev/null
+++ b/pkgs/source_maps/test/common.dart
@@ -0,0 +1,107 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Common input/output used by builder, parser and end2end tests
+library test.common;
+
+import 'package:source_maps/source_maps.dart';
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+/// Content of the source file
+const String inputContent = '''
+/** this is a comment. */
+int longVar1 = 3;
+
+// this is a comment too
+int longName(int longVar2) {
+ return longVar1 + longVar2;
+}
+''';
+final input = SourceFile.fromString(inputContent, url: 'input.dart');
+
+/// A span in the input file
+SourceMapSpan ispan(int start, int end, [bool isIdentifier = false]) =>
+ SourceMapFileSpan(input.span(start, end), isIdentifier: isIdentifier);
+
+SourceMapSpan inputVar1 = ispan(30, 38, true);
+SourceMapSpan inputFunction = ispan(74, 82, true);
+SourceMapSpan inputVar2 = ispan(87, 95, true);
+
+SourceMapSpan inputVar1NoSymbol = ispan(30, 38);
+SourceMapSpan inputFunctionNoSymbol = ispan(74, 82);
+SourceMapSpan inputVar2NoSymbol = ispan(87, 95);
+
+SourceMapSpan inputExpr = ispan(108, 127);
+
+/// Content of the target file
+const String outputContent = '''
+var x = 3;
+f(y) => x + y;
+''';
+final output = SourceFile.fromString(outputContent, url: 'output.dart');
+
+/// A span in the output file
+SourceMapSpan ospan(int start, int end, [bool isIdentifier = false]) =>
+ SourceMapFileSpan(output.span(start, end), isIdentifier: isIdentifier);
+
+SourceMapSpan outputVar1 = ospan(4, 5, true);
+SourceMapSpan outputFunction = ospan(11, 12, true);
+SourceMapSpan outputVar2 = ospan(13, 14, true);
+SourceMapSpan outputVar1NoSymbol = ospan(4, 5);
+SourceMapSpan outputFunctionNoSymbol = ospan(11, 12);
+SourceMapSpan outputVar2NoSymbol = ospan(13, 14);
+SourceMapSpan outputExpr = ospan(19, 24);
+
+/// Expected output mapping when recording the following four mappings:
+/// inputVar1 <= outputVar1
+/// inputFunction <= outputFunction
+/// inputVar2 <= outputVar2
+/// inputExpr <= outputExpr
+///
+/// This mapping is stored in the tests so we can independently test the builder
+/// and parser algorithms without relying entirely on end2end tests.
+const Map<String, dynamic> expectedMap = {
+ 'version': 3,
+ 'sourceRoot': '',
+ 'sources': ['input.dart'],
+ 'names': ['longVar1', 'longName', 'longVar2'],
+ 'mappings': 'IACIA;AAGAC,EAAaC,MACR',
+ 'file': 'output.dart'
+};
+
+void check(SourceSpan outputSpan, Mapping mapping, SourceMapSpan inputSpan,
+ bool realOffsets) {
+ var line = outputSpan.start.line;
+ var column = outputSpan.start.column;
+ var files = realOffsets ? {'input.dart': input} : null;
+ var span = mapping.spanFor(line, column, files: files)!;
+ var span2 = mapping.spanForLocation(outputSpan.start, files: files)!;
+
+ // Both mapping APIs are equivalent.
+ expect(span.start.offset, span2.start.offset);
+ expect(span.start.line, span2.start.line);
+ expect(span.start.column, span2.start.column);
+ expect(span.end.offset, span2.end.offset);
+ expect(span.end.line, span2.end.line);
+ expect(span.end.column, span2.end.column);
+
+ // Mapping matches our input location (modulo using real offsets)
+ expect(span.start.line, inputSpan.start.line);
+ expect(span.start.column, inputSpan.start.column);
+ expect(span.sourceUrl, inputSpan.sourceUrl);
+ expect(span.start.offset, realOffsets ? inputSpan.start.offset : 0);
+
+ // Mapping includes the identifier, if any
+ if (inputSpan.isIdentifier) {
+ expect(span.end.line, inputSpan.end.line);
+ expect(span.end.column, inputSpan.end.column);
+ expect(span.end.offset, span.start.offset + inputSpan.text.length);
+ if (realOffsets) expect(span.end.offset, inputSpan.end.offset);
+ } else {
+ expect(span.end.offset, span.start.offset);
+ expect(span.end.line, span.start.line);
+ expect(span.end.column, span.start.column);
+ }
+}
diff --git a/pkgs/source_maps/test/end2end_test.dart b/pkgs/source_maps/test/end2end_test.dart
new file mode 100644
index 0000000..84dd5ba
--- /dev/null
+++ b/pkgs/source_maps/test/end2end_test.dart
@@ -0,0 +1,160 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_maps/source_maps.dart';
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+import 'common.dart';
+
+void main() {
+ test('end-to-end setup', () {
+ expect(inputVar1.text, 'longVar1');
+ expect(inputFunction.text, 'longName');
+ expect(inputVar2.text, 'longVar2');
+ expect(inputVar1NoSymbol.text, 'longVar1');
+ expect(inputFunctionNoSymbol.text, 'longName');
+ expect(inputVar2NoSymbol.text, 'longVar2');
+ expect(inputExpr.text, 'longVar1 + longVar2');
+
+ expect(outputVar1.text, 'x');
+ expect(outputFunction.text, 'f');
+ expect(outputVar2.text, 'y');
+ expect(outputVar1NoSymbol.text, 'x');
+ expect(outputFunctionNoSymbol.text, 'f');
+ expect(outputVar2NoSymbol.text, 'y');
+ expect(outputExpr.text, 'x + y');
+ });
+
+ test('build + parse', () {
+ var map = (SourceMapBuilder()
+ ..addSpan(inputVar1, outputVar1)
+ ..addSpan(inputFunction, outputFunction)
+ ..addSpan(inputVar2, outputVar2)
+ ..addSpan(inputExpr, outputExpr))
+ .build(output.url.toString());
+ var mapping = parseJson(map);
+ check(outputVar1, mapping, inputVar1, false);
+ check(outputVar2, mapping, inputVar2, false);
+ check(outputFunction, mapping, inputFunction, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('build + parse - no symbols', () {
+ var map = (SourceMapBuilder()
+ ..addSpan(inputVar1NoSymbol, outputVar1NoSymbol)
+ ..addSpan(inputFunctionNoSymbol, outputFunctionNoSymbol)
+ ..addSpan(inputVar2NoSymbol, outputVar2NoSymbol)
+ ..addSpan(inputExpr, outputExpr))
+ .build(output.url.toString());
+ var mapping = parseJson(map);
+ check(outputVar1NoSymbol, mapping, inputVar1NoSymbol, false);
+ check(outputVar2NoSymbol, mapping, inputVar2NoSymbol, false);
+ check(outputFunctionNoSymbol, mapping, inputFunctionNoSymbol, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('build + parse, repeated entries', () {
+ var map = (SourceMapBuilder()
+ ..addSpan(inputVar1, outputVar1)
+ ..addSpan(inputVar1, outputVar1)
+ ..addSpan(inputFunction, outputFunction)
+ ..addSpan(inputFunction, outputFunction)
+ ..addSpan(inputVar2, outputVar2)
+ ..addSpan(inputVar2, outputVar2)
+ ..addSpan(inputExpr, outputExpr)
+ ..addSpan(inputExpr, outputExpr))
+ .build(output.url.toString());
+ var mapping = parseJson(map);
+ check(outputVar1, mapping, inputVar1, false);
+ check(outputVar2, mapping, inputVar2, false);
+ check(outputFunction, mapping, inputFunction, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('build + parse - no symbols, repeated entries', () {
+ var map = (SourceMapBuilder()
+ ..addSpan(inputVar1NoSymbol, outputVar1NoSymbol)
+ ..addSpan(inputVar1NoSymbol, outputVar1NoSymbol)
+ ..addSpan(inputFunctionNoSymbol, outputFunctionNoSymbol)
+ ..addSpan(inputFunctionNoSymbol, outputFunctionNoSymbol)
+ ..addSpan(inputVar2NoSymbol, outputVar2NoSymbol)
+ ..addSpan(inputVar2NoSymbol, outputVar2NoSymbol)
+ ..addSpan(inputExpr, outputExpr))
+ .build(output.url.toString());
+ var mapping = parseJson(map);
+ check(outputVar1NoSymbol, mapping, inputVar1NoSymbol, false);
+ check(outputVar2NoSymbol, mapping, inputVar2NoSymbol, false);
+ check(outputFunctionNoSymbol, mapping, inputFunctionNoSymbol, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('build + parse with file', () {
+ var json = (SourceMapBuilder()
+ ..addSpan(inputVar1, outputVar1)
+ ..addSpan(inputFunction, outputFunction)
+ ..addSpan(inputVar2, outputVar2)
+ ..addSpan(inputExpr, outputExpr))
+ .toJson(output.url.toString());
+ var mapping = parse(json);
+ check(outputVar1, mapping, inputVar1, true);
+ check(outputVar2, mapping, inputVar2, true);
+ check(outputFunction, mapping, inputFunction, true);
+ check(outputExpr, mapping, inputExpr, true);
+ });
+
+ test('printer projecting marks + parse', () {
+ var out = inputContent.replaceAll('long', '_s');
+ var file = SourceFile.fromString(out, url: 'output2.dart');
+ var printer = Printer('output2.dart');
+ printer.mark(ispan(0, 0));
+
+ var segments = inputContent.split('long');
+ expect(segments.length, 6);
+ printer.add(segments[0], projectMarks: true);
+ printer.mark(inputVar1);
+ printer.add('_s');
+ printer.add(segments[1], projectMarks: true);
+ printer.mark(inputFunction);
+ printer.add('_s');
+ printer.add(segments[2], projectMarks: true);
+ printer.mark(inputVar2);
+ printer.add('_s');
+ printer.add(segments[3], projectMarks: true);
+ printer.mark(inputExpr);
+ printer.add('_s');
+ printer.add(segments[4], projectMarks: true);
+ printer.add('_s');
+ printer.add(segments[5], projectMarks: true);
+
+ expect(printer.text, out);
+
+ var mapping = parse(printer.map);
+ void checkHelper(SourceMapSpan inputSpan, int adjustment) {
+ var start = inputSpan.start.offset - adjustment;
+ var end = (inputSpan.end.offset - adjustment) - 2;
+ var span = SourceMapFileSpan(file.span(start, end),
+ isIdentifier: inputSpan.isIdentifier);
+ check(span, mapping, inputSpan, true);
+ }
+
+ checkHelper(inputVar1, 0);
+ checkHelper(inputFunction, 2);
+ checkHelper(inputVar2, 4);
+ checkHelper(inputExpr, 6);
+
+ // We projected correctly lines that have no mappings
+ check(file.span(66, 66), mapping, ispan(45, 45), true);
+ check(file.span(63, 64), mapping, ispan(45, 45), true);
+ check(file.span(68, 68), mapping, ispan(70, 70), true);
+ check(file.span(71, 71), mapping, ispan(70, 70), true);
+
+ // Start of the last line
+ var oOffset = out.length - 2;
+ var iOffset = inputContent.length - 2;
+ check(file.span(oOffset, oOffset), mapping, ispan(iOffset, iOffset), true);
+ check(file.span(oOffset + 1, oOffset + 1), mapping, ispan(iOffset, iOffset),
+ true);
+ });
+}
diff --git a/pkgs/source_maps/test/parser_test.dart b/pkgs/source_maps/test/parser_test.dart
new file mode 100644
index 0000000..6cfe928
--- /dev/null
+++ b/pkgs/source_maps/test/parser_test.dart
@@ -0,0 +1,431 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: inference_failure_on_collection_literal
+// ignore_for_file: inference_failure_on_instance_creation
+
+import 'dart:convert';
+
+import 'package:source_maps/source_maps.dart';
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+import 'common.dart';
+
+const Map<String, dynamic> _mapWithNoSourceLocation = {
+ 'version': 3,
+ 'sourceRoot': '',
+ 'sources': ['input.dart'],
+ 'names': [],
+ 'mappings': 'A',
+ 'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocation = {
+ 'version': 3,
+ 'sourceRoot': '',
+ 'sources': ['input.dart'],
+ 'names': [],
+ 'mappings': 'AAAA',
+ 'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndMissingNames = {
+ 'version': 3,
+ 'sourceRoot': '',
+ 'sources': ['input.dart'],
+ 'mappings': 'AAAA',
+ 'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndName = {
+ 'version': 3,
+ 'sourceRoot': '',
+ 'sources': ['input.dart'],
+ 'names': ['var'],
+ 'mappings': 'AAAAA',
+ 'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndName1 = {
+ 'version': 3,
+ 'sourceRoot': 'pkg/',
+ 'sources': ['input1.dart'],
+ 'names': ['var1'],
+ 'mappings': 'AAAAA',
+ 'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndName2 = {
+ 'version': 3,
+ 'sourceRoot': 'pkg/',
+ 'sources': ['input2.dart'],
+ 'names': ['var2'],
+ 'mappings': 'AAAAA',
+ 'file': 'output2.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndName3 = {
+ 'version': 3,
+ 'sourceRoot': 'pkg/',
+ 'sources': ['input3.dart'],
+ 'names': ['var3'],
+ 'mappings': 'AAAAA',
+ 'file': '3/output.dart'
+};
+
+const _sourceMapBundle = [
+ _mapWithSourceLocationAndName1,
+ _mapWithSourceLocationAndName2,
+ _mapWithSourceLocationAndName3,
+];
+
+void main() {
+ test('parse', () {
+ var mapping = parseJson(expectedMap);
+ check(outputVar1, mapping, inputVar1, false);
+ check(outputVar2, mapping, inputVar2, false);
+ check(outputFunction, mapping, inputFunction, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('parse + json', () {
+ var mapping = parse(jsonEncode(expectedMap));
+ check(outputVar1, mapping, inputVar1, false);
+ check(outputVar2, mapping, inputVar2, false);
+ check(outputFunction, mapping, inputFunction, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('parse with file', () {
+ var mapping = parseJson(expectedMap);
+ check(outputVar1, mapping, inputVar1, true);
+ check(outputVar2, mapping, inputVar2, true);
+ check(outputFunction, mapping, inputFunction, true);
+ check(outputExpr, mapping, inputExpr, true);
+ });
+
+ test('parse with no source location', () {
+ var map = parse(jsonEncode(_mapWithNoSourceLocation)) as SingleMapping;
+ expect(map.lines.length, 1);
+ expect(map.lines.first.entries.length, 1);
+ var entry = map.lines.first.entries.first;
+
+ expect(entry.column, 0);
+ expect(entry.sourceUrlId, null);
+ expect(entry.sourceColumn, null);
+ expect(entry.sourceLine, null);
+ expect(entry.sourceNameId, null);
+ });
+
+ test('parse with source location and no name', () {
+ var map = parse(jsonEncode(_mapWithSourceLocation)) as SingleMapping;
+ expect(map.lines.length, 1);
+ expect(map.lines.first.entries.length, 1);
+ var entry = map.lines.first.entries.first;
+
+ expect(entry.column, 0);
+ expect(entry.sourceUrlId, 0);
+ expect(entry.sourceColumn, 0);
+ expect(entry.sourceLine, 0);
+ expect(entry.sourceNameId, null);
+ });
+
+ test('parse with source location and missing names entry', () {
+ var map = parse(jsonEncode(_mapWithSourceLocationAndMissingNames))
+ as SingleMapping;
+ expect(map.lines.length, 1);
+ expect(map.lines.first.entries.length, 1);
+ var entry = map.lines.first.entries.first;
+
+ expect(entry.column, 0);
+ expect(entry.sourceUrlId, 0);
+ expect(entry.sourceColumn, 0);
+ expect(entry.sourceLine, 0);
+ expect(entry.sourceNameId, null);
+ });
+
+ test('parse with source location and name', () {
+ var map = parse(jsonEncode(_mapWithSourceLocationAndName)) as SingleMapping;
+ expect(map.lines.length, 1);
+ expect(map.lines.first.entries.length, 1);
+ var entry = map.lines.first.entries.first;
+
+ expect(entry.sourceUrlId, 0);
+ expect(entry.sourceUrlId, 0);
+ expect(entry.sourceColumn, 0);
+ expect(entry.sourceLine, 0);
+ expect(entry.sourceNameId, 0);
+ });
+
+ test('parse with source root', () {
+ var inputMap = Map.from(_mapWithSourceLocation);
+ inputMap['sourceRoot'] = '/pkg/';
+ var mapping = parseJson(inputMap) as SingleMapping;
+ expect(mapping.spanFor(0, 0)?.sourceUrl, Uri.parse('/pkg/input.dart'));
+ expect(
+ mapping
+ .spanForLocation(
+ SourceLocation(0, sourceUrl: Uri.parse('ignored.dart')))
+ ?.sourceUrl,
+ Uri.parse('/pkg/input.dart'));
+
+ var newSourceRoot = '/new/';
+
+ mapping.sourceRoot = newSourceRoot;
+ inputMap['sourceRoot'] = newSourceRoot;
+
+ expect(mapping.toJson(), equals(inputMap));
+ });
+
+ test('parse with map URL', () {
+ var inputMap = Map.from(_mapWithSourceLocation);
+ inputMap['sourceRoot'] = 'pkg/';
+ var mapping = parseJson(inputMap, mapUrl: 'file:///path/to/map');
+ expect(mapping.spanFor(0, 0)?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input.dart'));
+ });
+
+ group('parse with bundle', () {
+ var mapping =
+ parseJsonExtended(_sourceMapBundle, mapUrl: 'file:///path/to/map');
+
+ test('simple', () {
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.file('/path/to/output.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.file('/path/to/output2.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.file('/path/to/3/output.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+
+ expect(
+ mapping.spanFor(0, 0, uri: 'file:///path/to/output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(
+ mapping.spanFor(0, 0, uri: 'file:///path/to/output2.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(
+ mapping
+ .spanFor(0, 0, uri: 'file:///path/to/3/output.dart')
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+ });
+
+ test('package uris', () {
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.parse('package:1/output.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.parse('package:2/output2.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.parse('package:3/output.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+
+ expect(mapping.spanFor(0, 0, uri: 'package:1/output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(mapping.spanFor(0, 0, uri: 'package:2/output2.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(mapping.spanFor(0, 0, uri: 'package:3/output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+ });
+
+ test('unmapped path', () {
+ var span = mapping.spanFor(0, 0, uri: 'unmapped_output.dart')!;
+ expect(span.sourceUrl, Uri.parse('unmapped_output.dart'));
+ expect(span.start.line, equals(0));
+ expect(span.start.column, equals(0));
+
+ span = mapping.spanFor(10, 5, uri: 'unmapped_output.dart')!;
+ expect(span.sourceUrl, Uri.parse('unmapped_output.dart'));
+ expect(span.start.line, equals(10));
+ expect(span.start.column, equals(5));
+ });
+
+ test('missing path', () {
+ expect(() => mapping.spanFor(0, 0), throwsA(anything));
+ });
+
+ test('incomplete paths', () {
+ expect(mapping.spanFor(0, 0, uri: 'output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(mapping.spanFor(0, 0, uri: 'output2.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(mapping.spanFor(0, 0, uri: '3/output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+ });
+
+ test('parseExtended', () {
+ var mapping = parseExtended(jsonEncode(_sourceMapBundle),
+ mapUrl: 'file:///path/to/map');
+
+ expect(mapping.spanFor(0, 0, uri: 'output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(mapping.spanFor(0, 0, uri: 'output2.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(mapping.spanFor(0, 0, uri: '3/output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+ });
+
+ test('build bundle incrementally', () {
+ var mapping = MappingBundle();
+
+ mapping.addMapping(parseJson(_mapWithSourceLocationAndName1,
+ mapUrl: 'file:///path/to/map') as SingleMapping);
+ expect(mapping.spanFor(0, 0, uri: 'output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+
+ expect(mapping.containsMapping('output2.dart'), isFalse);
+ mapping.addMapping(parseJson(_mapWithSourceLocationAndName2,
+ mapUrl: 'file:///path/to/map') as SingleMapping);
+ expect(mapping.containsMapping('output2.dart'), isTrue);
+ expect(mapping.spanFor(0, 0, uri: 'output2.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+
+ expect(mapping.containsMapping('3/output.dart'), isFalse);
+ mapping.addMapping(parseJson(_mapWithSourceLocationAndName3,
+ mapUrl: 'file:///path/to/map') as SingleMapping);
+ expect(mapping.containsMapping('3/output.dart'), isTrue);
+ expect(mapping.spanFor(0, 0, uri: '3/output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+ });
+
+ // Test that the source map can handle cases where the uri passed in is
+ // not from the expected host but it is still unambiguous which source
+ // map should be used.
+ test('different paths', () {
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.parse('http://localhost/output.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.parse('http://localhost/output2.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.parse('http://localhost/3/output.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+
+ expect(
+ mapping.spanFor(0, 0, uri: 'http://localhost/output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(
+ mapping
+ .spanFor(0, 0, uri: 'http://localhost/output2.dart')
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(
+ mapping
+ .spanFor(0, 0, uri: 'http://localhost/3/output.dart')
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+ });
+ });
+
+ test('parse and re-emit', () {
+ for (var expected in [
+ expectedMap,
+ _mapWithNoSourceLocation,
+ _mapWithSourceLocation,
+ _mapWithSourceLocationAndName
+ ]) {
+ var mapping = parseJson(expected) as SingleMapping;
+ expect(mapping.toJson(), equals(expected));
+
+ mapping = parseJsonExtended(expected) as SingleMapping;
+ expect(mapping.toJson(), equals(expected));
+ }
+
+ var mapping = parseJsonExtended(_sourceMapBundle) as MappingBundle;
+ expect(mapping.toJson(), equals(_sourceMapBundle));
+ });
+
+ test('parse extensions', () {
+ var map = Map.from(expectedMap);
+ map['x_foo'] = 'a';
+ map['x_bar'] = [3];
+ var mapping = parseJson(map) as SingleMapping;
+ expect(mapping.toJson(), equals(map));
+ expect(mapping.extensions['x_foo'], equals('a'));
+ expect((mapping.extensions['x_bar'] as List).first, equals(3));
+ });
+
+ group('source files', () {
+ group('from fromEntries()', () {
+ test('are null for non-FileLocations', () {
+ var mapping = SingleMapping.fromEntries([
+ Entry(SourceLocation(10, line: 1, column: 8), outputVar1.start, null)
+ ]);
+ expect(mapping.files, equals([null]));
+ });
+
+ test("use a file location's file", () {
+ var mapping = SingleMapping.fromEntries(
+ [Entry(inputVar1.start, outputVar1.start, null)]);
+ expect(mapping.files, equals([input]));
+ });
+ });
+
+ group('from parse()', () {
+ group('are null', () {
+ test('with no sourcesContent field', () {
+ var mapping = parseJson(expectedMap) as SingleMapping;
+ expect(mapping.files, equals([null]));
+ });
+
+ test('with null sourcesContent values', () {
+ var map = Map.from(expectedMap);
+ map['sourcesContent'] = [null];
+ var mapping = parseJson(map) as SingleMapping;
+ expect(mapping.files, equals([null]));
+ });
+
+ test('with a too-short sourcesContent', () {
+ var map = Map.from(expectedMap);
+ map['sourcesContent'] = [];
+ var mapping = parseJson(map) as SingleMapping;
+ expect(mapping.files, equals([null]));
+ });
+ });
+
+ test('are parsed from sourcesContent', () {
+ var map = Map.from(expectedMap);
+ map['sourcesContent'] = ['hello, world!'];
+ var mapping = parseJson(map) as SingleMapping;
+
+ var file = mapping.files[0]!;
+ expect(file.url, equals(Uri.parse('input.dart')));
+ expect(file.getText(0), equals('hello, world!'));
+ });
+ });
+ });
+}
diff --git a/pkgs/source_maps/test/printer_test.dart b/pkgs/source_maps/test/printer_test.dart
new file mode 100644
index 0000000..89265e3
--- /dev/null
+++ b/pkgs/source_maps/test/printer_test.dart
@@ -0,0 +1,126 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:source_maps/source_maps.dart';
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+import 'common.dart';
+
+void main() {
+ test('printer', () {
+ var printer = Printer('output.dart');
+ printer
+ ..add('var ')
+ ..mark(inputVar1)
+ ..add('x = 3;\n')
+ ..mark(inputFunction)
+ ..add('f(')
+ ..mark(inputVar2)
+ ..add('y) => ')
+ ..mark(inputExpr)
+ ..add('x + y;\n');
+ expect(printer.text, outputContent);
+ expect(printer.map, jsonEncode(expectedMap));
+ });
+
+ test('printer projecting marks', () {
+ var out = inputContent.replaceAll('long', '_s');
+ var printer = Printer('output2.dart');
+
+ var segments = inputContent.split('long');
+ expect(segments.length, 6);
+ printer
+ ..mark(ispan(0, 0))
+ ..add(segments[0], projectMarks: true)
+ ..mark(inputVar1)
+ ..add('_s')
+ ..add(segments[1], projectMarks: true)
+ ..mark(inputFunction)
+ ..add('_s')
+ ..add(segments[2], projectMarks: true)
+ ..mark(inputVar2)
+ ..add('_s')
+ ..add(segments[3], projectMarks: true)
+ ..mark(inputExpr)
+ ..add('_s')
+ ..add(segments[4], projectMarks: true)
+ ..add('_s')
+ ..add(segments[5], projectMarks: true);
+
+ expect(printer.text, out);
+ // 8 new lines in the source map:
+ expect(printer.map.split(';').length, 8);
+
+ SourceMapSpan asFixed(SourceMapSpan s) =>
+ SourceMapSpan(s.start, s.end, s.text, isIdentifier: s.isIdentifier);
+
+ // The result is the same if we use fixed positions
+ var printer2 = Printer('output2.dart');
+ printer2
+ ..mark(SourceLocation(0, sourceUrl: 'input.dart').pointSpan())
+ ..add(segments[0], projectMarks: true)
+ ..mark(asFixed(inputVar1))
+ ..add('_s')
+ ..add(segments[1], projectMarks: true)
+ ..mark(asFixed(inputFunction))
+ ..add('_s')
+ ..add(segments[2], projectMarks: true)
+ ..mark(asFixed(inputVar2))
+ ..add('_s')
+ ..add(segments[3], projectMarks: true)
+ ..mark(asFixed(inputExpr))
+ ..add('_s')
+ ..add(segments[4], projectMarks: true)
+ ..add('_s')
+ ..add(segments[5], projectMarks: true);
+
+ expect(printer2.text, out);
+ expect(printer2.map, printer.map);
+ });
+
+ group('nested printer', () {
+ test('simple use', () {
+ var printer = NestedPrinter();
+ printer
+ ..add('var ')
+ ..add('x = 3;\n', span: inputVar1)
+ ..add('f(', span: inputFunction)
+ ..add('y) => ', span: inputVar2)
+ ..add('x + y;\n', span: inputExpr)
+ ..build('output.dart');
+ expect(printer.text, outputContent);
+ expect(printer.map, jsonEncode(expectedMap));
+ });
+
+ test('nested use', () {
+ var printer = NestedPrinter();
+ printer
+ ..add('var ')
+ ..add(NestedPrinter()..add('x = 3;\n', span: inputVar1))
+ ..add('f(', span: inputFunction)
+ ..add(NestedPrinter()..add('y) => ', span: inputVar2))
+ ..add('x + y;\n', span: inputExpr)
+ ..build('output.dart');
+ expect(printer.text, outputContent);
+ expect(printer.map, jsonEncode(expectedMap));
+ });
+
+ test('add indentation', () {
+ var out = inputContent.replaceAll('long', '_s');
+ var lines = inputContent.trim().split('\n');
+ expect(lines.length, 7);
+ var printer = NestedPrinter();
+ for (var i = 0; i < lines.length; i++) {
+ if (i == 5) printer.indent++;
+ printer.addLine(lines[i].replaceAll('long', '_s').trim());
+ if (i == 5) printer.indent--;
+ }
+ printer.build('output.dart');
+ expect(printer.text, out);
+ });
+ });
+}
diff --git a/pkgs/source_maps/test/refactor_test.dart b/pkgs/source_maps/test/refactor_test.dart
new file mode 100644
index 0000000..5bc3818
--- /dev/null
+++ b/pkgs/source_maps/test/refactor_test.dart
@@ -0,0 +1,199 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_maps/parser.dart' show Mapping, parse;
+import 'package:source_maps/refactor.dart';
+import 'package:source_span/source_span.dart';
+import 'package:term_glyph/term_glyph.dart' as term_glyph;
+import 'package:test/test.dart';
+
+void main() {
+ setUpAll(() {
+ term_glyph.ascii = true;
+ });
+
+ group('conflict detection', () {
+ var original = '0123456789abcdefghij';
+ var file = SourceFile.fromString(original);
+
+ test('no conflict, in order', () {
+ var txn = TextEditTransaction(original, file);
+ txn.edit(2, 4, '.');
+ txn.edit(5, 5, '|');
+ txn.edit(6, 6, '-');
+ txn.edit(6, 7, '_');
+ expect((txn.commit()..build('')).text, '01.4|5-_789abcdefghij');
+ });
+
+ test('no conflict, out of order', () {
+ var txn = TextEditTransaction(original, file);
+ txn.edit(2, 4, '.');
+ txn.edit(5, 5, '|');
+
+ // Regresion test for issue #404: there is no conflict/overlap for edits
+ // that don't remove any of the original code.
+ txn.edit(6, 7, '_');
+ txn.edit(6, 6, '-');
+ expect((txn.commit()..build('')).text, '01.4|5-_789abcdefghij');
+ });
+
+ test('conflict', () {
+ var txn = TextEditTransaction(original, file);
+ txn.edit(2, 4, '.');
+ txn.edit(3, 3, '-');
+ expect(
+ () => txn.commit(),
+ throwsA(
+ predicate((e) => e.toString().contains('overlapping edits'))));
+ });
+ });
+
+ test('generated source maps', () {
+ var original =
+ '0123456789\n0*23456789\n01*3456789\nabcdefghij\nabcd*fghij\n';
+ var file = SourceFile.fromString(original);
+ var txn = TextEditTransaction(original, file);
+ txn.edit(27, 29, '__\n ');
+ txn.edit(34, 35, '___');
+ var printer = (txn.commit()..build(''));
+ var output = printer.text;
+ var map = parse(printer.map!);
+ expect(output,
+ '0123456789\n0*23456789\n01*34__\n 789\na___cdefghij\nabcd*fghij\n');
+
+ // Line 1 and 2 are unmodified: mapping any column returns the beginning
+ // of the corresponding line:
+ expect(
+ _span(1, 1, map, file),
+ 'line 1, column 1: \n'
+ ' ,\n'
+ '1 | 0123456789\n'
+ ' | ^\n'
+ " '");
+ expect(
+ _span(1, 5, map, file),
+ 'line 1, column 1: \n'
+ ' ,\n'
+ '1 | 0123456789\n'
+ ' | ^\n'
+ " '");
+ expect(
+ _span(2, 1, map, file),
+ 'line 2, column 1: \n'
+ ' ,\n'
+ '2 | 0*23456789\n'
+ ' | ^\n'
+ " '");
+ expect(
+ _span(2, 8, map, file),
+ 'line 2, column 1: \n'
+ ' ,\n'
+ '2 | 0*23456789\n'
+ ' | ^\n'
+ " '");
+
+ // Line 3 is modified part way: mappings before the edits have the right
+ // mapping, after the edits the mapping is null.
+ expect(
+ _span(3, 1, map, file),
+ 'line 3, column 1: \n'
+ ' ,\n'
+ '3 | 01*3456789\n'
+ ' | ^\n'
+ " '");
+ expect(
+ _span(3, 5, map, file),
+ 'line 3, column 1: \n'
+ ' ,\n'
+ '3 | 01*3456789\n'
+ ' | ^\n'
+ " '");
+
+ // Start of edits map to beginning of the edit secion:
+ expect(
+ _span(3, 6, map, file),
+ 'line 3, column 6: \n'
+ ' ,\n'
+ '3 | 01*3456789\n'
+ ' | ^\n'
+ " '");
+ expect(
+ _span(3, 7, map, file),
+ 'line 3, column 6: \n'
+ ' ,\n'
+ '3 | 01*3456789\n'
+ ' | ^\n'
+ " '");
+
+ // Lines added have no mapping (they should inherit the last mapping),
+ // but the end of the edit region continues were we left off:
+ expect(_span(4, 1, map, file), isNull);
+ expect(
+ _span(4, 5, map, file),
+ 'line 3, column 8: \n'
+ ' ,\n'
+ '3 | 01*3456789\n'
+ ' | ^\n'
+ " '");
+
+ // Subsequent lines are still mapped correctly:
+ // a (in a___cd...)
+ expect(
+ _span(5, 1, map, file),
+ 'line 4, column 1: \n'
+ ' ,\n'
+ '4 | abcdefghij\n'
+ ' | ^\n'
+ " '");
+ // _ (in a___cd...)
+ expect(
+ _span(5, 2, map, file),
+ 'line 4, column 2: \n'
+ ' ,\n'
+ '4 | abcdefghij\n'
+ ' | ^\n'
+ " '");
+ // _ (in a___cd...)
+ expect(
+ _span(5, 3, map, file),
+ 'line 4, column 2: \n'
+ ' ,\n'
+ '4 | abcdefghij\n'
+ ' | ^\n'
+ " '");
+ // _ (in a___cd...)
+ expect(
+ _span(5, 4, map, file),
+ 'line 4, column 2: \n'
+ ' ,\n'
+ '4 | abcdefghij\n'
+ ' | ^\n'
+ " '");
+ // c (in a___cd...)
+ expect(
+ _span(5, 5, map, file),
+ 'line 4, column 3: \n'
+ ' ,\n'
+ '4 | abcdefghij\n'
+ ' | ^\n'
+ " '");
+ expect(
+ _span(6, 1, map, file),
+ 'line 5, column 1: \n'
+ ' ,\n'
+ '5 | abcd*fghij\n'
+ ' | ^\n'
+ " '");
+ expect(
+ _span(6, 8, map, file),
+ 'line 5, column 1: \n'
+ ' ,\n'
+ '5 | abcd*fghij\n'
+ ' | ^\n'
+ " '");
+ });
+}
+
+String? _span(int line, int column, Mapping map, SourceFile file) =>
+ map.spanFor(line - 1, column - 1, files: {'': file})?.message('').trim();
diff --git a/pkgs/source_maps/test/utils_test.dart b/pkgs/source_maps/test/utils_test.dart
new file mode 100644
index 0000000..4abdce2
--- /dev/null
+++ b/pkgs/source_maps/test/utils_test.dart
@@ -0,0 +1,53 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Tests for the binary search utility algorithm.
+library test.utils_test;
+
+import 'package:source_maps/src/utils.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('binary search', () {
+ test('empty', () {
+ expect(binarySearch([], (x) => true), -1);
+ });
+
+ test('single element', () {
+ expect(binarySearch([1], (x) => true), 0);
+ expect(binarySearch([1], (x) => false), 1);
+ });
+
+ test('no matches', () {
+ var list = [1, 2, 3, 4, 5, 6, 7];
+ expect(binarySearch(list, (x) => false), list.length);
+ });
+
+ test('all match', () {
+ var list = [1, 2, 3, 4, 5, 6, 7];
+ expect(binarySearch(list, (x) => true), 0);
+ });
+
+ test('compare with linear search', () {
+ for (var size = 0; size < 100; size++) {
+ var list = <int>[];
+ for (var i = 0; i < size; i++) {
+ list.add(i);
+ }
+ for (var pos = 0; pos <= size; pos++) {
+ expect(binarySearch(list, (x) => x >= pos),
+ _linearSearch(list, (x) => x >= pos));
+ }
+ }
+ });
+ });
+}
+
+int _linearSearch<T>(List<T> list, bool Function(T) predicate) {
+ if (list.isEmpty) return -1;
+ for (var i = 0; i < list.length; i++) {
+ if (predicate(list[i])) return i;
+ }
+ return list.length;
+}
diff --git a/pkgs/source_maps/test/vlq_test.dart b/pkgs/source_maps/test/vlq_test.dart
new file mode 100644
index 0000000..4568cff
--- /dev/null
+++ b/pkgs/source_maps/test/vlq_test.dart
@@ -0,0 +1,59 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:math';
+
+import 'package:source_maps/src/vlq.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('encode and decode - simple values', () {
+ expect(encodeVlq(1).join(''), 'C');
+ expect(encodeVlq(2).join(''), 'E');
+ expect(encodeVlq(3).join(''), 'G');
+ expect(encodeVlq(100).join(''), 'oG');
+ expect(decodeVlq('C'.split('').iterator), 1);
+ expect(decodeVlq('E'.split('').iterator), 2);
+ expect(decodeVlq('G'.split('').iterator), 3);
+ expect(decodeVlq('oG'.split('').iterator), 100);
+ });
+
+ test('encode and decode', () {
+ for (var i = -10000; i < 10000; i++) {
+ _checkEncodeDecode(i);
+ }
+ });
+
+ test('only 32-bit ints allowed', () {
+ var maxInt = (pow(2, 31) as int) - 1;
+ var minInt = -(pow(2, 31) as int);
+ _checkEncodeDecode(maxInt - 1);
+ _checkEncodeDecode(minInt + 1);
+ _checkEncodeDecode(maxInt);
+ _checkEncodeDecode(minInt);
+
+ expect(encodeVlq(minInt).join(''), 'hgggggE');
+ expect(decodeVlq('hgggggE'.split('').iterator), minInt);
+
+ expect(() => encodeVlq(maxInt + 1), throwsA(anything));
+ expect(() => encodeVlq(maxInt + 2), throwsA(anything));
+ expect(() => encodeVlq(minInt - 1), throwsA(anything));
+ expect(() => encodeVlq(minInt - 2), throwsA(anything));
+
+ // if we allowed more than 32 bits, these would be the expected encodings
+ // for the large numbers above.
+ expect(() => decodeVlq('ggggggE'.split('').iterator), throwsA(anything));
+ expect(() => decodeVlq('igggggE'.split('').iterator), throwsA(anything));
+ expect(() => decodeVlq('jgggggE'.split('').iterator), throwsA(anything));
+ expect(() => decodeVlq('lgggggE'.split('').iterator), throwsA(anything));
+ },
+ // This test uses integers so large they overflow in JS.
+ testOn: 'dart-vm');
+}
+
+void _checkEncodeDecode(int value) {
+ var encoded = encodeVlq(value);
+ expect(decodeVlq(encoded.iterator), value);
+ expect(decodeVlq(encoded.join('').split('').iterator), value);
+}