Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sorting result.Issues implementation (golangci/golangci-lint#1217) #1218

Merged
merged 1 commit into from
Jul 12, 2020
Merged

Sorting result.Issues implementation (golangci/golangci-lint#1217) #1218

merged 1 commit into from
Jul 12, 2020

Conversation

butuzov
Copy link
Member

@butuzov butuzov commented Jul 7, 2020

Implements #1217

@boring-cyborg
Copy link

boring-cyborg bot commented Jul 7, 2020

Hey, thank you for opening your first Pull Request !

@CLAassistant
Copy link

CLAassistant commented Jul 7, 2020

CLA assistant check
All committers have signed the CLA.

pkg/lint/runner.go Outdated Show resolved Hide resolved
}

sort.Slice(issues, func(i, j int) bool {
return sr.cmp.Compare(&issues[i], &issues[j]) == Less
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Common way is just comparing to zero. We can simplify consts as well.

Copy link
Member Author

@butuzov butuzov Jul 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reasons as in res.isNeutral() related comment. Read it as a story: If the results of comparing a and b state Less, compare function receives true. I would keep names constants as it really simplifies the understanding of the code for a small price.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tricky one. Will it be "<=" instead of "==" ?

I didn't scan for all Comparator implementation to make sure that negative result will only be -1

// For sorting we are comparing (in next order): file names, line numbers,
// position, range numbers (if position not available), and finnally - giving up.
return &SortResults{
cmp: ByName{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

understand that your intention is to compose the sorting rules. However, if I looked at the Comparer we have now ByName along.

               ByName{
			next: ByLine{
                        ...
			},
		}

Comparing ByName implicitly checking Line number due to composition, but will ByName comparator should only deal with names, it doesn't require to know the next rule should be.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Chain of Responsibility pattern, current resolver only needs to know about the exitance of the next one (res.isNeutral() we calling defines should we proceed to the next rule or not, but rules are different, strings compare, single numbers compare (line and column), two numbers(range) ). Guess we suppose to see the Comparator interface, instead of chain of its implementations. ByName or any other implementation of Comparator needs to know only that there is the next rule in the chain.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if chain of responsibility pattern is correct one to use here. Maybe I am mixing two different things (comparator interface + design pattern), but if we can interface as Comparator, it should just to compare two entities and nothing else.

The logic of calling next comparator is kind of repetitive in currently (e.g. check res is neutral or not, if not check the next one is there, then call next one), so adding new Comparator will require a fairly some level of understanding of existing one.

Few snippets (based on your code) to illustrate my thinking:

type SortResults struct {
	compareFuncs []compareFunc
	cfg          *config.Config
}

type compareFunc func(result.Issue, result.Issue) compareResult

func NewSortResults(cfg *config.Config) *SortResults {
        // single responsibility function  
	byName := func(first result.Issue, second result.Issue) compareResult {
		return compareResult(strings.Compare(first.FilePath(), second.FilePath()))
	}

	byLine := func(first result.Issue, second result.Issue) compareResult {
		return compareResult(first.Line() - second.Line())
	}

	byCol := func(first result.Issue, second result.Issue) compareResult {
		return compareResult(first.Column() - second.Column())
	}

	byRange := func(first result.Issue, second result.Issue) compareResult {
		return compareResult(first.GetLineRange().From - second.GetLineRange().From)
	}

	return &SortResults{
		compareFuncs: []compareFunc{
			byName,
			byLine,
			byCol,
			byRange,
                        ... --> we can add more type of compareFunc here
		},
		cfg: cfg,
	}
}

// Process is performing sorting of the result issues.
func (sr SortResults) Process(issues []result.Issue) ([]result.Issue, error) {
	if !sr.cfg.Output.SortResults {
		return issues, nil
	}

	sort.Slice(issues, func(i, j int) bool {
		for _, c := range sr.compareFuncs {
			if res := c(issues[i], issues[j]); !res.isNeutral() {
				return res <= less
			}
		}
		return false
	})
	return issues, nil
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to use CoR in order to avoid making logic complicated (cmon who what to see 3-5 blocks of different if statements ?), CoR helps to pass the request along a chain of handlers. More of that - I can test each of them. And logic isn't really simple here (what if linter don't provide column? or line?) I did try to cover it with tests.

func numericCompare(a, b int) CompareResult {
	var (
		isValuesInvalid  = a < 0 || b < 0
		isZeroValuesBoth = a == 0 && b == 0
		isEqual          = a == b
		isZeroValueInA   = b > 0 && a == 0
		isZeroValueInB   = a > 0 && b == 0
	)

	switch {
	case isZeroValuesBoth || isEqual:
		return Equal
	case isValuesInvalid || isZeroValueInA || isZeroValueInB:
		return None
	case a > b:
		return Greater
	case a < b:
		return Less
	}

	return Equal
}

I see what you have written, and got the idea, and I wish I have more time for opensource, to rethink the structure and implement all checks, write new tests, cover edge cases to pleasure community, and maintainers in order to keep code quality high. But my family wouldn't agree with me. I am really sorry for wasting your time with this PR. Please reject this PR if you think code quality is way lower the level you will accept.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you have written, and got the idea, and I wish I have more time for opensource, to rethink the structure and implement all checks, write new tests, cover edge cases to pleasure community, and maintainers in order to keep code quality high. But my family wouldn't agree with me. I am really sorry for wasting your time with this PR. Please reject this PR if you think code quality is way lower the level you will accept.

Appreciate that you spend time to send PR with a very good coverage 💯 . Yeah, I personally understand your point about OSS involvement. While only few are lucky to have OSS as paid job, the rest of us (including myself) are voluntarily doing it either during night, weekends or our own spare time. In my opinion, OSS PR process is a chance to have open discussion, and in turn learn from each other. Sometimes it can take a little longer than we expect.

I could be a bit opinionated, or even wrong on this subject. So, let me park it right here, we can see if someone else from @golangci/team has any comment/idea in general.

In the meantime, I can continue reviewing on the basis that CoR is used. Hope it is fine to you.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate this. Using your way to solve the issue will require additional time to think about implementation and tests, and actually require to throw my code out almost entirely.

@sayboras sayboras requested a review from a team July 8, 2020 04:22
Copy link
Member

@sayboras sayboras left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have sometime to finish review now. Feel free to address when you see fit.

PS: Can you add something like Fixes https://github.com/golangci/golangci-lint/issues/1217 in PR description, so that underlying issue and PR can be linked together? Github didn't scan in PR title.

pkg/result/processors/sort_results.go Outdated Show resolved Hide resolved
pkg/result/processors/sort_results.go Show resolved Hide resolved
pkg/result/processors/sort_results.go Outdated Show resolved Hide resolved
pkg/result/processors/sort_results.go Outdated Show resolved Hide resolved
Copy link
Member

@sayboras sayboras left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added few last comments on run_tests and private const/structs.

LGTM on basis that CoR is used.

func (sr SortResults) Name() string { return "sort_results" }
func (sr SortResults) Finish() {}

type CompareResult int
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be private

Copy link
Member Author

@butuzov butuzov Jul 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, I also needed to add //nolint:golint directive to suppress warning on false positive message about returning unexported value.

type CompareResult int

const (
Less CompareResult = iota - 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be private

Copy link
Member Author

@butuzov butuzov Jul 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's controversial, but can we keep constants at least CamelCased?

}

// Comparator describe how to implement compare for two "issues" lexicographically
type Comparator interface {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this could be private

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

_ Comparator = (*ByColumn)(nil)
)

type ByName struct{ next Comparator }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: these comparator can be changed to private

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -80,6 +80,7 @@ func initFlagSet(fs *pflag.FlagSet, cfg *config.Config, m *lintersdb.Manager, is
fs.BoolVar(&oc.PrintIssuedLine, "print-issued-lines", true, wh("Print lines of code with issue"))
fs.BoolVar(&oc.PrintLinterName, "print-linter-name", true, wh("Print linter name in issue line"))
fs.BoolVar(&oc.UniqByLine, "uniq-by-line", true, wh("Make issues output unique by line"))
fs.BoolVar(&oc.SortResults, "sort-results", false, wh("Sort issues by line number"))
Copy link
Member

@sayboras sayboras Jul 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add the test for this option?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, @sayboras means to add the tests in the test/run_test.go because of the new config option

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SVilgelm @sayboras added, in addition, changed help text.

@SVilgelm SVilgelm added the enhancement New feature or improvement label Jul 12, 2020
Copy link
Member

@SVilgelm SVilgelm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Thank you
Tests are passed with rebasing over master branch

@SVilgelm SVilgelm requested a review from a team July 12, 2020 17:40
@SVilgelm SVilgelm merged commit 6e7c317 into golangci:master Jul 12, 2020
@golangci-automator
Copy link

Hey, @butuzov — we just merged your PR to golangci-lint! 🔥🚀

golangci-lint is built by awesome people like you. Let us say “thanks”: we just invited you to join the GolangCI organization on GitHub.
This will add you to our team of maintainers. Accept the invite by visiting this link.

By joining the team, you’ll be able to label issues, review pull requests, and merge approved pull requests.
More information about contributing is here.

Thanks again!

@butuzov butuzov deleted the sorted-results branch July 13, 2020 06:08
@butuzov
Copy link
Member Author

butuzov commented Jul 13, 2020

Thank everyone, that was quite a ride =)

@sayboras
Copy link
Member

Thank everyone, that was quite a ride =)

haha hope you enjoy it 😅. Looking forward to your next ride then 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or improvement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants