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