blob: bfa1ea6851c812f27ac6d283f630061aeb48afaa [file] [log] [blame] [edit]
// Copyright 2013 The Flutter Authors. 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:meta/meta.dart';
/// An identifier of a target that loosely follows [GN][] and [Bazel][] format.
///
/// [gn]: https://gn.googlesource.com/gn/+/master/docs/reference.md#labels
/// [bazel]: https://bazel.build/concepts/labels
///
/// A label in `engine_tool` are two string components separated by a colon in
/// the format `//path/to/package-name:target-name`, with constraints on the
/// characters that can be used in each component (see [package] and [target]).
///
/// Unlike counterparts in Bazel and GN:
/// - The package name is always a source-absolute path (i.e. starts with `//`).
/// - Valid identifier characters are `a-zA-Z0-9_-`, not starting with a digit.
/// - The target name is never empty, even when it is a default target.
@immutable
final class Label {
/// Creates a new label with the given package and target name.
///
/// If [target] is omitted, the final component of [package] is used as the
/// target name (e.g. `//name` becomes `//name:name`).
///
/// Throws a [FormatException] if the package or target name is invalid.
factory Label(String package, [String? target]) {
final FormatException? packageError = _checkPackage(package);
if (packageError != null) {
throw packageError;
}
target ??= package.substring(package.lastIndexOf('/') + 1);
final FormatException? targetError = _checkTarget(target);
if (targetError != null) {
throw targetError;
}
return Label._(package, target);
}
const Label._(this.package, this.target);
/// Parses a label from a string in the format `//package-name:target-name`.
///
/// Throws a [FormatException] if the label is invalid.
static Label parse(String label) {
final int colon = label.indexOf(':');
if (colon == -1) {
return Label(label);
}
return Label(label.substring(0, colon), label.substring(colon + 1));
}
/// Parses a label from a string in the format `//package-name:target-name`.
///
/// If a toolchain is present, it is removed.
///
/// Throws a [FormatException] if the label is invalid.
static Label parseGn(String label) {
// Remove (//build/toolchain/...) at the end of the label.
if (label.endsWith(')')) {
final int start = label.lastIndexOf('(', label.length - 2);
if (start != -1) {
label = label.substring(0, start);
}
}
return Label.parse(label);
}
/// A source-absolute package name, starting with `//`.
///
/// The package name must be a valid identifier, i.e. it must be a sequence of
/// identifier components separated by slashes and cannot end with a slash.
final String package;
/// A target name within the package.
///
/// The target name must be a valid identifier.
final String target;
@override
bool operator ==(Object other) {
return other is Label && package == other.package && target == other.target;
}
@override
int get hashCode => Object.hash(package, target);
/// Returns a string representation of the label (i.e. used by [parse]).
///
/// Use [toNinjaLabel] to remove the leading `//` from the package name.
@override
String toString() => '$package:$target';
/// Returns a Ninja-compatible string representation of the label.
///
/// `//package-name:target-name` becomes `package-name:target-name`.
String toNinjaLabel() => '${package.substring(2)}:$target';
static FormatException? _checkPackage(String package) {
// Must start with a double slash.
if (!package.startsWith('//')) {
return FormatException(
'Package name must start with "//".',
package,
);
}
// Cannot end with a slash.
if (package.endsWith('/')) {
return FormatException(
'Package name must not end with a slash.',
package,
);
}
// Check each component of the package name for valid identifier characters.
// We use a standard loop to give a better error message.
int i = 2;
while (true) {
final int j = package.indexOf('/', i);
final String component =
j == -1 ? package.substring(i) : package.substring(i, j);
if (!_identifier.hasMatch(component)) {
return FormatException(
'Package name component must be a valid identifier.',
package,
i,
);
}
if (j == -1) {
return null;
}
i = j + 1;
}
}
static FormatException? _checkTarget(String target) {
if (!_identifier.hasMatch(target)) {
return FormatException(
'Target name must be a valid identifier.',
target,
);
}
return null;
}
static final RegExp _identifier = RegExp(r'^[a-zA-Z_][a-zA-Z0-9_-]*$');
}
/// A generic target pattern that can be used to match multiple targets.
///
/// Similar to [Label], but supports wildcards in the package and target name:
/// - `//path/to/package/...` all targets in the package and its subpackages.
/// - `//path/to/package:all` all targets in the package.
///
/// Inspired by <https://bazel.build/run/build#specifying-build-targets>.
@immutable
final class TargetPattern {
/// Creates a new pattern with the given package and target name.
///
/// If [target] is omitted, the pattern matches all targets in the package.
factory TargetPattern(String package, [String? target]) {
// We are so close to a Label that we can reuse the same checks.
// The only difference is that the package name can end with `...`.
bool packageEndsWithWildcard = false;
if (package.endsWith('/...')) {
packageEndsWithWildcard = true;
package = package.substring(0, package.length - 4);
}
// Edgecase: //... is a valid pattern that matches all targets.
if (packageEndsWithWildcard && package == '/') {
return const TargetPattern._('//...', null);
}
// Throws a FormatException if the package or target name is invalid.
final Label label = Label(package, target);
return TargetPattern._(
packageEndsWithWildcard ? '${label.package}...' : label.package,
packageEndsWithWildcard ? null : label.target,
);
}
const TargetPattern._(this.package, this.target);
/// Parses a pattern from a string in the format `//package-name:target-name`.
///
/// Throws a [FormatException] if the pattern is invalid.
static TargetPattern parse(String pattern) {
final int colon = pattern.indexOf(':');
if (colon == -1) {
return TargetPattern(pattern);
}
return TargetPattern(
pattern.substring(0, colon),
pattern.substring(colon + 1),
);
}
/// A source-absolute package name optionally ending with `...`.
final String package;
/// A target name within the package, or `all` to match all targets.
///
/// If [package] ends with `...`, [target] must be `null`.
final String? target;
@override
bool operator ==(Object other) {
return other is TargetPattern &&
package == other.package &&
target == other.target;
}
@override
int get hashCode => Object.hash(package, target);
/// Returns a string representation of the target pattern.
///
/// The string representation is compatible with [parse].
@override
String toString() => target == null ? package : '$package:$target';
/// Returns a string representation of the target pattern in [GN format][gn].
///
/// [gn]: https://gn.googlesource.com/gn/+/master/docs/reference.md#labels
///
/// Specifically:
/// - `//package/to/package/...` is converted to `path/to/package/*`.
/// - `//package/to/package:all` is converted to `path/to/package:*`.
String toGnPattern() {
// Remove the leading `//` and replace `...` and `all`.
final String package = this.package.substring(2);
if (target == null) {
assert(package.endsWith('...'));
// Edgecase: //... is a valid pattern that matches all targets.
if (package == '...') {
return '/*';
}
return package.replaceRange(package.length - 3, package.length, '/*');
}
return '$package:${target == 'all' ? '*' : target}';
}
}