Skip to content

Commit d1dff31

Browse files
refactor(Glob): BC-BREAK Use RegExp to implement Glob SWITCH glob to use RegExp
Previously, trailing single wildcards '*' could be "zero-length" matches. The glob `foo.*` would match `foo` and `foo.bar` but not `foo.bar.baz`. Likewise, the glob `foo.*.*` would also match `foo`. Now, all single wildcards match one segment. The glob `foo.*` matches `foo.bar` but does not match `foo`. Use `foo.**` instead to match zero or more segments. test(Glob): Add more glob tests docs(Glob): Document glob Closes #2965
1 parent 4ede2fb commit d1dff31

File tree

6 files changed

+110
-66
lines changed

6 files changed

+110
-66
lines changed

src/common/glob.ts

+33-35
Original file line numberDiff line numberDiff line change
@@ -12,75 +12,73 @@
1212
* - [[HookMatchCriteria.retained]]
1313
* - [[HookMatchCriteria.entering]]
1414
*
15-
* A `Glob` string is a pattern which matches state names according to the following rules:
15+
* A `Glob` string is a pattern which matches state names.
16+
* Nested state names are split into segments (separated by a dot) when processing.
17+
* The state named `foo.bar.baz` is split into three segments ['foo', 'bar', 'baz']
18+
*
19+
* Globs work according to the following rules:
1620
*
1721
* ### Exact match:
1822
*
1923
* The glob `'A.B'` matches the state named exactly `'A.B'`.
2024
*
2125
* | Glob |Matches states named|Does not match state named|
22-
* |:------------|:--------------------|:-----------------|
23-
* | `'A'` | `'A'` | `'B'` , `'A.C'` |
24-
* | `'A.B'` | `'A.B'` | `'A'` , `'A.B.C'`|
26+
* |:------------|:--------------------|:---------------------|
27+
* | `'A'` | `'A'` | `'B'` , `'A.C'` |
28+
* | `'A.B'` | `'A.B'` | `'A'` , `'A.B.C'` |
29+
* | `'foo'` | `'foo'` | `'FOO'` , `'foo.bar'`|
2530
*
26-
* ### Single wildcard (`*`)
31+
* ### Single star (`*`)
2732
*
28-
* A single wildcard (`*`) matches any value for *a single segment* of a state name.
33+
* A single star (`*`) is a wildcard that matches exactly one segment.
2934
*
3035
* | Glob |Matches states named |Does not match state named |
3136
* |:------------|:---------------------|:--------------------------|
32-
* | `'A.*'` | `'A.B'` , `'A.C'` | `'A'` , `'A.B.C'` |
3337
* | `'*'` | `'A'` , `'Z'` | `'A.B'` , `'Z.Y.X'` |
38+
* | `'A.*'` | `'A.B'` , `'A.C'` | `'A'` , `'A.B.C'` |
3439
* | `'A.*.*'` | `'A.B.C'` , `'A.X.Y'`| `'A'`, `'A.B'` , `'Z.Y.X'`|
3540
*
41+
* ### Double star (`**`)
3642
*
37-
* ### Double wildcards (`**`)
43+
* A double star (`'**'`) is a wildcard that matches *zero or more segments*
3844
*
39-
* Double wildcards (`'**'`) act as a wildcard for *one or more segments*
40-
*
41-
* | Glob |Matches states named |Does not match state named|
42-
* |:------------|:----------------------------------------------|:-------------------------|
43-
* | `'**'` | `'A'` , `'A.B'`, `'Z.Y.X'` | (matches all states) |
44-
* | `'A.**'` | `'A.B'` , `'A.C'` , `'A.B.X'` | `'A'`, `'Z.Y.X'` |
45-
* | `'**.login'`| `'A.login'` , `'A.B.login'` , `'Z.Y.X.login'` | `'A'` , `'login'` , `'A.login.Z'` |
45+
* | Glob |Matches states named |Does not match state named |
46+
* |:------------|:----------------------------------------------|:----------------------------------|
47+
* | `'**'` | `'A'` , `'A.B'`, `'Z.Y.X'` | (matches all states) |
48+
* | `'A.**'` | `'A'` , `'A.B'` , `'A.C.X'` | `'Z.Y.X'` |
49+
* | `'**.X'` | `'X'` , `'A.X'` , `'Z.Y.X'` | `'A'` , `'A.login.Z'` |
50+
* | `'A.**.X'` | `'A.X'` , `'A.B.X'` , `'A.B.C.X'` | `'A'` , `'A.B.C'` |
4651
*
4752
*/
4853
export class Glob {
4954
text: string;
5055
glob: Array<string>;
56+
regexp: RegExp;
5157

5258
constructor(text: string) {
5359
this.text = text;
5460
this.glob = text.split('.');
55-
}
5661

57-
matches(name: string) {
58-
let segments = name.split('.');
59-
60-
// match single stars
61-
for (let i = 0, l = this.glob.length; i < l; i++) {
62-
if (this.glob[i] === '*') segments[i] = '*';
63-
}
62+
let regexpString = this.text.split('.')
63+
.map(seg => {
64+
if (seg === '**') return '(?:|(?:\\.[^.]*)*)';
65+
if (seg === '*') return '\\.[^.]*';
66+
return '\\.' + seg;
67+
}).join('');
6468

65-
// match greedy starts
66-
if (this.glob[0] === '**') {
67-
segments = segments.slice(segments.indexOf(this.glob[1]));
68-
segments.unshift('**');
69-
}
70-
// match greedy ends
71-
if (this.glob[this.glob.length - 1] === '**') {
72-
segments.splice(segments.indexOf(this.glob[this.glob.length - 2]) + 1, Number.MAX_VALUE);
73-
segments.push('**');
74-
}
75-
if (this.glob.length != segments.length) return false;
69+
this.regexp = new RegExp("^" + regexpString + "$");
70+
}
7671

77-
return segments.join('') === this.glob.join('');
72+
matches(name: string) {
73+
return this.regexp.test('.' + name);
7874
}
7975

76+
/** @deprecated whats the point? */
8077
static is(text: string) {
8178
return text.indexOf('*') > -1;
8279
}
8380

81+
/** @deprecated whats the point? */
8482
static fromString(text: string) {
8583
if (!this.is(text)) return null;
8684
return new Glob(text);

test/core/commonSpec.ts

-30
Original file line numberDiff line numberDiff line change
@@ -112,34 +112,4 @@ describe('common', function() {
112112
expect(isInjectable(fn)).toBeTruthy();
113113
});
114114
});
115-
116-
describe('Glob', function() {
117-
it('should match glob strings', function() {
118-
expect(Glob.is('*')).toBe(true);
119-
expect(Glob.is('**')).toBe(true);
120-
expect(Glob.is('*.*')).toBe(true);
121-
122-
expect(Glob.is('')).toBe(false);
123-
expect(Glob.is('.')).toBe(false);
124-
});
125-
126-
it('should construct glob matchers', function() {
127-
expect(Glob.fromString('')).toBeNull();
128-
129-
var state = 'about.person.item';
130-
131-
expect(Glob.fromString('*.person.*').matches(state)).toBe(true);
132-
expect(Glob.fromString('*.person.**').matches(state)).toBe(true);
133-
134-
expect(Glob.fromString('**.item.*').matches(state)).toBe(false);
135-
expect(Glob.fromString('**.item').matches(state)).toBe(true);
136-
expect(Glob.fromString('**.stuff.*').matches(state)).toBe(false);
137-
expect(Glob.fromString('*.*.*').matches(state)).toBe(true);
138-
139-
expect(Glob.fromString('about.*.*').matches(state)).toBe(true);
140-
expect(Glob.fromString('about.**').matches(state)).toBe(true);
141-
expect(Glob.fromString('*.about.*').matches(state)).toBe(false);
142-
expect(Glob.fromString('about.*.*').matches(state)).toBe(true);
143-
});
144-
});
145115
});

test/core/globSpec.ts

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {Glob} from "../../src/common/glob";
2+
3+
describe('Glob', function() {
4+
it('should match exact strings', function() {
5+
var state = 'about.person.item';
6+
7+
expect(new Glob('about.person.item').matches(state)).toBe(true);
8+
expect(new Glob('about.person.item.foo').matches(state)).toBe(false);
9+
expect(new Glob('foo.about.person.item').matches(state)).toBe(false);
10+
});
11+
12+
it('with a single wildcard (*) should match a top level state', function() {
13+
var glob = new Glob('*');
14+
15+
expect(glob.matches('foo')).toBe(true);
16+
expect(glob.matches('bar')).toBe(true);
17+
expect(glob.matches('baz')).toBe(true);
18+
expect(glob.matches('foo.bar')).toBe(false);
19+
expect(glob.matches('.baz')).toBe(false);
20+
});
21+
22+
it('with a single wildcard (*) should match any single non-empty segment', function() {
23+
var state = 'about.person.item';
24+
25+
expect(new Glob('*.person.item').matches(state)).toBe(true);
26+
expect(new Glob('*.*.item').matches(state)).toBe(true);
27+
expect(new Glob('*.person.*').matches(state)).toBe(true);
28+
expect(new Glob('*.*.*').matches(state)).toBe(true);
29+
30+
expect(new Glob('*.*.*.*').matches(state)).toBe(false);
31+
expect(new Glob('*.*.person.item').matches(state)).toBe(false);
32+
expect(new Glob('*.person.item.foo').matches(state)).toBe(false);
33+
expect(new Glob('foo.about.person.*').matches(state)).toBe(false);
34+
});
35+
36+
it('with a double wildcard (**) should match any valid state name', function() {
37+
var glob = new Glob('**');
38+
39+
expect(glob.matches('foo')).toBe(true);
40+
expect(glob.matches('bar')).toBe(true);
41+
expect(glob.matches('foo.bar')).toBe(true);
42+
});
43+
44+
it('with a double wildcard (**) should match zero or more segments', function() {
45+
var state = 'about.person.item';
46+
47+
expect(new Glob('**').matches(state)).toBe(true);
48+
expect(new Glob('**.**').matches(state)).toBe(true);
49+
expect(new Glob('**.*').matches(state)).toBe(true);
50+
expect(new Glob('**.person.item').matches(state)).toBe(true);
51+
expect(new Glob('**.person.**').matches(state)).toBe(true);
52+
expect(new Glob('**.person.**.item').matches(state)).toBe(true);
53+
expect(new Glob('**.person.**.*').matches(state)).toBe(true);
54+
expect(new Glob('**.item').matches(state)).toBe(true);
55+
expect(new Glob('about.**').matches(state)).toBe(true);
56+
expect(new Glob('about.**.person.item').matches(state)).toBe(true);
57+
expect(new Glob('about.person.item.**').matches(state)).toBe(true);
58+
expect(new Glob('**.about.person.item').matches(state)).toBe(true);
59+
expect(new Glob('**.about.**.person.item.**').matches(state)).toBe(true);
60+
expect(new Glob('**.**.about.person.item').matches(state)).toBe(true);
61+
62+
expect(new Glob('**.person.**.*.*').matches(state)).toBe(false);
63+
expect(new Glob('**.person.**.*.item').matches(state)).toBe(false);
64+
});
65+
});

test/matchers.ts

+10
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ beforeEach(function() {
1212
}
1313
},
1414

15+
toEqualValues: function() {
16+
return {
17+
compare: function(actual, expected) {
18+
let pass = Object.keys(expected)
19+
.reduce((acc, key) => acc && equals(actual[key], expected[key]), true);
20+
return { pass };
21+
}
22+
}
23+
},
24+
1525
toBeResolved: () => ({
1626
compare: actual => ({
1727
pass: !!testablePromise(actual).$$resolved

test/ng1/stateDirectivesSpec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,7 @@ describe('uiSrefActive', function() {
816816
}));
817817

818818
it('should support multiple <className, stateOrName> pairs', inject(function($compile, $rootScope, $state, $q) {
819-
el = $compile('<div ui-sref-active="{contacts: \'contacts.*\', admin: \'admin.roles({page: 1})\'}"></div>')($rootScope);
819+
el = $compile('<div ui-sref-active="{contacts: \'contacts.**\', admin: \'admin.roles({page: 1})\'}"></div>')($rootScope);
820820
$state.transitionTo('contacts');
821821
$q.flush();
822822
timeoutFlush();

typings/jasmine/jasmine.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,7 @@ declare module jasmine {
500500
export interface Matchers {
501501
toBeResolved(): boolean
502502
toEqualData(expected: any): boolean
503+
toEqualValues(expected: any): boolean
503504
toHaveClass(expected: any): boolean
504505
}
505506
}

0 commit comments

Comments
 (0)