Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: lib/matcher.js

Issue 29375915: Issue 4878 - Start using ESLint for adblockpluscore (Closed)
Patch Set: Remove the arrow-parens rule Created March 9, 2017, 10:23 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
1 /* 1 /*
2 * This file is part of Adblock Plus <https://adblockplus.org/>, 2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-2016 Eyeo GmbH 3 * Copyright (C) 2006-2016 Eyeo GmbH
4 * 4 *
5 * Adblock Plus is free software: you can redistribute it and/or modify 5 * Adblock Plus is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 3 as 6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation. 7 * published by the Free Software Foundation.
8 * 8 *
9 * Adblock Plus is distributed in the hope that it will be useful, 9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details. 12 * GNU General Public License for more details.
13 * 13 *
14 * You should have received a copy of the GNU General Public License 14 * You should have received a copy of the GNU General Public License
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
16 */ 16 */
17 17
18 "use strict";
19
18 /** 20 /**
19 * @fileOverview Matcher class implementing matching addresses against a list of filters. 21 * @fileOverview Matcher class implementing matching addresses against
22 * a list of filters.
20 */ 23 */
21 24
22 let {Filter, RegExpFilter, WhitelistFilter} = require("filterClasses"); 25 const {Filter, WhitelistFilter} = require("filterClasses");
23 26
24 /** 27 /**
25 * Blacklist/whitelist filter matching 28 * Blacklist/whitelist filter matching
26 * @constructor 29 * @constructor
27 */ 30 */
28 function Matcher() 31 function Matcher()
29 { 32 {
30 this.clear(); 33 this.clear();
31 } 34 }
32 exports.Matcher = Matcher; 35 exports.Matcher = Matcher;
33 36
34 Matcher.prototype = { 37 Matcher.prototype = {
35 /** 38 /**
36 * Lookup table for filters by their associated keyword 39 * Lookup table for filters by their associated keyword
37 * @type Object 40 * @type {Object}
38 */ 41 */
39 filterByKeyword: null, 42 filterByKeyword: null,
40 43
41 /** 44 /**
42 * Lookup table for keywords by the filter text 45 * Lookup table for keywords by the filter text
43 * @type Object 46 * @type {Object}
44 */ 47 */
45 keywordByFilter: null, 48 keywordByFilter: null,
46 49
47 /** 50 /**
48 * Removes all known filters 51 * Removes all known filters
49 */ 52 */
50 clear: function() 53 clear()
51 { 54 {
52 this.filterByKeyword = Object.create(null); 55 this.filterByKeyword = Object.create(null);
53 this.keywordByFilter = Object.create(null); 56 this.keywordByFilter = Object.create(null);
54 }, 57 },
55 58
56 /** 59 /**
57 * Adds a filter to the matcher 60 * Adds a filter to the matcher
58 * @param {RegExpFilter} filter 61 * @param {RegExpFilter} filter
59 */ 62 */
60 add: function(filter) 63 add(filter)
61 { 64 {
62 if (filter.text in this.keywordByFilter) 65 if (filter.text in this.keywordByFilter)
63 return; 66 return;
64 67
65 // Look for a suitable keyword 68 // Look for a suitable keyword
66 let keyword = this.findKeyword(filter); 69 let keyword = this.findKeyword(filter);
67 let oldEntry = this.filterByKeyword[keyword]; 70 let oldEntry = this.filterByKeyword[keyword];
68 if (typeof oldEntry == "undefined") 71 if (typeof oldEntry == "undefined")
69 this.filterByKeyword[keyword] = filter; 72 this.filterByKeyword[keyword] = filter;
70 else if (oldEntry.length == 1) 73 else if (oldEntry.length == 1)
71 this.filterByKeyword[keyword] = [oldEntry, filter]; 74 this.filterByKeyword[keyword] = [oldEntry, filter];
72 else 75 else
73 oldEntry.push(filter); 76 oldEntry.push(filter);
74 this.keywordByFilter[filter.text] = keyword; 77 this.keywordByFilter[filter.text] = keyword;
75 }, 78 },
76 79
77 /** 80 /**
78 * Removes a filter from the matcher 81 * Removes a filter from the matcher
79 * @param {RegExpFilter} filter 82 * @param {RegExpFilter} filter
80 */ 83 */
81 remove: function(filter) 84 remove(filter)
82 { 85 {
83 if (!(filter.text in this.keywordByFilter)) 86 if (!(filter.text in this.keywordByFilter))
84 return; 87 return;
85 88
86 let keyword = this.keywordByFilter[filter.text]; 89 let keyword = this.keywordByFilter[filter.text];
87 let list = this.filterByKeyword[keyword]; 90 let list = this.filterByKeyword[keyword];
88 if (list.length <= 1) 91 if (list.length <= 1)
89 delete this.filterByKeyword[keyword]; 92 delete this.filterByKeyword[keyword];
90 else 93 else
91 { 94 {
92 let index = list.indexOf(filter); 95 let index = list.indexOf(filter);
93 if (index >= 0) 96 if (index >= 0)
94 { 97 {
95 list.splice(index, 1); 98 list.splice(index, 1);
96 if (list.length == 1) 99 if (list.length == 1)
97 this.filterByKeyword[keyword] = list[0]; 100 this.filterByKeyword[keyword] = list[0];
98 } 101 }
99 } 102 }
100 103
101 delete this.keywordByFilter[filter.text]; 104 delete this.keywordByFilter[filter.text];
102 }, 105 },
103 106
104 /** 107 /**
105 * Chooses a keyword to be associated with the filter 108 * Chooses a keyword to be associated with the filter
106 * @param {String} text text representation of the filter 109 * @param {Filter} filter
107 * @return {String} keyword (might be empty string) 110 * @return {string} keyword or an empty string if no keyword could be found
108 */ 111 */
109 findKeyword: function(filter) 112 findKeyword(filter)
110 { 113 {
111 let result = ""; 114 let result = "";
112 let text = filter.text; 115 let {text} = filter;
113 if (Filter.regexpRegExp.test(text)) 116 if (Filter.regexpRegExp.test(text))
114 return result; 117 return result;
115 118
116 // Remove options 119 // Remove options
117 let match = Filter.optionsRegExp.exec(text); 120 let match = Filter.optionsRegExp.exec(text);
118 if (match) 121 if (match)
119 text = match.input.substr(0, match.index); 122 text = match.input.substr(0, match.index);
120 123
121 // Remove whitelist marker 124 // Remove whitelist marker
122 if (text.substr(0, 2) == "@@") 125 if (text.substr(0, 2) == "@@")
123 text = text.substr(2); 126 text = text.substr(2);
124 127
125 let candidates = text.toLowerCase().match(/[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0 -9%*])/g); 128 let candidates = text.toLowerCase().match(
129 /[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/g
130 );
126 if (!candidates) 131 if (!candidates)
127 return result; 132 return result;
128 133
129 let hash = this.filterByKeyword; 134 let hash = this.filterByKeyword;
130 let resultCount = 0xFFFFFF; 135 let resultCount = 0xFFFFFF;
131 let resultLength = 0; 136 let resultLength = 0;
132 for (let i = 0, l = candidates.length; i < l; i++) 137 for (let i = 0, l = candidates.length; i < l; i++)
133 { 138 {
134 let candidate = candidates[i].substr(1); 139 let candidate = candidates[i].substr(1);
135 let count = (candidate in hash ? hash[candidate].length : 0); 140 let count = (candidate in hash ? hash[candidate].length : 0);
136 if (count < resultCount || (count == resultCount && candidate.length > res ultLength)) 141 if (count < resultCount ||
142 (count == resultCount && candidate.length > resultLength))
137 { 143 {
138 result = candidate; 144 result = candidate;
139 resultCount = count; 145 resultCount = count;
140 resultLength = candidate.length; 146 resultLength = candidate.length;
141 } 147 }
142 } 148 }
143 return result; 149 return result;
144 }, 150 },
145 151
146 /** 152 /**
147 * Checks whether a particular filter is being matched against. 153 * Checks whether a particular filter is being matched against.
154 * @param {RegExpFilter} filter
155 * @return {boolean}
148 */ 156 */
149 hasFilter: function(/**RegExpFilter*/ filter) /**Boolean*/ 157 hasFilter(filter)
150 { 158 {
151 return (filter.text in this.keywordByFilter); 159 return (filter.text in this.keywordByFilter);
152 }, 160 },
153 161
154 /** 162 /**
155 * Returns the keyword used for a filter, null for unknown filters. 163 * Returns the keyword used for a filter, null for unknown filters.
164 * @param {RegExpFilter} filter
165 * @return {string}
156 */ 166 */
157 getKeywordForFilter: function(/**RegExpFilter*/ filter) /**String*/ 167 getKeywordForFilter(filter)
158 { 168 {
159 if (filter.text in this.keywordByFilter) 169 if (filter.text in this.keywordByFilter)
160 return this.keywordByFilter[filter.text]; 170 return this.keywordByFilter[filter.text];
161 else 171 return null;
162 return null;
163 }, 172 },
164 173
165 /** 174 /**
166 * Checks whether the entries for a particular keyword match a URL 175 * Checks whether the entries for a particular keyword match a URL
176 * @param {string} keyword
177 * @param {string} location
178 * @param {number} typeMask
179 * @param {string} docDomain
180 * @param {boolean} thirdParty
181 * @param {string} sitekey
182 * @param {boolean} specificOnly
183 * @return {?Filter}
167 */ 184 */
168 _checkEntryMatch: function(keyword, location, typeMask, docDomain, thirdParty, sitekey, specificOnly) 185 _checkEntryMatch(keyword, location, typeMask, docDomain, thirdParty, sitekey,
186 specificOnly)
169 { 187 {
170 let list = this.filterByKeyword[keyword]; 188 let list = this.filterByKeyword[keyword];
171 for (let i = 0; i < list.length; i++) 189 for (let i = 0; i < list.length; i++)
172 { 190 {
173 let filter = list[i]; 191 let filter = list[i];
174 192
175 if (specificOnly && filter.isGeneric() && 193 if (specificOnly && filter.isGeneric() &&
176 !(filter instanceof WhitelistFilter)) 194 !(filter instanceof WhitelistFilter))
177 continue; 195 continue;
178 196
179 if (filter.matches(location, typeMask, docDomain, thirdParty, sitekey)) 197 if (filter.matches(location, typeMask, docDomain, thirdParty, sitekey))
180 return filter; 198 return filter;
181 } 199 }
182 return null; 200 return null;
183 }, 201 },
184 202
185 /** 203 /**
186 * Tests whether the URL matches any of the known filters 204 * Tests whether the URL matches any of the known filters
187 * @param {String} location URL to be tested 205 * @param {string} location
188 * @param {number} typeMask bitmask of content / request types to match 206 * URL to be tested
189 * @param {String} docDomain domain name of the document that loads the URL 207 * @param {number} typeMask
190 * @param {Boolean} thirdParty should be true if the URL is a third-party requ est 208 * bitmask of content / request types to match
191 * @param {String} sitekey public key provided by the document 209 * @param {string} docDomain
192 * @param {Boolean} specificOnly should be true if generic matches should be i gnored 210 * domain name of the document that loads the URL
193 * @return {RegExpFilter} matching filter or null 211 * @param {boolean} thirdParty
212 * should be true if the URL is a third-party request
213 * @param {string} sitekey
214 * public key provided by the document
215 * @param {boolean} specificOnly
216 * should be true if generic matches should be ignored
217 * @return {?RegExpFilter}
218 * matching filter or null
194 */ 219 */
195 matchesAny: function(location, typeMask, docDomain, thirdParty, sitekey, speci ficOnly) 220 matchesAny(location, typeMask, docDomain, thirdParty, sitekey, specificOnly)
196 { 221 {
197 let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g); 222 let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g);
198 if (candidates === null) 223 if (candidates === null)
199 candidates = []; 224 candidates = [];
200 candidates.push(""); 225 candidates.push("");
201 for (let i = 0, l = candidates.length; i < l; i++) 226 for (let i = 0, l = candidates.length; i < l; i++)
202 { 227 {
203 let substr = candidates[i]; 228 let substr = candidates[i];
204 if (substr in this.filterByKeyword) 229 if (substr in this.filterByKeyword)
205 { 230 {
206 let result = this._checkEntryMatch(substr, location, typeMask, docDomain , thirdParty, sitekey, specificOnly); 231 let result = this._checkEntryMatch(substr, location, typeMask,
232 docDomain, thirdParty, sitekey,
233 specificOnly);
207 if (result) 234 if (result)
208 return result; 235 return result;
209 } 236 }
210 } 237 }
211 238
212 return null; 239 return null;
213 } 240 }
214 }; 241 };
215 242
216 /** 243 /**
217 * Combines a matcher for blocking and exception rules, automatically sorts 244 * Combines a matcher for blocking and exception rules, automatically sorts
218 * rules into two Matcher instances. 245 * rules into two Matcher instances.
219 * @constructor 246 * @constructor
220 */ 247 */
221 function CombinedMatcher() 248 function CombinedMatcher()
222 { 249 {
223 this.blacklist = new Matcher(); 250 this.blacklist = new Matcher();
224 this.whitelist = new Matcher(); 251 this.whitelist = new Matcher();
225 this.resultCache = Object.create(null); 252 this.resultCache = Object.create(null);
226 } 253 }
227 exports.CombinedMatcher = CombinedMatcher; 254 exports.CombinedMatcher = CombinedMatcher;
228 255
229 /** 256 /**
230 * Maximal number of matching cache entries to be kept 257 * Maximal number of matching cache entries to be kept
231 * @type Number 258 * @type {number}
232 */ 259 */
233 CombinedMatcher.maxCacheEntries = 1000; 260 CombinedMatcher.maxCacheEntries = 1000;
234 261
235 CombinedMatcher.prototype = 262 CombinedMatcher.prototype =
236 { 263 {
237 /** 264 /**
238 * Matcher for blocking rules. 265 * Matcher for blocking rules.
239 * @type Matcher 266 * @type {Matcher}
240 */ 267 */
241 blacklist: null, 268 blacklist: null,
242 269
243 /** 270 /**
244 * Matcher for exception rules. 271 * Matcher for exception rules.
245 * @type Matcher 272 * @type {Matcher}
246 */ 273 */
247 whitelist: null, 274 whitelist: null,
248 275
249 /** 276 /**
250 * Lookup table of previous matchesAny results 277 * Lookup table of previous matchesAny results
251 * @type Object 278 * @type {Object}
252 */ 279 */
253 resultCache: null, 280 resultCache: null,
254 281
255 /** 282 /**
256 * Number of entries in resultCache 283 * Number of entries in resultCache
257 * @type Number 284 * @type {number}
258 */ 285 */
259 cacheEntries: 0, 286 cacheEntries: 0,
260 287
261 /** 288 /**
262 * @see Matcher#clear 289 * @see Matcher#clear
263 */ 290 */
264 clear: function() 291 clear()
265 { 292 {
266 this.blacklist.clear(); 293 this.blacklist.clear();
267 this.whitelist.clear(); 294 this.whitelist.clear();
268 this.resultCache = Object.create(null); 295 this.resultCache = Object.create(null);
269 this.cacheEntries = 0; 296 this.cacheEntries = 0;
270 }, 297 },
271 298
272 /** 299 /**
273 * @see Matcher#add 300 * @see Matcher#add
301 * @param {Filter} filter
274 */ 302 */
275 add: function(filter) 303 add(filter)
276 { 304 {
277 if (filter instanceof WhitelistFilter) 305 if (filter instanceof WhitelistFilter)
278 this.whitelist.add(filter); 306 this.whitelist.add(filter);
279 else 307 else
280 this.blacklist.add(filter); 308 this.blacklist.add(filter);
281 309
282 if (this.cacheEntries > 0) 310 if (this.cacheEntries > 0)
283 { 311 {
284 this.resultCache = Object.create(null); 312 this.resultCache = Object.create(null);
285 this.cacheEntries = 0; 313 this.cacheEntries = 0;
286 } 314 }
287 }, 315 },
288 316
289 /** 317 /**
290 * @see Matcher#remove 318 * @see Matcher#remove
319 * @param {Filter} filter
291 */ 320 */
292 remove: function(filter) 321 remove(filter)
293 { 322 {
294 if (filter instanceof WhitelistFilter) 323 if (filter instanceof WhitelistFilter)
295 this.whitelist.remove(filter); 324 this.whitelist.remove(filter);
296 else 325 else
297 this.blacklist.remove(filter); 326 this.blacklist.remove(filter);
298 327
299 if (this.cacheEntries > 0) 328 if (this.cacheEntries > 0)
300 { 329 {
301 this.resultCache = Object.create(null); 330 this.resultCache = Object.create(null);
302 this.cacheEntries = 0; 331 this.cacheEntries = 0;
303 } 332 }
304 }, 333 },
305 334
306 /** 335 /**
307 * @see Matcher#findKeyword 336 * @see Matcher#findKeyword
337 * @param {Filter} filter
338 * @return {string} keyword
308 */ 339 */
309 findKeyword: function(filter) 340 findKeyword(filter)
310 { 341 {
311 if (filter instanceof WhitelistFilter) 342 if (filter instanceof WhitelistFilter)
312 return this.whitelist.findKeyword(filter); 343 return this.whitelist.findKeyword(filter);
313 else 344 return this.blacklist.findKeyword(filter);
314 return this.blacklist.findKeyword(filter);
315 }, 345 },
316 346
317 /** 347 /**
318 * @see Matcher#hasFilter 348 * @see Matcher#hasFilter
349 * @param {Filter} filter
350 * @return {boolean}
319 */ 351 */
320 hasFilter: function(filter) 352 hasFilter(filter)
321 { 353 {
322 if (filter instanceof WhitelistFilter) 354 if (filter instanceof WhitelistFilter)
323 return this.whitelist.hasFilter(filter); 355 return this.whitelist.hasFilter(filter);
324 else 356 return this.blacklist.hasFilter(filter);
325 return this.blacklist.hasFilter(filter);
326 }, 357 },
327 358
328 /** 359 /**
329 * @see Matcher#getKeywordForFilter 360 * @see Matcher#getKeywordForFilter
361 * @param {Filter} filter
362 * @return {string} keyword
330 */ 363 */
331 getKeywordForFilter: function(filter) 364 getKeywordForFilter(filter)
332 { 365 {
333 if (filter instanceof WhitelistFilter) 366 if (filter instanceof WhitelistFilter)
334 return this.whitelist.getKeywordForFilter(filter); 367 return this.whitelist.getKeywordForFilter(filter);
335 else 368 return this.blacklist.getKeywordForFilter(filter);
336 return this.blacklist.getKeywordForFilter(filter);
337 }, 369 },
338 370
339 /** 371 /**
340 * Checks whether a particular filter is slow 372 * Checks whether a particular filter is slow
373 * @param {RegExpFilter} filter
374 * @return {boolean}
341 */ 375 */
342 isSlowFilter: function(/**RegExpFilter*/ filter) /**Boolean*/ 376 isSlowFilter(filter)
343 { 377 {
344 let matcher = (filter instanceof WhitelistFilter ? this.whitelist : this.bla cklist); 378 let matcher = (
379 filter instanceof WhitelistFilter ? this.whitelist : this.blacklist
380 );
345 if (matcher.hasFilter(filter)) 381 if (matcher.hasFilter(filter))
346 return !matcher.getKeywordForFilter(filter); 382 return !matcher.getKeywordForFilter(filter);
347 else 383 return !matcher.findKeyword(filter);
348 return !matcher.findKeyword(filter);
349 }, 384 },
350 385
351 /** 386 /**
352 * Optimized filter matching testing both whitelist and blacklist matchers 387 * Optimized filter matching testing both whitelist and blacklist matchers
353 * simultaneously. For parameters see Matcher.matchesAny(). 388 * simultaneously. For parameters see Matcher.matchesAny().
354 * @see Matcher#matchesAny 389 * @see Matcher#matchesAny
355 */ 390 */
356 matchesAnyInternal: function(location, typeMask, docDomain, thirdParty, siteke y, specificOnly) 391 matchesAnyInternal(location, typeMask, docDomain, thirdParty, sitekey,
392 specificOnly)
357 { 393 {
358 let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g); 394 let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g);
359 if (candidates === null) 395 if (candidates === null)
360 candidates = []; 396 candidates = [];
361 candidates.push(""); 397 candidates.push("");
362 398
363 let blacklistHit = null; 399 let blacklistHit = null;
364 for (let i = 0, l = candidates.length; i < l; i++) 400 for (let i = 0, l = candidates.length; i < l; i++)
365 { 401 {
366 let substr = candidates[i]; 402 let substr = candidates[i];
367 if (substr in this.whitelist.filterByKeyword) 403 if (substr in this.whitelist.filterByKeyword)
368 { 404 {
369 let result = this.whitelist._checkEntryMatch(substr, location, typeMask, docDomain, thirdParty, sitekey); 405 let result = this.whitelist._checkEntryMatch(
406 substr, location, typeMask, docDomain, thirdParty, sitekey
407 );
370 if (result) 408 if (result)
371 return result; 409 return result;
372 } 410 }
373 if (substr in this.blacklist.filterByKeyword && blacklistHit === null) 411 if (substr in this.blacklist.filterByKeyword && blacklistHit === null)
374 blacklistHit = this.blacklist._checkEntryMatch(substr, location, typeMas k, docDomain, thirdParty, sitekey, specificOnly); 412 {
413 blacklistHit = this.blacklist._checkEntryMatch(
414 substr, location, typeMask, docDomain, thirdParty, sitekey,
415 specificOnly
416 );
417 }
375 } 418 }
376 return blacklistHit; 419 return blacklistHit;
377 }, 420 },
378 421
379 /** 422 /**
380 * @see Matcher#matchesAny 423 * @see Matcher#matchesAny
381 */ 424 */
382 matchesAny: function(location, typeMask, docDomain, thirdParty, sitekey, speci ficOnly) 425 matchesAny(location, typeMask, docDomain, thirdParty, sitekey, specificOnly)
383 { 426 {
384 let key = location + " " + typeMask + " " + docDomain + " " + thirdParty + " " + sitekey + " " + specificOnly; 427 let key = location + " " + typeMask + " " + docDomain + " " + thirdParty +
428 " " + sitekey + " " + specificOnly;
385 if (key in this.resultCache) 429 if (key in this.resultCache)
386 return this.resultCache[key]; 430 return this.resultCache[key];
387 431
388 let result = this.matchesAnyInternal(location, typeMask, docDomain, thirdPar ty, sitekey, specificOnly); 432 let result = this.matchesAnyInternal(location, typeMask, docDomain,
433 thirdParty, sitekey, specificOnly);
389 434
390 if (this.cacheEntries >= CombinedMatcher.maxCacheEntries) 435 if (this.cacheEntries >= CombinedMatcher.maxCacheEntries)
391 { 436 {
392 this.resultCache = Object.create(null); 437 this.resultCache = Object.create(null);
393 this.cacheEntries = 0; 438 this.cacheEntries = 0;
394 } 439 }
395 440
396 this.resultCache[key] = result; 441 this.resultCache[key] = result;
397 this.cacheEntries++; 442 this.cacheEntries++;
398 443
399 return result; 444 return result;
400 } 445 }
401 } 446 };
402 447
403 /** 448 /**
404 * Shared CombinedMatcher instance that should usually be used. 449 * Shared CombinedMatcher instance that should usually be used.
405 * @type CombinedMatcher 450 * @type {CombinedMatcher}
406 */ 451 */
407 let defaultMatcher = exports.defaultMatcher = new CombinedMatcher(); 452 exports.defaultMatcher = new CombinedMatcher();
OLDNEW

Powered by Google App Engine
This is Rietveld