Source file src/cmd/vendor/golang.org/x/tools/internal/analysisinternal/analysis.go

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package analysisinternal provides helper functions for use in both
     6  // the analysis drivers in go/analysis and gopls, and in various
     7  // analyzers.
     8  //
     9  // TODO(adonovan): this is not ideal as it may lead to unnecessary
    10  // dependencies between drivers and analyzers. Split into analyzerlib
    11  // and driverlib?
    12  package analysisinternal
    13  
    14  import (
    15  	"cmp"
    16  	"fmt"
    17  	"go/token"
    18  	"os"
    19  	"slices"
    20  
    21  	"golang.org/x/tools/go/analysis"
    22  )
    23  
    24  // ReadFile reads a file and adds it to the FileSet in pass
    25  // so that we can report errors against it using lineStart.
    26  func ReadFile(pass *analysis.Pass, filename string) ([]byte, *token.File, error) {
    27  	readFile := pass.ReadFile
    28  	if readFile == nil {
    29  		readFile = os.ReadFile
    30  	}
    31  	content, err := readFile(filename)
    32  	if err != nil {
    33  		return nil, nil, err
    34  	}
    35  	tf := pass.Fset.AddFile(filename, -1, len(content))
    36  	tf.SetLinesForContent(content)
    37  	return content, tf, nil
    38  }
    39  
    40  // A ReadFileFunc is a function that returns the
    41  // contents of a file, such as [os.ReadFile].
    42  type ReadFileFunc = func(filename string) ([]byte, error)
    43  
    44  // CheckedReadFile returns a wrapper around a Pass.ReadFile
    45  // function that performs the appropriate checks.
    46  func CheckedReadFile(pass *analysis.Pass, readFile ReadFileFunc) ReadFileFunc {
    47  	return func(filename string) ([]byte, error) {
    48  		if err := CheckReadable(pass, filename); err != nil {
    49  			return nil, err
    50  		}
    51  		return readFile(filename)
    52  	}
    53  }
    54  
    55  // CheckReadable enforces the access policy defined by the ReadFile field of [analysis.Pass].
    56  func CheckReadable(pass *analysis.Pass, filename string) error {
    57  	if slices.Contains(pass.OtherFiles, filename) ||
    58  		slices.Contains(pass.IgnoredFiles, filename) {
    59  		return nil
    60  	}
    61  	for _, f := range pass.Files {
    62  		if pass.Fset.File(f.FileStart).Name() == filename {
    63  			return nil
    64  		}
    65  	}
    66  	return fmt.Errorf("Pass.ReadFile: %s is not among OtherFiles, IgnoredFiles, or names of Files", filename)
    67  }
    68  
    69  // ValidateFixes validates the set of fixes for a single diagnostic.
    70  // Any error indicates a bug in the originating analyzer.
    71  //
    72  // It updates fixes so that fixes[*].End.IsValid().
    73  //
    74  // It may be used as part of an analysis driver implementation.
    75  func ValidateFixes(fset *token.FileSet, a *analysis.Analyzer, fixes []analysis.SuggestedFix) error {
    76  	fixMessages := make(map[string]bool)
    77  	for i := range fixes {
    78  		fix := &fixes[i]
    79  		if fixMessages[fix.Message] {
    80  			return fmt.Errorf("analyzer %q suggests two fixes with same Message (%s)", a.Name, fix.Message)
    81  		}
    82  		fixMessages[fix.Message] = true
    83  		if err := validateFix(fset, fix); err != nil {
    84  			return fmt.Errorf("analyzer %q suggests invalid fix (%s): %v", a.Name, fix.Message, err)
    85  		}
    86  	}
    87  	return nil
    88  }
    89  
    90  // validateFix validates a single fix.
    91  // Any error indicates a bug in the originating analyzer.
    92  //
    93  // It updates fix so that fix.End.IsValid().
    94  func validateFix(fset *token.FileSet, fix *analysis.SuggestedFix) error {
    95  
    96  	// Stably sort edits by Pos. This ordering puts insertions
    97  	// (end = start) before deletions (end > start) at the same
    98  	// point, but uses a stable sort to preserve the order of
    99  	// multiple insertions at the same point.
   100  	slices.SortStableFunc(fix.TextEdits, func(x, y analysis.TextEdit) int {
   101  		if sign := cmp.Compare(x.Pos, y.Pos); sign != 0 {
   102  			return sign
   103  		}
   104  		return cmp.Compare(x.End, y.End)
   105  	})
   106  
   107  	var prev *analysis.TextEdit
   108  	for i := range fix.TextEdits {
   109  		edit := &fix.TextEdits[i]
   110  
   111  		// Validate edit individually.
   112  		start := edit.Pos
   113  		file := fset.File(start)
   114  		if file == nil {
   115  			return fmt.Errorf("no token.File for TextEdit.Pos (%v)", edit.Pos)
   116  		}
   117  		fileEnd := token.Pos(file.Base() + file.Size())
   118  		if end := edit.End; end.IsValid() {
   119  			if end < start {
   120  				return fmt.Errorf("TextEdit.Pos (%v) > TextEdit.End (%v)", edit.Pos, edit.End)
   121  			}
   122  			endFile := fset.File(end)
   123  			if endFile != file && end < fileEnd+10 {
   124  				// Relax the checks below in the special case when the end position
   125  				// is only slightly beyond EOF, as happens when End is computed
   126  				// (as in ast.{Struct,Interface}Type) rather than based on
   127  				// actual token positions. In such cases, truncate end to EOF.
   128  				//
   129  				// This is a workaround for #71659; see:
   130  				// https://github.com/golang/go/issues/71659#issuecomment-2651606031
   131  				// A better fix would be more faithful recording of token
   132  				// positions (or their absence) in the AST.
   133  				edit.End = fileEnd
   134  				continue
   135  			}
   136  			if endFile == nil {
   137  				return fmt.Errorf("no token.File for TextEdit.End (%v; File(start).FileEnd is %d)", end, file.Base()+file.Size())
   138  			}
   139  			if endFile != file {
   140  				return fmt.Errorf("edit #%d spans files (%v and %v)",
   141  					i, file.Position(edit.Pos), endFile.Position(edit.End))
   142  			}
   143  		} else {
   144  			edit.End = start // update the SuggestedFix
   145  		}
   146  		if eof := fileEnd; edit.End > eof {
   147  			return fmt.Errorf("end is (%v) beyond end of file (%v)", edit.End, eof)
   148  		}
   149  
   150  		// Validate the sequence of edits:
   151  		// properly ordered, no overlapping deletions
   152  		if prev != nil && edit.Pos < prev.End {
   153  			xpos := fset.Position(prev.Pos)
   154  			xend := fset.Position(prev.End)
   155  			ypos := fset.Position(edit.Pos)
   156  			yend := fset.Position(edit.End)
   157  			return fmt.Errorf("overlapping edits to %s (%d:%d-%d:%d and %d:%d-%d:%d)",
   158  				xpos.Filename,
   159  				xpos.Line, xpos.Column,
   160  				xend.Line, xend.Column,
   161  				ypos.Line, ypos.Column,
   162  				yend.Line, yend.Column,
   163  			)
   164  		}
   165  		prev = edit
   166  	}
   167  
   168  	return nil
   169  }
   170  
   171  // Range returns an [analysis.Range] for the specified start and end positions.
   172  func Range(pos, end token.Pos) analysis.Range {
   173  	return tokenRange{pos, end}
   174  }
   175  
   176  // tokenRange is an implementation of the [analysis.Range] interface.
   177  type tokenRange struct{ StartPos, EndPos token.Pos }
   178  
   179  func (r tokenRange) Pos() token.Pos { return r.StartPos }
   180  func (r tokenRange) End() token.Pos { return r.EndPos }
   181  

View as plain text