-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Conversation
Hey, thank you for opening your first Pull Request ! |
} | ||
|
||
sort.Slice(issues, func(i, j int) bool { | ||
return sr.cmp.Compare(&issues[i], &issues[j]) == Less |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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{ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this could be private
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this could be private
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
pkg/commands/run.go
Outdated
@@ -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")) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this 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
Hey, @butuzov — we just merged your PR to
By joining the team, you’ll be able to label issues, review pull requests, and merge approved pull requests. Thanks again! |
Thank everyone, that was quite a ride =) |
haha hope you enjoy it 😅. Looking forward to your next ride then 👍 |
Implements #1217