github.com/vanstinator/golangci-lint@v0.0.0-20240223191551-cc572f00d9d1/pkg/golinters/gofmt_common.go (about)

     1  package golinters
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"go/token"
     7  	"strings"
     8  
     9  	diffpkg "github.com/sourcegraph/go-diff/diff"
    10  
    11  	"github.com/vanstinator/golangci-lint/pkg/config"
    12  	"github.com/vanstinator/golangci-lint/pkg/lint/linter"
    13  	"github.com/vanstinator/golangci-lint/pkg/logutils"
    14  	"github.com/vanstinator/golangci-lint/pkg/result"
    15  )
    16  
    17  type Change struct {
    18  	LineRange   result.Range
    19  	Replacement result.Replacement
    20  }
    21  
    22  type diffLineType string
    23  
    24  const (
    25  	diffLineAdded    diffLineType = "added"
    26  	diffLineOriginal diffLineType = "original"
    27  	diffLineDeleted  diffLineType = "deleted"
    28  )
    29  
    30  type diffLine struct {
    31  	originalNumber int // 1-based original line number
    32  	typ            diffLineType
    33  	data           string // "+" or "-" stripped line
    34  }
    35  
    36  type hunkChangesParser struct {
    37  	// needed because we merge currently added lines with the last original line
    38  	lastOriginalLine *diffLine
    39  
    40  	// if the first line of diff is an adding we save all additions to replacementLinesToPrepend
    41  	replacementLinesToPrepend []string
    42  
    43  	log logutils.Log
    44  
    45  	lines []diffLine
    46  
    47  	ret []Change
    48  }
    49  
    50  func (p *hunkChangesParser) parseDiffLines(h *diffpkg.Hunk) {
    51  	lines := bytes.Split(h.Body, []byte{'\n'})
    52  	currentOriginalLineNumber := int(h.OrigStartLine)
    53  	var ret []diffLine
    54  
    55  	for i, line := range lines {
    56  		dl := diffLine{
    57  			originalNumber: currentOriginalLineNumber,
    58  		}
    59  
    60  		lineStr := string(line)
    61  
    62  		if strings.HasPrefix(lineStr, "-") {
    63  			dl.typ = diffLineDeleted
    64  			dl.data = strings.TrimPrefix(lineStr, "-")
    65  			currentOriginalLineNumber++
    66  		} else if strings.HasPrefix(lineStr, "+") {
    67  			dl.typ = diffLineAdded
    68  			dl.data = strings.TrimPrefix(lineStr, "+")
    69  		} else {
    70  			if i == len(lines)-1 && lineStr == "" {
    71  				// handle last \n: don't add an empty original line
    72  				break
    73  			}
    74  
    75  			dl.typ = diffLineOriginal
    76  			dl.data = strings.TrimPrefix(lineStr, " ")
    77  			currentOriginalLineNumber++
    78  		}
    79  
    80  		ret = append(ret, dl)
    81  	}
    82  
    83  	// if > 0, then the original file had a 'No newline at end of file' mark
    84  	if h.OrigNoNewlineAt > 0 {
    85  		dl := diffLine{
    86  			originalNumber: currentOriginalLineNumber + 1,
    87  			typ:            diffLineAdded,
    88  			data:           "",
    89  		}
    90  		ret = append(ret, dl)
    91  	}
    92  
    93  	p.lines = ret
    94  }
    95  
    96  func (p *hunkChangesParser) handleOriginalLine(line diffLine, i *int) {
    97  	if len(p.replacementLinesToPrepend) == 0 {
    98  		p.lastOriginalLine = &line
    99  		*i++
   100  		return
   101  	}
   102  
   103  	// check following added lines for the case:
   104  	// + added line 1
   105  	// original line
   106  	// + added line 2
   107  
   108  	*i++
   109  	var followingAddedLines []string
   110  	for ; *i < len(p.lines) && p.lines[*i].typ == diffLineAdded; *i++ {
   111  		followingAddedLines = append(followingAddedLines, p.lines[*i].data)
   112  	}
   113  
   114  	p.ret = append(p.ret, Change{
   115  		LineRange: result.Range{
   116  			From: line.originalNumber,
   117  			To:   line.originalNumber,
   118  		},
   119  		Replacement: result.Replacement{
   120  			NewLines: append(p.replacementLinesToPrepend, append([]string{line.data}, followingAddedLines...)...),
   121  		},
   122  	})
   123  	p.replacementLinesToPrepend = nil
   124  	p.lastOriginalLine = &line
   125  }
   126  
   127  func (p *hunkChangesParser) handleDeletedLines(deletedLines []diffLine, addedLines []string) {
   128  	change := Change{
   129  		LineRange: result.Range{
   130  			From: deletedLines[0].originalNumber,
   131  			To:   deletedLines[len(deletedLines)-1].originalNumber,
   132  		},
   133  	}
   134  
   135  	if len(addedLines) != 0 {
   136  		//nolint:gocritic
   137  		change.Replacement.NewLines = append(p.replacementLinesToPrepend, addedLines...)
   138  		if len(p.replacementLinesToPrepend) != 0 {
   139  			p.replacementLinesToPrepend = nil
   140  		}
   141  
   142  		p.ret = append(p.ret, change)
   143  		return
   144  	}
   145  
   146  	// delete-only change with possible prepending
   147  	if len(p.replacementLinesToPrepend) != 0 {
   148  		change.Replacement.NewLines = p.replacementLinesToPrepend
   149  		p.replacementLinesToPrepend = nil
   150  	} else {
   151  		change.Replacement.NeedOnlyDelete = true
   152  	}
   153  
   154  	p.ret = append(p.ret, change)
   155  }
   156  
   157  func (p *hunkChangesParser) handleAddedOnlyLines(addedLines []string) {
   158  	if p.lastOriginalLine == nil {
   159  		// the first line is added; the diff looks like:
   160  		// 1. + ...
   161  		// 2. - ...
   162  		// or
   163  		// 1. + ...
   164  		// 2. ...
   165  
   166  		p.replacementLinesToPrepend = addedLines
   167  		return
   168  	}
   169  
   170  	// add-only change merged into the last original line with possible prepending
   171  	p.ret = append(p.ret, Change{
   172  		LineRange: result.Range{
   173  			From: p.lastOriginalLine.originalNumber,
   174  			To:   p.lastOriginalLine.originalNumber,
   175  		},
   176  		Replacement: result.Replacement{
   177  			NewLines: append(p.replacementLinesToPrepend, append([]string{p.lastOriginalLine.data}, addedLines...)...),
   178  		},
   179  	})
   180  	p.replacementLinesToPrepend = nil
   181  }
   182  
   183  func (p *hunkChangesParser) parse(h *diffpkg.Hunk) []Change {
   184  	p.parseDiffLines(h)
   185  
   186  	for i := 0; i < len(p.lines); {
   187  		line := p.lines[i]
   188  		if line.typ == diffLineOriginal {
   189  			p.handleOriginalLine(line, &i)
   190  			continue
   191  		}
   192  
   193  		var deletedLines []diffLine
   194  		for ; i < len(p.lines) && p.lines[i].typ == diffLineDeleted; i++ {
   195  			deletedLines = append(deletedLines, p.lines[i])
   196  		}
   197  
   198  		var addedLines []string
   199  		for ; i < len(p.lines) && p.lines[i].typ == diffLineAdded; i++ {
   200  			addedLines = append(addedLines, p.lines[i].data)
   201  		}
   202  
   203  		if len(deletedLines) != 0 {
   204  			p.handleDeletedLines(deletedLines, addedLines)
   205  			continue
   206  		}
   207  
   208  		// no deletions, only additions
   209  		p.handleAddedOnlyLines(addedLines)
   210  	}
   211  
   212  	if len(p.replacementLinesToPrepend) != 0 {
   213  		p.log.Infof("The diff contains only additions: no original or deleted lines: %#v", p.lines)
   214  		return nil
   215  	}
   216  
   217  	return p.ret
   218  }
   219  
   220  func getErrorTextForLinter(settings *config.LintersSettings, linterName string) string {
   221  	text := "File is not formatted"
   222  	switch linterName {
   223  	case gciName:
   224  		text = getErrorTextForGci(settings.Gci)
   225  	case gofumptName:
   226  		text = "File is not `gofumpt`-ed"
   227  		if settings.Gofumpt.ExtraRules {
   228  			text += " with `-extra`"
   229  		}
   230  	case gofmtName:
   231  		text = "File is not `gofmt`-ed"
   232  		if settings.Gofmt.Simplify {
   233  			text += " with `-s`"
   234  		}
   235  		for _, rule := range settings.Gofmt.RewriteRules {
   236  			text += fmt.Sprintf(" `-r '%s -> %s'`", rule.Pattern, rule.Replacement)
   237  		}
   238  	case goimportsName:
   239  		text = "File is not `goimports`-ed"
   240  		if settings.Goimports.LocalPrefixes != "" {
   241  			text += " with -local " + settings.Goimports.LocalPrefixes
   242  		}
   243  	}
   244  	return text
   245  }
   246  
   247  func extractIssuesFromPatch(patch string, lintCtx *linter.Context, linterName string) ([]result.Issue, error) {
   248  	diffs, err := diffpkg.ParseMultiFileDiff([]byte(patch))
   249  	if err != nil {
   250  		return nil, fmt.Errorf("can't parse patch: %w", err)
   251  	}
   252  
   253  	if len(diffs) == 0 {
   254  		return nil, fmt.Errorf("got no diffs from patch parser: %v", patch)
   255  	}
   256  
   257  	var issues []result.Issue
   258  	for _, d := range diffs {
   259  		if len(d.Hunks) == 0 {
   260  			lintCtx.Log.Warnf("Got no hunks in diff %+v", d)
   261  			continue
   262  		}
   263  
   264  		for _, hunk := range d.Hunks {
   265  			p := hunkChangesParser{log: lintCtx.Log}
   266  
   267  			changes := p.parse(hunk)
   268  
   269  			for _, change := range changes {
   270  				change := change // fix scope
   271  				i := result.Issue{
   272  					FromLinter: linterName,
   273  					Pos: token.Position{
   274  						Filename: d.NewName,
   275  						Line:     change.LineRange.From,
   276  					},
   277  					Text:        getErrorTextForLinter(lintCtx.Settings(), linterName),
   278  					Replacement: &change.Replacement,
   279  				}
   280  				if change.LineRange.From != change.LineRange.To {
   281  					i.LineRange = &change.LineRange
   282  				}
   283  
   284  				issues = append(issues, i)
   285  			}
   286  		}
   287  	}
   288  
   289  	return issues, nil
   290  }