@ttsc/evidence 0.25.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +279 -0
  3. package/lib/index.d.ts +65 -0
  4. package/lib/index.js +87 -0
  5. package/lib/internal/loadPrismaModels.d.ts +1 -0
  6. package/lib/internal/loadPrismaModels.js +197 -0
  7. package/lib/internal/loadSwaggerOperations.d.ts +1 -0
  8. package/lib/internal/loadSwaggerOperations.js +156 -0
  9. package/lib/internal/normalizeSwaggerDocument.d.ts +1 -0
  10. package/lib/internal/normalizeSwaggerDocument.js +7 -0
  11. package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +32 -0
  12. package/lib/structures/ITtscEvidenceDocumentedConfig.js +2 -0
  13. package/lib/structures/ITtscEvidenceGraphClaim.d.ts +14 -0
  14. package/lib/structures/ITtscEvidenceGraphClaim.js +2 -0
  15. package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +102 -0
  16. package/lib/structures/ITtscEvidenceGraphClaimBase.js +2 -0
  17. package/lib/structures/ITtscEvidenceGraphConfig.d.ts +19 -0
  18. package/lib/structures/ITtscEvidenceGraphConfig.js +2 -0
  19. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +40 -0
  20. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.js +2 -0
  21. package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +76 -0
  22. package/lib/structures/ITtscEvidenceGraphMarkdownReference.js +2 -0
  23. package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +65 -0
  24. package/lib/structures/ITtscEvidenceGraphPrismaClaim.js +2 -0
  25. package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +75 -0
  26. package/lib/structures/ITtscEvidenceGraphPrismaReference.js +2 -0
  27. package/lib/structures/ITtscEvidenceGraphReference.d.ts +14 -0
  28. package/lib/structures/ITtscEvidenceGraphReference.js +2 -0
  29. package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +73 -0
  30. package/lib/structures/ITtscEvidenceGraphReferenceBase.js +2 -0
  31. package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +39 -0
  32. package/lib/structures/ITtscEvidenceGraphSwaggerReference.js +2 -0
  33. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +54 -0
  34. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.js +2 -0
  35. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +83 -0
  36. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.js +2 -0
  37. package/lib/structures/index.d.ts +13 -0
  38. package/lib/structures/index.js +29 -0
  39. package/lib/typings/ITtscLintContributorRules.d.ts +34 -0
  40. package/lib/typings/ITtscLintContributorRules.js +2 -0
  41. package/lib/typings/ITtscLintRuleOptionsMap.d.ts +20 -0
  42. package/lib/typings/ITtscLintRuleOptionsMap.js +2 -0
  43. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +32 -0
  44. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.js +2 -0
  45. package/lib/typings/TtscEvidenceGraphPrismaSymbol.d.ts +38 -0
  46. package/lib/typings/TtscEvidenceGraphPrismaSymbol.js +2 -0
  47. package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -0
  48. package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.js +2 -0
  49. package/lib/typings/index.d.ts +5 -0
  50. package/lib/typings/index.js +21 -0
  51. package/native/base.go +298 -0
  52. package/native/config.go +798 -0
  53. package/native/declaration.go +240 -0
  54. package/native/documented.go +353 -0
  55. package/native/exports.go +484 -0
  56. package/native/glob.go +207 -0
  57. package/native/graph.go +1409 -0
  58. package/native/health.go +45 -0
  59. package/native/hints.go +261 -0
  60. package/native/imports.go +373 -0
  61. package/native/inputs.go +125 -0
  62. package/native/loader.go +424 -0
  63. package/native/markdown.go +423 -0
  64. package/native/model.go +296 -0
  65. package/native/prisma.go +719 -0
  66. package/native/prisma_cache.go +213 -0
  67. package/native/prisma_locator.go +417 -0
  68. package/native/singular.go +369 -0
  69. package/native/swagger.go +331 -0
  70. package/native/swagger_cache.go +274 -0
  71. package/native/todo.go +138 -0
  72. package/native/typescript.go +1199 -0
  73. package/native/typescript_cache.go +96 -0
  74. package/package.json +64 -0
@@ -0,0 +1,484 @@
1
+ package evidence
2
+
3
+ import (
4
+ "sort"
5
+ "strings"
6
+
7
+ shimast "github.com/microsoft/typescript-go/shim/ast"
8
+ )
9
+
10
+ // moduleExport is one name a module offers to whoever imports it.
11
+ //
12
+ // A local export names a declaration in this file; a re-export names one in
13
+ // another. Keeping the two apart is what lets identity stay with the declaring
14
+ // file while reachability is decided by whoever re-exports it.
15
+ type moduleExport struct {
16
+ // Public is the name an importer sees. Empty for `export * from`, which
17
+ // contributes its target's names rather than a name of its own.
18
+ Public string
19
+ // Local is the declaration's own name in this file, empty for a re-export.
20
+ Local string
21
+ // Specifier is the module a re-export pulls from, empty for a local export.
22
+ Specifier string
23
+ // Imported is the name taken from that module, empty for a star re-export.
24
+ Imported string
25
+ // Namespace marks `export * as ns from`, which nests the target's whole
26
+ // surface one segment deeper instead of flattening it.
27
+ Namespace bool
28
+ }
29
+
30
+ // collectModuleExports reads a file's public surface as importers see it.
31
+ //
32
+ // The materializer deliberately skips re-exports when building units, because a
33
+ // re-export declares nothing and must not create a second unit. That rule is
34
+ // untouched here: this table records reachability, and identity is resolved
35
+ // separately by following the specifier to the file that declares the symbol.
36
+ func collectModuleExports(file *shimast.SourceFile) []moduleExport {
37
+ exports := []moduleExport{}
38
+ if file == nil || file.Statements == nil {
39
+ return exports
40
+ }
41
+ locals := collectLocalExportNames(file.Statements)
42
+ for _, statement := range file.Statements.Nodes {
43
+ if statement == nil {
44
+ continue
45
+ }
46
+ if statement.Kind == shimast.KindExportDeclaration {
47
+ exports = append(exports, exportDeclarationEntries(statement, locals)...)
48
+ continue
49
+ }
50
+ if !isSyntacticallyExported(statement) {
51
+ continue
52
+ }
53
+ for _, declared := range topLevelDeclaredNames(statement) {
54
+ exports = append(exports, moduleExport{
55
+ Public: declared.Name,
56
+ Local: declared.Name,
57
+ })
58
+ for _, exported := range locals[declared.Name] {
59
+ if exported.Public != declared.Name {
60
+ exports = append(exports, moduleExport{
61
+ Public: exported.Public,
62
+ Local: declared.Name,
63
+ })
64
+ }
65
+ }
66
+ }
67
+ }
68
+ for local, names := range locals {
69
+ for _, exported := range names {
70
+ if exported.Public == "" {
71
+ continue
72
+ }
73
+ exports = append(exports, moduleExport{
74
+ Public: exported.Public,
75
+ Local: local,
76
+ })
77
+ }
78
+ }
79
+ return dedupeModuleExports(exports)
80
+ }
81
+
82
+ func exportDeclarationEntries(
83
+ statement *shimast.Node,
84
+ locals map[string][]exportedName,
85
+ ) []moduleExport {
86
+ declaration := statement.AsExportDeclaration()
87
+ if declaration == nil {
88
+ return nil
89
+ }
90
+ specifier := stringLiteralText(declaration.ModuleSpecifier)
91
+ if specifier == "" {
92
+ // A local export list is already covered by the locals table, which the
93
+ // caller folds in; recording it twice would double every aliased name.
94
+ return nil
95
+ }
96
+ if declaration.ExportClause == nil {
97
+ return []moduleExport{{Specifier: specifier}}
98
+ }
99
+ switch declaration.ExportClause.Kind {
100
+ case shimast.KindNamespaceExport:
101
+ name := declarationName(declaration.ExportClause.Name())
102
+ if name == "" {
103
+ return nil
104
+ }
105
+ return []moduleExport{{
106
+ Public: name,
107
+ Specifier: specifier,
108
+ Namespace: true,
109
+ }}
110
+ case shimast.KindNamedExports:
111
+ named := declaration.ExportClause.AsNamedExports()
112
+ if named == nil || named.Elements == nil {
113
+ return nil
114
+ }
115
+ entries := []moduleExport{}
116
+ for _, element := range named.Elements.Nodes {
117
+ if element == nil || element.Kind != shimast.KindExportSpecifier {
118
+ continue
119
+ }
120
+ exportSpecifier := element.AsExportSpecifier()
121
+ if exportSpecifier == nil {
122
+ continue
123
+ }
124
+ public := declarationName(element.Name())
125
+ imported := public
126
+ if exportSpecifier.PropertyName != nil {
127
+ imported = declarationName(exportSpecifier.PropertyName)
128
+ }
129
+ if public == "" || imported == "" {
130
+ continue
131
+ }
132
+ entries = append(entries, moduleExport{
133
+ Public: public,
134
+ Specifier: specifier,
135
+ Imported: imported,
136
+ })
137
+ }
138
+ return entries
139
+ }
140
+ return nil
141
+ }
142
+
143
+ func dedupeModuleExports(exports []moduleExport) []moduleExport {
144
+ seen := map[moduleExport]bool{}
145
+ unique := make([]moduleExport, 0, len(exports))
146
+ for _, export := range exports {
147
+ if seen[export] {
148
+ continue
149
+ }
150
+ seen[export] = true
151
+ unique = append(unique, export)
152
+ }
153
+ sort.SliceStable(unique, func(left int, right int) bool {
154
+ return unique[left].Public < unique[right].Public
155
+ })
156
+ return unique
157
+ }
158
+
159
+ // reachedSymbol is one public address an entry traversal arrived at.
160
+ // traversedPopulation is what selecting modules and walking their exports
161
+ // yields: the obligations, the scopes above them, and the addresses that reach
162
+ // them.
163
+ type traversedPopulation struct {
164
+ Units []*evidenceUnit
165
+ Reached []*evidenceUnit
166
+ // Hidden are the reached declarations that withdrew themselves from the
167
+ // public surface with an `@internal`, `@hidden`, or `@ignore` documentation
168
+ // tag. They are addressed like any other reached unit and selected as none,
169
+ // so a citation of one is answered with its cause.
170
+ Hidden []*evidenceUnit
171
+ Published []publishedAddress
172
+ }
173
+
174
+ // publishedAddress is one module publishing one unit under one address.
175
+ //
176
+ // A symbol is citable from every module that exposes it, under the name that
177
+ // module gives it. Recording the pair keeps import-scope resolution honest when
178
+ // several selected modules publish the same declaration: each address is legal
179
+ // exactly where it is written, and none of them is legal everywhere.
180
+ type publishedAddress struct {
181
+ Module string
182
+ Address string
183
+ Unit *evidenceUnit
184
+ }
185
+
186
+ type reachedSymbol struct {
187
+ // Address is the accessor path from the entry, segment by segment.
188
+ Address []string
189
+ // Path is the file that declares the symbol, which owns its identity.
190
+ Path string
191
+ // Local is the name that file's inventory gives the declaration, which is
192
+ // the name it exposes rather than the binding it wrote. `export { a as b }`
193
+ // declares one unit called `b`, so matching on `a` would find nothing.
194
+ Local string
195
+ }
196
+
197
+ // traverseEntryExports walks a module's export graph and reports every public
198
+ // symbol it reaches, with the address the entry gives it.
199
+ //
200
+ // Membership comes from reachability and identity comes from the declaring
201
+ // file, so a symbol re-exported through two barrels is reached twice and still
202
+ // resolves to one unit. Cycles are bounded by the visited set: a barrel that
203
+ // re-exports itself is a real shape, not a reason to hang the build.
204
+ func traverseEntryExports(
205
+ loader *typeScriptLoader,
206
+ entry string,
207
+ prefix []string,
208
+ visited map[string]bool,
209
+ ) []reachedSymbol {
210
+ if visited[entry] {
211
+ return nil
212
+ }
213
+ visited[entry] = true
214
+ defer delete(visited, entry)
215
+
216
+ inventory := loader.inventory(entry)
217
+ if inventory == nil {
218
+ return nil
219
+ }
220
+ reached := []reachedSymbol{}
221
+ // Named re-exports are grouped by the module they come from and that module
222
+ // is walked once. Walking it per specifier would re-traverse the whole
223
+ // subtree for every name a barrel forwards, which is quadratic on exactly
224
+ // the wide generated barrels this selection exists to describe.
225
+ surfaces := map[string]map[string]reachedSymbol{}
226
+ surfaceOf := func(target string) map[string]reachedSymbol {
227
+ if surface, built := surfaces[target]; built {
228
+ return surface
229
+ }
230
+ surface := map[string]reachedSymbol{}
231
+ for _, nested := range traverseEntryExports(loader, target, nil, visited) {
232
+ if len(nested.Address) != 1 {
233
+ continue
234
+ }
235
+ if _, taken := surface[nested.Address[0]]; !taken {
236
+ surface[nested.Address[0]] = nested
237
+ }
238
+ }
239
+ surfaces[target] = surface
240
+ return surface
241
+ }
242
+ for _, export := range inventory.Exports {
243
+ if export.Specifier == "" {
244
+ // A declaration this module exposes is inventoried under the name it
245
+ // exposes it as. `export { local as renamed }` is one unit named
246
+ // `renamed`, so the local binding never identifies it.
247
+ reached = append(reached, reachedSymbol{
248
+ Address: append(append([]string{}, prefix...), export.Public),
249
+ Path: entry,
250
+ Local: export.Public,
251
+ })
252
+ continue
253
+ }
254
+ target := loader.resolve(entry, export.Specifier)
255
+ if target == "" {
256
+ continue
257
+ }
258
+ switch {
259
+ case export.Namespace:
260
+ reached = append(reached, traverseEntryExports(
261
+ loader,
262
+ target,
263
+ append(append([]string{}, prefix...), export.Public),
264
+ visited,
265
+ )...)
266
+ case export.Imported == "":
267
+ reached = append(reached, traverseEntryExports(
268
+ loader,
269
+ target,
270
+ prefix,
271
+ visited,
272
+ )...)
273
+ default:
274
+ nested, found := surfaceOf(target)[export.Imported]
275
+ if !found {
276
+ continue
277
+ }
278
+ reached = append(reached, reachedSymbol{
279
+ Address: append(append([]string{}, prefix...), export.Public),
280
+ Path: nested.Path,
281
+ Local: nested.Local,
282
+ })
283
+ }
284
+ }
285
+ return reached
286
+ }
287
+
288
+ // materializeEntryUnits turns reached symbols into the units an obligation
289
+ // counts, giving each its entry-relative address.
290
+ //
291
+ // A declaration's descendants travel with it: reaching `ISale` also reaches
292
+ // `ISale.price`, because the property is addressable exactly when its owner is.
293
+ // Addresses are rebuilt from identity segments rather than by rewriting the
294
+ // joined target, so a literal dot inside a name cannot collapse into
295
+ // qualification.
296
+ //
297
+ // Several entries union into one population. A glob selects modules, not
298
+ // symbols, so a barrel it matched carries in everything that barrel re-exports;
299
+ // a declaration two entries both reach stays one unit answering to both
300
+ // addresses.
301
+ //
302
+ // Units are the selected obligation set, Reached is everything the traversal
303
+ // saw — which keeps an unselected ancestor addressable as the aggregate scope
304
+ // of the units below it — and Published records where each address is legal.
305
+ func materializeEntryUnits(
306
+ loader *typeScriptLoader,
307
+ entries []string,
308
+ symbols symbolSet,
309
+ ) traversedPopulation {
310
+ byID := map[string]*evidenceUnit{}
311
+ order := []string{}
312
+ published := []publishedAddress{}
313
+ candidates := newOwnedUnitIndex(loader)
314
+ for _, entry := range entries {
315
+ for _, symbol := range traverseEntryExports(loader, entry, nil, map[string]bool{}) {
316
+ if symbol.Local == "" {
317
+ continue
318
+ }
319
+ for _, unit := range candidates.of(symbol.Path, symbol.Local) {
320
+ suffix, owned := identitySuffix(unit.Identity, symbol.Local)
321
+ if !owned {
322
+ continue
323
+ }
324
+ address := append(append([]string{}, symbol.Address...), suffix...)
325
+ target := strings.Join(address, ".")
326
+ current := byID[unit.ID]
327
+ if current == nil {
328
+ clone := *unit
329
+ clone.Target = target
330
+ clone.Identity = address
331
+ clone.Aliases = nil
332
+ current = &clone
333
+ byID[unit.ID] = current
334
+ order = append(order, unit.ID)
335
+ } else if current.Target != target &&
336
+ !containsString(current.Aliases, target) {
337
+ // Two addresses for one declaration. The shorter one reads as
338
+ // the canonical path, and the rest resolve to the same unit so
339
+ // the obligation is still counted once.
340
+ if len(address) < len(current.Identity) {
341
+ current.Aliases = append(current.Aliases, current.Target)
342
+ current.Target = target
343
+ current.Identity = address
344
+ } else {
345
+ current.Aliases = append(current.Aliases, target)
346
+ }
347
+ }
348
+ published = append(published, publishedAddress{
349
+ Module: entry,
350
+ Address: target,
351
+ Unit: current,
352
+ })
353
+ }
354
+ }
355
+ }
356
+ population := traversedPopulation{Published: published}
357
+ for _, id := range order {
358
+ unit := byID[id]
359
+ sort.Strings(unit.Aliases)
360
+ // A declaration withdrawn from the surface by its own documentation tag
361
+ // is neither an obligation nor an aggregate scope. It is kept apart
362
+ // rather than dropped so a citation naming it can be told why, which a
363
+ // bare unresolved target could not say.
364
+ if unit.Hidden != "" {
365
+ population.Hidden = append(population.Hidden, unit)
366
+ continue
367
+ }
368
+ // Every reached declaration stays available as an aggregate scope, even
369
+ // when its own kind is unselected. The selector is the obligation
370
+ // denominator, not the list of targets an author may write.
371
+ population.Reached = append(population.Reached, unit)
372
+ if symbols.contains(unit.Symbol) {
373
+ population.Units = append(population.Units, unit)
374
+ }
375
+ }
376
+ return population
377
+ }
378
+
379
+ // narrowTraversedPopulation keeps the entry's addresses and the narrowing's
380
+ // membership.
381
+ //
382
+ // Both arguments describe the same declarations; they differ only in which
383
+ // module each was reached from, and therefore in the address each unit answers
384
+ // to. Identity is what joins them, because a re-export decides reachability and
385
+ // never identity — the same declaration carries the same unit ID however it was
386
+ // reached.
387
+ //
388
+ // Reached and Published stay whole. Reached is read only to walk the parent
389
+ // chain above a selected unit, and an ancestor is addressable because it is an
390
+ // ancestor rather than because it was selected, so narrowing it would be work
391
+ // with nothing to change. Published is what a citation resolves against, and it
392
+ // is the entry's addresses that a consumer can write.
393
+ func narrowTraversedPopulation(
394
+ published traversedPopulation,
395
+ membership traversedPopulation,
396
+ ) traversedPopulation {
397
+ selected := make(map[string]bool, len(membership.Units))
398
+ for _, unit := range membership.Units {
399
+ selected[unit.ID] = true
400
+ }
401
+ withdrawn := make(map[string]bool, len(membership.Hidden))
402
+ for _, unit := range membership.Hidden {
403
+ withdrawn[unit.ID] = true
404
+ }
405
+ narrowed := traversedPopulation{
406
+ Reached: published.Reached,
407
+ Published: published.Published,
408
+ }
409
+ for _, unit := range published.Units {
410
+ if selected[unit.ID] {
411
+ narrowed.Units = append(narrowed.Units, unit)
412
+ }
413
+ }
414
+ for _, unit := range published.Hidden {
415
+ if withdrawn[unit.ID] {
416
+ narrowed.Hidden = append(narrowed.Hidden, unit)
417
+ }
418
+ }
419
+ return narrowed
420
+ }
421
+
422
+ // ownedUnitIndex narrows a file's units to the ones a reached name can own.
423
+ //
424
+ // Membership is decided by identity prefix, so only units whose first segment
425
+ // equals the reached name are candidates. Scanning the whole file instead costs
426
+ // one pass per reached name, which is quadratic in a module's own width — and a
427
+ // generated declaration barrel is exactly where that width is largest.
428
+ type ownedUnitIndex struct {
429
+ loader *typeScriptLoader
430
+ byPath map[string]map[string][]*evidenceUnit
431
+ }
432
+
433
+ func newOwnedUnitIndex(loader *typeScriptLoader) *ownedUnitIndex {
434
+ return &ownedUnitIndex{
435
+ loader: loader,
436
+ byPath: map[string]map[string][]*evidenceUnit{},
437
+ }
438
+ }
439
+
440
+ // of lists the units of one file that may be owned by one reached name.
441
+ func (index *ownedUnitIndex) of(path string, local string) []*evidenceUnit {
442
+ roots, built := index.byPath[path]
443
+ if !built {
444
+ roots = map[string][]*evidenceUnit{}
445
+ if inventory := index.loader.inventory(path); inventory != nil {
446
+ for _, unit := range inventory.Units {
447
+ if len(unit.Identity) == 0 {
448
+ continue
449
+ }
450
+ roots[unit.Identity[0]] = append(roots[unit.Identity[0]], unit)
451
+ }
452
+ }
453
+ index.byPath[path] = roots
454
+ }
455
+ root := local
456
+ if cut := strings.Index(local, "."); cut != -1 {
457
+ root = local[:cut]
458
+ }
459
+ return roots[root]
460
+ }
461
+
462
+ // identitySuffix reports the segments below a reached declaration, and whether
463
+ // the unit belongs to it at all.
464
+ func identitySuffix(identity []string, local string) ([]string, bool) {
465
+ owner := strings.Split(local, ".")
466
+ if len(identity) < len(owner) {
467
+ return nil, false
468
+ }
469
+ for index, segment := range owner {
470
+ if identity[index] != segment {
471
+ return nil, false
472
+ }
473
+ }
474
+ return identity[len(owner):], true
475
+ }
476
+
477
+ func containsString(values []string, wanted string) bool {
478
+ for _, value := range values {
479
+ if value == wanted {
480
+ return true
481
+ }
482
+ }
483
+ return false
484
+ }
package/native/glob.go ADDED
@@ -0,0 +1,207 @@
1
+ package evidence
2
+
3
+ import (
4
+ "errors"
5
+ "path/filepath"
6
+ "strings"
7
+ )
8
+
9
+ type globSet struct {
10
+ Patterns []globPattern
11
+ }
12
+
13
+ type globPattern struct {
14
+ Raw string
15
+ Segments []string
16
+ Exclude bool
17
+ }
18
+
19
+ func newGlobSet(patterns []string) (globSet, error) {
20
+ set := globSet{Patterns: make([]globPattern, 0, len(patterns))}
21
+ positive := false
22
+ for _, raw := range patterns {
23
+ pattern, err := compileGlob(raw)
24
+ if err != nil {
25
+ return globSet{}, err
26
+ }
27
+ if !pattern.Exclude {
28
+ positive = true
29
+ }
30
+ set.Patterns = append(set.Patterns, pattern)
31
+ }
32
+ if !positive {
33
+ return globSet{}, errors.New("the files array must contain at least one positive glob")
34
+ }
35
+ return set, nil
36
+ }
37
+
38
+ func compileGlob(raw string) (globPattern, error) {
39
+ if strings.TrimSpace(raw) == "" {
40
+ return globPattern{}, errors.New("glob strings must not be empty")
41
+ }
42
+ exclude := strings.HasPrefix(raw, "!")
43
+ value := raw
44
+ if exclude {
45
+ value = strings.TrimPrefix(value, "!")
46
+ }
47
+ value = strings.ReplaceAll(value, "\\", "/")
48
+ for strings.HasPrefix(value, "./") {
49
+ value = strings.TrimPrefix(value, "./")
50
+ }
51
+ if value == "" {
52
+ return globPattern{}, errors.New("the exclusion marker '!' must be followed by a glob")
53
+ }
54
+ if filepath.IsAbs(value) ||
55
+ hasWindowsDrivePrefix(value) ||
56
+ strings.HasPrefix(value, "/") {
57
+ return globPattern{}, errors.New("glob '" + raw + "' is absolute; every files pattern must be project-relative")
58
+ }
59
+ value = strings.TrimSuffix(value, "/")
60
+ segments := strings.Split(value, "/")
61
+ for _, segment := range segments {
62
+ if segment == "" {
63
+ return globPattern{}, errors.New("glob '" + raw + "' contains an empty path segment")
64
+ }
65
+ if segment == ".." {
66
+ return globPattern{}, errors.New("glob '" + raw + "' escapes the project root through '..'")
67
+ }
68
+ }
69
+ return globPattern{Raw: raw, Segments: segments, Exclude: exclude}, nil
70
+ }
71
+
72
+ func hasWindowsDrivePrefix(value string) bool {
73
+ if len(value) < 2 || value[1] != ':' {
74
+ return false
75
+ }
76
+ letter := value[0]
77
+ return (letter >= 'A' && letter <= 'Z') ||
78
+ (letter >= 'a' && letter <= 'z')
79
+ }
80
+
81
+ func (set globSet) matches(path string) bool {
82
+ segments := splitProjectPath(path)
83
+ included := false
84
+ for _, pattern := range set.Patterns {
85
+ if !matchGlobSegments(pattern.Segments, segments) {
86
+ continue
87
+ }
88
+ if pattern.Exclude {
89
+ included = false
90
+ } else {
91
+ included = true
92
+ }
93
+ }
94
+ return included
95
+ }
96
+
97
+ func (set globSet) couldMatchDescendant(directory string) bool {
98
+ prefix := splitProjectPath(directory)
99
+ for _, pattern := range set.Patterns {
100
+ if pattern.Exclude || !couldMatchGlobBelow(pattern.Segments, prefix) {
101
+ continue
102
+ }
103
+ return true
104
+ }
105
+ return false
106
+ }
107
+
108
+ func couldMatchGlobBelow(pattern []string, prefix []string) bool {
109
+ type state struct{ pattern, prefix int }
110
+ memo := map[state]bool{}
111
+ known := map[state]bool{}
112
+ var match func(int, int) bool
113
+ match = func(patternIndex int, prefixIndex int) bool {
114
+ key := state{pattern: patternIndex, prefix: prefixIndex}
115
+ if known[key] {
116
+ return memo[key]
117
+ }
118
+ known[key] = true
119
+ result := false
120
+ switch {
121
+ case prefixIndex == len(prefix):
122
+ result = patternIndex < len(pattern)
123
+ case patternIndex == len(pattern):
124
+ result = false
125
+ case pattern[patternIndex] == "**":
126
+ result = match(patternIndex+1, prefixIndex) ||
127
+ match(patternIndex, prefixIndex+1)
128
+ case matchGlobSegment(pattern[patternIndex], prefix[prefixIndex]):
129
+ result = match(patternIndex+1, prefixIndex+1)
130
+ }
131
+ memo[key] = result
132
+ return result
133
+ }
134
+ return match(0, 0)
135
+ }
136
+
137
+ func splitProjectPath(value string) []string {
138
+ value = strings.ReplaceAll(value, "\\", "/")
139
+ for strings.HasPrefix(value, "./") {
140
+ value = strings.TrimPrefix(value, "./")
141
+ }
142
+ value = strings.Trim(value, "/")
143
+ if value == "" {
144
+ return nil
145
+ }
146
+ return strings.Split(value, "/")
147
+ }
148
+
149
+ func matchGlobSegments(pattern []string, path []string) bool {
150
+ type state struct{ pattern, path int }
151
+ memo := map[state]bool{}
152
+ known := map[state]bool{}
153
+ var match func(int, int) bool
154
+ match = func(patternIndex int, pathIndex int) bool {
155
+ key := state{pattern: patternIndex, path: pathIndex}
156
+ if known[key] {
157
+ return memo[key]
158
+ }
159
+ known[key] = true
160
+ result := false
161
+ switch {
162
+ case patternIndex == len(pattern):
163
+ result = pathIndex == len(path)
164
+ case pattern[patternIndex] == "**":
165
+ result = match(patternIndex+1, pathIndex)
166
+ if !result && pathIndex < len(path) {
167
+ result = match(patternIndex, pathIndex+1)
168
+ }
169
+ case pathIndex < len(path) && matchGlobSegment(pattern[patternIndex], path[pathIndex]):
170
+ result = match(patternIndex+1, pathIndex+1)
171
+ }
172
+ memo[key] = result
173
+ return result
174
+ }
175
+ return match(0, 0)
176
+ }
177
+
178
+ func matchGlobSegment(pattern string, value string) bool {
179
+ patternRunes := []rune(pattern)
180
+ valueRunes := []rune(value)
181
+ patternIndex := 0
182
+ valueIndex := 0
183
+ starPattern := -1
184
+ starValue := 0
185
+ for valueIndex < len(valueRunes) {
186
+ switch {
187
+ case patternIndex < len(patternRunes) && patternRunes[patternIndex] == '*':
188
+ starPattern = patternIndex
189
+ starValue = valueIndex
190
+ patternIndex++
191
+ case patternIndex < len(patternRunes) &&
192
+ (patternRunes[patternIndex] == '?' || patternRunes[patternIndex] == valueRunes[valueIndex]):
193
+ patternIndex++
194
+ valueIndex++
195
+ case starPattern >= 0:
196
+ patternIndex = starPattern + 1
197
+ starValue++
198
+ valueIndex = starValue
199
+ default:
200
+ return false
201
+ }
202
+ }
203
+ for patternIndex < len(patternRunes) && patternRunes[patternIndex] == '*' {
204
+ patternIndex++
205
+ }
206
+ return patternIndex == len(patternRunes)
207
+ }