Index: pkg/glob/lib/src/ast.dart |
diff --git a/pkg/glob/lib/src/ast.dart b/pkg/glob/lib/src/ast.dart |
index 5e1da1a72c2fddff8fac2c64c6f3d132d8c0c0f9..380aa5164b4e08c22b4001c6aecb580ffac203dc 100644 |
--- a/pkg/glob/lib/src/ast.dart |
+++ b/pkg/glob/lib/src/ast.dart |
@@ -5,6 +5,7 @@ |
library glob.ast; |
import 'package:path/path.dart' as p; |
+import 'package:collection/collection.dart'; |
import 'utils.dart'; |
@@ -25,6 +26,17 @@ abstract class AstNode { |
/// Either this or [canMatchRelative] or both will be true. |
final bool canMatchRelative = true; |
+ /// 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])]); |
+ |
/// Returns whether this glob matches [string]. |
bool matches(String string) { |
if (_regExp == null) _regExp = new RegExp('^${_toRegExp()}\$'); |
@@ -46,8 +58,118 @@ class SequenceNode extends AstNode { |
SequenceNode(Iterable<AstNode> nodes) |
: nodes = nodes.toList(); |
+ OptionsNode flattenOptions() { |
+ if (nodes.isEmpty) return new OptionsNode([this]); |
+ |
+ 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([], (combined, node) { |
+ if (combined.isEmpty || combined.last is! LiteralNode || |
+ node is! LiteralNode) { |
+ return combined..add(node); |
+ } |
+ |
+ combined[combined.length - 1] = |
+ new LiteralNode(combined.last.text + node.text); |
+ return combined; |
+ })); |
+ })); |
+ } |
+ |
+ /// 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 = []; |
+ var currentComponent; |
+ |
+ addNode(node) { |
+ if (currentComponent == null) currentComponent = []; |
+ currentComponent.add(node); |
+ } |
+ |
+ finishComponent() { |
+ if (currentComponent == null) return; |
+ componentsToReturn.add(new SequenceNode(currentComponent)); |
+ currentComponent = null; |
+ } |
+ |
+ for (var node in nodes) { |
+ if (node is! LiteralNode || !node.text.contains('/')) { |
+ addNode(node); |
+ continue; |
+ } |
+ |
+ var text = node.text; |
+ if (context.style == p.Style.windows) text = text.replaceAll("/", "\\"); |
+ var 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)); |
+ } |
+ 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)); |
+ 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)); |
+ if (node.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(); |
} |
@@ -57,6 +179,10 @@ class StarNode extends AstNode { |
String _toRegExp() => '[^/]*'; |
+ bool operator==(Object other) => other is StarNode; |
+ |
+ int get hashCode => 0; |
+ |
String toString() => '*'; |
} |
@@ -94,6 +220,10 @@ class DoubleStarNode extends AstNode { |
return buffer.toString(); |
} |
+ bool operator==(Object other) => other is DoubleStarNode; |
+ |
+ int get hashCode => 1; |
+ |
String toString() => '**'; |
} |
@@ -103,6 +233,10 @@ class AnyCharNode extends AstNode { |
String _toRegExp() => '[^/]'; |
+ bool operator==(Object other) => other is AnyCharNode; |
+ |
+ int get hashCode => 2; |
+ |
String toString() => '?'; |
} |
@@ -119,6 +253,20 @@ class RangeNode extends AstNode { |
RangeNode(Iterable<Range> ranges, {this.negated}) |
: ranges = ranges.toSet(); |
+ 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])) |
+ ]); |
+ })); |
+ } |
+ |
String _toRegExp() { |
var buffer = new StringBuffer(); |
@@ -148,6 +296,14 @@ class RangeNode extends AstNode { |
return buffer.toString(); |
} |
+ bool operator==(Object other) { |
+ if (other is! RangeNode) return false; |
+ if (other.negated != negated) return false; |
+ return const SetEquality().equals(ranges, other.ranges); |
+ } |
+ |
+ int get hashCode => (negated ? 1 : 3) * const SetEquality().hash(ranges); |
+ |
String toString() { |
var buffer = new StringBuffer()..write('['); |
for (var range in ranges) { |
@@ -172,9 +328,17 @@ class OptionsNode extends AstNode { |
OptionsNode(Iterable<SequenceNode> options) |
: options = options.toList(); |
+ OptionsNode flattenOptions() => new OptionsNode( |
+ options.expand((option) => option.flattenOptions().options)); |
+ |
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(',')}}'; |
} |
@@ -196,9 +360,13 @@ class LiteralNode extends AstNode { |
bool get canMatchRelative => !canMatchAbsolute; |
- LiteralNode(this.text, this._context); |
+ LiteralNode(this.text, [this._context]); |
String _toRegExp() => regExpQuote(text); |
+ bool operator==(Object other) => other is LiteralNode && other.text == text; |
+ |
+ int get hashCode => text.hashCode; |
+ |
String toString() => text; |
} |