Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISSUE-105: Introduce the isDynamicPattern method (alternative for hasMagic) #230

Merged
merged 4 commits into from
Sep 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 77 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ This package provides methods for traversing the file system and returning pathn
* [Stream](#stream)
* [patterns](#patterns)
* [[options]](#options)
* [Options](#options-1)
* [Helpers](#helpers)
* [generateTasks](#generatetaskspatterns-options)
* [isDynamicPattern](#isdynamicpatternpattern-options)
* [Options](#options-3)
* [Common](#common)
* [concurrency](#concurrency)
* [cwd](#cwd)
Expand All @@ -48,6 +51,7 @@ This package provides methods for traversing the file system and returning pathn
* [globstar](#globstar)
* [baseNameMatch](#basenamematch)
* [FAQ](#faq)
* [What is a static or dynamic pattern?](#what-is-a-static-or-dynamic-pattern)
* [How to write patterns on Windows?](#how-to-write-patterns-on-windows)
* [Why are parentheses match wrong?](#why-are-parentheses-match-wrong)
* [How to exclude directory from reading?](#how-to-exclude-directory-from-reading)
Expand Down Expand Up @@ -198,9 +202,62 @@ Any correct pattern(s).
#### [options]

* Required: `false`
* Type: [`Options`](#options-1)
* Type: [`Options`](#options-3)

See [Options](#options-1) section.
See [Options](#options-3) section.

### Helpers

#### `generateTasks(patterns, [options])`

```js
fg.generateTasks('*');

[{
base: '.', // Parent directory for all patterns inside this task
dynamic: true, // Dynamic or static patterns are in this task
patterns: ['*'],
positive: ['*'],
negative: []
}]
```

##### patterns

* Required: `true`
* Type: `string | string[]`

Any correct pattern(s).

##### [options]

* Required: `false`
* Type: [`Options`](#options-3)

See [Options](#options-3) section.

#### `isDynamicPattern(pattern, [options])`

> :1234: [What is a static or dynamic pattern?](#what-is-a-static-or-dynamic-pattern)

```js
fg.isDynamicPattern('*'); // true
fg.isDynamicPattern('abc'); // false
```

##### pattern

* Required: `true`
* Type: `string`

Any correct pattern.

##### [options]

* Required: `false`
* Type: [`Options`](#options-3)

See [Options](#options-3) section.

## Options

Expand Down Expand Up @@ -538,6 +595,23 @@ fg.sync('*.md', { baseNameMatch: true }); // ['one/file.md']

## FAQ

## What is a static or dynamic pattern?

All patterns can be divided into two types:

* **static**. A pattern is considered static if it can be used to get an entry on the file system without using matching mechanisms. For example, the `file.js` pattern is a static pattern because we can just verify that it exists on the file system.
* **dynamic**. A pattern is considered dynamic if it cannot be used directly to find occurrences without using a matching mechanisms. For example, the `*` pattern is a dynamic pattern because we cannot use this pattern directly.

A pattern is considered dynamic if it contains the following characters (`…` — any characters or their absence) or options:

* The [`caseSensitiveMatch`](#casesensitivematch) option is disabled
* `\\` (the escape character)
* `*`, `?`, `!` (at the beginning of line)
* `[…]`
* `(…|…)`
* `@(…)`, `!(…)`, `*(…)`, `?(…)`, `+(…)` (respects the [`extglob`](#extglob) option)
* `{…,…}`, `{…..…}` (respects the [`braceExpansion`](#braceexpansion) option)

## How to write patterns on Windows?

Always use forward-slashes in glob expressions (patterns and [`ignore`](#ignore) option). Use backslashes for escaping characters. With the [`cwd`](#cwd) option use a convenient format.
Expand Down
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"@types/execa": "^0.9.0",
"@types/glob": "^7.1.1",
"@types/glob-parent": "^5.1.0",
"@types/is-glob": "^4.0.1",
"@types/merge2": "^1.1.4",
"@types/micromatch": "^3.1.0",
"@types/minimist": "^1.2.0",
Expand All @@ -52,7 +51,6 @@
"@nodelib/fs.stat": "^2.0.1",
"@nodelib/fs.walk": "^1.2.1",
"glob-parent": "^5.1.0",
"is-glob": "^4.0.1",
"merge2": "^1.2.3",
"micromatch": "^4.0.2"
},
Expand Down
10 changes: 10 additions & 0 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,14 @@ describe('Package', () => {
assert.deepStrictEqual(actual, expected);
});
});

describe('.isDynamicPattern', () => {
it('should return true for dynamic pattern', () => {
assert.ok(pkg.isDynamicPattern('*'));
});

it('should return false for static pattern', () => {
assert.ok(!pkg.isDynamicPattern('abc'));
});
});
});
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ namespace FastGlob {

return taskManager.generate(patterns, settings);
}

export function isDynamicPattern(source: PatternInternal, options?: OptionsInternal): boolean {
assertPatternsInput(source);

const settings = new Settings(options);

return utils.pattern.isDynamicPattern(source, settings);
}
}

function getWorks<T>(source: PatternInternal | PatternInternal[], _Provider: new (settings: Settings) => Provider<T>, options?: OptionsInternal): T[] {
Expand Down
8 changes: 2 additions & 6 deletions src/managers/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,8 @@ export function generate(patterns: Pattern[], settings: Settings): Task[] {
const positivePatterns = getPositivePatterns(patterns);
const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore);

/**
* When the `caseSensitiveMatch` option is disabled, all patterns must be marked as dynamic, because we cannot check
* filepath directly (without read directory).
*/
const staticPatterns = !settings.caseSensitiveMatch ? [] : positivePatterns.filter(utils.pattern.isStaticPattern);
const dynamicPatterns = !settings.caseSensitiveMatch ? positivePatterns : positivePatterns.filter(utils.pattern.isDynamicPattern);
const staticPatterns = positivePatterns.filter((pattern) => utils.pattern.isStaticPattern(pattern, settings));
const dynamicPatterns = positivePatterns.filter((pattern) => utils.pattern.isDynamicPattern(pattern, settings));

const staticTasks = convertPatternsToTasks(staticPatterns, negativePatterns, /* dynamic */ false);
const dynamicTasks = convertPatternsToTasks(dynamicPatterns, negativePatterns, /* dynamic */ true);
Expand Down
133 changes: 111 additions & 22 deletions src/utils/pattern.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,117 @@ describe('Utils → Pattern', () => {
});

describe('.isDynamicPattern', () => {
it('should return true for dynamic pattern', () => {
const actual = util.isDynamicPattern('*');

assert.ok(actual);
});

it('should return true for dynamic pattern with question mark glob', () => {
const actual = util.isDynamicPattern('?');

assert.ok(actual);
});

it('should return true for pattern with escape symbol', () => {
const actual = util.isDynamicPattern('\\*');

assert.ok(actual);
});

it('should return false for static pattern', () => {
const actual = util.isDynamicPattern('dir');

assert.ok(!actual);
describe('Without options', () => {
it('should return true for patterns that include the escape symbol', () => {
assert.ok(util.isDynamicPattern('\\'));
});

it('should return true for patterns that include common glob symbols', () => {
assert.ok(util.isDynamicPattern('*'));
assert.ok(util.isDynamicPattern('abc/*'));
assert.ok(util.isDynamicPattern('?'));
assert.ok(util.isDynamicPattern('abc/?'));
assert.ok(util.isDynamicPattern('!abc'));
});

it('should return true for patterns that include regex group symbols', () => {
assert.ok(util.isDynamicPattern('(a|)'));
assert.ok(util.isDynamicPattern('(a|b)'));
assert.ok(util.isDynamicPattern('abc/(a|b)'));
});

it('should return true for patterns that include regex character class symbols', () => {
assert.ok(util.isDynamicPattern('[abc]'));
assert.ok(util.isDynamicPattern('abc/[abc]'));
assert.ok(util.isDynamicPattern('[^abc]'));
assert.ok(util.isDynamicPattern('abc/[^abc]'));
assert.ok(util.isDynamicPattern('[1-3]'));
assert.ok(util.isDynamicPattern('abc/[1-3]'));
assert.ok(util.isDynamicPattern('[[:alpha:][:digit:]]'));
assert.ok(util.isDynamicPattern('abc/[[:alpha:][:digit:]]'));
});

it('should return true for patterns that include glob extension symbols', () => {
assert.ok(util.isDynamicPattern('@()'));
assert.ok(util.isDynamicPattern('@(a)'));
assert.ok(util.isDynamicPattern('@(a|b)'));
assert.ok(util.isDynamicPattern('abc/!(a|b)'));
assert.ok(util.isDynamicPattern('*(a|b)'));
assert.ok(util.isDynamicPattern('?(a|b)'));
assert.ok(util.isDynamicPattern('+(a|b)'));
});

it('should return true for patterns that include brace expansions symbols', () => {
assert.ok(util.isDynamicPattern('{,}'));
assert.ok(util.isDynamicPattern('{a,}'));
assert.ok(util.isDynamicPattern('{,b}'));
assert.ok(util.isDynamicPattern('{a,b}'));
assert.ok(util.isDynamicPattern('{1..3}'));
});

it('should return false for "!" symbols when a symbol is not specified first in the string', () => {
assert.ok(!util.isDynamicPattern('abc!'));
});

it('should return false for a completely static pattern', () => {
assert.ok(!util.isDynamicPattern(''));
assert.ok(!util.isDynamicPattern('.'));
assert.ok(!util.isDynamicPattern('abc'));
assert.ok(!util.isDynamicPattern('~abc'));
assert.ok(!util.isDynamicPattern('~/abc'));
assert.ok(!util.isDynamicPattern('+~/abc'));
assert.ok(!util.isDynamicPattern('@.(abc)'));
assert.ok(!util.isDynamicPattern('(a b)'));
assert.ok(!util.isDynamicPattern('(a b)'));
assert.ok(!util.isDynamicPattern('[abc'));
});

it('should return false for unfinished regex character class', () => {
assert.ok(!util.isDynamicPattern('['));
assert.ok(!util.isDynamicPattern('[abc'));
});

it('should return false for unfinished regex group', () => {
assert.ok(!util.isDynamicPattern('(a|b'));
assert.ok(!util.isDynamicPattern('abc/(a|b'));
});

it('should return false for unfinished glob extension', () => {
assert.ok(!util.isDynamicPattern('@('));
assert.ok(!util.isDynamicPattern('@(a'));
assert.ok(!util.isDynamicPattern('@(a|'));
assert.ok(!util.isDynamicPattern('@(a|b'));
});

it('should return false for unfinished brace expansions', () => {
assert.ok(!util.isDynamicPattern('{'));
assert.ok(!util.isDynamicPattern('{a'));
assert.ok(!util.isDynamicPattern('{,'));
assert.ok(!util.isDynamicPattern('{a,'));
assert.ok(!util.isDynamicPattern('{a,b'));
});
});

describe('With options', () => {
it('should return true for patterns that include "*?" symbols even when the "extglob" option is disabled', () => {
assert.ok(util.isDynamicPattern('*(a|b)', { extglob: false }));
assert.ok(util.isDynamicPattern('?(a|b)', { extglob: false }));
});

it('should return true when the "caseSensitiveMatch" option is enabled', () => {
assert.ok(util.isDynamicPattern('a', { caseSensitiveMatch: false }));
});

it('should return false for glob extension when the "extglob" option is disabled', () => {
assert.ok(!util.isDynamicPattern('@(a|b)', { extglob: false }));
assert.ok(!util.isDynamicPattern('abc/!(a|b)', { extglob: false }));
assert.ok(!util.isDynamicPattern('+(a|b)', { extglob: false }));
});

it('should return false for brace expansions when the "braceExpansion" option is disabled', () => {
assert.ok(!util.isDynamicPattern('{a,b}', { braceExpansion: false }));
assert.ok(!util.isDynamicPattern('{1..3}', { braceExpansion: false }));
});
});
});

Expand Down
41 changes: 36 additions & 5 deletions src/utils/pattern.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,51 @@
import * as path from 'path';

import globParent = require('glob-parent');
import isGlob = require('is-glob');
import micromatch = require('micromatch');

import { MicromatchOptions, Pattern, PatternRe } from '../types/index';

const GLOBSTAR = '**';
const ESCAPE_SYMBOL = '\\';

export function isStaticPattern(pattern: Pattern): boolean {
return !isDynamicPattern(pattern);
const COMMON_GLOB_SYMBOLS_RE = /[*?]|^!/;
const REGEX_CHARACTER_CLASS_SYMBOLS_RE = /\[.*]/;
const REGEX_GROUP_SYMBOLS_RE = /(?:^|[^@!*?+])\(.*\|.*\)/;
const GLOB_EXTENSION_SYMBOLS_RE = /[@!*?+]\(.*\)/;
const BRACE_EXPANSIONS_SYMBOL_RE = /{.*(?:,|\.\.).*}/;

interface PatternTypeOptions {
braceExpansion?: boolean;
caseSensitiveMatch?: boolean;
extglob?: boolean;
}

export function isStaticPattern(pattern: Pattern, options: PatternTypeOptions = {}): boolean {
return !isDynamicPattern(pattern, options);
}

export function isDynamicPattern(pattern: Pattern): boolean {
return isGlob(pattern, { strict: false }) || pattern.indexOf(ESCAPE_SYMBOL) !== -1;
export function isDynamicPattern(pattern: Pattern, options: PatternTypeOptions = {}): boolean {
/**
* When the `caseSensitiveMatch` option is disabled, all patterns must be marked as dynamic, because we cannot check
* filepath directly (without read directory).
*/
if (options.caseSensitiveMatch === false || pattern.includes(ESCAPE_SYMBOL)) {
return true;
}

if (COMMON_GLOB_SYMBOLS_RE.test(pattern) || REGEX_CHARACTER_CLASS_SYMBOLS_RE.test(pattern) || REGEX_GROUP_SYMBOLS_RE.test(pattern)) {
return true;
}

if (options.extglob !== false && GLOB_EXTENSION_SYMBOLS_RE.test(pattern)) {
return true;
}

if (options.braceExpansion !== false && BRACE_EXPANSIONS_SYMBOL_RE.test(pattern)) {
return true;
}

return false;
}

export function convertToPositivePattern(pattern: Pattern): Pattern {
Expand Down