@mrclrchtr/supi-code-intelligence 6.1.0 → 6.3.0

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 (141) hide show
  1. package/README.md +3 -1
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  6. package/node_modules/@mrclrchtr/supi-lsp/README.md +23 -3
  7. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  8. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  9. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/README.md +52 -0
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/index.js +785 -129
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/legacy.js +660 -98
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/package.json +10 -4
  15. package/node_modules/@mrclrchtr/supi-lsp/package.json +4 -5
  16. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +16 -7
  17. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +1 -1
  18. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +0 -5
  19. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +6 -3
  20. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +2 -1
  21. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +5 -2
  22. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +2 -1
  23. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +17 -5
  24. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +40 -18
  25. package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +164 -30
  26. package/node_modules/@mrclrchtr/supi-lsp/src/config/lsp-settings.ts +9 -87
  27. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +0 -1
  28. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +4 -5
  29. package/node_modules/@mrclrchtr/supi-lsp/src/config/types.ts +0 -29
  30. package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +7 -21
  31. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-severity.ts +6 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +11 -14
  33. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +0 -5
  34. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +0 -1
  35. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +29 -49
  36. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +1 -1
  37. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +4 -8
  38. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +1 -9
  39. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-process-crash-report.ts +106 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +6 -3
  41. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +9 -1
  42. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +26 -41
  43. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +577 -152
  44. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +4 -1
  45. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +59 -28
  46. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +27 -19
  47. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +54 -0
  48. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registration.ts +0 -17
  49. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +74 -19
  50. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +5 -2
  51. package/node_modules/@mrclrchtr/supi-lsp/src/session/scanner.ts +24 -21
  52. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +29 -3
  53. package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +19 -40
  54. package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +13 -19
  55. package/node_modules/@mrclrchtr/supi-lsp/src/workspace-path-policy.ts +296 -0
  56. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +6 -18
  57. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  58. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  59. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  60. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  61. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/README.md +29 -1
  62. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.cjs +99 -19
  63. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.cjs.map +4 -4
  64. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.js +94 -18
  65. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.js.map +4 -4
  66. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
  67. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +14 -14
  68. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +13 -8
  69. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.cjs +97 -19
  70. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.cjs.map +4 -4
  71. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.cts +28 -8
  72. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.cts.map +1 -1
  73. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.ts +28 -8
  74. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.ts.map +1 -1
  75. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.js +92 -18
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.js.map +4 -4
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +17 -17
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +7 -6
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm +0 -0
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +2 -2
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm +0 -0
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +2 -2
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +1 -11
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/src/coordinates.ts +19 -0
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +5 -23
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/src/operation-support.ts +3 -21
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +5 -9
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +10 -20
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +7 -1
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +9 -5
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +134 -9
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-name.ts +96 -0
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +11 -12
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +39 -56
  96. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +9 -7
  97. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/js-binding-pattern.ts +26 -0
  98. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +2 -21
  99. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-html-sql.ts +144 -12
  100. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline.ts +50 -37
  101. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/scope.ts +138 -0
  102. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +0 -7
  103. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +1 -14
  104. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +0 -10
  105. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +6 -8
  106. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +34 -10
  107. package/package.json +5 -5
  108. package/src/analysis/capability/capability-warnings.ts +4 -22
  109. package/src/analysis/search/ast-scan-timing.ts +4 -2
  110. package/src/analysis/search/ast-scan.ts +1 -1
  111. package/src/api.ts +1 -0
  112. package/src/extension.ts +4 -2
  113. package/src/session/health-refresh.ts +26 -2
  114. package/src/session/health-types.ts +19 -0
  115. package/src/session/input/health-refactor.ts +16 -14
  116. package/src/session/refactor-types.ts +14 -1
  117. package/src/session/refactor-workflow.ts +21 -16
  118. package/src/substrate/lsp/lifecycle.ts +0 -3
  119. package/src/substrate/lsp/maintenance.ts +4 -5
  120. package/src/substrate/lsp/settings.ts +7 -9
  121. package/src/substrate/lsp/state.ts +0 -2
  122. package/src/substrate/workspace-provider-host.ts +2 -2
  123. package/src/tool/code_health/file-scope-markdown.ts +43 -0
  124. package/src/tool/code_health/markdown.ts +66 -66
  125. package/src/tool/code_health/refresh-outcome.ts +76 -0
  126. package/src/tool/code_health/refresh-status.ts +212 -23
  127. package/src/tool/code_health/result.ts +1 -0
  128. package/src/tool/code_health/tui.ts +18 -5
  129. package/src/tool/code_refactor_apply/guidance.ts +3 -2
  130. package/src/tool/code_refactor_plan/guidance.ts +1 -1
  131. package/src/tool/register.ts +5 -2
  132. package/src/tool/schemas.ts +21 -11
  133. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-actions.ts +0 -59
  134. package/node_modules/@mrclrchtr/supi-lsp/src/pattern-matcher.ts +0 -24
  135. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-kotlin-wasm.mjs +0 -132
  136. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-sql-wasm.mjs +0 -158
  137. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/vendor-wasm.mjs +0 -167
  138. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -42
  139. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-registration.ts +0 -37
  140. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/service-registry.ts +0 -30
  141. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/structure.ts +0 -8
@@ -9,7 +9,14 @@ const UNDEFINED = undefined
9
9
  const EMPTY = ''
10
10
  const SPACE = ' '
11
11
  const ESCAPE = '\\'
12
- const REGEX_TEST_BLANK_LINE = /^\s+$/
12
+
13
+ // The characters that carry a meaning of their own inside a regular
14
+ // expression, so a literal one has to be escaped before it is emitted.
15
+ const REGEX_LITERAL_SPECIAL = /[.*+?()[\]{}^$|\\/]/
16
+ // A line of only spaces is blank -- the trailing-space trimming empties it --
17
+ // but a line holding a tab is a pattern for a tab-named path, since git
18
+ // never trims a tab.
19
+ const REGEX_TEST_BLANK_LINE = /^ +$/
13
20
  const REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/
14
21
  const REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/
15
22
  const REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/
@@ -23,9 +30,10 @@ const REGEX_SPLITALL_CRLF = /\r?\n/g
23
30
  // - ..
24
31
  // Valid:
25
32
  // - .foo
26
- const REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/
33
+ const DOUBLE_SLASH = '//'
34
+ const SLASH_CODE = 47
35
+ const DOT_CODE = 46
27
36
 
28
- const REGEX_TEST_TRAILING_SLASH = /\/$/
29
37
 
30
38
  const SLASH = '/'
31
39
 
@@ -42,36 +50,302 @@ const define = (object, key, value) => {
42
50
  return value
43
51
  }
44
52
 
45
- const REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g
46
-
47
53
  const RETURN_FALSE = () => false
48
54
 
49
- // Sanitize the range of a regular expression
50
- // The cases are complicated, see test cases for details
51
- const sanitizeRange = range => range.replace(
52
- REGEX_REGEXP_RANGE,
53
- (match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0)
54
- ? match
55
- // Invalid range (out of order) which is ok for gitignore rules but
56
- // fatal for JavaScript regular expression, so eliminate it.
57
- : EMPTY
58
- )
59
-
60
- // > An optional `!` or `^` at the start of a class negates it, so that it
61
- // > matches any character not in the set. (gitignore(5), fnmatch(3))
62
- // The leading `^` has already been escaped to `\^` by the metacharacter
63
- // escaper, so we strip the literal `!` or escaped `^` and emit a single
64
- // regex `^` which is the JavaScript negation token.
65
- const negateRange = range => range.startsWith('!') || range.startsWith('\\^')
66
- ? `^${range.slice(range[0] === '!' ? 1 : 2)}`
67
- : range
68
-
69
55
  // See fixtures #59
70
56
  const cleanRangeBackSlash = slashes => {
71
57
  const {length} = slashes
72
58
  return slashes.slice(0, length - length % 2)
73
59
  }
74
60
 
61
+ // > The range notation, e.g. [a-zA-Z],
62
+ // > can be used to match one of the characters in a range.
63
+ //
64
+ // gitignore(5) defers to fnmatch(3) for this, and git implements it in
65
+ // `wildmatch.c`. A bracket expression has a sub-grammar of its own, which
66
+ // is neither the surrounding pattern grammar nor the JavaScript one:
67
+ //
68
+ // - a `]` right after `[` or `[!` is a literal member, not the terminator
69
+ // - `[:alpha:]` names one of twelve POSIX classes
70
+ // - `\` escapes the next member, `]` included
71
+ // - `*`, `?` and `.` are plain literal members
72
+ // - an unterminated expression makes the whole pattern match nothing
73
+ //
74
+ // which means the expression can not be located -- let alone translated -- by
75
+ // a regular expression. It is scanned out of the pattern before the
76
+ // replacers below run, and put back once they are done, so that neither the
77
+ // metacharacter escaper nor the `?` / `*` replacers ever see its body.
78
+
79
+ // git classifies with its own ASCII-only ctype macros (`wildmatch.c`), never
80
+ // with the C library ones, so these must not be mapped onto `\d` / `\w` /
81
+ // `\s`, which are wider. `/` is left out of every expansion, because a
82
+ // bracket expression never matches a path separator.
83
+ const POSIX_CLASSES = {
84
+ alnum: '0-9A-Za-z',
85
+ alpha: 'A-Za-z',
86
+ blank: ' \\t',
87
+ cntrl: '\\x00-\\x1f\\x7f',
88
+ digit: '0-9',
89
+ graph: '!-.0-~',
90
+ lower: 'a-z',
91
+ print: ' -.0-~',
92
+ punct: '!-.:-@\\[-`{-~',
93
+ // git's `sane-ctype.h` classifies \v and \f as control, not space,
94
+ // unlike C's `isspace`
95
+ space: ' \\t\\n\\r',
96
+ upper: 'A-Z',
97
+ xdigit: '0-9A-Fa-f'
98
+ }
99
+
100
+ const CLASS_MEMBERS_TO_ESCAPE = '\\]^-['
101
+
102
+ const escapeMember = char => CLASS_MEMBERS_TO_ESCAPE.indexOf(char) < 0
103
+ ? char
104
+ : ESCAPE + char
105
+
106
+ // > if (matched == negated || ((flags & WM_PATHNAME) && text_ch == '/'))
107
+ // > return WM_ABORT_TO_STARSTAR; (git, `wildmatch.c`)
108
+ // A bracket expression never matches a path separator, whatever its members
109
+ // say, so a negated class gets `/` as one more excluded character, while a
110
+ // plain one -- where a literal member or a range could still let `/` in --
111
+ // is guarded with a lookahead, `/` being impossible to subtract from a
112
+ // JavaScript character class.
113
+ const NON_SLASH = '(?!\\/)'
114
+
115
+ const classSource = (negated, body) => {
116
+ if (negated) {
117
+ return `[^\\/${body}]`
118
+ }
119
+
120
+ const source = `[${body}]`
121
+
122
+ return new RegExp(source).test('/')
123
+ ? NON_SLASH + source
124
+ : source
125
+ }
126
+
127
+ // Scan the bracket expression that starts at `pattern[start] === '['`,
128
+ // mirroring the member loop of git's `wildmatch.c`.
129
+ // @returns {{end: number, source: string} | null} `null` if the expression is
130
+ // never terminated, which makes the whole pattern match nothing.
131
+ const scanBracket = (pattern, start) => {
132
+ const {length} = pattern
133
+ let index = start + 1
134
+ let negated = EMPTY
135
+
136
+ const lead = pattern[index]
137
+ if (lead === '!' || lead === '^') {
138
+ negated = '^'
139
+ index ++
140
+ }
141
+
142
+ let body = EMPTY
143
+
144
+ // The member a `-` could start a range from, or EMPTY when the previous
145
+ // member can not open one (start of the body, or a range / POSIX class
146
+ // that has just closed)
147
+ let prev = EMPTY
148
+
149
+ // git scans the members with a do-while, so the first one is consumed
150
+ // unconditionally. That is the whole reason a leading `]` is a member
151
+ // and not the terminator.
152
+ for (;;) {
153
+ const char = pattern[index]
154
+
155
+ if (char === UNDEFINED) {
156
+ return null
157
+ }
158
+
159
+ if (char === ESCAPE) {
160
+ const escaped = pattern[index + 1]
161
+ if (escaped === UNDEFINED) {
162
+ return null
163
+ }
164
+ body += escapeMember(escaped)
165
+ prev = escaped
166
+ index ++
167
+ } else if (
168
+ char === '-'
169
+ && prev
170
+ && index + 1 < length
171
+ && pattern[index + 1] !== ']'
172
+ ) {
173
+ index ++
174
+ let to = pattern[index]
175
+ if (to === ESCAPE) {
176
+ // A pattern can not end on a lone backslash -- `checkPattern` has
177
+ // already thrown it away -- so there is an upper bound to read.
178
+ to = pattern[index += 1]
179
+ }
180
+ // An out-of-order range matches nothing in git but is a syntax error in
181
+ // JavaScript, so it is dropped. Its lower bound stays: git tests it
182
+ // as a plain member before it ever looks at the `-`, so `[c-a]` does
183
+ // match `c`.
184
+ if (prev <= to) {
185
+ body += `-${escapeMember(to)}`
186
+ }
187
+ prev = EMPTY
188
+ } else if (char === '[' && pattern[index + 1] === ':') {
189
+ const nameStart = index + 2
190
+ let end = nameStart
191
+ while (end < length && pattern[end] !== ']') {
192
+ end ++
193
+ }
194
+
195
+ if (end === length) {
196
+ return null
197
+ }
198
+
199
+ if (end > nameStart && pattern[end - 1] === ':') {
200
+ const expanded = POSIX_CLASSES[pattern.slice(nameStart, end - 1)]
201
+
202
+ // An unknown class name makes the whole pattern match nothing
203
+ if (expanded === UNDEFINED) {
204
+ return null
205
+ }
206
+
207
+ body += expanded
208
+ prev = EMPTY
209
+ index = end
210
+ } else {
211
+ // No `:]` to close it, so the `[` is a plain member and scanning
212
+ // resumes right after it.
213
+ body += escapeMember('[')
214
+ prev = '['
215
+ index = nameStart - 2
216
+ }
217
+ } else {
218
+ body += escapeMember(char)
219
+ prev = char
220
+ }
221
+
222
+ index ++
223
+
224
+ if (pattern[index] === ']') {
225
+ return {
226
+ end: index,
227
+ source: classSource(negated, body)
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ // An empty JavaScript class can never match, which is how a pattern that git
234
+ // gives up on (`WM_ABORT_ALL`) is expressed here.
235
+ const NEVER_MATCH = '[]'
236
+
237
+ // A NUL can appear in neither a `.gitignore` line nor a path, which makes it
238
+ // the one safe placeholder character. A literal one in the pattern is
239
+ // held aside all the same, so a collision is impossible by construction.
240
+ const PLACEHOLDER = '\u0000'
241
+ const REGEX_RESTORE_PLACEHOLDER = new RegExp(
242
+ `${PLACEHOLDER}(\\d+)${PLACEHOLDER}`, 'g'
243
+ )
244
+
245
+ // The one wildcard the chain does not expand for itself is a trailing `*`. How
246
+ // it expands depends on the mode (`regex` vs `checkRegex`), so `_make` is
247
+ // left to do it, and until then the pending wildcard travels as this marker.
248
+ // That is what keeps it apart from a user-escaped literal `\*`: the unescape
249
+ // steps collapse the escaped one to the exact `\*` a wildcard would leave
250
+ // behind, so by the time `_make` runs the two are otherwise the same string
251
+ // and a literal star gets wrongly rewritten into a wildcard. This marker is a
252
+ // private-use character no compiled pattern carries, and it never survives
253
+ // into a `RegExp` -- `_make` always turns it back into a real wildcard first.
254
+ const TRAILING_WILDCARD = '\uE000'
255
+
256
+ // Replace every bracket expression with a placeholder the replacers below
257
+ // leave alone, and translate it separately.
258
+ const extractBrackets = pattern => {
259
+ const sources = []
260
+ const hold = source =>
261
+ `${PLACEHOLDER}${sources.push(source) - 1}${PLACEHOLDER}`
262
+
263
+ const {length} = pattern
264
+ let out = EMPTY
265
+ let index = 0
266
+
267
+ while (index < length) {
268
+ const char = pattern[index]
269
+
270
+ if (char === ESCAPE) {
271
+ // > Put a backslash ("\") in front of ... a character to make it literal.
272
+ // (gitignore(5) -> fnmatch(3))
273
+ // A backslash quotes the next character, whatever it is, so `\d` is a
274
+ // literal `d`, not the regex digit class, and `\?` is a literal `?`,
275
+ // not a wildcard. Held aside as its literal here, the escape never
276
+ // reaches the replacers below, which would otherwise let `\d`, `\b`,
277
+ // `\1`, `\/` keep their regular-expression meaning, and would turn a
278
+ // `\?` into `[^\/]`.
279
+ //
280
+ // Four escapes are left for the chain, each with dedicated handling it
281
+ // would be wrong to bypass: `\*` (a literal star, told apart from a
282
+ // wildcard by `TRAILING_WILDCARD` and the wildcard replacers), `\[`
283
+ // (a literal bracket, the one `[` the bracket replacer still expects),
284
+ // `\ ` (a quoted trailing space), and `\\` (a literal backslash). A
285
+ // lone trailing backslash never reaches here -- `checkPattern` throws
286
+ // it out first.
287
+ const escaped = pattern[index + 1]
288
+
289
+ if (
290
+ escaped === '*'
291
+ || escaped === '['
292
+ || escaped === SPACE
293
+ || escaped === ESCAPE
294
+ ) {
295
+ out += pattern.slice(index, index + 2)
296
+ } else {
297
+ out += hold(
298
+ REGEX_LITERAL_SPECIAL.test(escaped)
299
+ ? ESCAPE + escaped
300
+ : escaped
301
+ )
302
+ }
303
+
304
+ index += 2
305
+ } else if (char === PLACEHOLDER) {
306
+ // Hold a literal placeholder character aside as well, so that pattern
307
+ // text can never be mistaken for a placeholder we emitted.
308
+ out += hold(`[${PLACEHOLDER}]`)
309
+ index ++
310
+ } else if (char === '[') {
311
+ const scanned = scanBracket(pattern, index)
312
+
313
+ if (scanned === null) {
314
+ // git gives up on the whole pattern (`WM_ABORT_ALL`), so whatever
315
+ // follows can not make it match either.
316
+ out += hold(NEVER_MATCH)
317
+ index = length
318
+ } else {
319
+ out += hold(scanned.source)
320
+ index = scanned.end + 1
321
+ }
322
+ } else {
323
+ out += char
324
+ index ++
325
+ }
326
+ }
327
+
328
+ return {
329
+ source: out,
330
+ sources
331
+ }
332
+ }
333
+
334
+ // A step of the chain below is normally a `[matcher, replacer]` pair handed
335
+ // to `String.replace`. `DIRECT` marks the ones that are plain string work
336
+ // instead, and take `(source, pattern)`.
337
+ //
338
+ // Anchoring the two ends is exactly that -- a test of one character and a
339
+ // concatenation -- and putting it through the regular expression engine
340
+ // cost a third of this chain for nothing: 167ns where 10ns does the same
341
+ // job. The two are still steps in the same list, in the same places,
342
+ // because their position in the order is part of what they mean.
343
+ const DIRECT = null
344
+
345
+ // A separator at the beginning or in the middle of a pattern, as opposed to
346
+ // one at the very end.
347
+ const REGEX_INNER_SLASH = /\/(?!$)/
348
+
75
349
  // > If the pattern ends with a slash,
76
350
  // > it is removed for the purpose of the following description,
77
351
  // > but it would only find a match with a directory.
@@ -83,6 +357,21 @@ const cleanRangeBackSlash = slashes => {
83
357
  // you could use option `mark: true` with `glob`
84
358
 
85
359
  // '`foo/`' should not continue with the '`..`'
360
+ // The chain that turns one gitignore pattern into a regular expression
361
+ // source, in order. A step is either
362
+ //
363
+ // [matcher, replacer] handed to `String.replace`
364
+ // [matcher, replacer, required] the same, but skipped outright unless
365
+ // `required` appears in the string, which
366
+ // the matcher cannot match without
367
+ // [DIRECT, transform] plain string work, taking (source, pattern)
368
+ //
369
+ // The `required` character is only ever a shortcut: finding it does not mean
370
+ // the matcher will match, and not finding it means it cannot. It is there
371
+ // because scanning for one character costs a fraction of running a matcher
372
+ // that then finds nothing -- for the wildcard step, whose `[^\\]+`
373
+ // backtracks its way through the whole string before giving up, 11ns
374
+ // against 400ns.
86
375
  const REPLACERS = [
87
376
 
88
377
  [
@@ -90,16 +379,30 @@ const REPLACERS = [
90
379
  // TODO:
91
380
  // Other similar zero-width characters?
92
381
  /^\uFEFF/,
382
+ () => EMPTY,
383
+ '\uFEFF'
384
+ ],
385
+
386
+ [
387
+ // A trailing line terminator, left on when a whole file's contents are
388
+ // added as one pattern rather than split into lines. git never sees one
389
+ // -- it reads a `.gitignore` line by line -- so it is not part of the
390
+ // pattern and is dropped here, apart from the trailing-space trimming,
391
+ // which follows git in touching spaces and nothing else.
392
+ /[\r\n]+$/,
93
393
  () => EMPTY
94
394
  ],
95
395
 
96
396
  // > Trailing spaces are ignored unless they are quoted with backslash ("\")
97
397
  [
398
+ // Only spaces, never tabs or other whitespace: git trims a trailing run
399
+ // of `' '` and nothing else (dir.c, `trim_trailing_spaces`, a single
400
+ // `case ' '`), so a pattern ending in a tab keeps it as a literal.
98
401
  // (a\ ) -> (a )
99
402
  // (a ) -> (a)
100
403
  // (a ) -> (a)
101
404
  // (a \ ) -> (a )
102
- /((?:\\\\)*?)(\\?\s+)$/,
405
+ /((?:\\\\)*?)(\\? +)$/,
103
406
  (_, m1, m2) => m1 + (
104
407
  m2.indexOf('\\') === 0
105
408
  ? SPACE
@@ -108,11 +411,14 @@ const REPLACERS = [
108
411
  ],
109
412
 
110
413
  // Replace (\ ) with ' '
414
+ // Only a space: an escaped tab or other whitespace is already a literal by
415
+ // the time it reaches here, and a bare tab must be left as one, not turned
416
+ // into a space.
111
417
  // (\ ) -> ' '
112
418
  // (\\ ) -> '\\ '
113
419
  // (\\\ ) -> '\\ '
114
420
  [
115
- /(\\+?)\s/g,
421
+ /(\\+?) /g,
116
422
  (_, m1) => {
117
423
  const {length} = m1
118
424
  return m1.slice(0, length - length % 2) + SPACE
@@ -144,7 +450,8 @@ const REPLACERS = [
144
450
  [
145
451
  // > a question mark (?) matches a single character
146
452
  /(?!\\)\?/g,
147
- () => '[^/]'
453
+ () => '[^/]',
454
+ '?'
148
455
  ],
149
456
 
150
457
  // leading slash
@@ -154,13 +461,15 @@ const REPLACERS = [
154
461
  // > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
155
462
  // A leading slash matches the beginning of the pathname
156
463
  /^\//,
157
- () => '^'
464
+ () => '^',
465
+ SLASH
158
466
  ],
159
467
 
160
468
  // replace special metacharacter slash after the leading slash
161
469
  [
162
470
  /\//g,
163
- () => '\\/'
471
+ () => '\\/',
472
+ SLASH
164
473
  ],
165
474
 
166
475
  [
@@ -173,7 +482,8 @@ const REPLACERS = [
173
482
  /^\^*(?:\\\*\\\*\\\/)+/,
174
483
 
175
484
  // '**/foo' <-> 'foo'
176
- () => '^(?:.*\\/)?'
485
+ () => '^(?:.*\\/)?',
486
+ '*'
177
487
  ],
178
488
 
179
489
  // starting
@@ -181,10 +491,15 @@ const REPLACERS = [
181
491
  // there will be no leading '/'
182
492
  // (which has been replaced by section "leading slash")
183
493
  // If starts with '**', adding a '^' to the regular expression also works
184
- /^(?=[^^])/,
185
- function startingReplacer () {
494
+ DIRECT,
495
+ (source, pattern) => {
496
+ // Nothing to anchor to, or already anchored
497
+ if (!source || source[0] === '^') {
498
+ return source
499
+ }
500
+
186
501
  // If has a slash `/` at the beginning or middle
187
- return !/\/(?!$)/.test(this)
502
+ const anchor = !REGEX_INNER_SLASH.test(pattern)
188
503
  // > Prior to 2.22.1
189
504
  // > If the pattern does not contain a slash /,
190
505
  // > Git treats it as a shell glob pattern
@@ -202,6 +517,8 @@ const REPLACERS = [
202
517
  // > Otherwise, Git treats the pattern as a shell glob suitable for
203
518
  // > consumption by fnmatch(3)
204
519
  : '^'
520
+
521
+ return anchor + source
205
522
  }
206
523
  ],
207
524
 
@@ -216,18 +533,27 @@ const REPLACERS = [
216
533
  // Check if it is not the last `'/**'`
217
534
  (_, index, str) => index + 6 < str.length
218
535
 
219
- // case: /**/
220
- // > A slash followed by two consecutive asterisks then a slash matches
221
- // > zero or more directories.
222
- // > For example, "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on.
223
- // '/**/'
224
- ? '(?:\\/[^\\/]+)*'
536
+ // case: /**/ at the end of the pattern, i.e. a trailing `'/**/'`
537
+ // > A trailing `"/**/"` (a trailing `"/**"` restricted to directories)
538
+ // > matches everything inside, but it should not match the current
539
+ // > folder itself, so it requires at least one directory segment.
540
+ // 'a/**/' matches 'a/b/', 'a/x/y/' but not 'a/'
541
+ ? str.slice(index + 6) === '\\/'
542
+ ? '(?:\\/[^\\/]+)+'
543
+
544
+ // case: /**/
545
+ // > A slash followed by two consecutive asterisks then a slash matches
546
+ // > zero or more directories.
547
+ // > For example, "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on.
548
+ // '/**/'
549
+ : '(?:\\/[^\\/]+)*'
225
550
 
226
551
  // case: /**
227
552
  // > A trailing `"/**"` matches everything inside.
228
553
 
229
554
  // #21: everything inside but it should not include the current folder
230
- : '\\/.+'
555
+ : '\\/.+',
556
+ '*'
231
557
  ],
232
558
 
233
559
  // normal intermediate wildcards
@@ -250,7 +576,33 @@ const REPLACERS = [
250
576
  // > and will match according to the previous rules.
251
577
  const unescaped = p2.replace(/\\\*/g, '[^\\/]*')
252
578
  return p1 + unescaped
253
- }
579
+ },
580
+ '*'
581
+ ],
582
+
583
+ // trailing wildcard, held apart from a literal star
584
+ [
585
+ // The step above leaves a trailing `*` alone, so a single `\*` is all that
586
+ // can be left at the end here. Whether it is a wildcard or a literal
587
+ // turns on the backslashes the user put in front of it: the escaper has
588
+ // since doubled every one, so what stands here is those `2N` doubled
589
+ // backslashes and then the star's own escape. An even number of the
590
+ // original `N` leaves the star unescaped -- a wildcard -- and an odd
591
+ // number escapes it -- a literal. This runs while the two are still
592
+ // distinct, before the unescape steps below collapse the literal onto
593
+ // the very `\*` a wildcard leaves behind.
594
+ /(^|[^\\])((?:\\\\)*)\\\*$/,
595
+
596
+ (match, p1, p2) =>
597
+ // `p2` holds the doubled user backslashes; half of them is `N`.
598
+ (p2.length / 2) % 2 === 0
599
+ // A real wildcard: carry it to `_make` as the marker, so the unescape
600
+ // steps and the trailing-wildcard rewrite can never mistake it for a
601
+ // literal `\*` (nor the reverse).
602
+ ? p1 + p2 + TRAILING_WILDCARD
603
+ // A literal star: leave it exactly as it stands for the unescape steps.
604
+ : match,
605
+ '*'
254
606
  ],
255
607
 
256
608
  [
@@ -258,41 +610,36 @@ const REPLACERS = [
258
610
  // For example, if a user escape a '\\*',
259
611
  // after step 3, the result will be '\\\\\\*'
260
612
  /\\\\\\(?=[$.|*+(){^])/g,
261
- () => ESCAPE
613
+ () => ESCAPE,
614
+ ESCAPE + ESCAPE
262
615
  ],
263
616
 
264
617
  [
265
618
  // '\\\\' -> '\\'
266
619
  /\\\\/g,
267
- () => ESCAPE
620
+ () => ESCAPE,
621
+ ESCAPE + ESCAPE
268
622
  ],
269
623
 
270
624
  [
271
- // > The range notation, e.g. [a-zA-Z],
272
- // > can be used to match one of the characters in a range.
625
+ // Every real bracket expression -- POSIX classes included -- has already
626
+ // been held aside by `extractBrackets`, so the only `[` left in the
627
+ // pattern is an escaped, literal one.
273
628
 
274
629
  // `\` is escaped by step 3
275
- /(\\)?\[([^\]/]*?)(\\*)($|\])/g,
276
- (match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE
277
- // '\\[bar]' -> '\\\\[bar\\]'
278
- ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}`
279
- : close === ']'
280
- ? endEscape.length % 2 === 0
281
- // A normal case, and it is a range notation
282
- // '[bar]'
283
- // '[bar\\\\]'
284
- ? `[${negateRange(sanitizeRange(range))}${endEscape}]`
285
- // Invalid range notaton
286
- // '[bar\\]' -> '[bar\\\\]'
287
- : '[]'
288
- : '[]'
630
+ /\\\[([^\]/]*?)(\\*)($|\])/g,
631
+
632
+ // '\\[bar]' -> '\\\\[bar\\]'
633
+ (match, range, endEscape, close) =>
634
+ `\\[${range}${cleanRangeBackSlash(endEscape)}${close}`,
635
+ '['
289
636
  ],
290
637
 
291
638
  // ending
292
639
  [
293
640
  // 'js' will not match 'js.'
294
641
  // 'ab' will not match 'abc'
295
- /(?:[^*])$/,
642
+ DIRECT,
296
643
 
297
644
  // WTF!
298
645
  // https://git-scm.com/docs/gitignore
@@ -306,15 +653,26 @@ const REPLACERS = [
306
653
  // 'js*' will not match 'a.js'
307
654
  // 'js/' will not match 'a.js'
308
655
  // 'js' will match 'a.js' and 'a.js/'
309
- match => /\/$/.test(match)
310
- // foo/ will not match 'foo'
311
- ? `${match}$`
312
- // foo matches 'foo' and 'foo/'
313
- : `${match}(?=$|\\/$)`
656
+ source => {
657
+ const last = source[source.length - 1]
658
+
659
+ // The pattern is empty, or ends in the pending trailing wildcard the next
660
+ // step owns. A trailing `*` that is not the marker is a literal star,
661
+ // which anchors like any other final character.
662
+ if (!last || last === TRAILING_WILDCARD) {
663
+ return source
664
+ }
665
+
666
+ return last === SLASH
667
+ // foo/ will not match 'foo'
668
+ ? `${source}$`
669
+ // foo matches 'foo' and 'foo/'
670
+ : `${source}(?=$|\\/$)`
671
+ }
314
672
  ]
315
673
  ]
316
674
 
317
- const REGEX_REPLACE_TRAILING_WILDCARD = /(^|\\\/)?\\\*$/
675
+ const REGEX_REPLACE_TRAILING_WILDCARD = /(^|\\\/)?\uE000$/
318
676
  const MODE_IGNORE = 'regex'
319
677
  const MODE_CHECK_IGNORE = 'checkRegex'
320
678
  const UNDERSCORE = '_'
@@ -352,12 +710,250 @@ const TRAILING_WILD_CARD_REPLACERS = {
352
710
  }
353
711
  }
354
712
 
713
+ const WILDCARD = '[^\\/]*'
714
+
715
+ // Where a run of non-slash wildcards is split by single fixed characters --
716
+ // `[^/]*x[^/]*y...` -- the engine has many equivalent ways to lay the
717
+ // input across the wildcards, and on input that does not match it works
718
+ // through all of them, so the time grows with the number of wildcards
719
+ // rather than the length of the path. Every wildcard but the last in such a
720
+ // run can be pinned to stop at the character that follows it, which leaves
721
+ // one way to lay out the input and no rewinding, without changing which
722
+ // paths match: the fixed characters still bound the count, and the last
723
+ // wildcard still absorbs the rest.
724
+ //
725
+ // The source is read one token at a time -- a wildcard, a single-character
726
+ // piece (a literal, an escape, a class), or a parenthesised group or anchor
727
+ // that ends the run -- so only a genuine wildcard is touched.
728
+ const pinWildcards = source => {
729
+ if (source.indexOf(WILDCARD) < 0) {
730
+ return source
731
+ }
732
+
733
+ const tokens = []
734
+ const {length} = source
735
+ let index = 0
736
+
737
+ while (index < length) {
738
+ const char = source[index]
739
+
740
+ if (source.startsWith(WILDCARD, index)) {
741
+ tokens.push({wildcard: true})
742
+ index += WILDCARD.length
743
+ } else if (char === '[') {
744
+ let end = index + 1
745
+
746
+ if (source[end] === '^') {
747
+ end ++
748
+ }
749
+
750
+ if (source[end] === ']') {
751
+ end ++
752
+ }
753
+
754
+ while (end < length && source[end] !== ']') {
755
+ end += source[end] === ESCAPE
756
+ ? 2
757
+ : 1
758
+ }
759
+
760
+ end ++
761
+ tokens.push({single: source.slice(index, end)})
762
+ index = end
763
+ } else if (char === ESCAPE) {
764
+ tokens.push({single: source.slice(index, index + 2)})
765
+ index += 2
766
+ } else if (char === '(') {
767
+ let depth = 0
768
+ let end = index
769
+
770
+ do {
771
+ if (source[end] === ESCAPE) {
772
+ end ++
773
+ } else if (source[end] === '(') {
774
+ depth ++
775
+ } else if (source[end] === ')') {
776
+ depth --
777
+ }
778
+
779
+ end ++
780
+ } while (end < length && depth > 0)
781
+
782
+ if ('*+?'.indexOf(source[end]) >= 0) {
783
+ end ++
784
+ }
785
+
786
+ tokens.push({boundary: source.slice(index, end)})
787
+ index = end
788
+ } else if (char === '^' || char === '$') {
789
+ tokens.push({boundary: char})
790
+ index ++
791
+ } else {
792
+ tokens.push({single: char})
793
+ index ++
794
+ }
795
+ }
796
+
797
+ let out = EMPTY
798
+ let run = []
799
+
800
+ const flush = () => {
801
+ let lastWildcard
802
+
803
+ run.forEach((token, at) => {
804
+ if (token.wildcard) {
805
+ lastWildcard = at
806
+ }
807
+ })
808
+
809
+ run.forEach((token, at) => {
810
+ if (!token.wildcard) {
811
+ out += token.single
812
+ return
813
+ }
814
+
815
+ // A wildcard that is not the last in the run is always immediately
816
+ // followed by the single character that separates it from the next
817
+ // one, because a run never holds two wildcards in a row, so it can be
818
+ // pinned to stop there. The last wildcard stays as it is and takes up
819
+ // the rest.
820
+ out += at === lastWildcard
821
+ ? WILDCARD
822
+ : `(?:(?!${run[at + 1].single})[^\\/])*`
823
+ })
824
+
825
+ run = []
826
+ }
827
+
828
+ tokens.forEach(token => {
829
+ if (token.boundary === undefined) {
830
+ run.push(token)
831
+ return
832
+ }
833
+
834
+ flush()
835
+ out += token.boundary
836
+ })
837
+
838
+ flush()
839
+
840
+ return out
841
+ }
842
+
355
843
  // @param {pattern}
356
- const makeRegexPrefix = pattern => REPLACERS.reduce(
357
- (prev, [matcher, replacer]) =>
358
- prev.replace(matcher, replacer.bind(pattern)),
359
- pattern
360
- )
844
+ const makeRegexPrefix = pattern => {
845
+ const {source, sources} = extractBrackets(pattern)
846
+
847
+ const replaced = REPLACERS.reduce(
848
+ // A pass whose matcher finds nothing hands back the very string it was
849
+ // given, so asking first costs a search and saves a rewrite. Ten of the
850
+ // fifteen passes never fire for a typical .gitignore line, and between
851
+ // them they were 45% of this chain.
852
+ (prev, [matcher, replacer, required]) => {
853
+ if (matcher === DIRECT) {
854
+ return replacer(prev, pattern)
855
+ }
856
+
857
+ if (required !== UNDEFINED && prev.indexOf(required) < 0) {
858
+ return prev
859
+ }
860
+
861
+ // A pass whose matcher finds nothing hands back the very string it was
862
+ // given, so asking first costs a search and saves a rewrite.
863
+ return matcher.test(prev)
864
+ ? prev.replace(matcher, replacer.bind(pattern))
865
+ : prev
866
+ },
867
+ source
868
+ )
869
+
870
+ // Most patterns hold no bracket expression at all, and then there is
871
+ // nothing to put back.
872
+ return sources.length
873
+ ? replaced.replace(
874
+ REGEX_RESTORE_PLACEHOLDER,
875
+ (match, index) => sources[index]
876
+ )
877
+ : replaced
878
+ }
879
+
880
+ // A trailing slash does not stop a pattern being basename-only: it restricts
881
+ // the match to a directory, it does not let the pattern reach across one.
882
+ // Everything else a pattern can hold -- a wildcard, a character class, an
883
+ // escape -- stays inside a single path segment too, so a pattern with no
884
+ // separator in it can only ever describe the last one.
885
+ const matchesBasename = body => {
886
+ const index = body.indexOf(SLASH)
887
+
888
+ return index < 0 || index === body.length - 1
889
+ }
890
+
891
+ // The last segment of a path, keeping a trailing slash, because a pattern that
892
+ // ends in one matches only a directory.
893
+ // 'a/b/c.js' -> 'c.js'; 'a/b/' -> 'b/'; 'c.js' -> 'c.js' (no copy made)
894
+ const basenameOf = path => {
895
+ const end = path.length - 1
896
+
897
+ const index = path.lastIndexOf(
898
+ SLASH,
899
+ path[end] === SLASH
900
+ ? end - 1
901
+ : end
902
+ )
903
+
904
+ return index < 0
905
+ ? path
906
+ : path.slice(index + 1)
907
+ }
908
+
909
+ // The parent directory of a path, with its trailing separator, or EMPTY when
910
+ // the path has none.
911
+ // 'a/b/c' -> 'a/b/'; 'a/b/' -> 'a/'; 'a' -> EMPTY; 'a/' -> EMPTY
912
+ //
913
+ // A path holding an empty segment has to be taken apart, because its
914
+ // ancestors are not prefixes of it: the parent of 'a//b' is 'a/', not
915
+ // 'a//', and the parent of '/a/' is nothing at all. Both shapes reach here
916
+ // -- 'a//b' is accepted outright, and `checkIgnore` does not put the path
917
+ // it is given through the relative-path check.
918
+ //
919
+ // Every other path is a prefix of itself, and cutting one costs a fraction of
920
+ // splitting it into an array and joining that back at every level: 73ns
921
+ // against 207ns.
922
+ const parentOf = path => {
923
+ if (
924
+ path.charCodeAt(0) === SLASH_CODE
925
+ || path.indexOf(DOUBLE_SLASH) >= 0
926
+ ) {
927
+ const slices = path.split(SLASH).filter(Boolean)
928
+
929
+ slices.pop()
930
+
931
+ return slices.length
932
+ ? slices.join(SLASH) + SLASH
933
+ : EMPTY
934
+ }
935
+
936
+ const end = path.length - 1
937
+
938
+ // Look back from before a trailing separator, since that one belongs to the
939
+ // path itself.
940
+ //
941
+ // A negative place to start would be a trap here: `lastIndexOf` clamps one
942
+ // to 0 and searches there rather than reporting no match, so '/' would
943
+ // come back as its own parent and the walk would never end. It cannot
944
+ // happen -- that is the only path short enough to produce one, and the
945
+ // test above has already sent it the other way.
946
+ const cut = path.lastIndexOf(
947
+ SLASH,
948
+ path.charCodeAt(end) === SLASH_CODE
949
+ ? end - 1
950
+ : end
951
+ )
952
+
953
+ return cut < 0
954
+ ? EMPTY
955
+ : path.slice(0, cut + 1)
956
+ }
361
957
 
362
958
  const isString = subject => typeof subject === 'string'
363
959
 
@@ -392,6 +988,15 @@ class IgnoreRule {
392
988
  define(this, 'regexPrefix', prefix)
393
989
  }
394
990
 
991
+ // Worked out on first use and kept behind an own property, the way `regex`
992
+ // caches itself in `_regex`. Deciding it in the constructor instead would
993
+ // add a fourth `defineProperty` to every rule ever built, which cost 4% of
994
+ // every compile -- including the compiles of rules that are never matched
995
+ // against anything.
996
+ get _basenameOnly () {
997
+ return define(this, '_basenameOnly', matchesBasename(this.body))
998
+ }
999
+
395
1000
  get regex () {
396
1001
  const key = UNDERSCORE + MODE_IGNORE
397
1002
 
@@ -413,12 +1018,12 @@ class IgnoreRule {
413
1018
  }
414
1019
 
415
1020
  _make (mode, key) {
416
- const str = this.regexPrefix.replace(
1021
+ const str = pinWildcards(this.regexPrefix.replace(
417
1022
  REGEX_REPLACE_TRAILING_WILDCARD,
418
1023
 
419
1024
  // It does not need to bind pattern
420
1025
  TRAILING_WILD_CARD_REPLACERS[mode]
421
- )
1026
+ ))
422
1027
 
423
1028
  const regex = this.ignoreCase
424
1029
  ? new RegExp(str, 'i')
@@ -465,12 +1070,28 @@ class RuleManager {
465
1070
  constructor (ignoreCase) {
466
1071
  this._ignoreCase = ignoreCase
467
1072
  this._rules = []
1073
+
1074
+ // How many of the rules git would tag `EXC_FLAG_NODIR`.
1075
+ //
1076
+ // The scan uses it to decide, once for the whole set, whether handing
1077
+ // those rules the basename is worth what it costs the others. The
1078
+ // shortcut saves a full-path scan on every rule it applies to and costs
1079
+ // a check on every rule it does not, so a set where almost nothing is
1080
+ // basename-only comes out behind -- a 955 pattern set with 44 of them
1081
+ // measured 19% slower with the shortcut always on.
1082
+ //
1083
+ // Deciding this by measurement rather than by meaning is safe: a
1084
+ // basename-only pattern gives the very same answer against the whole
1085
+ // path, it just takes longer to say so. The choice can only change how
1086
+ // fast the scan runs, never what it returns.
1087
+ this._basenameCount = 0
468
1088
  }
469
1089
 
470
1090
  _add (pattern) {
471
1091
  // #32
472
1092
  if (pattern && pattern[KEY_IGNORE]) {
473
1093
  this._rules = this._rules.concat(pattern._rules._rules)
1094
+ this._basenameCount += pattern._rules._basenameCount
474
1095
  this._added = true
475
1096
  return
476
1097
  }
@@ -485,6 +1106,13 @@ class RuleManager {
485
1106
  const rule = createRule(pattern, this._ignoreCase)
486
1107
  this._added = true
487
1108
  this._rules.push(rule)
1109
+
1110
+ // Deliberately not `rule._basenameOnly`: reading that would materialise
1111
+ // the rule's own copy, and the whole point of leaving it lazy is that
1112
+ // a rule which is compiled and never matched never pays for it.
1113
+ if (matchesBasename(rule.body)) {
1114
+ this._basenameCount ++
1115
+ }
488
1116
  }
489
1117
  }
490
1118
 
@@ -514,7 +1142,26 @@ class RuleManager {
514
1142
  let unignored = false
515
1143
  let matchedRule
516
1144
 
517
- this._rules.forEach(rule => {
1145
+ // Most of a .gitignore is patterns with no slash in them, and running
1146
+ // those against the whole path makes the regular expression engine walk
1147
+ // every directory name on the way to the only segment that could match.
1148
+ // Handing them the basename instead is what git does, and it is where
1149
+ // the time in a directory walk goes: the rule scan was two thirds of it.
1150
+ const rules = this._rules
1151
+ const {length} = rules
1152
+
1153
+ const shortcut = this._basenameCount * 2 >= length
1154
+
1155
+ const basename = shortcut
1156
+ ? basenameOf(path)
1157
+ : path
1158
+
1159
+ // A plain loop rather than `forEach`, so that `path`, `basename` and
1160
+ // `shortcut` are locals. As a callback they became closure variables,
1161
+ // and reaching for one of those once per rule cost 10% of a scan over a
1162
+ // large rule set -- more than the shortcut they were there to serve.
1163
+ for (let index = 0; index < length; index ++) {
1164
+ const rule = rules[index]
518
1165
  const {negative} = rule
519
1166
 
520
1167
  // | ignored : unignored
@@ -528,26 +1175,22 @@ class RuleManager {
528
1175
  // - TEST: always test
529
1176
  // - TESTIF: only test if checkUnignored
530
1177
  // - X: that never happen
531
- if (
532
- unignored === negative && ignored !== unignored
1178
+ const skip = unignored === negative && ignored !== unignored
533
1179
  || negative && !ignored && !unignored && !checkUnignored
534
- ) {
535
- return
536
- }
537
-
538
- const matched = rule[mode].test(path)
539
1180
 
540
- if (!matched) {
541
- return
1181
+ if (!skip && rule[mode].test(
1182
+ shortcut && rule._basenameOnly
1183
+ ? basename
1184
+ : path
1185
+ )) {
1186
+ ignored = !negative
1187
+ unignored = negative
1188
+
1189
+ matchedRule = negative
1190
+ ? UNDEFINED
1191
+ : rule
542
1192
  }
543
-
544
- ignored = !negative
545
- unignored = negative
546
-
547
- matchedRule = negative
548
- ? UNDEFINED
549
- : rule
550
- })
1193
+ }
551
1194
 
552
1195
  const ret = {
553
1196
  ignored,
@@ -591,7 +1234,43 @@ const checkPath = (path, originalPath, doThrow) => {
591
1234
  return true
592
1235
  }
593
1236
 
594
- const isNotRelative = path => REGEX_TEST_INVALID_PATH.test(path)
1237
+ // > pathname should be a `path.relative()`d one
1238
+ //
1239
+ // The same thing `REGEX_TEST_INVALID_PATH` says, spelled out: a path is not
1240
+ // relative if it begins with a separator, or with `./` or `../`, or is
1241
+ // nothing but `.` or `..`. Every match is decided by the first three
1242
+ // characters, and this runs on every path handed to the library -- where it
1243
+ // was 55% of a cached lookup, more than the cache lookup itself.
1244
+ const isNotRelative = path => {
1245
+ const first = path.charCodeAt(0)
1246
+
1247
+ if (first === SLASH_CODE) {
1248
+ return true
1249
+ }
1250
+
1251
+ if (first !== DOT_CODE) {
1252
+ return false
1253
+ }
1254
+
1255
+ // '.'
1256
+ if (path.length === 1) {
1257
+ return true
1258
+ }
1259
+
1260
+ const second = path.charCodeAt(1)
1261
+
1262
+ // './'
1263
+ if (second === SLASH_CODE) {
1264
+ return true
1265
+ }
1266
+
1267
+ if (second !== DOT_CODE) {
1268
+ return false
1269
+ }
1270
+
1271
+ // '..' or '../'
1272
+ return path.length === 2 || path.charCodeAt(2) === SLASH_CODE
1273
+ }
595
1274
 
596
1275
  checkPath.isNotRelative = isNotRelative
597
1276
 
@@ -638,7 +1317,7 @@ class Ignore {
638
1317
  }
639
1318
 
640
1319
  // @returns {TestResult}
641
- _test (originalPath, cache, checkUnignored, slices) {
1320
+ _test (originalPath, cache, checkUnignored) {
642
1321
  const path = originalPath
643
1322
  // Supports nullable path
644
1323
  && checkPath.convert(originalPath)
@@ -651,26 +1330,20 @@ class Ignore {
651
1330
  : RETURN_FALSE
652
1331
  )
653
1332
 
654
- return this._t(path, cache, checkUnignored, slices)
1333
+ return this._t(path, cache, checkUnignored)
655
1334
  }
656
1335
 
657
1336
  checkIgnore (path) {
658
1337
  // If the path doest not end with a slash, `.ignores()` is much equivalent
659
1338
  // to `git check-ignore`
660
- if (!REGEX_TEST_TRAILING_SLASH.test(path)) {
1339
+ if (path.charCodeAt(path.length - 1) !== SLASH_CODE) {
661
1340
  return this.test(path)
662
1341
  }
663
1342
 
664
- const slices = path.split(SLASH).filter(Boolean)
665
- slices.pop()
1343
+ const parentPath = parentOf(path)
666
1344
 
667
- if (slices.length) {
668
- const parent = this._t(
669
- slices.join(SLASH) + SLASH,
670
- this._testCache,
671
- true,
672
- slices
673
- )
1345
+ if (parentPath) {
1346
+ const parent = this._t(parentPath, this._testCache, true)
674
1347
 
675
1348
  if (parent.ignored) {
676
1349
  return parent
@@ -688,37 +1361,20 @@ class Ignore {
688
1361
  cache,
689
1362
 
690
1363
  // Whether should check if the path is unignored
691
- checkUnignored,
692
-
693
- // The path slices
694
- slices
1364
+ checkUnignored
695
1365
  ) {
696
1366
  if (path in cache) {
697
1367
  return cache[path]
698
1368
  }
699
1369
 
700
- if (!slices) {
701
- // path/to/a.js
702
- // ['path', 'to', 'a.js']
703
- slices = path.split(SLASH).filter(Boolean)
704
- }
705
-
706
- slices.pop()
707
-
708
- // If the path has no parent directory, just test it
709
- if (!slices.length) {
710
- return cache[path] = this._rules.test(path, checkUnignored, MODE_IGNORE)
711
- }
712
-
713
- const parent = this._t(
714
- slices.join(SLASH) + SLASH,
715
- cache,
716
- checkUnignored,
717
- slices
718
- )
1370
+ const parentPath = parentOf(path)
719
1371
 
720
1372
  // If the path contains a parent directory, check the parent first
721
- return cache[path] = parent.ignored
1373
+ const parent = parentPath
1374
+ ? this._t(parentPath, cache, checkUnignored)
1375
+ : UNDEFINED
1376
+
1377
+ return cache[path] = parent && parent.ignored
722
1378
  // > It is not possible to re-include a file if a parent directory of
723
1379
  // > that file is excluded.
724
1380
  ? parent