diff --git a/.github/ISSUE_TEMPLATE/source_maps.md b/.github/ISSUE_TEMPLATE/source_maps.md new file mode 100644 index 000000000..a1e390a75 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/source_maps.md @@ -0,0 +1,5 @@ +--- +name: "package:source_maps" +about: "Create a bug or file a feature request against package:source_maps." +labels: "package:source_maps" +--- \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml index 09e5425d0..85a16b4aa 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -96,6 +96,10 @@ - changed-files: - any-glob-to-any-file: 'pkgs/source_map_stack_trace/**' +'package:source_maps': + - changed-files: + - any-glob-to-any-file: 'pkgs/source_maps/**' + 'package:unified_analytics': - changed-files: - any-glob-to-any-file: 'pkgs/unified_analytics/**' diff --git a/.github/workflows/source_maps.yaml b/.github/workflows/source_maps.yaml new file mode 100644 index 000000000..2ae0f20c5 --- /dev/null +++ b/.github/workflows/source_maps.yaml @@ -0,0 +1,72 @@ +name: package:source_maps + +on: + # Run on PRs and pushes to the default branch. + push: + branches: [ main ] + paths: + - '.github/workflows/source_maps.yaml' + - 'pkgs/source_maps/**' + pull_request: + branches: [ main ] + paths: + - '.github/workflows/source_maps.yaml' + - 'pkgs/source_maps/**' + schedule: + - cron: "0 0 * * 0" + +env: + PUB_ENVIRONMENT: bot.github + + +defaults: + run: + working-directory: pkgs/source_maps/ + +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/README.md b/README.md index 7846ba911..41e78cf33 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ don't naturally belong to other topic monorepos (like | [pool](pkgs/pool/) | Manage a finite pool of resources. Useful for controlling concurrent file system or network requests. | [![package issues](https://img.shields.io/badge/package:pool-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apool) | [![pub package](https://img.shields.io/pub/v/pool.svg)](https://pub.dev/packages/pool) | | [pub_semver](pkgs/pub_semver/) | Versions and version constraints implementing pub's versioning policy. This is very similar to vanilla semver, with a few corner cases. | [![package issues](https://img.shields.io/badge/package:pub_semver-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apub_semver) | [![pub package](https://img.shields.io/pub/v/pub_semver.svg)](https://pub.dev/packages/pub_semver) | | [source_map_stack_trace](pkgs/source_map_stack_trace/) | A package for applying source maps to stack traces. | [![package issues](https://img.shields.io/badge/package:source_map_stack_trace-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_map_stack_trace) | [![pub package](https://img.shields.io/pub/v/source_map_stack_trace.svg)](https://pub.dev/packages/source_map_stack_trace) | +| [source_maps](pkgs/source_maps/) | A library to programmatically manipulate source map files. | [![package issues](https://img.shields.io/badge/package:source_maps-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_maps) | [![pub package](https://img.shields.io/pub/v/source_maps.svg)](https://pub.dev/packages/source_maps) | | [unified_analytics](pkgs/unified_analytics/) | A package for logging analytics for all Dart and Flutter related tooling to Google Analytics. | [![package issues](https://img.shields.io/badge/package:unified_analytics-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aunified_analytics) | [![pub package](https://img.shields.io/pub/v/unified_analytics.svg)](https://pub.dev/packages/unified_analytics) | ## Publishing automation diff --git a/pkgs/source_maps/.gitignore b/pkgs/source_maps/.gitignore new file mode 100644 index 000000000..f73b2f917 --- /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 000000000..ae7711e57 --- /dev/null +++ b/pkgs/source_maps/CHANGELOG.md @@ -0,0 +1,131 @@ +## 0.10.13 + +* Require Dart 3.3 +* Move to `dart-lang/tools` monorepo. + +## 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 000000000..162572a44 --- /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 000000000..cf8029177 --- /dev/null +++ b/pkgs/source_maps/README.md @@ -0,0 +1,25 @@ +[![Build Status](https://github.com/dart-lang/tools/actions/workflows/source_maps.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/source_maps.yaml) +[![pub package](https://img.shields.io/pub/v/source_maps.svg)](https://pub.dev/packages/source_maps) +[![package publisher](https://img.shields.io/pub/publisher/source_maps.svg)](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 000000000..d978f811c --- /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 000000000..54ba7433f --- /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 _entries = []; + + /// 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 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 { + /// 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 000000000..b699ac728 --- /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? 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? 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? 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? 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(), 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? files, String? uri}); + + /// Returns the span associated with [location]. + SourceMapSpan? spanForLocation(SourceLocation location, + {Map? 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 _lineStart = []; + + /// For each section, the start column offset. + final List _columnStart = []; + + /// For each section, the actual source map information, which is not adjusted + /// for offsets. + final List _maps = []; + + /// Creates a section mapping from json. + MultiSectionMapping.fromJson(List sections, Map? otherMaps, + {/*String|Uri*/ Object? mapUrl}) { + for (var section in sections.cast()) { + 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? 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 _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( + 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? files, String? uri}) { + // TODO: Remove type arg https://github.com/dart-lang/sdk/issues/42227 + uri = ArgumentError.checkNotNull(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 urls; + + /// Source names used in the mapping, indexed by id. + final List 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 files; + + /// Entries indicating the beginning of each span. + final List lines; + + /// Url of the target file. + String? targetUrl; + + /// Source root prepended to all entries in [urls]. + String? sourceRoot; + + final Uri? _mapUrl; + + final Map extensions; + + SingleMapping._(this.targetUrl, this.files, this.urls, this.names, this.lines) + : _mapUrl = null, + extensions = {}; + + factory SingleMapping.fromEntries(Iterable entries, + [String? fileUrl]) { + // The entries needs to be sorted by the target offsets. + var sourceEntries = entries.toList()..sort(); + var lines = []; + + // 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 = {}; + + // 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 = {}; + + /// The file for each URL, indexed by [urls]' values. + var files = {}; + + int? lineNum; + late List targetEntries; + for (var sourceEntry in sourceEntries) { + if (lineNum == null || sourceEntry.target.line > lineNum) { + lineNum = sourceEntry.target.line; + targetEntries = []; + 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 map, {Object? mapUrl}) + : targetUrl = map['file'] as String?, + urls = List.from(map['sources'] as List), + names = List.from((map['names'] as List?) ?? []), + files = List.filled((map['sources'] as List).length, null), + sourceRoot = map['sourceRoot'] as String?, + lines = [], + _mapUrl = mapUrl is String ? Uri.parse(mapUrl) : (mapUrl as Uri?), + extensions = {} { + var sourcesContent = map['sourcesContent'] == null + ? const [] + : List.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 = []; + + while (tokenizer.hasTokens) { + if (tokenizer.nextKind.isNewLine) { + if (entries.isNotEmpty) { + lines.add(TargetLineEntry(line, entries)); + entries = []; + } + 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 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 = { + '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? 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 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 { + 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(''); + try { + buff.write(current); + // TODO: Determine whether this try / catch can be removed. + // ignore: avoid_catching_errors + } on RangeError catch (_) {} + buff.write(''); + 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 000000000..17733cdd3 --- /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 _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 000000000..98e0c9345 --- /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 000000000..58f805a3a --- /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 000000000..aad8a32c6 --- /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 000000000..f70531e95 --- /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(List 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 000000000..61b476839 --- /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 _digits = () { + var map = {}; + 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 encodeVlq(int value) { + if (value < minInt32 || value > maxInt32) { + throw ArgumentError('expected 32 bit int, got: $value'); + } + var res = []; + 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 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 000000000..8518fa756 --- /dev/null +++ b/pkgs/source_maps/pubspec.yaml @@ -0,0 +1,15 @@ +name: source_maps +version: 0.10.13 +description: A library to programmatically manipulate source map files. +repository: https://github.com/dart-lang/tools/tree/main/pkgs/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 000000000..4f773e773 --- /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 000000000..f6139de47 --- /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 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 000000000..84dd5badc --- /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 000000000..6cfe928f2 --- /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 _mapWithNoSourceLocation = { + 'version': 3, + 'sourceRoot': '', + 'sources': ['input.dart'], + 'names': [], + 'mappings': 'A', + 'file': 'output.dart' +}; + +const Map _mapWithSourceLocation = { + 'version': 3, + 'sourceRoot': '', + 'sources': ['input.dart'], + 'names': [], + 'mappings': 'AAAA', + 'file': 'output.dart' +}; + +const Map _mapWithSourceLocationAndMissingNames = { + 'version': 3, + 'sourceRoot': '', + 'sources': ['input.dart'], + 'mappings': 'AAAA', + 'file': 'output.dart' +}; + +const Map _mapWithSourceLocationAndName = { + 'version': 3, + 'sourceRoot': '', + 'sources': ['input.dart'], + 'names': ['var'], + 'mappings': 'AAAAA', + 'file': 'output.dart' +}; + +const Map _mapWithSourceLocationAndName1 = { + 'version': 3, + 'sourceRoot': 'pkg/', + 'sources': ['input1.dart'], + 'names': ['var1'], + 'mappings': 'AAAAA', + 'file': 'output.dart' +}; + +const Map _mapWithSourceLocationAndName2 = { + 'version': 3, + 'sourceRoot': 'pkg/', + 'sources': ['input2.dart'], + 'names': ['var2'], + 'mappings': 'AAAAA', + 'file': 'output2.dart' +}; + +const Map _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 000000000..89265e36b --- /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 000000000..5bc3818e5 --- /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 000000000..4abdce298 --- /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 = []; + 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(List 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 000000000..4568cffc4 --- /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); +}