OLD | NEW |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library glob.ast; | |
6 | |
7 import 'package:path/path.dart' as p; | 5 import 'package:path/path.dart' as p; |
8 import 'package:collection/collection.dart'; | 6 import 'package:collection/collection.dart'; |
9 | 7 |
10 import 'utils.dart'; | 8 import 'utils.dart'; |
11 | 9 |
12 const _SEPARATOR = 0x2F; // "/" | 10 const _SEPARATOR = 0x2F; // "/" |
13 | 11 |
14 /// A node in the abstract syntax tree for a glob. | 12 /// A node in the abstract syntax tree for a glob. |
15 abstract class AstNode { | 13 abstract class AstNode { |
16 /// The cached regular expression that this AST was compiled into. | 14 /// The cached regular expression that this AST was compiled into. |
17 RegExp _regExp; | 15 RegExp _regExp; |
18 | 16 |
| 17 /// Whether this node matches case-sensitively or not. |
| 18 final bool caseSensitive; |
| 19 |
19 /// Whether this glob could match an absolute path. | 20 /// Whether this glob could match an absolute path. |
20 /// | 21 /// |
21 /// Either this or [canMatchRelative] or both will be true. | 22 /// Either this or [canMatchRelative] or both will be true. |
22 final bool canMatchAbsolute = false; | 23 bool get canMatchAbsolute => false; |
23 | 24 |
24 /// Whether this glob could match a relative path. | 25 /// Whether this glob could match a relative path. |
25 /// | 26 /// |
26 /// Either this or [canMatchRelative] or both will be true. | 27 /// Either this or [canMatchRelative] or both will be true. |
27 final bool canMatchRelative = true; | 28 bool get canMatchRelative => true; |
| 29 |
| 30 AstNode._(this.caseSensitive); |
28 | 31 |
29 /// Returns a new glob with all the options bubbled to the top level. | 32 /// Returns a new glob with all the options bubbled to the top level. |
30 /// | 33 /// |
31 /// In particular, this returns a glob AST with two guarantees: | 34 /// In particular, this returns a glob AST with two guarantees: |
32 /// | 35 /// |
33 /// 1. There are no [OptionsNode]s other than the one at the top level. | 36 /// 1. There are no [OptionsNode]s other than the one at the top level. |
34 /// 2. It matches the same set of paths as [this]. | 37 /// 2. It matches the same set of paths as [this]. |
35 /// | 38 /// |
36 /// For example, given the glob `{foo,bar}/{click/clack}`, this would return | 39 /// For example, given the glob `{foo,bar}/{click/clack}`, this would return |
37 /// `{foo/click,foo/clack,bar/click,bar/clack}`. | 40 /// `{foo/click,foo/clack,bar/click,bar/clack}`. |
38 OptionsNode flattenOptions() => new OptionsNode([new SequenceNode([this])]); | 41 OptionsNode flattenOptions() => new OptionsNode( |
| 42 [new SequenceNode([this], caseSensitive: caseSensitive)], |
| 43 caseSensitive: caseSensitive); |
39 | 44 |
40 /// Returns whether this glob matches [string]. | 45 /// Returns whether this glob matches [string]. |
41 bool matches(String string) { | 46 bool matches(String string) { |
42 if (_regExp == null) _regExp = new RegExp('^${_toRegExp()}\$'); | 47 if (_regExp == null) { |
| 48 _regExp = new RegExp('^${_toRegExp()}\$', caseSensitive: caseSensitive); |
| 49 } |
43 return _regExp.hasMatch(string); | 50 return _regExp.hasMatch(string); |
44 } | 51 } |
45 | 52 |
46 /// Subclasses should override this to return a regular expression component. | 53 /// Subclasses should override this to return a regular expression component. |
47 String _toRegExp(); | 54 String _toRegExp(); |
48 } | 55 } |
49 | 56 |
50 /// A sequence of adjacent AST nodes. | 57 /// A sequence of adjacent AST nodes. |
51 class SequenceNode extends AstNode { | 58 class SequenceNode extends AstNode { |
52 /// The nodes in the sequence. | 59 /// The nodes in the sequence. |
53 final List<AstNode> nodes; | 60 final List<AstNode> nodes; |
54 | 61 |
55 bool get canMatchAbsolute => nodes.first.canMatchAbsolute; | 62 bool get canMatchAbsolute => nodes.first.canMatchAbsolute; |
56 bool get canMatchRelative => nodes.first.canMatchRelative; | 63 bool get canMatchRelative => nodes.first.canMatchRelative; |
57 | 64 |
58 SequenceNode(Iterable<AstNode> nodes) | 65 SequenceNode(Iterable<AstNode> nodes, {bool caseSensitive: true}) |
59 : nodes = nodes.toList(); | 66 : nodes = nodes.toList(), |
| 67 super._(caseSensitive); |
60 | 68 |
61 OptionsNode flattenOptions() { | 69 OptionsNode flattenOptions() { |
62 if (nodes.isEmpty) return new OptionsNode([this]); | 70 if (nodes.isEmpty) { |
| 71 return new OptionsNode([this], caseSensitive: caseSensitive); |
| 72 } |
63 | 73 |
64 var sequences = nodes.first.flattenOptions().options | 74 var sequences = nodes.first.flattenOptions().options |
65 .map((sequence) => sequence.nodes); | 75 .map((sequence) => sequence.nodes); |
66 for (var node in nodes.skip(1)) { | 76 for (var node in nodes.skip(1)) { |
67 // Concatenate all sequences in the next options node ([nextSequences]) | 77 // Concatenate all sequences in the next options node ([nextSequences]) |
68 // onto all previous sequences ([sequences]). | 78 // onto all previous sequences ([sequences]). |
69 var nextSequences = node.flattenOptions().options; | 79 var nextSequences = node.flattenOptions().options; |
70 sequences = sequences.expand((sequence) { | 80 sequences = sequences.expand((sequence) { |
71 return nextSequences.map((nextSequence) { | 81 return nextSequences.map((nextSequence) { |
72 return sequence.toList()..addAll(nextSequence.nodes); | 82 return sequence.toList()..addAll(nextSequence.nodes); |
73 }); | 83 }); |
74 }); | 84 }); |
75 } | 85 } |
76 | 86 |
77 return new OptionsNode(sequences.map((sequence) { | 87 return new OptionsNode(sequences.map((sequence) { |
78 // Combine any adjacent LiteralNodes in [sequence]. | 88 // Combine any adjacent LiteralNodes in [sequence]. |
79 return new SequenceNode(sequence.fold([], (combined, node) { | 89 return new SequenceNode(sequence.fold/*<List<AstNode>>*/([], (combined, no
de) { |
80 if (combined.isEmpty || combined.last is! LiteralNode || | 90 if (combined.isEmpty || combined.last is! LiteralNode || |
81 node is! LiteralNode) { | 91 node is! LiteralNode) { |
82 return combined..add(node); | 92 return combined..add(node); |
83 } | 93 } |
84 | 94 |
85 combined[combined.length - 1] = | 95 combined[combined.length - 1] = new LiteralNode( |
86 new LiteralNode(combined.last.text + node.text); | 96 // TODO(nweiz): Avoid casting when sdk#25565 is fixed. |
| 97 (combined.last as LiteralNode).text + (node as LiteralNode).text, |
| 98 caseSensitive: caseSensitive); |
87 return combined; | 99 return combined; |
88 })); | 100 }), caseSensitive: caseSensitive); |
89 })); | 101 }), caseSensitive: caseSensitive); |
90 } | 102 } |
91 | 103 |
92 /// Splits this glob into components along its path separators. | 104 /// Splits this glob into components along its path separators. |
93 /// | 105 /// |
94 /// For example, given the glob `foo/*/*.dart`, this would return three globs: | 106 /// For example, given the glob `foo/*/*.dart`, this would return three globs: |
95 /// `foo`, `*`, and `*.dart`. | 107 /// `foo`, `*`, and `*.dart`. |
96 /// | 108 /// |
97 /// Path separators within options nodes are not split. For example, | 109 /// Path separators within options nodes are not split. For example, |
98 /// `foo/{bar,baz/bang}/qux` will return three globs: `foo`, `{bar,baz/bang}`, | 110 /// `foo/{bar,baz/bang}/qux` will return three globs: `foo`, `{bar,baz/bang}`, |
99 /// and `qux`. | 111 /// and `qux`. |
100 /// | 112 /// |
101 /// [context] is used to determine what absolute roots look like for this | 113 /// [context] is used to determine what absolute roots look like for this |
102 /// glob. | 114 /// glob. |
103 List<SequenceNode> split(p.Context context) { | 115 List<SequenceNode> split(p.Context context) { |
104 var componentsToReturn = []; | 116 var componentsToReturn = <SequenceNode>[]; |
105 var currentComponent; | 117 List<AstNode> currentComponent; |
106 | 118 |
107 addNode(node) { | 119 addNode(AstNode node) { |
108 if (currentComponent == null) currentComponent = []; | 120 if (currentComponent == null) currentComponent = []; |
109 currentComponent.add(node); | 121 currentComponent.add(node); |
110 } | 122 } |
111 | 123 |
112 finishComponent() { | 124 finishComponent() { |
113 if (currentComponent == null) return; | 125 if (currentComponent == null) return; |
114 componentsToReturn.add(new SequenceNode(currentComponent)); | 126 componentsToReturn.add( |
| 127 new SequenceNode(currentComponent, caseSensitive: caseSensitive)); |
115 currentComponent = null; | 128 currentComponent = null; |
116 } | 129 } |
117 | 130 |
118 for (var node in nodes) { | 131 for (var node in nodes) { |
119 if (node is! LiteralNode || !node.text.contains('/')) { | 132 if (node is! LiteralNode) { |
120 addNode(node); | 133 addNode(node); |
121 continue; | 134 continue; |
122 } | 135 } |
123 | 136 |
124 var text = node.text; | 137 // TODO(nweiz): Avoid casting when sdk#25565 is fixed. |
| 138 var literal = node as LiteralNode; |
| 139 if (!literal.text.contains('/')) { |
| 140 addNode(literal); |
| 141 continue; |
| 142 } |
| 143 |
| 144 var text = literal.text; |
125 if (context.style == p.Style.windows) text = text.replaceAll("/", "\\"); | 145 if (context.style == p.Style.windows) text = text.replaceAll("/", "\\"); |
126 var components = context.split(text); | 146 var components = context.split(text); |
127 | 147 |
128 // If the first component is absolute, that means it's a separator (on | 148 // If the first component is absolute, that means it's a separator (on |
129 // Windows some non-separator things are also absolute, but it's invalid | 149 // Windows some non-separator things are also absolute, but it's invalid |
130 // to have "C:" show up in the middle of a path anyway). | 150 // to have "C:" show up in the middle of a path anyway). |
131 if (context.isAbsolute(components.first)) { | 151 if (context.isAbsolute(components.first)) { |
132 // If this is the first component, it's the root. | 152 // If this is the first component, it's the root. |
133 if (componentsToReturn.isEmpty && currentComponent == null) { | 153 if (componentsToReturn.isEmpty && currentComponent == null) { |
134 var root = components.first; | 154 var root = components.first; |
135 if (context.style == p.Style.windows) { | 155 if (context.style == p.Style.windows) { |
136 // Above, we switched to backslashes to make [context.split] handle | 156 // Above, we switched to backslashes to make [context.split] handle |
137 // roots properly. That means that if there is a root, it'll still | 157 // roots properly. That means that if there is a root, it'll still |
138 // have backslashes, where forward slashes are required for globs. | 158 // have backslashes, where forward slashes are required for globs. |
139 // So we switch it back here. | 159 // So we switch it back here. |
140 root = root.replaceAll("\\", "/"); | 160 root = root.replaceAll("\\", "/"); |
141 } | 161 } |
142 addNode(new LiteralNode(root)); | 162 addNode(new LiteralNode(root, caseSensitive: caseSensitive)); |
143 } | 163 } |
144 finishComponent(); | 164 finishComponent(); |
145 components = components.skip(1); | 165 components = components.skip(1); |
146 if (components.isEmpty) continue; | 166 if (components.isEmpty) continue; |
147 } | 167 } |
148 | 168 |
149 // For each component except the last one, add a separate sequence to | 169 // For each component except the last one, add a separate sequence to |
150 // [sequences] containing only that component. | 170 // [sequences] containing only that component. |
151 for (var component in components.take(components.length - 1)) { | 171 for (var component in components.take(components.length - 1)) { |
152 addNode(new LiteralNode(component)); | 172 addNode(new LiteralNode(component, caseSensitive: caseSensitive)); |
153 finishComponent(); | 173 finishComponent(); |
154 } | 174 } |
155 | 175 |
156 // For the final component, only end its sequence (by adding a new empty | 176 // For the final component, only end its sequence (by adding a new empty |
157 // sequence) if it ends with a separator. | 177 // sequence) if it ends with a separator. |
158 addNode(new LiteralNode(components.last)); | 178 addNode(new LiteralNode(components.last, caseSensitive: caseSensitive)); |
159 if (node.text.endsWith('/')) finishComponent(); | 179 if (literal.text.endsWith('/')) finishComponent(); |
160 } | 180 } |
161 | 181 |
162 finishComponent(); | 182 finishComponent(); |
163 return componentsToReturn; | 183 return componentsToReturn; |
164 } | 184 } |
165 | 185 |
166 String _toRegExp() => nodes.map((node) => node._toRegExp()).join(); | 186 String _toRegExp() => nodes.map((node) => node._toRegExp()).join(); |
167 | 187 |
168 bool operator==(Object other) => other is SequenceNode && | 188 bool operator==(Object other) => other is SequenceNode && |
169 const IterableEquality().equals(nodes, other.nodes); | 189 const IterableEquality().equals(nodes, other.nodes); |
170 | 190 |
171 int get hashCode => const IterableEquality().hash(nodes); | 191 int get hashCode => const IterableEquality().hash(nodes); |
172 | 192 |
173 String toString() => nodes.join(); | 193 String toString() => nodes.join(); |
174 } | 194 } |
175 | 195 |
176 /// A node matching zero or more non-separator characters. | 196 /// A node matching zero or more non-separator characters. |
177 class StarNode extends AstNode { | 197 class StarNode extends AstNode { |
178 StarNode(); | 198 StarNode({bool caseSensitive: true}) : super._(caseSensitive); |
179 | 199 |
180 String _toRegExp() => '[^/]*'; | 200 String _toRegExp() => '[^/]*'; |
181 | 201 |
182 bool operator==(Object other) => other is StarNode; | 202 bool operator==(Object other) => other is StarNode; |
183 | 203 |
184 int get hashCode => 0; | 204 int get hashCode => 0; |
185 | 205 |
186 String toString() => '*'; | 206 String toString() => '*'; |
187 } | 207 } |
188 | 208 |
189 /// A node matching zero or more characters that may be separators. | 209 /// A node matching zero or more characters that may be separators. |
190 class DoubleStarNode extends AstNode { | 210 class DoubleStarNode extends AstNode { |
191 /// The path context for the glob. | 211 /// The path context for the glob. |
192 /// | 212 /// |
193 /// This is used to determine what absolute paths look like. | 213 /// This is used to determine what absolute paths look like. |
194 final p.Context _context; | 214 final p.Context _context; |
195 | 215 |
196 DoubleStarNode(this._context); | 216 DoubleStarNode(this._context, {bool caseSensitive: true}) |
| 217 : super._(caseSensitive); |
197 | 218 |
198 String _toRegExp() { | 219 String _toRegExp() { |
199 // Double star shouldn't match paths with a leading "../", since these paths | 220 // Double star shouldn't match paths with a leading "../", since these paths |
200 // wouldn't be listed with this glob. We only check for "../" at the | 221 // wouldn't be listed with this glob. We only check for "../" at the |
201 // beginning since the paths are normalized before being checked against the | 222 // beginning since the paths are normalized before being checked against the |
202 // glob. | 223 // glob. |
203 var buffer = new StringBuffer()..write(r'(?!^(?:\.\./|'); | 224 var buffer = new StringBuffer()..write(r'(?!^(?:\.\./|'); |
204 | 225 |
205 // A double star at the beginning of the glob also shouldn't match absolute | 226 // A double star at the beginning of the glob also shouldn't match absolute |
206 // paths, since those also wouldn't be listed. Which root patterns we look | 227 // paths, since those also wouldn't be listed. Which root patterns we look |
(...skipping 15 matching lines...) Expand all Loading... |
222 | 243 |
223 bool operator==(Object other) => other is DoubleStarNode; | 244 bool operator==(Object other) => other is DoubleStarNode; |
224 | 245 |
225 int get hashCode => 1; | 246 int get hashCode => 1; |
226 | 247 |
227 String toString() => '**'; | 248 String toString() => '**'; |
228 } | 249 } |
229 | 250 |
230 /// A node matching a single non-separator character. | 251 /// A node matching a single non-separator character. |
231 class AnyCharNode extends AstNode { | 252 class AnyCharNode extends AstNode { |
232 AnyCharNode(); | 253 AnyCharNode({bool caseSensitive: true}) : super._(caseSensitive); |
233 | 254 |
234 String _toRegExp() => '[^/]'; | 255 String _toRegExp() => '[^/]'; |
235 | 256 |
236 bool operator==(Object other) => other is AnyCharNode; | 257 bool operator==(Object other) => other is AnyCharNode; |
237 | 258 |
238 int get hashCode => 2; | 259 int get hashCode => 2; |
239 | 260 |
240 String toString() => '?'; | 261 String toString() => '?'; |
241 } | 262 } |
242 | 263 |
243 /// A node matching a single character in a range of options. | 264 /// A node matching a single character in a range of options. |
244 class RangeNode extends AstNode { | 265 class RangeNode extends AstNode { |
245 /// The ranges matched by this node. | 266 /// The ranges matched by this node. |
246 /// | 267 /// |
247 /// The ends of the ranges are unicode code points. | 268 /// The ends of the ranges are unicode code points. |
248 final Set<Range> ranges; | 269 final Set<Range> ranges; |
249 | 270 |
250 /// Whether this range was negated. | 271 /// Whether this range was negated. |
251 final bool negated; | 272 final bool negated; |
252 | 273 |
253 RangeNode(Iterable<Range> ranges, {this.negated}) | 274 RangeNode(Iterable<Range> ranges, {this.negated, bool caseSensitive: true}) |
254 : ranges = ranges.toSet(); | 275 : ranges = ranges.toSet(), |
| 276 super._(caseSensitive); |
255 | 277 |
256 OptionsNode flattenOptions() { | 278 OptionsNode flattenOptions() { |
257 if (negated || ranges.any((range) => !range.isSingleton)) { | 279 if (negated || ranges.any((range) => !range.isSingleton)) { |
258 return super.flattenOptions(); | 280 return super.flattenOptions(); |
259 } | 281 } |
260 | 282 |
261 // If a range explicitly lists a set of characters, return each character as | 283 // If a range explicitly lists a set of characters, return each character as |
262 // a separate expansion. | 284 // a separate expansion. |
263 return new OptionsNode(ranges.map((range) { | 285 return new OptionsNode(ranges.map((range) { |
264 return new SequenceNode([ | 286 return new SequenceNode([ |
265 new LiteralNode(new String.fromCharCodes([range.min])) | 287 new LiteralNode(new String.fromCharCodes([range.min]), |
266 ]); | 288 caseSensitive: caseSensitive) |
267 })); | 289 ], caseSensitive: caseSensitive); |
| 290 }), caseSensitive: caseSensitive); |
268 } | 291 } |
269 | 292 |
270 String _toRegExp() { | 293 String _toRegExp() { |
271 var buffer = new StringBuffer(); | 294 var buffer = new StringBuffer(); |
272 | 295 |
273 var containsSeparator = ranges.any((range) => range.contains(_SEPARATOR)); | 296 var containsSeparator = ranges.any((range) => range.contains(_SEPARATOR)); |
274 if (!negated && containsSeparator) { | 297 if (!negated && containsSeparator) { |
275 // Add `(?!/)` because ranges are never allowed to match separators. | 298 // Add `(?!/)` because ranges are never allowed to match separators. |
276 buffer.write('(?!/)'); | 299 buffer.write('(?!/)'); |
277 } | 300 } |
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
318 } | 341 } |
319 | 342 |
320 /// A node that matches one of several options. | 343 /// A node that matches one of several options. |
321 class OptionsNode extends AstNode { | 344 class OptionsNode extends AstNode { |
322 /// The options to match. | 345 /// The options to match. |
323 final List<SequenceNode> options; | 346 final List<SequenceNode> options; |
324 | 347 |
325 bool get canMatchAbsolute => options.any((node) => node.canMatchAbsolute); | 348 bool get canMatchAbsolute => options.any((node) => node.canMatchAbsolute); |
326 bool get canMatchRelative => options.any((node) => node.canMatchRelative); | 349 bool get canMatchRelative => options.any((node) => node.canMatchRelative); |
327 | 350 |
328 OptionsNode(Iterable<SequenceNode> options) | 351 OptionsNode(Iterable<SequenceNode> options, {bool caseSensitive: true}) |
329 : options = options.toList(); | 352 : options = options.toList(), |
| 353 super._(caseSensitive); |
330 | 354 |
331 OptionsNode flattenOptions() => new OptionsNode( | 355 OptionsNode flattenOptions() => new OptionsNode( |
332 options.expand((option) => option.flattenOptions().options)); | 356 options.expand((option) => option.flattenOptions().options), |
| 357 caseSensitive: caseSensitive); |
333 | 358 |
334 String _toRegExp() => | 359 String _toRegExp() => |
335 '(?:${options.map((option) => option._toRegExp()).join("|")})'; | 360 '(?:${options.map((option) => option._toRegExp()).join("|")})'; |
336 | 361 |
337 bool operator==(Object other) => other is OptionsNode && | 362 bool operator==(Object other) => other is OptionsNode && |
338 const UnorderedIterableEquality().equals(options, other.options); | 363 const UnorderedIterableEquality().equals(options, other.options); |
339 | 364 |
340 int get hashCode => const UnorderedIterableEquality().hash(options); | 365 int get hashCode => const UnorderedIterableEquality().hash(options); |
341 | 366 |
342 String toString() => '{${options.join(',')}}'; | 367 String toString() => '{${options.join(',')}}'; |
(...skipping 10 matching lines...) Expand all Loading... |
353 final p.Context _context; | 378 final p.Context _context; |
354 | 379 |
355 bool get canMatchAbsolute { | 380 bool get canMatchAbsolute { |
356 var nativeText = _context.style == p.Style.windows ? | 381 var nativeText = _context.style == p.Style.windows ? |
357 text.replaceAll('/', '\\') : text; | 382 text.replaceAll('/', '\\') : text; |
358 return _context.isAbsolute(nativeText); | 383 return _context.isAbsolute(nativeText); |
359 } | 384 } |
360 | 385 |
361 bool get canMatchRelative => !canMatchAbsolute; | 386 bool get canMatchRelative => !canMatchAbsolute; |
362 | 387 |
363 LiteralNode(this.text, [this._context]); | 388 LiteralNode(this.text, {p.Context context, bool caseSensitive: true}) |
| 389 : _context = context, |
| 390 super._(caseSensitive); |
364 | 391 |
365 String _toRegExp() => regExpQuote(text); | 392 String _toRegExp() => regExpQuote(text); |
366 | 393 |
367 bool operator==(Object other) => other is LiteralNode && other.text == text; | 394 bool operator==(Object other) => other is LiteralNode && other.text == text; |
368 | 395 |
369 int get hashCode => text.hashCode; | 396 int get hashCode => text.hashCode; |
370 | 397 |
371 String toString() => text; | 398 String toString() => text; |
372 } | 399 } |
OLD | NEW |