@@ -65,10 +65,12 @@ type View struct {
65
65
66
66
vulns map [span.URI ]* govulncheck.Result
67
67
68
- // keep track of files by uri and by basename, a single file may be mapped
69
- // to multiple uris, and the same basename may map to multiple files
70
- filesByURI map [span.URI ]* fileBase
71
- filesByBase map [string ][]* fileBase
68
+ // filesByURI maps URIs to the canonical URI for the file it denotes.
69
+ // We also keep a set of candidates for a given basename
70
+ // to reduce the set of pairs that need to be tested for sameness.
71
+ filesByMu sync.Mutex
72
+ filesByURI map [span.URI ]span.URI // key is noncanonical URI (alias)
73
+ filesByBase map [string ][]canonicalURI // key is basename
72
74
73
75
// initCancelFirstAttempt can be used to terminate the view's first
74
76
// attempt at initialization.
@@ -205,28 +207,6 @@ const (
205
207
tempModfile
206
208
)
207
209
208
- // fileBase holds the common functionality for all files.
209
- // It is intended to be embedded in the file implementations
210
- type fileBase struct {
211
- uris []span.URI
212
- fname string
213
-
214
- view * View
215
- }
216
-
217
- func (f * fileBase ) URI () span.URI {
218
- return f .uris [0 ]
219
- }
220
-
221
- func (f * fileBase ) filename () string {
222
- return f .fname
223
- }
224
-
225
- func (f * fileBase ) addURI (uri span.URI ) int {
226
- f .uris = append (f .uris , uri )
227
- return len (f .uris )
228
- }
229
-
230
210
func (v * View ) ID () string { return v .id }
231
211
232
212
// tempModFile creates a temporary go.mod file based on the contents
@@ -493,18 +473,6 @@ func (v *View) filterFunc() func(span.URI) bool {
493
473
}
494
474
}
495
475
496
- func (v * View ) mapFile (uri span.URI , f * fileBase ) {
497
- v .filesByURI [uri ] = f
498
- if f .addURI (uri ) == 1 {
499
- basename := basename (f .filename ())
500
- v .filesByBase [basename ] = append (v .filesByBase [basename ], f )
501
- }
502
- }
503
-
504
- func basename (filename string ) string {
505
- return strings .ToLower (filepath .Base (filename ))
506
- }
507
-
508
476
func (v * View ) relevantChange (c source.FileModification ) bool {
509
477
// If the file is known to the view, the change is relevant.
510
478
if v .knownFile (c .URI ) {
@@ -530,64 +498,73 @@ func (v *View) relevantChange(c source.FileModification) bool {
530
498
return v .contains (c .URI )
531
499
}
532
500
501
+ // knownFile reports whether the specified valid URI (or an alias) is known to the view.
533
502
func (v * View ) knownFile (uri span.URI ) bool {
534
- v .mu .Lock ()
535
- defer v .mu .Unlock ()
536
-
537
- f , err := v .findFile (uri )
538
- return f != nil && err == nil
503
+ _ , known := v .canonicalURI (uri , false )
504
+ return known
539
505
}
540
506
541
- // getFile returns a file for the given URI.
542
- func (v * View ) getFile (uri span.URI ) * fileBase {
543
- v .mu .Lock ()
544
- defer v .mu .Unlock ()
545
-
546
- f , _ := v .findFile (uri )
547
- if f != nil {
548
- return f
549
- }
550
- f = & fileBase {
551
- view : v ,
552
- fname : uri .Filename (),
553
- }
554
- v .mapFile (uri , f )
555
- return f
507
+ // TODO(adonovan): opt: eliminate 'filename' optimization. I doubt the
508
+ // cost of allocation is significant relative to the
509
+ // stat/open/fstat/close operations that follow on Windows.
510
+ type canonicalURI struct {
511
+ uri span.URI
512
+ filename string // = uri.Filename(), an optimization (on Windows)
556
513
}
557
514
558
- // findFile checks the cache for any file matching the given uri.
515
+ // canonicalURI returns the canonical URI that denotes the same file
516
+ // as uri, which may differ due to case insensitivity, unclean paths,
517
+ // soft or hard links, and so on. If no previous alias was found, or
518
+ // the file is missing, insert determines whether to make uri the
519
+ // canonical representative of the file or to return false.
559
520
//
560
- // An error is only returned for an irreparable failure, for example, if the
561
- // filename in question does not exist.
562
- func (v * View ) findFile (uri span.URI ) (* fileBase , error ) {
563
- if f := v .filesByURI [uri ]; f != nil {
564
- // a perfect match
565
- return f , nil
566
- }
567
- // no exact match stored, time to do some real work
568
- // check for any files with the same basename
569
- fname := uri .Filename ()
570
- basename := basename (fname )
521
+ // The cache grows indefinitely without invalidation: file system
522
+ // operations may cause two URIs that used to denote the same file to
523
+ // no longer to do so. Also, the basename cache grows without bound.
524
+ // TODO(adonovan): fix both bugs.
525
+ func (v * View ) canonicalURI (uri span.URI , insert bool ) (span.URI , bool ) {
526
+ v .filesByMu .Lock ()
527
+ defer v .filesByMu .Unlock ()
528
+
529
+ // Have we seen this exact URI before?
530
+ if canonical , ok := v .filesByURI [uri ]; ok {
531
+ return canonical , true
532
+ }
533
+
534
+ // Inspect all candidates with the same lowercase basename.
535
+ // This heuristic is easily defeated by symbolic links to files.
536
+ // Files with some basenames (e.g. doc.go) are very numerous.
537
+ //
538
+ // The set of candidates grows without bound, and incurs a
539
+ // linear sequence of SameFile queries to the file system.
540
+ //
541
+ // It is tempting to fetch the device/inode pair that
542
+ // uniquely identifies a file once, and then compare those
543
+ // pairs, but that would cause us to cache stale file system
544
+ // state (in addition to the filesByURI staleness).
545
+ filename := uri .Filename ()
546
+ basename := strings .ToLower (filepath .Base (filename ))
571
547
if candidates := v .filesByBase [basename ]; candidates != nil {
572
- pathStat , err := os .Stat (fname )
573
- if os .IsNotExist (err ) {
574
- return nil , err
575
- }
576
- if err != nil {
577
- return nil , nil // the file may exist, return without an error
578
- }
579
- for _ , c := range candidates {
580
- if cStat , err := os .Stat (c .filename ()); err == nil {
581
- if os .SameFile (pathStat , cStat ) {
582
- // same file, map it
583
- v .mapFile (uri , c )
584
- return c , nil
548
+ if pathStat , _ := os .Stat (filename ); pathStat != nil {
549
+ for _ , c := range candidates {
550
+ if cStat , _ := os .Stat (c .filename ); cStat != nil {
551
+ // On Windows, SameFile is more expensive as it must
552
+ // open the file and use the equivalent of fstat(2).
553
+ if os .SameFile (pathStat , cStat ) {
554
+ v .filesByURI [uri ] = c .uri
555
+ return c .uri , true
556
+ }
585
557
}
586
558
}
587
559
}
588
560
}
589
- // no file with a matching name was found, it wasn't in our cache
590
- return nil , nil
561
+
562
+ // No candidates, stat failed, or no candidate matched.
563
+ if insert {
564
+ v .filesByURI [uri ] = uri
565
+ v .filesByBase [basename ] = append (v .filesByBase [basename ], canonicalURI {uri , filename })
566
+ }
567
+ return uri , insert
591
568
}
592
569
593
570
// shutdown releases resources associated with the view, and waits for ongoing
0 commit comments