blob: 809d4bcf5f280b293954171bd0bd0559e8a33275 [file] [log] [blame]
// 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:path/path.dart' as p;
import 'package:collection/collection.dart';
import 'utils.dart';
const _SEPARATOR = 0x2F; // "/"
/// A node in the abstract syntax tree for a glob.
abstract class AstNode {
/// The cached regular expression that this AST was compiled into.
RegExp _regExp;
/// Whether this node matches case-sensitively or not.
final bool caseSensitive;
/// Whether this glob could match an absolute path.
///
/// Either this or [canMatchRelative] or both will be true.
bool get canMatchAbsolute => false;
/// Whether this glob could match a relative path.
///
/// Either this or [canMatchRelative] or both will be true.
bool get canMatchRelative => true;
AstNode._(this.caseSensitive);
/// Returns a new glob with all the options bubbled to the top level.
///
/// In particular, this returns a glob AST with two guarantees:
///
/// 1. There are no [OptionsNode]s other than the one at the top level.
/// 2. It matches the same set of paths as [this].
///
/// For example, given the glob `{foo,bar}/{click/clack}`, this would return
/// `{foo/click,foo/clack,bar/click,bar/clack}`.
OptionsNode flattenOptions() => new OptionsNode([
new SequenceNode([this], caseSensitive: caseSensitive)
], caseSensitive: caseSensitive);
/// Returns whether this glob matches [string].
bool matches(String string) {
if (_regExp == null) {
_regExp = new RegExp('^${_toRegExp()}\$', caseSensitive: caseSensitive);
}
return _regExp.hasMatch(string);
}
/// Subclasses should override this to return a regular expression component.
String _toRegExp();
}
/// A sequence of adjacent AST nodes.
class SequenceNode extends AstNode {
/// The nodes in the sequence.
final List<AstNode> nodes;
bool get canMatchAbsolute => nodes.first.canMatchAbsolute;
bool get canMatchRelative => nodes.first.canMatchRelative;
SequenceNode(Iterable<AstNode> nodes, {bool caseSensitive: true})
: nodes = nodes.toList(),
super._(caseSensitive);
OptionsNode flattenOptions() {
if (nodes.isEmpty) {
return new OptionsNode([this], caseSensitive: caseSensitive);
}
var sequences =
nodes.first.flattenOptions().options.map((sequence) => sequence.nodes);
for (var node in nodes.skip(1)) {
// Concatenate all sequences in the next options node ([nextSequences])
// onto all previous sequences ([sequences]).
var nextSequences = node.flattenOptions().options;
sequences = sequences.expand((sequence) {
return nextSequences.map((nextSequence) {
return sequence.toList()..addAll(nextSequence.nodes);
});
});
}
return new OptionsNode(sequences.map((sequence) {
// Combine any adjacent LiteralNodes in [sequence].
return new SequenceNode(
sequence.fold<List<AstNode>>([], (combined, node) {
if (combined.isEmpty ||
combined.last is! LiteralNode ||
node is! LiteralNode) {
return combined..add(node);
}
combined[combined.length - 1] = new LiteralNode(
// TODO(nweiz): Avoid casting when sdk#25565 is fixed.
(combined.last as LiteralNode).text +
(node as LiteralNode).text,
caseSensitive: caseSensitive);
return combined;
}),
caseSensitive: caseSensitive);
}), caseSensitive: caseSensitive);
}
/// Splits this glob into components along its path separators.
///
/// For example, given the glob `foo/*/*.dart`, this would return three globs:
/// `foo`, `*`, and `*.dart`.
///
/// Path separators within options nodes are not split. For example,
/// `foo/{bar,baz/bang}/qux` will return three globs: `foo`, `{bar,baz/bang}`,
/// and `qux`.
///
/// [context] is used to determine what absolute roots look like for this
/// glob.
List<SequenceNode> split(p.Context context) {
var componentsToReturn = <SequenceNode>[];
List<AstNode> currentComponent;
addNode(AstNode node) {
if (currentComponent == null) currentComponent = [];
currentComponent.add(node);
}
finishComponent() {
if (currentComponent == null) return;
componentsToReturn.add(
new SequenceNode(currentComponent, caseSensitive: caseSensitive));
currentComponent = null;
}
for (var node in nodes) {
if (node is! LiteralNode) {
addNode(node);
continue;
}
// TODO(nweiz): Avoid casting when sdk#25565 is fixed.
var literal = node as LiteralNode;
if (!literal.text.contains('/')) {
addNode(literal);
continue;
}
var text = literal.text;
if (context.style == p.Style.windows) text = text.replaceAll("/", "\\");
Iterable<String> components = context.split(text);
// If the first component is absolute, that means it's a separator (on
// Windows some non-separator things are also absolute, but it's invalid
// to have "C:" show up in the middle of a path anyway).
if (context.isAbsolute(components.first)) {
// If this is the first component, it's the root.
if (componentsToReturn.isEmpty && currentComponent == null) {
var root = components.first;
if (context.style == p.Style.windows) {
// Above, we switched to backslashes to make [context.split] handle
// roots properly. That means that if there is a root, it'll still
// have backslashes, where forward slashes are required for globs.
// So we switch it back here.
root = root.replaceAll("\\", "/");
}
addNode(new LiteralNode(root, caseSensitive: caseSensitive));
}
finishComponent();
components = components.skip(1);
if (components.isEmpty) continue;
}
// For each component except the last one, add a separate sequence to
// [sequences] containing only that component.
for (var component in components.take(components.length - 1)) {
addNode(new LiteralNode(component, caseSensitive: caseSensitive));
finishComponent();
}
// For the final component, only end its sequence (by adding a new empty
// sequence) if it ends with a separator.
addNode(new LiteralNode(components.last, caseSensitive: caseSensitive));
if (literal.text.endsWith('/')) finishComponent();
}
finishComponent();
return componentsToReturn;
}
String _toRegExp() => nodes.map((node) => node._toRegExp()).join();
bool operator ==(Object other) =>
other is SequenceNode &&
const IterableEquality().equals(nodes, other.nodes);
int get hashCode => const IterableEquality().hash(nodes);
String toString() => nodes.join();
}
/// A node matching zero or more non-separator characters.
class StarNode extends AstNode {
StarNode({bool caseSensitive: true}) : super._(caseSensitive);
String _toRegExp() => '[^/]*';
bool operator ==(Object other) => other is StarNode;
int get hashCode => 0;
String toString() => '*';
}
/// A node matching zero or more characters that may be separators.
class DoubleStarNode extends AstNode {
/// The path context for the glob.
///
/// This is used to determine what absolute paths look like.
final p.Context _context;
DoubleStarNode(this._context, {bool caseSensitive: true})
: super._(caseSensitive);
String _toRegExp() {
// Double star shouldn't match paths with a leading "../", since these paths
// wouldn't be listed with this glob. We only check for "../" at the
// beginning since the paths are normalized before being checked against the
// glob.
var buffer = new StringBuffer()..write(r'(?!^(?:\.\./|');
// A double star at the beginning of the glob also shouldn't match absolute
// paths, since those also wouldn't be listed. Which root patterns we look
// for depends on the style of path we're matching.
if (_context.style == p.Style.posix) {
buffer.write(r'/');
} else if (_context.style == p.Style.windows) {
buffer.write(r'//|[A-Za-z]:/');
} else {
assert(_context.style == p.Style.url);
buffer.write(r'[a-zA-Z][-+.a-zA-Z\d]*://|/');
}
// Use `[^]` rather than `.` so that it matches newlines as well.
buffer.write(r'))[^]*');
return buffer.toString();
}
bool operator ==(Object other) => other is DoubleStarNode;
int get hashCode => 1;
String toString() => '**';
}
/// A node matching a single non-separator character.
class AnyCharNode extends AstNode {
AnyCharNode({bool caseSensitive: true}) : super._(caseSensitive);
String _toRegExp() => '[^/]';
bool operator ==(Object other) => other is AnyCharNode;
int get hashCode => 2;
String toString() => '?';
}
/// A node matching a single character in a range of options.
class RangeNode extends AstNode {
/// The ranges matched by this node.
///
/// The ends of the ranges are unicode code points.
final Set<Range> ranges;
/// Whether this range was negated.
final bool negated;
RangeNode(Iterable<Range> ranges, {this.negated, bool caseSensitive: true})
: ranges = ranges.toSet(),
super._(caseSensitive);
OptionsNode flattenOptions() {
if (negated || ranges.any((range) => !range.isSingleton)) {
return super.flattenOptions();
}
// If a range explicitly lists a set of characters, return each character as
// a separate expansion.
return new OptionsNode(ranges.map((range) {
return new SequenceNode([
new LiteralNode(new String.fromCharCodes([range.min]),
caseSensitive: caseSensitive)
], caseSensitive: caseSensitive);
}), caseSensitive: caseSensitive);
}
String _toRegExp() {
var buffer = new StringBuffer();
var containsSeparator = ranges.any((range) => range.contains(_SEPARATOR));
if (!negated && containsSeparator) {
// Add `(?!/)` because ranges are never allowed to match separators.
buffer.write('(?!/)');
}
buffer.write('[');
if (negated) {
buffer.write('^');
// If the range doesn't itself exclude separators, exclude them ourselves,
// since ranges are never allowed to match them.
if (!containsSeparator) buffer.write('/');
}
for (var range in ranges) {
var start = new String.fromCharCodes([range.min]);
buffer.write(regExpQuote(start));
if (range.isSingleton) continue;
buffer.write('-');
buffer.write(regExpQuote(new String.fromCharCodes([range.max])));
}
buffer.write(']');
return buffer.toString();
}
bool operator ==(Object other) {
if (other is! RangeNode) return false;
if ((other as RangeNode).negated != negated) return false;
return const SetEquality().equals(ranges, (other as RangeNode).ranges);
}
int get hashCode => (negated ? 1 : 3) * const SetEquality().hash(ranges);
String toString() {
var buffer = new StringBuffer()..write('[');
for (var range in ranges) {
buffer.writeCharCode(range.min);
if (range.isSingleton) continue;
buffer.write('-');
buffer.writeCharCode(range.max);
}
buffer.write(']');
return buffer.toString();
}
}
/// A node that matches one of several options.
class OptionsNode extends AstNode {
/// The options to match.
final List<SequenceNode> options;
bool get canMatchAbsolute => options.any((node) => node.canMatchAbsolute);
bool get canMatchRelative => options.any((node) => node.canMatchRelative);
OptionsNode(Iterable<SequenceNode> options, {bool caseSensitive: true})
: options = options.toList(),
super._(caseSensitive);
OptionsNode flattenOptions() => new OptionsNode(
options.expand((option) => option.flattenOptions().options),
caseSensitive: caseSensitive);
String _toRegExp() =>
'(?:${options.map((option) => option._toRegExp()).join("|")})';
bool operator ==(Object other) =>
other is OptionsNode &&
const UnorderedIterableEquality().equals(options, other.options);
int get hashCode => const UnorderedIterableEquality().hash(options);
String toString() => '{${options.join(',')}}';
}
/// A node that matches a literal string.
class LiteralNode extends AstNode {
/// The string to match.
final String text;
/// The path context for the glob.
///
/// This is used to determine whether this could match an absolute path.
final p.Context _context;
bool get canMatchAbsolute {
var nativeText =
_context.style == p.Style.windows ? text.replaceAll('/', '\\') : text;
return _context.isAbsolute(nativeText);
}
bool get canMatchRelative => !canMatchAbsolute;
LiteralNode(this.text, {p.Context context, bool caseSensitive: true})
: _context = context,
super._(caseSensitive);
String _toRegExp() => regExpQuote(text);
bool operator ==(Object other) => other is LiteralNode && other.text == text;
int get hashCode => text.hashCode;
String toString() => text;
}