gurkencheck 0.0.6 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +134 -3
  2. package/dist/config-parser.d.ts.map +1 -1
  3. package/dist/config-parser.js +7 -1
  4. package/dist/config-parser.js.map +1 -1
  5. package/dist/diagnostics.d.ts +34 -0
  6. package/dist/diagnostics.d.ts.map +1 -0
  7. package/dist/diagnostics.js +53 -0
  8. package/dist/diagnostics.js.map +1 -0
  9. package/dist/exit-codes.d.ts +10 -0
  10. package/dist/exit-codes.d.ts.map +1 -0
  11. package/dist/exit-codes.js +10 -0
  12. package/dist/exit-codes.js.map +1 -0
  13. package/dist/feature-finder.d.ts +38 -0
  14. package/dist/feature-finder.d.ts.map +1 -1
  15. package/dist/feature-finder.js +86 -8
  16. package/dist/feature-finder.js.map +1 -1
  17. package/dist/formatters/index.d.ts +44 -4
  18. package/dist/formatters/index.d.ts.map +1 -1
  19. package/dist/formatters/index.js +52 -15
  20. package/dist/formatters/index.js.map +1 -1
  21. package/dist/formatters/stylish.d.ts +3 -0
  22. package/dist/formatters/stylish.d.ts.map +1 -1
  23. package/dist/formatters/stylish.js +32 -1
  24. package/dist/formatters/stylish.js.map +1 -1
  25. package/dist/formatters/tap.d.ts +14 -0
  26. package/dist/formatters/tap.d.ts.map +1 -1
  27. package/dist/formatters/tap.js +22 -7
  28. package/dist/formatters/tap.js.map +1 -1
  29. package/dist/gherkin/parse.d.ts +42 -1
  30. package/dist/gherkin/parse.d.ts.map +1 -1
  31. package/dist/gherkin/parse.js +68 -3
  32. package/dist/gherkin/parse.js.map +1 -1
  33. package/dist/index.d.ts +27 -9
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +15 -5
  36. package/dist/index.js.map +1 -1
  37. package/dist/linter.d.ts +31 -3
  38. package/dist/linter.d.ts.map +1 -1
  39. package/dist/linter.js +82 -15
  40. package/dist/linter.js.map +1 -1
  41. package/dist/logger.d.ts +2 -0
  42. package/dist/logger.d.ts.map +1 -1
  43. package/dist/logger.js +4 -0
  44. package/dist/logger.js.map +1 -1
  45. package/dist/main.d.ts +9 -1
  46. package/dist/main.d.ts.map +1 -1
  47. package/dist/main.js +158 -31
  48. package/dist/main.js.map +1 -1
  49. package/dist/rules/no-dupe-feature-names.d.ts.map +1 -1
  50. package/dist/rules/no-dupe-feature-names.js +26 -13
  51. package/dist/rules/no-dupe-feature-names.js.map +1 -1
  52. package/dist/rules/no-dupe-file-names.d.ts.map +1 -1
  53. package/dist/rules/no-dupe-file-names.js +32 -17
  54. package/dist/rules/no-dupe-file-names.js.map +1 -1
  55. package/dist/rules/no-dupe-scenario-names.d.ts.map +1 -1
  56. package/dist/rules/no-dupe-scenario-names.js +34 -22
  57. package/dist/rules/no-dupe-scenario-names.js.map +1 -1
  58. package/dist/rules.d.ts +41 -3
  59. package/dist/rules.d.ts.map +1 -1
  60. package/dist/rules.js +112 -4
  61. package/dist/rules.js.map +1 -1
  62. package/dist/stats/collect.d.ts +31 -0
  63. package/dist/stats/collect.d.ts.map +1 -0
  64. package/dist/stats/collect.js +249 -0
  65. package/dist/stats/collect.js.map +1 -0
  66. package/dist/stats/command.d.ts +4 -0
  67. package/dist/stats/command.d.ts.map +1 -0
  68. package/dist/stats/command.js +113 -0
  69. package/dist/stats/command.js.map +1 -0
  70. package/dist/stats/format/index.d.ts +25 -0
  71. package/dist/stats/format/index.d.ts.map +1 -0
  72. package/dist/stats/format/index.js +25 -0
  73. package/dist/stats/format/index.js.map +1 -0
  74. package/dist/stats/format/json.d.ts +10 -0
  75. package/dist/stats/format/json.d.ts.map +1 -0
  76. package/dist/stats/format/json.js +5 -0
  77. package/dist/stats/format/json.js.map +1 -0
  78. package/dist/stats/format/markdown.d.ts +8 -0
  79. package/dist/stats/format/markdown.d.ts.map +1 -0
  80. package/dist/stats/format/markdown.js +123 -0
  81. package/dist/stats/format/markdown.js.map +1 -0
  82. package/dist/stats/format/shared.d.ts +34 -0
  83. package/dist/stats/format/shared.d.ts.map +1 -0
  84. package/dist/stats/format/shared.js +37 -0
  85. package/dist/stats/format/shared.js.map +1 -0
  86. package/dist/stats/format/text.d.ts +5 -0
  87. package/dist/stats/format/text.d.ts.map +1 -0
  88. package/dist/stats/format/text.js +175 -0
  89. package/dist/stats/format/text.js.map +1 -0
  90. package/dist/stats/index.d.ts +21 -0
  91. package/dist/stats/index.d.ts.map +1 -0
  92. package/dist/stats/index.js +17 -0
  93. package/dist/stats/index.js.map +1 -0
  94. package/dist/stats/normalise.d.ts +38 -0
  95. package/dist/stats/normalise.d.ts.map +1 -0
  96. package/dist/stats/normalise.js +65 -0
  97. package/dist/stats/normalise.js.map +1 -0
  98. package/dist/stats/similar.d.ts +61 -0
  99. package/dist/stats/similar.d.ts.map +1 -0
  100. package/dist/stats/similar.js +173 -0
  101. package/dist/stats/similar.js.map +1 -0
  102. package/dist/stats/types.d.ts +159 -0
  103. package/dist/stats/types.d.ts.map +1 -0
  104. package/dist/stats/types.js +10 -0
  105. package/dist/stats/types.js.map +1 -0
  106. package/dist/types.d.ts +47 -3
  107. package/dist/types.d.ts.map +1 -1
  108. package/dist/util/glob.d.ts +18 -0
  109. package/dist/util/glob.d.ts.map +1 -1
  110. package/dist/util/glob.js +126 -22
  111. package/dist/util/glob.js.map +1 -1
  112. package/dist/util/stream.d.ts +31 -0
  113. package/dist/util/stream.d.ts.map +1 -0
  114. package/dist/util/stream.js +67 -0
  115. package/dist/util/stream.js.map +1 -0
  116. package/dist/watch.d.ts +36 -0
  117. package/dist/watch.d.ts.map +1 -0
  118. package/dist/watch.js +203 -0
  119. package/dist/watch.js.map +1 -0
  120. package/package.json +1 -1
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Finding steps that are nearly, but not quite, the same step.
3
+ *
4
+ * `I am logged in` and `I'm logged in` are one behaviour written twice, and
5
+ * they cost two step definitions. Normalisation cannot join them - they
6
+ * really are different sentences - so they are grouped by how many single
7
+ * character edits it takes to turn one into the other.
8
+ *
9
+ * Edit distance is quadratic in the number of distinct steps, which on a
10
+ * large suite is millions of comparisons, so two cheap tests run first. Both
11
+ * are lower bounds on the distance, never overestimates, so a pair they
12
+ * reject could not have been close enough anyway:
13
+ *
14
+ * - two strings are at least as far apart as their lengths differ;
15
+ * - every edit changes the tally of characters by at most two, so half the
16
+ * difference between two tallies is a floor on the distance as well.
17
+ */
18
+ export const DEFAULT_SIMILARITY = { ratio: 0.85, minLength: 8, maxEdits: 3 };
19
+ /** How many buckets the character tally uses. */
20
+ const TALLY_SIZE = 32;
21
+ /**
22
+ * A rough tally of the characters in a string.
23
+ *
24
+ * Characters share buckets, which can only make two tallies look more alike
25
+ * than they are - and since the tally is used to rule pairs out, looking more
26
+ * alike is the safe direction to be wrong in.
27
+ */
28
+ function tally(text) {
29
+ const counts = new Array(TALLY_SIZE).fill(0);
30
+ for (let index = 0; index < text.length; index++) {
31
+ const bucket = text.charCodeAt(index) % TALLY_SIZE;
32
+ counts[bucket] = (counts[bucket] ?? 0) + 1;
33
+ }
34
+ return counts;
35
+ }
36
+ /**
37
+ * True when two tallies are far enough apart that no run of `limit` edits
38
+ * could join the strings behind them.
39
+ *
40
+ * This is the test almost every pair fails, so it stops as soon as the answer
41
+ * is settled rather than finishing the count.
42
+ */
43
+ function tallyRulesOut(left, right, limit) {
44
+ const budget = limit * 2;
45
+ let sum = 0;
46
+ for (let bucket = 0; bucket < TALLY_SIZE; bucket++) {
47
+ sum += Math.abs((left[bucket] ?? 0) - (right[bucket] ?? 0));
48
+ if (sum > budget) {
49
+ return true;
50
+ }
51
+ }
52
+ return false;
53
+ }
54
+ /**
55
+ * The Levenshtein distance between two strings, giving up as soon as it is
56
+ * certain to exceed `limit` and returning `limit + 1` in that case.
57
+ *
58
+ * Every value in a row of the table is a lower bound on everything below it,
59
+ * so once the whole row is past the limit there is no way back.
60
+ */
61
+ export function boundedEditDistance(left, right, limit) {
62
+ if (left === right) {
63
+ return 0;
64
+ }
65
+ if (Math.abs(left.length - right.length) > limit) {
66
+ return limit + 1;
67
+ }
68
+ const width = right.length;
69
+ let previous = Array.from({ length: width + 1 }, (_, index) => index);
70
+ let current = new Array(width + 1).fill(0);
71
+ for (let row = 1; row <= left.length; row++) {
72
+ current[0] = row;
73
+ let rowMinimum = row;
74
+ const leftCharacter = left.charCodeAt(row - 1);
75
+ for (let column = 1; column <= width; column++) {
76
+ const substitution = (previous[column - 1] ?? 0) + (leftCharacter === right.charCodeAt(column - 1) ? 0 : 1);
77
+ const deletion = (previous[column] ?? 0) + 1;
78
+ const insertion = (current[column - 1] ?? 0) + 1;
79
+ const best = Math.min(substitution, deletion, insertion);
80
+ current[column] = best;
81
+ if (best < rowMinimum) {
82
+ rowMinimum = best;
83
+ }
84
+ }
85
+ if (rowMinimum > limit) {
86
+ return limit + 1;
87
+ }
88
+ const finished = previous;
89
+ previous = current;
90
+ current = finished;
91
+ }
92
+ return previous[width] ?? limit + 1;
93
+ }
94
+ /** Disjoint sets, used to join pairs of close steps into groups. */
95
+ function makeSets(size) {
96
+ const parent = Array.from({ length: size }, (_, index) => index);
97
+ function find(item) {
98
+ let root = item;
99
+ while ((parent[root] ?? root) !== root) {
100
+ root = parent[root] ?? root;
101
+ }
102
+ let walk = item;
103
+ while ((parent[walk] ?? walk) !== root) {
104
+ const next = parent[walk] ?? walk;
105
+ parent[walk] = root;
106
+ walk = next;
107
+ }
108
+ return root;
109
+ }
110
+ return {
111
+ find,
112
+ union(a, b) {
113
+ const rootA = find(a);
114
+ const rootB = find(b);
115
+ if (rootA !== rootB) {
116
+ parent[rootB] = rootA;
117
+ }
118
+ },
119
+ };
120
+ }
121
+ /**
122
+ * Groups entries whose text is close enough to be the same step written two
123
+ * ways. Entries that are close to nothing are left out, so every group
124
+ * returned has at least two members, in the order they were given in.
125
+ */
126
+ export function groupSimilar(entries, options = DEFAULT_SIMILARITY) {
127
+ // Sorting by length lets the search stop early: once two steps are too
128
+ // different in length, every step after this one is longer still.
129
+ const candidates = entries
130
+ .map((entry, index) => ({ index, text: entry.text, tally: tally(entry.text) }))
131
+ .filter((candidate) => candidate.text.length >= options.minLength)
132
+ .sort((a, b) => a.text.length - b.text.length);
133
+ const sets = makeSets(entries.length);
134
+ let joined = false;
135
+ for (let i = 0; i < candidates.length; i++) {
136
+ const left = candidates[i];
137
+ for (let j = i + 1; j < candidates.length; j++) {
138
+ const right = candidates[j];
139
+ const limit = Math.min(options.maxEdits, Math.floor((1 - options.ratio) * right.text.length));
140
+ // Sorted by length, so once a step is too much longer than this one,
141
+ // every step after it is longer still.
142
+ if (right.text.length - left.text.length > limit) {
143
+ break;
144
+ }
145
+ if (limit < 1 || tallyRulesOut(left.tally, right.tally, limit)) {
146
+ continue;
147
+ }
148
+ if (boundedEditDistance(left.text, right.text, limit) <= limit) {
149
+ sets.union(left.index, right.index);
150
+ joined = true;
151
+ }
152
+ }
153
+ }
154
+ if (!joined) {
155
+ return [];
156
+ }
157
+ const groups = new Map();
158
+ for (const candidate of candidates) {
159
+ const root = sets.find(candidate.index);
160
+ const member = { index: candidate.index, entry: entries[candidate.index] };
161
+ const group = groups.get(root);
162
+ if (group === undefined) {
163
+ groups.set(root, [member]);
164
+ }
165
+ else {
166
+ group.push(member);
167
+ }
168
+ }
169
+ return [...groups.values()]
170
+ .filter((group) => group.length > 1)
171
+ .map((group) => group.sort((a, b) => a.index - b.index).map((member) => member.entry));
172
+ }
173
+ //# sourceMappingURL=similar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"similar.js","sourceRoot":"","sources":["../../src/stats/similar.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The shape of a statistics run.
3
+ *
4
+ * Everything here is complete: no list is cut short, no number is rounded to
5
+ * fit a screen. Trimming is the formatters' job, so that the JSON output is a
6
+ * full dataset - one a later run can be compared against - while the text
7
+ * report stays short enough to read.
8
+ */
9
+ /** A summary of a set of counts, for numbers whose average hides the shape. */
10
+ export interface Distribution {
11
+ /** How many values went into the summary. */
12
+ count: number;
13
+ min: number;
14
+ median: number;
15
+ /** The value nine tenths of the set are at or below. */
16
+ p90: number;
17
+ max: number;
18
+ mean: number;
19
+ }
20
+ /** One scenario, named well enough to go and find it. */
21
+ export interface ScenarioRef {
22
+ name: string;
23
+ file: string;
24
+ line: number;
25
+ /** Steps written in the scenario itself, Background steps excluded. */
26
+ steps: number;
27
+ }
28
+ /** One entry of the step vocabulary: a normalised step and where it is used. */
29
+ export interface StepEntry {
30
+ /** The step text after normalisation - what makes two steps "the same". */
31
+ text: string;
32
+ /** How many written steps normalise to this text. */
33
+ count: number;
34
+ /** The step text as somebody actually wrote it, the first time it appears. */
35
+ example: string;
36
+ /** Where that first sighting is. */
37
+ file: string;
38
+ line: number;
39
+ }
40
+ /** Normalised steps that differ only slightly from each other. */
41
+ export interface SimilarGroup {
42
+ /** The steps in the group, most used first. */
43
+ members: StepEntry[];
44
+ /** Written steps covered by the whole group. */
45
+ total: number;
46
+ }
47
+ /** How often a tag is written. */
48
+ export interface TagEntry {
49
+ name: string;
50
+ count: number;
51
+ }
52
+ /** A file the Gherkin parser refused, and so could not be counted. */
53
+ export interface UnreadableFile {
54
+ file: string;
55
+ /** The parser's complaint, or the rule name it maps onto. */
56
+ reason: string;
57
+ line: number;
58
+ }
59
+ /** How many files were written in each Gherkin dialect. */
60
+ export interface LanguageEntry {
61
+ /** The Gherkin language code, such as `en` or `de`. */
62
+ code: string;
63
+ files: number;
64
+ }
65
+ /** What is in the feature files, counted. */
66
+ export interface Inventory {
67
+ features: number;
68
+ rules: number;
69
+ backgrounds: number;
70
+ /** Scenarios that are not Scenario Outlines. */
71
+ scenarios: number;
72
+ scenarioOutlines: number;
73
+ examplesTables: number;
74
+ /** Body rows across every Examples table; header rows are not counted. */
75
+ examplesRows: number;
76
+ /** Steps as written, Background steps included. */
77
+ steps: number;
78
+ dataTables: number;
79
+ docStrings: number;
80
+ }
81
+ /** How the steps of the suite divide between Given, When and Then. */
82
+ export interface KeywordMix {
83
+ given: number;
84
+ when: number;
85
+ then: number;
86
+ /**
87
+ * Steps whose keyword resolves to nothing: an `And` or `But` written before
88
+ * anything for it to carry on from.
89
+ */
90
+ other: number;
91
+ }
92
+ /** What the steps of the suite look like taken together. */
93
+ export interface StepStats {
94
+ /** Steps as written, Background steps included. */
95
+ total: number;
96
+ /** Distinct steps after normalisation. */
97
+ unique: number;
98
+ /**
99
+ * `unique / total`. Low means the team shares a vocabulary; high means
100
+ * everybody invents their own phrasing, and the step definitions rot.
101
+ */
102
+ uniqueRatio: number;
103
+ /** Distinct steps written exactly once - usually accidental one-offs. */
104
+ usedOnce: number;
105
+ /** Every distinct step, most used first. */
106
+ vocabulary: StepEntry[];
107
+ /** Steps that are nearly, but not quite, each other. */
108
+ similar: SimilarGroup[];
109
+ /** Given/When/Then mix, with `And` and `But` resolved to what they follow. */
110
+ keywords: KeywordMix;
111
+ /** Words per step, as a proxy for how detailed the steps have become. */
112
+ wordsPerStep: Distribution;
113
+ }
114
+ /** What the scenarios of the suite look like taken together. */
115
+ export interface ScenarioStats {
116
+ /**
117
+ * The number of test cases: one per Scenario, and one per row of every
118
+ * Examples table. This is the number that predicts how long a suite takes
119
+ * to run, and almost nobody's mental count of it is right.
120
+ */
121
+ effective: number;
122
+ /** Steps per scenario, Background steps excluded. */
123
+ stepsPerScenario: Distribution;
124
+ /** Every scenario, longest first. */
125
+ largest: ScenarioRef[];
126
+ }
127
+ /** What the tags of the suite look like taken together. */
128
+ export interface TagStats {
129
+ /** Tags as written, counting a tag once for every node carrying it. */
130
+ total: number;
131
+ unique: number;
132
+ /** Every distinct tag, most used first. */
133
+ vocabulary: TagEntry[];
134
+ /** Tags written exactly once - very often a typo of one written often. */
135
+ usedOnce: string[];
136
+ /**
137
+ * Scenarios carrying no tag of their own and inheriting none, and so
138
+ * reachable by no tag expression.
139
+ */
140
+ untaggedScenarios: number;
141
+ }
142
+ /** Everything one statistics run found. */
143
+ export interface Statistics {
144
+ files: {
145
+ /** Files the search matched. */
146
+ total: number;
147
+ /** Files the parser accepted, and so the ones every other number is from. */
148
+ parsed: number;
149
+ /** Files the parser refused. Nothing in them is counted. */
150
+ unreadable: UnreadableFile[];
151
+ };
152
+ inventory: Inventory;
153
+ scenarios: ScenarioStats;
154
+ steps: StepStats;
155
+ tags: TagStats;
156
+ /** Dialects in use, most common first. A single stray entry is a bug. */
157
+ languages: LanguageEntry[];
158
+ }
159
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/stats/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The shape of a statistics run.
3
+ *
4
+ * Everything here is complete: no list is cut short, no number is rounded to
5
+ * fit a screen. Trimming is the formatters' job, so that the JSON output is a
6
+ * full dataset - one a later run can be compared against - while the text
7
+ * report stays short enough to read.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/stats/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
package/dist/types.d.ts CHANGED
@@ -50,6 +50,30 @@ export type Severity = 'error' | 'warning';
50
50
  export type RuleConfig = RuleState | readonly [RuleState, unknown];
51
51
  /** The parsed contents of a `.gurkencheckrc` file. */
52
52
  export type Configuration = Record<string, RuleConfig>;
53
+ /**
54
+ * Somewhere for a rule to keep what it learns as a run goes on.
55
+ *
56
+ * A rule looking for duplicates has to remember what it has already seen.
57
+ * Keeping that in the module makes it process-global, so two runs at once - a
58
+ * language server checking two folders, a test suite running cases side by
59
+ * side - quietly corrupt each other's state. A context belongs to one rule in
60
+ * one run, so there is nothing shared and nothing to reset.
61
+ */
62
+ export interface RunContext {
63
+ /** This rule's state for this run, made the first time it is asked for. */
64
+ state<T>(create: () => T): T;
65
+ }
66
+ /**
67
+ * A finding that could only be worked out once every file had been seen, so
68
+ * it has to say which file it is about.
69
+ */
70
+ export interface RunFinding extends RuleError {
71
+ /**
72
+ * The file this is about, as it was given to the linter. Left out for a
73
+ * finding about the run as a whole rather than about any one file.
74
+ */
75
+ filePath?: string;
76
+ }
53
77
  /**
54
78
  * A lint rule.
55
79
  *
@@ -68,11 +92,31 @@ export interface LintRule {
68
92
  /**
69
93
  * Returns the violations found in this file. A rule that needs to wait for
70
94
  * something - reading a file, asking a service - may return a promise.
95
+ *
96
+ * `context` is the same object for every file of a run, and a different one
97
+ * for every other run, so it is where anything remembered between files
98
+ * belongs.
99
+ */
100
+ run(feature: Feature | undefined, file: FeatureFile, configuration: unknown, context: RunContext): RuleError[] | Promise<RuleError[]>;
101
+ /**
102
+ * Called once before the first file, for a rule with something to set up.
103
+ * Rules that only accumulate as they go need nothing here: the context
104
+ * makes their state the first time they ask for it.
105
+ */
106
+ onRunStart?(configuration: unknown, context: RunContext): void;
107
+ /**
108
+ * The findings that could only be worked out once every file had been seen,
109
+ * such as two files sharing a name. Called once after the last file, and
110
+ * only for a rule the configuration switched on.
71
111
  */
72
- run(feature: Feature | undefined, file: FeatureFile, configuration: unknown): RuleError[] | Promise<RuleError[]>;
112
+ onRunEnd?(configuration: unknown, context: RunContext): RunFinding[] | Promise<RunFinding[]>;
73
113
  /**
74
- * Clears any state kept between files. Implemented by the rules that look
75
- * for duplicates across a whole run; called once before each lint run.
114
+ * Clears any state kept between files.
115
+ *
116
+ * @deprecated Keep state in the `RunContext` handed to `run` instead. State
117
+ * in the module is shared by every run in the process, so two at once tread
118
+ * on each other. Still called before each run, for rules written before
119
+ * contexts existed.
76
120
  */
77
121
  reset?(): void;
78
122
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAEhD,6CAA6C;AAC7C,MAAM,WAAW,WAAW;IAC1B,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,+BAA+B;AAC/B,MAAM,WAAW,SAAS;IACxB,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,4CAA4C;AAC5C,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,SAAS,EAAE,CAAC;CACrB;AAED;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,KAAK,CAAC;AAE9C,kCAAkC;AAClC,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AAE3C;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;AAEnE,sDAAsD;AACtD,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,WAAW,QAAQ;IACvB,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC;;;OAGG;IACH,GAAG,CACD,OAAO,EAAE,OAAO,GAAG,SAAS,EAC5B,IAAI,EAAE,WAAW,EACjB,aAAa,EAAE,OAAO,GACrB,SAAS,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACtC;;;OAGG;IACH,KAAK,CAAC,IAAI,IAAI,CAAC;CAChB;AAED,mDAAmD;AACnD,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAEhD,6CAA6C;AAC7C,MAAM,WAAW,WAAW;IAC1B,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,+BAA+B;AAC/B,MAAM,WAAW,SAAS;IACxB,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,4CAA4C;AAC5C,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,SAAS,EAAE,CAAC;CACrB;AAED;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,KAAK,CAAC;AAE9C,kCAAkC;AAClC,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AAE3C;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;AAEnE,sDAAsD;AACtD,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAEvD;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,2EAA2E;IAC3E,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,UAAW,SAAQ,SAAS;IAC3C;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,QAAQ;IACvB,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC;;;;;;;OAOG;IACH,GAAG,CACD,OAAO,EAAE,OAAO,GAAG,SAAS,EAC5B,IAAI,EAAE,WAAW,EACjB,aAAa,EAAE,OAAO,EACtB,OAAO,EAAE,UAAU,GAClB,SAAS,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACtC;;;;OAIG;IACH,UAAU,CAAC,CAAC,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,CACP,aAAa,EAAE,OAAO,EACtB,OAAO,EAAE,UAAU,GAClB,UAAU,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IACxC;;;;;;;OAOG;IACH,KAAK,CAAC,IAAI,IAAI,CAAC;CAChB;AAED,mDAAmD;AACnD,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC"}
@@ -3,6 +3,12 @@
3
3
  * slash-separated paths.
4
4
  */
5
5
  export declare function globToRegExp(pattern: string): RegExp;
6
+ /**
7
+ * The directory a pattern's search starts from - the leading run of literal
8
+ * segments, resolved against `cwd`. What you would watch to be told about
9
+ * anything the pattern could match.
10
+ */
11
+ export declare function globRoot(pattern: string, cwd?: string): string;
6
12
  export interface GlobOptions {
7
13
  /** Directory that relative patterns and returned paths are relative to. */
8
14
  cwd?: string;
@@ -17,4 +23,16 @@ export interface GlobOptions {
17
23
  * forward slashes. Directories are never returned. Results are sorted.
18
24
  */
19
25
  export declare function globSync(pattern: string, options?: GlobOptions): string[];
26
+ /**
27
+ * The same search, handing each match over as it is found.
28
+ *
29
+ * Collecting a whole tree before anything else starts is time in which
30
+ * nothing is read, parsed or checked. This hands each match over as it is
31
+ * reached, so the work after it starts on the first file rather than on the
32
+ * last.
33
+ *
34
+ * The order is the order `globSync` returns, without anything being collected
35
+ * to sort: `byPathOrder` is what makes the two agree.
36
+ */
37
+ export declare function globStream(pattern: string, options?: GlobOptions): AsyncGenerator<string>;
20
38
  //# sourceMappingURL=glob.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../../src/util/glob.ts"],"names":[],"mappings":"AAeA;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA8DpD;AA0DD,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AAyBD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,EAAE,CAmC7E"}
1
+ {"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../../src/util/glob.ts"],"names":[],"mappings":"AAeA;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA8DpD;AAgCD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,CAE7E;AAsFD,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AA0DD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,EAAE,CAsB7E;AAED;;;;;;;;;;GAUG;AACH,wBAAuB,UAAU,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,WAAgB,GACxB,cAAc,CAAC,MAAM,CAAC,CAsBxB"}
package/dist/util/glob.js CHANGED
@@ -109,19 +109,44 @@ function staticPrefix(pattern) {
109
109
  }
110
110
  return literal.join('/');
111
111
  }
112
+ /**
113
+ * The directory a pattern's search starts from - the leading run of literal
114
+ * segments, resolved against `cwd`. What you would watch to be told about
115
+ * anything the pattern could match.
116
+ */
117
+ export function globRoot(pattern, cwd = process.cwd()) {
118
+ return path.resolve(cwd, staticPrefix(pattern.split(path.sep).join('/')));
119
+ }
112
120
  function isHidden(name) {
113
121
  return name.startsWith('.');
114
122
  }
115
- function walk(directory, onFile) {
116
- let entries;
123
+ /**
124
+ * Orders the entries of a directory so that walking depth-first reaches files
125
+ * in the same order that sorting the finished paths would.
126
+ *
127
+ * Everything inside a directory `d` has a path starting `d/`, so `d` has to
128
+ * be ordered as `d/` rather than as `d`. Otherwise a sibling file called
129
+ * `d-1.feature` comes after the directory here and before it in a sorted
130
+ * list, because `-` sorts before `/` - and a walk that hands files over as it
131
+ * finds them would quietly report them in a different order from one that
132
+ * collects and sorts.
133
+ */
134
+ function byPathOrder(a, b) {
135
+ const left = a.isDirectory() ? `${a.name}/` : a.name;
136
+ const right = b.isDirectory() ? `${b.name}/` : b.name;
137
+ return left < right ? -1 : left > right ? 1 : 0;
138
+ }
139
+ function readEntries(directory) {
117
140
  try {
118
- entries = fs.readdirSync(directory, { withFileTypes: true });
141
+ return fs.readdirSync(directory, { withFileTypes: true }).sort(byPathOrder);
119
142
  }
120
143
  catch {
121
144
  // A missing or unreadable directory simply contributes no matches.
122
- return;
145
+ return [];
123
146
  }
124
- for (const entry of entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
147
+ }
148
+ function walk(directory, onFile) {
149
+ for (const entry of readEntries(directory)) {
125
150
  if (isHidden(entry.name)) {
126
151
  continue;
127
152
  }
@@ -134,6 +159,43 @@ function walk(directory, onFile) {
134
159
  }
135
160
  }
136
161
  }
162
+ /**
163
+ * The same walk, handing each file over as it is reached.
164
+ *
165
+ * Each directory is still read synchronously. `fs.promises.readdir` was tried
166
+ * and is about 40% slower over a tree of a couple of thousand directories -
167
+ * a promise for each directory costs more than reading it does - and it
168
+ * bought nothing, because the only thing waiting on this walk is the work
169
+ * being fed by it. What matters is that the walk is no longer one long block:
170
+ * it gives way at every file, and the reading and checking downstream happen
171
+ * in the gaps.
172
+ */
173
+ async function* walkStream(root) {
174
+ // An explicit stack rather than recursion: a recursive async generator
175
+ // delegates through one `yield*` per level of nesting for every file it
176
+ // hands over, so a deep tree pays for its depth on every single file.
177
+ const stack = [{ directory: root, entries: readEntries(root), index: 0 }];
178
+ while (stack.length > 0) {
179
+ const top = stack[stack.length - 1];
180
+ if (top.index >= top.entries.length) {
181
+ stack.pop();
182
+ continue;
183
+ }
184
+ const entry = top.entries[top.index++];
185
+ if (isHidden(entry.name)) {
186
+ continue;
187
+ }
188
+ const absolutePath = path.join(top.directory, entry.name);
189
+ if (entry.isDirectory()) {
190
+ // Pushed so that its contents come before the entries after it here,
191
+ // which is what a depth-first walk means.
192
+ stack.push({ directory: absolutePath, entries: readEntries(absolutePath), index: 0 });
193
+ }
194
+ else if (entry.isFile()) {
195
+ yield absolutePath;
196
+ }
197
+ }
198
+ }
137
199
  /**
138
200
  * True when the path, or any directory leading to it, matches one of the
139
201
  * patterns.
@@ -156,42 +218,84 @@ function isIgnored(relativePath, ignores) {
156
218
  }
157
219
  return false;
158
220
  }
159
- /**
160
- * Returns the files matching `pattern`, as paths relative to `cwd` using
161
- * forward slashes. Directories are never returned. Results are sorted.
162
- */
163
- export function globSync(pattern, options = {}) {
221
+ function newSearch(pattern, options) {
164
222
  const cwd = options.cwd ?? process.cwd();
165
223
  const normalisedPattern = pattern.split(path.sep).join('/');
166
224
  const matches = globToRegExp(normalisedPattern);
167
225
  const ignores = (options.ignore ?? []).map((ignorePattern) => globToRegExp(ignorePattern.split(path.sep).join('/')));
168
- const searchRoot = path.resolve(cwd, staticPrefix(normalisedPattern));
169
- const found = [];
170
226
  // An absolute pattern is matched against absolute paths: comparing it with
171
227
  // a path relative to cwd can never match, and the search would come back
172
228
  // empty rather than wrong, which reads as "nothing to report".
173
229
  const absolutePattern = path.isAbsolute(normalisedPattern);
230
+ return {
231
+ searchRoot: path.resolve(cwd, staticPrefix(normalisedPattern)),
232
+ match(absolutePath) {
233
+ const relativePath = path.relative(cwd, absolutePath).split(path.sep).join('/');
234
+ const candidate = absolutePattern ? absolutePath.split(path.sep).join('/') : relativePath;
235
+ if (!matches.test(candidate))
236
+ return undefined;
237
+ if (isIgnored(relativePath, ignores))
238
+ return undefined;
239
+ return relativePath;
240
+ },
241
+ };
242
+ }
243
+ /**
244
+ * Returns the files matching `pattern`, as paths relative to `cwd` using
245
+ * forward slashes. Directories are never returned. Results are sorted.
246
+ */
247
+ export function globSync(pattern, options = {}) {
248
+ const search = newSearch(pattern, options);
249
+ const found = [];
174
250
  const consider = (absolutePath) => {
175
- const relativePath = path.relative(cwd, absolutePath).split(path.sep).join('/');
176
- const candidate = absolutePattern ? absolutePath.split(path.sep).join('/') : relativePath;
177
- if (!matches.test(candidate))
178
- return;
179
- if (isIgnored(relativePath, ignores))
180
- return;
181
- found.push(relativePath);
251
+ const relativePath = search.match(absolutePath);
252
+ if (relativePath !== undefined) {
253
+ found.push(relativePath);
254
+ }
182
255
  };
183
- const stats = statOrUndefined(searchRoot);
256
+ const stats = statOrUndefined(search.searchRoot);
184
257
  if (stats === undefined) {
185
258
  return [];
186
259
  }
187
260
  if (stats.isFile()) {
188
- consider(searchRoot);
261
+ consider(search.searchRoot);
189
262
  }
190
263
  else {
191
- walk(searchRoot, consider);
264
+ walk(search.searchRoot, consider);
192
265
  }
193
266
  return found.sort();
194
267
  }
268
+ /**
269
+ * The same search, handing each match over as it is found.
270
+ *
271
+ * Collecting a whole tree before anything else starts is time in which
272
+ * nothing is read, parsed or checked. This hands each match over as it is
273
+ * reached, so the work after it starts on the first file rather than on the
274
+ * last.
275
+ *
276
+ * The order is the order `globSync` returns, without anything being collected
277
+ * to sort: `byPathOrder` is what makes the two agree.
278
+ */
279
+ export async function* globStream(pattern, options = {}) {
280
+ const search = newSearch(pattern, options);
281
+ const stats = statOrUndefined(search.searchRoot);
282
+ if (stats === undefined) {
283
+ return;
284
+ }
285
+ if (stats.isFile()) {
286
+ const relativePath = search.match(search.searchRoot);
287
+ if (relativePath !== undefined) {
288
+ yield relativePath;
289
+ }
290
+ return;
291
+ }
292
+ for await (const absolutePath of walkStream(search.searchRoot)) {
293
+ const relativePath = search.match(absolutePath);
294
+ if (relativePath !== undefined) {
295
+ yield relativePath;
296
+ }
297
+ }
298
+ }
195
299
  function statOrUndefined(target) {
196
300
  try {
197
301
  return fs.statSync(target);