// Copyright (c) 2019, 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:typed_data'; import 'errors.dart'; import 'package_config_impl.dart'; import 'package_config_json.dart'; /// A package configuration. /// /// Associates configuration data to packages and files in packages. /// /// More members may be added to this class in the future, /// so classes outside of this package must not implement [PackageConfig] /// or any subclass of it. abstract class PackageConfig { /// The largest configuration version currently recognized. static const int maxVersion = 2; /// An empty package configuration. /// /// A package configuration with no available packages. /// Is used as a default value where a package configuration /// is expected, but none have been specified or found. static const PackageConfig empty = SimplePackageConfig.empty(); /// Creates a package configuration with the provided available [packages]. /// /// The packages must be valid packages (valid package name, valid /// absolute directory URIs, valid language version, if any), /// and there must not be two packages with the same name. /// /// The package's root ([Package.root]) and package-root /// ([Package.packageUriRoot]) paths must satisfy a number of constraints /// We say that one path (which we know ends with a `/` character) /// is inside another path, if the latter path is a prefix of the former path, /// including the two paths being the same. /// /// * No package's root must be the same as another package's root. /// * The package-root of a package must be inside the package's root. /// * If one package's package-root is inside another package's root, /// then the latter package's package root must not be inside the former /// package's root. (No getting between a package and its package root!) /// This also disallows a package's root being the same as another /// package's package root. /// /// If supplied, the [extraData] will be available as the /// [PackageConfig.extraData] of the created configuration. /// /// The version of the resulting configuration is always [maxVersion]. factory PackageConfig(Iterable packages, {Object? extraData}) => SimplePackageConfig(maxVersion, packages, extraData); /// Parses a package configuration file. /// /// The [bytes] must be an UTF-8 encoded JSON object /// containing a valid package configuration. /// /// The [baseUri] is used as the base for resolving relative /// URI references in the configuration file. If the configuration /// has been read from a file, the [baseUri] can be the URI of that /// file, or of the directory it occurs in. /// /// If [onError] is provided, errors found during parsing or building /// the configuration are reported by calling [onError] instead of /// throwing, and parser makes a *best effort* attempt to continue /// despite the error. The input must still be valid JSON. /// The result may be [PackageConfig.empty] if there is no way to /// extract useful information from the bytes. static PackageConfig parseBytes(Uint8List bytes, Uri baseUri, {void Function(Object error)? onError}) => parsePackageConfigBytes(bytes, baseUri, onError ?? throwError); /// Parses a package configuration file. /// /// The [configuration] must be a JSON object /// containing a valid package configuration. /// /// The [baseUri] is used as the base for resolving relative /// URI references in the configuration file. If the configuration /// has been read from a file, the [baseUri] can be the URI of that /// file, or of the directory it occurs in. /// /// If [onError] is provided, errors found during parsing or building /// the configuration are reported by calling [onError] instead of /// throwing, and parser makes a *best effort* attempt to continue /// despite the error. The input must still be valid JSON. /// The result may be [PackageConfig.empty] if there is no way to /// extract useful information from the bytes. static PackageConfig parseString(String configuration, Uri baseUri, {void Function(Object error)? onError}) => parsePackageConfigString(configuration, baseUri, onError ?? throwError); /// Parses the JSON data of a package configuration file. /// /// The [jsonData] must be a JSON-like Dart data structure, /// like the one provided by parsing JSON text using `dart:convert`, /// containing a valid package configuration. /// /// The [baseUri] is used as the base for resolving relative /// URI references in the configuration file. If the configuration /// has been read from a file, the [baseUri] can be the URI of that /// file, or of the directory it occurs in. /// /// If [onError] is provided, errors found during parsing or building /// the configuration are reported by calling [onError] instead of /// throwing, and parser makes a *best effort* attempt to continue /// despite the error. The input must still be valid JSON. /// The result may be [PackageConfig.empty] if there is no way to /// extract useful information from the bytes. static PackageConfig parseJson(Object? jsonData, Uri baseUri, {void Function(Object error)? onError}) => parsePackageConfigJson(jsonData, baseUri, onError ?? throwError); /// Writes a configuration file for this configuration on [output]. /// /// If [baseUri] is provided, URI references in the generated file /// will be made relative to [baseUri] where possible. static void writeBytes(PackageConfig configuration, Sink output, [Uri? baseUri]) { writePackageConfigJsonUtf8(configuration, baseUri, output); } /// Writes a configuration JSON text for this configuration on [output]. /// /// If [baseUri] is provided, URI references in the generated file /// will be made relative to [baseUri] where possible. static void writeString(PackageConfig configuration, StringSink output, [Uri? baseUri]) { writePackageConfigJsonString(configuration, baseUri, output); } /// Converts a configuration to a JSON-like data structure. /// /// If [baseUri] is provided, URI references in the generated data /// will be made relative to [baseUri] where possible. static Map toJson(PackageConfig configuration, [Uri? baseUri]) => packageConfigToJson(configuration, baseUri); /// The configuration version number. /// /// Currently this is 1 or 2, where /// * Version one is the `.packages` file format and /// * Version two is the first `package_config.json` format. /// /// Instances of this class supports both, and the version /// is only useful for detecting which kind of file the configuration /// was read from. int get version; /// All the available packages of this configuration. /// /// No two of these packages have the same name, /// and no two [Package.root] directories overlap. Iterable get packages; /// Look up a package by name. /// /// Returns the [Package] from [packages] with [packageName] as /// [Package.name]. Returns `null` if the package is not available in the /// current configuration. Package? operator [](String packageName); /// Provides the associated package for a specific [file] (or directory). /// /// Returns a [Package] which contains the [file]'s path, if any. /// That is, the [Package.root] directory is a parent directory /// of the [file]'s location. /// /// Returns `null` if the file does not belong to any package. Package? packageOf(Uri file); /// Resolves a `package:` URI to a non-package URI /// /// The [packageUri] must be a valid package URI. That means: /// * A URI with `package` as scheme, /// * with no authority part (`package://...`), /// * with a path starting with a valid package name followed by a slash, and /// * with no query or fragment part. /// /// Throws an [ArgumentError] (which also implements [PackageConfigError]) /// if the package URI is not valid. /// /// Returns `null` if the package name of [packageUri] is not available /// in this package configuration. /// Returns the remaining path of the package URI resolved relative to the /// [Package.packageUriRoot] of the corresponding package. Uri? resolve(Uri packageUri); /// The package URI which resolves to [nonPackageUri]. /// /// The [nonPackageUri] must not have any query or fragment part, /// and it must not have `package` as scheme. /// Throws an [ArgumentError] (which also implements [PackageConfigError]) /// if the non-package URI is not valid. /// /// Returns a package URI which [resolve] will convert to [nonPackageUri], /// if any such URI exists. Returns `null` if no such package URI exists. Uri? toPackageUri(Uri nonPackageUri); /// Extra data associated with the package configuration. /// /// The data may be in any format, depending on who introduced it. /// The standard `package_config.json` file storage will only store /// JSON-like list/map data structures. Object? get extraData; } /// Configuration data for a single package. abstract class Package { /// Creates a package with the provided properties. /// /// The [name] must be a valid package name. /// The [root] must be an absolute directory URI, meaning an absolute URI /// with no query or fragment path and a path starting and ending with `/`. /// The [packageUriRoot], if provided, must be either an absolute /// directory URI or a relative URI reference which is then resolved /// relative to [root]. It must then also be a subdirectory of [root], /// or the same directory, and must end with `/`. /// If [languageVersion] is supplied, it must be a valid Dart language /// version, which means two decimal integer literals separated by a `.`, /// where the integer literals have no leading zeros unless they are /// a single zero digit. /// /// The [relativeRoot] controls whether the [root] is written as /// relative to the `package_config.json` file when the package /// configuration is written to a file. It defaults to being relative. /// /// If [extraData] is supplied, it will be available as the /// [Package.extraData] of the created package. factory Package(String name, Uri root, {Uri? packageUriRoot, LanguageVersion? languageVersion, Object? extraData, bool relativeRoot = true}) => SimplePackage.validate(name, root, packageUriRoot, languageVersion, extraData, relativeRoot, throwError)!; /// The package-name of the package. String get name; /// The location of the root of the package. /// /// Is always an absolute URI with no query or fragment parts, /// and with a path ending in `/`. /// /// All files in the [root] directory are considered /// part of the package for purposes where that that matters. Uri get root; /// The root of the files available through `package:` URIs. /// /// A `package:` URI with [name] as the package name is /// resolved relative to this location. /// /// Is always an absolute URI with no query or fragment part /// with a path ending in `/`, /// and with a location which is a subdirectory /// of the [root], or the same as the [root]. Uri get packageUriRoot; /// The default language version associated with this package. /// /// Each package may have a default language version associated, /// which is the language version used to parse and compile /// Dart files in the package. /// A package version is defined by two non-negative numbers, /// the *major* and *minor* version numbers. /// /// A package may have no language version associated with it /// in the package configuration, in which case tools should /// use a default behavior for the package. LanguageVersion? get languageVersion; /// Extra data associated with the specific package. /// /// The data may be in any format, depending on who introduced it. /// The standard `package_config.json` file storage will only store /// JSON-like list/map data structures. Object? get extraData; /// Whether the [root] URI should be written as relative. /// /// When the configuration is written to a `package_config.json` /// file, the [root] URI can be either relative to the file /// location or absolute, controller by this value. bool get relativeRoot; } /// A language version. /// /// A language version is represented by two non-negative integers, /// the [major] and [minor] version numbers. /// /// If errors during parsing are handled using an `onError` handler, /// then an *invalid* language version may be represented by an /// [InvalidLanguageVersion] object. abstract class LanguageVersion implements Comparable { /// The maximal value allowed by [major] and [minor] values; static const int maxValue = 0x7FFFFFFF; /// Constructs a [LanguageVersion] with the specified /// [major] and [minor] version numbers. /// /// Both [major] and [minor] must be greater than or equal to 0 /// and less than or equal to [maxValue]. factory LanguageVersion(int major, int minor) { RangeError.checkValueInInterval(major, 0, maxValue, 'major'); RangeError.checkValueInInterval(minor, 0, maxValue, 'minor'); return SimpleLanguageVersion(major, minor, null); } /// Parses a language version string. /// /// A valid language version string has the form /// /// > *decimalNumber* `.` *decimalNumber* /// /// where a *decimalNumber* is a non-empty sequence of decimal digits /// with no unnecessary leading zeros (the decimal number only starts /// with a zero digit if that digit is the entire number). /// No spaces are allowed in the string. /// /// If the [source] is valid then it is parsed into a valid /// [LanguageVersion] object. /// If not, then the [onError] is called with a [FormatException]. /// If [onError] is not supplied, it defaults to throwing the exception. /// If the call does not throw, then an [InvalidLanguageVersion] is returned /// containing the original [source]. static LanguageVersion parse(String source, {void Function(Object error)? onError}) => parseLanguageVersion(source, onError ?? throwError); /// The major language version. /// /// A non-negative integer less than 231. /// /// The value is negative for objects representing *invalid* language /// versions ([InvalidLanguageVersion]). int get major; /// The minor language version. /// /// A non-negative integer less than 231. /// /// The value is negative for objects representing *invalid* language /// versions ([InvalidLanguageVersion]). int get minor; /// Compares language versions. /// /// Two language versions are considered equal if they have the /// same major and minor version numbers. /// /// A language version is greater than another if the former's major version /// is greater than the latter's major version, or if they have /// the same major version and the former's minor version is greater than /// the latter's. @override int compareTo(LanguageVersion other); /// Valid language versions with the same [major] and [minor] values are /// equal. /// /// Invalid language versions ([InvalidLanguageVersion]) are not equal to /// any other object. @override bool operator ==(Object other); @override int get hashCode; /// A string representation of the language version. /// /// A valid language version is represented as /// `"${version.major}.${version.minor}"`. @override String toString(); } /// An *invalid* language version. /// /// Stored in a [Package] when the original language version string /// was invalid and a `onError` handler was passed to the parser /// which did not throw on an error. abstract class InvalidLanguageVersion implements LanguageVersion { /// The value -1 for an invalid language version. @override int get major; /// The value -1 for an invalid language version. @override int get minor; /// An invalid language version is only equal to itself. @override bool operator ==(Object other); @override int get hashCode; /// The original invalid version string. @override String toString(); } /// Relational operators for [LanguageVersion] that /// compare valid versions with [LanguageVersion.compareTo]. /// /// If either operand is an [InvalidLanguageVersion], a [StateError] is thrown. /// Versions should be verified as valid after parsing and before using them. extension LanguageVersionRelationalOperators on LanguageVersion { /// Whether this language version is less than [other]. /// /// If either version being compared is an [InvalidLanguageVersion], /// a [StateError] is thrown. Verify versions are valid before comparing them. /// /// For details on how valid language versions are compared, /// check out [LanguageVersion.compareTo]. bool operator <(LanguageVersion other) { // Throw an error if comparing as or with an invalid language version. if (this is InvalidLanguageVersion) { _throwThisInvalid(); } else if (other is InvalidLanguageVersion) { _throwOtherInvalid(); } return compareTo(other) < 0; } /// Whether this language version is less than or equal to [other]. /// /// If either version being compared is an [InvalidLanguageVersion], /// a [StateError] is thrown. Verify versions are valid before comparing them. /// /// For details on how valid language versions are compared, /// check out [LanguageVersion.compareTo]. bool operator <=(LanguageVersion other) { // Throw an error if comparing as or with an invalid language version. if (this is InvalidLanguageVersion) { _throwThisInvalid(); } else if (other is InvalidLanguageVersion) { _throwOtherInvalid(); } return compareTo(other) <= 0; } /// Whether this language version is greater than [other]. /// /// If either version being compared is an [InvalidLanguageVersion], /// a [StateError] is thrown. Verify versions are valid before comparing them. /// /// For details on how valid language versions are compared, /// check out [LanguageVersion.compareTo]. bool operator >(LanguageVersion other) { // Throw an error if comparing as or with an invalid language version. if (this is InvalidLanguageVersion) { _throwThisInvalid(); } else if (other is InvalidLanguageVersion) { _throwOtherInvalid(); } return compareTo(other) > 0; } /// Whether this language version is greater than or equal to [other]. /// /// If either version being compared is an [InvalidLanguageVersion], /// a [StateError] is thrown. Verify versions are valid before comparing them. /// /// For details on how valid language versions are compared, /// check out [LanguageVersion.compareTo]. bool operator >=(LanguageVersion other) { // Throw an error if comparing as or with an invalid language version. if (this is InvalidLanguageVersion) { _throwThisInvalid(); } else if (other is InvalidLanguageVersion) { _throwOtherInvalid(); } return compareTo(other) >= 0; } static Never _throwThisInvalid() => throw StateError( 'Can\'t compare an invalid language version to another language version. ' 'Verify language versions are valid after parsing.'); static Never _throwOtherInvalid() => throw StateError( 'Can\'t compare a language version to an invalid language version. ' 'Verify language versions are valid after parsing.'); }