Skip to content

Commit 0923f9b

Browse files
authored
Merge pull request #249 from mrmlnc/ISSUE-156_partial_matcher
ISSUE-156: add partial matcher for deep filter
2 parents d16282c + 2f5f18e commit 0923f9b

File tree

14 files changed

+497
-44
lines changed

14 files changed

+497
-44
lines changed

package.json

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"@types/glob": "^7.1.1",
2727
"@types/glob-parent": "^5.1.0",
2828
"@types/merge2": "^1.1.4",
29-
"@types/micromatch": "^3.1.0",
29+
"@types/micromatch": "^4.0.0",
3030
"@types/minimist": "^1.2.0",
3131
"@types/mocha": "^5.2.7",
3232
"@types/node": "^12.7.8",
@@ -65,14 +65,20 @@
6565
"build": "npm run clean && npm run compile && npm run lint && npm test",
6666
"watch": "npm run clean && npm run compile -- --sourceMap --watch",
6767
"bench": "npm run bench-async && npm run bench-stream && npm run bench-sync",
68-
"bench-async": "npm run bench-async-flatten && npm run bench-async-deep",
69-
"bench-stream": "npm run bench-stream-flatten && npm run bench-stream-deep",
70-
"bench-sync": "npm run bench-sync-flatten && npm run bench-sync-deep",
68+
"bench-async": "npm run bench-async-flatten && npm run bench-async-deep && npm run bench-async-partial-flatten && npm run bench-async-partial-deep",
69+
"bench-stream": "npm run bench-stream-flatten && npm run bench-stream-deep && npm run bench-stream-partial-flatten && npm run bench-stream-partial-deep",
70+
"bench-sync": "npm run bench-sync-flatten && npm run bench-sync-deep && npm run bench-sync-partial-flatten && npm run bench-sync-partial-deep",
7171
"bench-async-flatten": "node ./out/benchmark --mode async --pattern \"*\"",
7272
"bench-async-deep": "node ./out/benchmark --mode async --pattern \"**\"",
73+
"bench-async-partial-flatten": "node ./out/benchmark --mode async --pattern \"{fixtures,out}/{first,second}/*\"",
74+
"bench-async-partial-deep": "node ./out/benchmark --mode async --pattern \"{fixtures,out}/**\"",
7375
"bench-stream-flatten": "node ./out/benchmark --mode stream --pattern \"*\"",
7476
"bench-stream-deep": "node ./out/benchmark --mode stream --pattern \"**\"",
77+
"bench-stream-partial-flatten": "node ./out/benchmark --mode stream --pattern \"{fixtures,out}/{first,second}/*\"",
78+
"bench-stream-partial-deep": "node ./out/benchmark --mode stream --pattern \"{fixtures,out}/**\"",
7579
"bench-sync-flatten": "node ./out/benchmark --mode sync --pattern \"*\"",
76-
"bench-sync-deep": "node ./out/benchmark --mode sync --pattern \"**\""
80+
"bench-sync-deep": "node ./out/benchmark --mode sync --pattern \"**\"",
81+
"bench-sync-partial-flatten": "node ./out/benchmark --mode sync --pattern \"{fixtures,out}/{first,second}/*\"",
82+
"bench-sync-partial-deep": "node ./out/benchmark --mode sync --pattern \"{fixtures,out}/**\""
7783
}
7884
}

src/providers/filters/deep.spec.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -65,39 +65,37 @@ describe('Providers → Filters → Deep', () => {
6565
});
6666
});
6767

68-
describe('Max pattern depth', () => {
69-
it('should return `false` when the depth of entry is greater that the pattern depth', () => {
70-
const filter = getFilter('root', ['root/*'], []);
71-
const entry = tests.entry.builder().path('root/directory').directory().build();
68+
describe('options.followSymbolicLinks', () => {
69+
it('should return `false` when an entry is symbolic link and option is disabled', () => {
70+
const filter = getFilter('.', ['**/*'], [], { followSymbolicLinks: false });
71+
const entry = tests.entry.builder().path('root/directory').directory().symlink().build();
7272

7373
const actual = filter(entry);
7474

7575
assert.ok(!actual);
7676
});
7777
});
7878

79-
describe('options.followSymbolicLinks', () => {
80-
it('should return `false` when an entry is symbolic link and option is disabled', () => {
81-
const filter = getFilter('.', ['**/*'], [], { followSymbolicLinks: false });
82-
const entry = tests.entry.builder().path('root/directory').directory().symlink().build();
79+
describe('Positive pattern', () => {
80+
it('should return `false` when an entry does not match to the positive pattern', () => {
81+
const filter = getFilter('.', ['non-root/directory'], []);
82+
const entry = tests.entry.builder().path('root').directory().build();
8383

8484
const actual = filter(entry);
8585

8686
assert.ok(!actual);
8787
});
88-
});
8988

90-
describe('Pattern', () => {
91-
it('should return `false` when an entry match to the negative pattern', () => {
92-
const filter = getFilter('.', ['**/*'], ['root/**']);
93-
const entry = tests.entry.builder().path('root/directory').directory().build();
89+
it('should return `false` when an entry starts with "./" and does not match to the positive pattern', () => {
90+
const filter = getFilter('.', ['non-root/directory'], []);
91+
const entry = tests.entry.builder().path('./root').directory().build();
9492

9593
const actual = filter(entry);
9694

9795
assert.ok(!actual);
9896
});
9997

100-
it('should return `true` when the positive pattern has no affect to depth reading, but the `baseNameMatch` is enabled', () => {
98+
it('should return `true` when the positive pattern does not match, but the `baseNameMatch` is enabled', () => {
10199
const filter = getFilter('.', ['*'], [], { baseNameMatch: true });
102100
const entry = tests.entry.builder().path('root/directory').directory().build();
103101

@@ -106,17 +104,28 @@ describe('Providers → Filters → Deep', () => {
106104
assert.ok(actual);
107105
});
108106

109-
it('should return `true` when the negative pattern has no effect to depth reading', () => {
110-
const filter = getFilter('.', ['**/*'], ['**/*']);
107+
it('should return `true` when the positive pattern has a globstar', () => {
108+
const filter = getFilter('.', ['**/*'], []);
111109
const entry = tests.entry.builder().path('root/directory').directory().build();
112110

113111
const actual = filter(entry);
114112

115113
assert.ok(actual);
116114
});
115+
});
117116

118-
it('should return `true`', () => {
119-
const filter = getFilter('.', ['**/*'], []);
117+
describe('Negative pattern', () => {
118+
it('should return `false` when an entry match to the negative pattern', () => {
119+
const filter = getFilter('.', ['**/*'], ['root/**']);
120+
const entry = tests.entry.builder().path('root/directory').directory().build();
121+
122+
const actual = filter(entry);
123+
124+
assert.ok(!actual);
125+
});
126+
127+
it('should return `true` when the negative pattern has no effect to depth reading', () => {
128+
const filter = getFilter('.', ['**/*'], ['**/*']);
120129
const entry = tests.entry.builder().path('root/directory').directory().build();
121130

122131
const actual = filter(entry);

src/providers/filters/deep.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
1+
import { Entry, MicromatchOptions, EntryFilterFunction, Pattern, PatternRe } from '../../types';
12
import Settings from '../../settings';
2-
import { Entry, EntryFilterFunction, MicromatchOptions, Pattern, PatternRe } from '../../types';
33
import * as utils from '../../utils';
4+
import PartialMatcher from '../matchers/partial';
45

56
export default class DeepFilter {
67
constructor(private readonly _settings: Settings, private readonly _micromatchOptions: MicromatchOptions) { }
78

89
public getFilter(basePath: string, positive: Pattern[], negative: Pattern[]): EntryFilterFunction {
9-
const maxPatternDepth = this._getMaxPatternDepth(positive);
10+
const matcher = this._getMatcher(positive);
1011
const negativeRe = this._getNegativePatternsRe(negative);
1112

12-
return (entry) => this._filter(basePath, entry, negativeRe, maxPatternDepth);
13+
return (entry) => this._filter(basePath, entry, matcher, negativeRe);
1314
}
1415

15-
private _getMaxPatternDepth(patterns: Pattern[]): number {
16-
const globstar = patterns.some(utils.pattern.hasGlobStar);
17-
18-
return globstar ? Infinity : utils.pattern.getMaxNaivePatternsDepth(patterns);
16+
private _getMatcher(patterns: Pattern[]): PartialMatcher {
17+
return new PartialMatcher(patterns, this._micromatchOptions);
1918
}
2019

2120
private _getNegativePatternsRe(patterns: Pattern[]): PatternRe[] {
@@ -24,41 +23,47 @@ export default class DeepFilter {
2423
return utils.pattern.convertPatternsToRe(affectDepthOfReadingPatterns, this._micromatchOptions);
2524
}
2625

27-
private _filter(basePath: string, entry: Entry, negativeRe: PatternRe[], maxPatternDepth: number): boolean {
28-
const depth = this._getEntryDepth(basePath, entry.path);
26+
private _filter(basePath: string, entry: Entry, matcher: PartialMatcher, negativeRe: PatternRe[]): boolean {
27+
const depth = this._getEntryLevel(basePath, entry.path);
2928

3029
if (this._isSkippedByDeep(depth)) {
3130
return false;
3231
}
3332

34-
if (this._isSkippedByMaxPatternDepth(depth, maxPatternDepth)) {
33+
if (this._isSkippedSymbolicLink(entry)) {
3534
return false;
3635
}
3736

38-
if (this._isSkippedSymbolicLink(entry)) {
37+
if (this._isSkippedByPositivePatterns(entry, matcher)) {
3938
return false;
4039
}
4140

4241
return this._isSkippedByNegativePatterns(entry, negativeRe);
4342
}
4443

45-
private _getEntryDepth(basePath: string, entryPath: string): number {
44+
private _isSkippedByDeep(entryDepth: number): boolean {
45+
return entryDepth >= this._settings.deep;
46+
}
47+
48+
private _isSkippedSymbolicLink(entry: Entry): boolean {
49+
return !this._settings.followSymbolicLinks && entry.dirent.isSymbolicLink();
50+
}
51+
52+
private _getEntryLevel(basePath: string, entryPath: string): number {
4653
const basePathDepth = basePath.split('/').length;
4754
const entryPathDepth = entryPath.split('/').length;
4855

4956
return entryPathDepth - (basePath === '' ? 0 : basePathDepth);
5057
}
5158

52-
private _isSkippedByDeep(entryDepth: number): boolean {
53-
return entryDepth >= this._settings.deep;
54-
}
59+
private _isSkippedByPositivePatterns(entry: Entry, matcher: PartialMatcher): boolean {
60+
const filepath = entry.path.replace(/^\.[/\\]/, '');
5561

56-
private _isSkippedByMaxPatternDepth(entryDepth: number, maxPatternDepth: number): boolean {
57-
return !this._settings.baseNameMatch && maxPatternDepth !== Infinity && entryDepth > maxPatternDepth;
58-
}
62+
const parts = filepath.split('/');
63+
const level = parts.length - 1;
64+
const part = parts[level];
5965

60-
private _isSkippedSymbolicLink(entry: Entry): boolean {
61-
return !this._settings.followSymbolicLinks && entry.dirent.isSymbolicLink();
66+
return !this._settings.baseNameMatch && !matcher.match(level, part);
6267
}
6368

6469
private _isSkippedByNegativePatterns(entry: Entry, negativeRe: PatternRe[]): boolean {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as assert from 'assert';
2+
3+
import * as tests from '../../tests';
4+
import { Pattern, MicromatchOptions } from '../../types';
5+
import Matcher, { PatternInfo } from './matcher';
6+
7+
class TestMatcher extends Matcher {
8+
public get storage(): PatternInfo[] {
9+
return this._storage;
10+
}
11+
}
12+
13+
function getMatcher(patterns: Pattern[], options: MicromatchOptions = {}): TestMatcher {
14+
return new TestMatcher(patterns, options);
15+
}
16+
17+
describe('Providers → Matchers → Matcher', () => {
18+
describe('.storage', () => {
19+
it('should return created storage', () => {
20+
const matcher = getMatcher(['a*', 'a/**/b']);
21+
22+
const expected: PatternInfo[] = [
23+
tests.pattern.info()
24+
.section(tests.pattern.segment().dynamic().pattern('a*').build())
25+
.build(),
26+
tests.pattern.info()
27+
.section(tests.pattern.segment().pattern('a').build())
28+
.section(tests.pattern.segment().pattern('b').build())
29+
.build()
30+
];
31+
32+
const actual = matcher.storage;
33+
34+
assert.deepStrictEqual(actual, expected);
35+
});
36+
});
37+
});

src/providers/matchers/matcher.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Pattern, MicromatchOptions, PatternRe } from '../../types';
2+
import * as utils from '../../utils';
3+
4+
export type PatternSegment = StaticPatternSegment | DynamicPatternSegment;
5+
6+
type StaticPatternSegment = {
7+
dynamic: false;
8+
pattern: Pattern;
9+
};
10+
11+
type DynamicPatternSegment = {
12+
dynamic: true;
13+
pattern: Pattern;
14+
patternRe: PatternRe;
15+
};
16+
17+
export type PatternSection = PatternSegment[];
18+
19+
export type PatternInfo = {
20+
/**
21+
* Indicates that the pattern has a globstar (more than a single section).
22+
*/
23+
complete: boolean;
24+
pattern: Pattern;
25+
segments: PatternSegment[];
26+
sections: PatternSection[];
27+
};
28+
29+
export default abstract class Matcher {
30+
protected readonly _storage: PatternInfo[] = [];
31+
32+
constructor(private readonly _patterns: Pattern[], private readonly _options: MicromatchOptions) {
33+
this._fillStorage();
34+
}
35+
36+
private _fillStorage(): void {
37+
/**
38+
* The original pattern may include `{,*,**,a/*}`, which will lead to problems with matching (unresolved level).
39+
* So, before expand patterns with brace expansion into separated patterns.
40+
*/
41+
const patterns = utils.pattern.expandPatternsWithBraceExpansion(this._patterns);
42+
43+
for (const pattern of patterns) {
44+
const segments = this._getPatternSegments(pattern);
45+
const sections = this._splitSegmentsIntoSections(segments);
46+
47+
this._storage.push({
48+
complete: sections.length <= 1,
49+
pattern,
50+
segments,
51+
sections
52+
});
53+
}
54+
}
55+
56+
private _getPatternSegments(pattern: Pattern): PatternSegment[] {
57+
const parts = utils.pattern.getPatternParts(pattern, this._options);
58+
59+
return parts.map((part) => {
60+
const dynamic = utils.pattern.isDynamicPattern(part);
61+
62+
if (!dynamic) {
63+
return {
64+
dynamic: false,
65+
pattern: part
66+
};
67+
}
68+
69+
return {
70+
dynamic: true,
71+
pattern: part,
72+
patternRe: utils.pattern.makeRe(part, this._options)
73+
};
74+
});
75+
}
76+
77+
private _splitSegmentsIntoSections(segments: PatternSegment[]): PatternSection[] {
78+
return utils.array.splitWhen(segments, (segment) => segment.dynamic && utils.pattern.hasGlobStar(segment.pattern));
79+
}
80+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import * as assert from 'assert';
2+
3+
import { Pattern, MicromatchOptions } from '../../types';
4+
import Matcher from './partial';
5+
6+
function getMatcher(patterns: Pattern[], options: MicromatchOptions = {}): Matcher {
7+
return new Matcher(patterns, options);
8+
}
9+
10+
function assertMatch(patterns: Pattern[], level: number, part: string): void | never {
11+
const matcher = getMatcher(patterns);
12+
13+
assert.ok(matcher.match(level, part));
14+
}
15+
16+
function assertNotMatch(patterns: Pattern[], level: number, part: string): void | never {
17+
const matcher = getMatcher(patterns);
18+
19+
assert.ok(!matcher.match(level, part));
20+
}
21+
22+
describe('Providers → Matchers → Partial', () => {
23+
describe('.match', () => {
24+
it('should handle patterns with globstar', () => {
25+
assertMatch(['**'], 0, 'a');
26+
assertMatch(['**'], 1, 'b');
27+
assertMatch(['**/a'], 0, 'a');
28+
assertMatch(['**/a'], 1, 'a');
29+
assertNotMatch(['a/**'], 0, 'b');
30+
assertMatch(['a/**'], 1, 'b');
31+
});
32+
33+
it('should do not match the latest segment', () => {
34+
assertMatch(['b', 'b/*'], 0, 'b');
35+
assertNotMatch(['*'], 0, 'a');
36+
assertNotMatch(['a/*'], 1, 'b');
37+
});
38+
39+
it('should trying to match all patterns', () => {
40+
assertMatch(['a/*', 'b/*'], 0, 'b');
41+
});
42+
43+
it('should match a static segment', () => {
44+
assertMatch(['a/b'], 0, 'a');
45+
assertNotMatch(['b/b'], 0, 'a');
46+
});
47+
48+
it('should match a dynamic segment', () => {
49+
assertMatch(['*/b'], 0, 'a');
50+
assertMatch(['{a,b}/*'], 0, 'a');
51+
assertNotMatch(['{a,b}/*'], 0, 'c');
52+
});
53+
});
54+
});

0 commit comments

Comments
 (0)