move source code lines extraction to processor and store source lines in output json
This commit is contained in:
		
							parent
							
								
									f81283f38d
								
							
						
					
					
						commit
						e58c27e463
					
				| @ -273,6 +273,26 @@ func (e *Executor) setOutputToDevNull() (savedStdout, savedStderr *os.File) { | |||||||
| 	return | 	return | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (e *Executor) setExitCodeIfIssuesFound(issues <-chan result.Issue) <-chan result.Issue { | ||||||
|  | 	resCh := make(chan result.Issue, 1024) | ||||||
|  | 
 | ||||||
|  | 	go func() { | ||||||
|  | 		issuesFound := false | ||||||
|  | 		for i := range issues { | ||||||
|  | 			issuesFound = true | ||||||
|  | 			resCh <- i | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if issuesFound { | ||||||
|  | 			e.exitCode = e.cfg.Run.ExitCodeIfIssuesFound | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		close(resCh) | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	return resCh | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (e *Executor) runAndPrint(ctx context.Context, args []string) error { | func (e *Executor) runAndPrint(ctx context.Context, args []string) error { | ||||||
| 	if !logutils.HaveDebugTag("linters_output") { | 	if !logutils.HaveDebugTag("linters_output") { | ||||||
| 		// Don't allow linters and loader to print anything | 		// Don't allow linters and loader to print anything | ||||||
| @ -293,14 +313,10 @@ func (e *Executor) runAndPrint(ctx context.Context, args []string) error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	gotAnyIssues, err := p.Print(ctx, issues) | 	issues = e.setExitCodeIfIssuesFound(issues) | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("can't print %d issues: %s", len(issues), err) |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	if gotAnyIssues { | 	if err = p.Print(ctx, issues); err != nil { | ||||||
| 		e.exitCode = e.cfg.Run.ExitCodeIfIssuesFound | 		return fmt.Errorf("can't print %d issues: %s", len(issues), err) | ||||||
| 		return nil |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
|  | |||||||
| @ -55,6 +55,7 @@ func NewRunner(astCache *astcache.Cache, cfg *config.Config, log logutils.Log) ( | |||||||
| 			processors.NewMaxPerFileFromLinter(), | 			processors.NewMaxPerFileFromLinter(), | ||||||
| 			processors.NewMaxSameIssues(icfg.MaxSameIssues, log.Child("max_same_issues")), | 			processors.NewMaxSameIssues(icfg.MaxSameIssues, log.Child("max_same_issues")), | ||||||
| 			processors.NewMaxFromLinter(icfg.MaxIssuesPerLinter, log.Child("max_from_linter")), | 			processors.NewMaxFromLinter(icfg.MaxIssuesPerLinter, log.Child("max_from_linter")), | ||||||
|  | 			processors.NewSourceCode(log.Child("source_code")), | ||||||
| 		}, | 		}, | ||||||
| 		Log: log, | 		Log: log, | ||||||
| 	}, nil | 	}, nil | ||||||
|  | |||||||
| @ -36,7 +36,7 @@ func NewCheckstyle() *Checkstyle { | |||||||
| 	return &Checkstyle{} | 	return &Checkstyle{} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (Checkstyle) Print(ctx context.Context, issues <-chan result.Issue) (bool, error) { | func (Checkstyle) Print(ctx context.Context, issues <-chan result.Issue) error { | ||||||
| 	out := checkstyleOutput{ | 	out := checkstyleOutput{ | ||||||
| 		Version: "5.0", | 		Version: "5.0", | ||||||
| 	} | 	} | ||||||
| @ -71,9 +71,9 @@ func (Checkstyle) Print(ctx context.Context, issues <-chan result.Issue) (bool, | |||||||
| 
 | 
 | ||||||
| 	data, err := xml.Marshal(&out) | 	data, err := xml.Marshal(&out) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fmt.Fprintf(logutils.StdOut, "%s%s\n", xml.Header, data) | 	fmt.Fprintf(logutils.StdOut, "%s%s\n", xml.Header, data) | ||||||
| 	return len(files) > 0, nil | 	return nil | ||||||
| } | } | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ type JSONResult struct { | |||||||
| 	Report *report.Data | 	Report *report.Data | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p JSON) Print(ctx context.Context, issues <-chan result.Issue) (bool, error) { | func (p JSON) Print(ctx context.Context, issues <-chan result.Issue) error { | ||||||
| 	allIssues := []result.Issue{} | 	allIssues := []result.Issue{} | ||||||
| 	for i := range issues { | 	for i := range issues { | ||||||
| 		allIssues = append(allIssues, i) | 		allIssues = append(allIssues, i) | ||||||
| @ -38,9 +38,9 @@ func (p JSON) Print(ctx context.Context, issues <-chan result.Issue) (bool, erro | |||||||
| 
 | 
 | ||||||
| 	outputJSON, err := json.Marshal(res) | 	outputJSON, err := json.Marshal(res) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fmt.Fprint(logutils.StdOut, string(outputJSON)) | 	fmt.Fprint(logutils.StdOut, string(outputJSON)) | ||||||
| 	return len(allIssues) != 0, nil | 	return nil | ||||||
| } | } | ||||||
|  | |||||||
| @ -7,5 +7,5 @@ import ( | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type Printer interface { | type Printer interface { | ||||||
| 	Print(ctx context.Context, issues <-chan result.Issue) (bool, error) | 	Print(ctx context.Context, issues <-chan result.Issue) error | ||||||
| } | } | ||||||
|  | |||||||
| @ -28,24 +28,18 @@ func (p Tab) SprintfColored(ca color.Attribute, format string, args ...interface | |||||||
| 	return c.Sprintf(format, args...) | 	return c.Sprintf(format, args...) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *Tab) Print(ctx context.Context, issues <-chan result.Issue) (bool, error) { | func (p *Tab) Print(ctx context.Context, issues <-chan result.Issue) error { | ||||||
| 	w := tabwriter.NewWriter(logutils.StdOut, 0, 0, 2, ' ', 0) | 	w := tabwriter.NewWriter(logutils.StdOut, 0, 0, 2, ' ', 0) | ||||||
| 
 | 
 | ||||||
| 	issuesN := 0 |  | ||||||
| 	for i := range issues { | 	for i := range issues { | ||||||
| 		issuesN++ |  | ||||||
| 		p.printIssue(&i, w) | 		p.printIssue(&i, w) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if issuesN != 0 { |  | ||||||
| 		p.log.Infof("Found %d issues", issuesN) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if err := w.Flush(); err != nil { | 	if err := w.Flush(); err != nil { | ||||||
| 		p.log.Warnf("Can't flush tab writer: %s", err) | 		p.log.Warnf("Can't flush tab writer: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return issuesN != 0, nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p Tab) printIssue(i *result.Issue, w io.Writer) { | func (p Tab) printIssue(i *result.Issue, w io.Writer) { | ||||||
|  | |||||||
| @ -1,27 +1,20 @@ | |||||||
| package printers | package printers | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" |  | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" |  | ||||||
| 	"time" |  | ||||||
| 
 | 
 | ||||||
| 	"github.com/fatih/color" | 	"github.com/fatih/color" | ||||||
| 	"github.com/golangci/golangci-lint/pkg/logutils" | 	"github.com/golangci/golangci-lint/pkg/logutils" | ||||||
| 	"github.com/golangci/golangci-lint/pkg/result" | 	"github.com/golangci/golangci-lint/pkg/result" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type linesCache [][]byte |  | ||||||
| type filesCache map[string]linesCache |  | ||||||
| 
 |  | ||||||
| type Text struct { | type Text struct { | ||||||
| 	printIssuedLine bool | 	printIssuedLine bool | ||||||
| 	useColors       bool | 	useColors       bool | ||||||
| 	printLinterName bool | 	printLinterName bool | ||||||
| 
 | 
 | ||||||
| 	cache filesCache | 	log logutils.Log | ||||||
| 	log   logutils.Log |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewText(printIssuedLine, useColors, printLinterName bool, log logutils.Log) *Text { | func NewText(printIssuedLine, useColors, printLinterName bool, log logutils.Log) *Text { | ||||||
| @ -29,7 +22,6 @@ func NewText(printIssuedLine, useColors, printLinterName bool, log logutils.Log) | |||||||
| 		printIssuedLine: printIssuedLine, | 		printIssuedLine: printIssuedLine, | ||||||
| 		useColors:       useColors, | 		useColors:       useColors, | ||||||
| 		printLinterName: printLinterName, | 		printLinterName: printLinterName, | ||||||
| 		cache:           filesCache{}, |  | ||||||
| 		log:             log, | 		log:             log, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @ -43,56 +35,19 @@ func (p Text) SprintfColored(ca color.Attribute, format string, args ...interfac | |||||||
| 	return c.Sprintf(format, args...) | 	return c.Sprintf(format, args...) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *Text) getFileLinesForIssue(i *result.Issue) (linesCache, error) { | func (p *Text) Print(ctx context.Context, issues <-chan result.Issue) error { | ||||||
| 	fc := p.cache[i.FilePath()] |  | ||||||
| 	if fc != nil { |  | ||||||
| 		return fc, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// TODO: make more optimal algorithm: don't load all files into memory |  | ||||||
| 	fileBytes, err := ioutil.ReadFile(i.FilePath()) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("can't read file %s for printing issued line: %s", i.FilePath(), err) |  | ||||||
| 	} |  | ||||||
| 	lines := bytes.Split(fileBytes, []byte("\n")) // TODO: what about \r\n? |  | ||||||
| 	fc = lines |  | ||||||
| 	p.cache[i.FilePath()] = fc |  | ||||||
| 	return fc, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (p *Text) Print(ctx context.Context, issues <-chan result.Issue) (bool, error) { |  | ||||||
| 	var issuedLineExtractingDuration time.Duration |  | ||||||
| 	defer func() { |  | ||||||
| 		p.log.Infof("Extracting issued lines took %s", issuedLineExtractingDuration) |  | ||||||
| 	}() |  | ||||||
| 
 |  | ||||||
| 	issuesN := 0 |  | ||||||
| 	for i := range issues { | 	for i := range issues { | ||||||
| 		issuesN++ |  | ||||||
| 		p.printIssue(&i) | 		p.printIssue(&i) | ||||||
| 
 | 
 | ||||||
| 		if !p.printIssuedLine { | 		if !p.printIssuedLine { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		startedAt := time.Now() | 		p.printSourceCode(&i) | ||||||
| 		lines, err := p.getFileLinesForIssue(&i) | 		p.printUnderLinePointer(&i) | ||||||
| 		if err != nil { |  | ||||||
| 			return false, err |  | ||||||
| 		} |  | ||||||
| 		issuedLineExtractingDuration += time.Since(startedAt) |  | ||||||
| 
 |  | ||||||
| 		p.printIssuedLines(&i, lines) |  | ||||||
| 		if i.Line()-1 < len(lines) { |  | ||||||
| 			p.printUnderLinePointer(&i, string(lines[i.Line()-1])) |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if issuesN != 0 { | 	return nil | ||||||
| 		p.log.Infof("Found %d issues", issuesN) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return issuesN != 0, nil |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p Text) printIssue(i *result.Issue) { | func (p Text) printIssue(i *result.Issue) { | ||||||
| @ -107,32 +62,19 @@ func (p Text) printIssue(i *result.Issue) { | |||||||
| 	fmt.Fprintf(logutils.StdOut, "%s: %s\n", pos, text) | 	fmt.Fprintf(logutils.StdOut, "%s: %s\n", pos, text) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p Text) printIssuedLines(i *result.Issue, lines linesCache) { | func (p Text) printSourceCode(i *result.Issue) { | ||||||
| 	lineRange := i.GetLineRange() | 	for _, line := range i.SourceLines { | ||||||
| 	var lineStr string | 		fmt.Fprintln(logutils.StdOut, line) | ||||||
| 	for line := lineRange.From; line <= lineRange.To; line++ { |  | ||||||
| 		if line == 0 { // some linters, e.g. gas can do it: it really means first line |  | ||||||
| 			line = 1 |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		zeroIndexedLine := line - 1 |  | ||||||
| 		if zeroIndexedLine >= len(lines) { |  | ||||||
| 			p.log.Warnf("No line %d in file %s", line, i.FilePath()) |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		lineStr = string(bytes.Trim(lines[zeroIndexedLine], "\r")) |  | ||||||
| 		fmt.Fprintln(logutils.StdOut, lineStr) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p Text) printUnderLinePointer(i *result.Issue, line string) { | func (p Text) printUnderLinePointer(i *result.Issue) { | ||||||
| 	lineRange := i.GetLineRange() | 	if len(i.SourceLines) != 1 || i.Pos.Column == 0 { | ||||||
| 	if lineRange.From != lineRange.To || i.Pos.Column == 0 { |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	col0 := i.Pos.Column - 1 | 	col0 := i.Pos.Column - 1 | ||||||
|  | 	line := i.SourceLines[0] | ||||||
| 	prefixRunes := make([]rune, 0, len(line)) | 	prefixRunes := make([]rune, 0, len(line)) | ||||||
| 	for j := 0; j < len(line) && j < col0; j++ { | 	for j := 0; j < len(line) && j < col0; j++ { | ||||||
| 		if line[j] == '\t' { | 		if line[j] == '\t' { | ||||||
|  | |||||||
| @ -13,6 +13,8 @@ type Issue struct { | |||||||
| 	Pos       token.Position | 	Pos       token.Position | ||||||
| 	LineRange *Range `json:",omitempty"` | 	LineRange *Range `json:",omitempty"` | ||||||
| 	HunkPos   int    `json:",omitempty"` | 	HunkPos   int    `json:",omitempty"` | ||||||
|  | 
 | ||||||
|  | 	SourceLines []string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (i Issue) FilePath() string { | func (i Issue) FilePath() string { | ||||||
|  | |||||||
							
								
								
									
										81
									
								
								pkg/result/processors/source_code.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								pkg/result/processors/source_code.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,81 @@ | |||||||
|  | package processors | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 
 | ||||||
|  | 	"github.com/golangci/golangci-lint/pkg/logutils" | ||||||
|  | 	"github.com/golangci/golangci-lint/pkg/result" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type linesCache [][]byte | ||||||
|  | type filesLineCache map[string]linesCache | ||||||
|  | 
 | ||||||
|  | type SourceCode struct { | ||||||
|  | 	cache filesLineCache | ||||||
|  | 	log   logutils.Log | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var _ Processor = SourceCode{} | ||||||
|  | 
 | ||||||
|  | func NewSourceCode(log logutils.Log) *SourceCode { | ||||||
|  | 	return &SourceCode{ | ||||||
|  | 		cache: filesLineCache{}, | ||||||
|  | 		log:   log, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p SourceCode) Name() string { | ||||||
|  | 	return "source_code" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p SourceCode) Process(issues []result.Issue) ([]result.Issue, error) { | ||||||
|  | 	return transformIssues(issues, func(i *result.Issue) *result.Issue { | ||||||
|  | 		lines, err := p.getFileLinesForIssue(i) | ||||||
|  | 		if err != nil { | ||||||
|  | 			p.log.Warnf("Failed to get lines for file %s: %s", i.FilePath(), err) | ||||||
|  | 			return i | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		newI := *i | ||||||
|  | 
 | ||||||
|  | 		lineRange := i.GetLineRange() | ||||||
|  | 		var lineStr string | ||||||
|  | 		for line := lineRange.From; line <= lineRange.To; line++ { | ||||||
|  | 			if line == 0 { // some linters, e.g. gas can do it: it really means first line | ||||||
|  | 				line = 1 | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			zeroIndexedLine := line - 1 | ||||||
|  | 			if zeroIndexedLine >= len(lines) { | ||||||
|  | 				p.log.Warnf("No line %d in file %s", line, i.FilePath()) | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			lineStr = string(bytes.Trim(lines[zeroIndexedLine], "\r")) | ||||||
|  | 			newI.SourceLines = append(newI.SourceLines, lineStr) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return &newI | ||||||
|  | 	}), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *SourceCode) getFileLinesForIssue(i *result.Issue) (linesCache, error) { | ||||||
|  | 	fc := p.cache[i.FilePath()] | ||||||
|  | 	if fc != nil { | ||||||
|  | 		return fc, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// TODO: make more optimal algorithm: don't load all files into memory | ||||||
|  | 	fileBytes, err := ioutil.ReadFile(i.FilePath()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("can't read file %s for printing issued line: %s", i.FilePath(), err) | ||||||
|  | 	} | ||||||
|  | 	lines := bytes.Split(fileBytes, []byte("\n")) // TODO: what about \r\n? | ||||||
|  | 	fc = lines | ||||||
|  | 	p.cache[i.FilePath()] = fc | ||||||
|  | 	return fc, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p SourceCode) Finish() {} | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Denis Isaev
						Denis Isaev