-
-
Notifications
You must be signed in to change notification settings - Fork 239
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
chore: extract a result sorting function #737
Conversation
@P0lip Ready for review |
src/formatters/utils/sortResults.ts
Outdated
return [...results].sort((resultA, resultB) => { | ||
const diff = resultA.range.start.line - resultB.range.start.line; | ||
const safeCastToString = (code: string | number | undefined): string => { | ||
if (code === undefined) { |
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.
@P0lip Are there any use case where code is either undefined
or a number
? Wouldn't that be the case, as 5.0
is around the corner, could we make this a non nullable string?
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.
AFAIK our own code includes code in each diagnostic result, but that doesn't necessarily need to be the case when it comes to end-user code.
Moreover, if we wanted to make it required, we'd need to make the change over here https://github.com/stoplightio/types/blob/master/src/diagnostics.ts#L63, as that's is the interface SpectralDiagnostic
inherits from, or, alternatively, we could make code
required over here https://github.com/stoplightio/spectral/blob/develop/src/types/spectral.ts#L42#L44.
Either way, code
is rather an additional metadata, not something really required, so I'd keep it optional.
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.
and yeah, we should not be throwing either.
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.
and yeah, we should not be throwing either.
Makes sense. Pushed another proposal.
src/formatters/utils/sortResults.ts
Outdated
return code; | ||
}; | ||
|
||
const ARTIFICIAL_ROOT = 'root'; |
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.
Let's have a symbol here or something really unique. Root can be a potential source
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.
@P0lip Fixed. However, I can't help but feeling slightly displeased with result. The toString()
call isn't really self explicit with regards to the uniqueness intent.
Moreover, although quite unlikely, nothing prevents a caller from feeding source
with a string containing Symbol(root)
.
Thoughts?
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.
Let safeSource
return a symbol or string. If there is a symbol returned, shift the item to the left (return -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.
@P0lip I went a different way. Do you like it better?
src/formatters/utils/sortResults.ts
Outdated
export const sortResults = (results: IRuleResult[]) => { | ||
return [...results].sort((resultA, resultB) => { | ||
const diff = resultA.range.start.line - resultB.range.start.line; | ||
const compareCode = (one: string | number | undefined, another: string | number | undefined): 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.
const compareCode = (one: string | number | undefined, another: string | number | undefined): number => { | |
const compareCode = (left: IDiagnostic['code'], right: IDiagnostic['code']): -1 | 0 | 1 => { |
Not sure why, but I like left and right as names for such comparison functions. 😆
or codeA
and codeB
Feel free to leave it as is though.
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.
@P0lip Fixed
return one.localeCompare(another); | ||
}; | ||
|
||
const normalize = (value: number): -1 | 0 | 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.
const normalize = (value: number): -1 | 0 | 1 => { | |
const normalize = (value: number): -1 | 0 | 1 => Math.max(-1, Math.min(1, Math.floor(value))) as -1 | 0 | 1; |
Math.floor is optional, but it's meant to protect us from returning other values than integers.
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.
@P0lip Hmmm. TBH, I do believe that this proposal is less easier to parse than the original (boring) code. I tend to avoid those one-liner smart constructs for readability purposes.
However, if you really want me to update the code, I'll do it, of course.
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.
Nah, I don't really mind. :)
src/formatters/utils/sortResults.ts
Outdated
return 1; | ||
} | ||
|
||
return one.toString().localeCompare(another.toString(), undefined, { numeric: true }); |
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.
return one.toString().localeCompare(another.toString(), undefined, { numeric: true }); | |
return String(one).localeCompare(String(another), void 0, { numeric: true }); |
String is a slightly safer alternative. String.prototype.toString
or Number.prototype.toString
can always be monkey-patched in JS 😷
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.
Fixed
end: { line: 99, character: 99 }, | ||
}, | ||
message: '99', | ||
severity: 99, |
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.
severity: 99, | |
severity: DiagnosticSeverity.Error, // or any other level, it's irrelevant |
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.
Fixed
|
||
export const deduplicateResults = (results: IRuleResult[]) => { | ||
const seen: Dictionary<Dictionary<string, string>, symbol> = {}; | ||
const sorted = results.sort(resultsComparer); |
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 guess you don't want to mutate results
.
const sorted = results.sort(resultsComparer); | |
const sorted = [...results].sort(resultsComparer); |
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.
@P0lip Dammit! Thanks for this.
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.
Looking good in general! Appreciate your work a lot. Thanks.
|
||
export const deduplicateResults = (results: IRuleResult[]) => { | ||
const seen: Dictionary<Dictionary<string, string>, symbol> = {}; | ||
const sorted = results.sort(resultsComparer); |
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 suspect you don't intent to mutate results
.
const sorted = results.sort(resultsComparer); | |
const sorted = [...results].sort(resultsComparer); |
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.
You might need to update some other places in the code to reflect the above change.
I bet certain tests will fail, i.e src/cli/services/__tests__/linter.test.ts
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 bet certain tests will fail, i.e src/cli/services/tests/linter.test.ts
I was expecting them to also fail as well, but the CI server doesn't complain yet. It's possible the current harness doesn't contain enough different broken rules that hit the same line and char range for the "wrongly ordered" results to appear.
|
||
const ARTIFICIAL_ROOT = Symbol('root'); | ||
const howMany = results.length; |
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.
const howMany = results.length; | |
const totalResults = results.length; |
sounds a tiny bit better to me, but I'm terrible at naming.
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.
Fixed
src/formatters/utils/sortResults.ts
Outdated
|
||
return diff; | ||
}); | ||
export const resultsComparer = (resultA: IRuleResult, resultB: IRuleResult): -1 | 0 | 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.
Now that I think of it, I believe we could name it in the way other functions are named in this file.
compareResults
What do you think?
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.
Fixed
filtered.push(sorted[i]); | ||
} | ||
|
||
if (resultsComparer(sorted[howMany - 1], filtered[filtered.length - 1]) !== 0) { |
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.
Technically there is a possibility of out of bounds access here (in case of filtered
array).
Let's avoid it.
if (resultsComparer(sorted[howMany - 1], filtered[filtered.length - 1]) !== 0) { | |
if (filtered.length > 0 && resultsComparer(sorted[howMany - 1], filtered[filtered.length - 1]) !== 0) { |
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.
Hmmm. It's possible I'm lacking a test case here. Let me see how I can put that under the light.
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.
@P0lip There was a bug. And thanks to you I've found it. Added a test case and changed the algo to make it backward looking (rather than forward looking). It also brings the slight benefit of making the whole thing easier to understand (and hopefully review).
src/formatters/utils/sortResults.ts
Outdated
return diff; | ||
}); | ||
export const resultsComparer = (resultA: IRuleResult, resultB: IRuleResult): -1 | 0 | 1 => { | ||
const diffSource = compareSource(resultA.source, resultB.source); |
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.
Also, how do you feel about having a sort of loop here?
The performance difference will be negliable, but the code will look nicer, IMHO.
return normalize([
compareSource(resultA.source, resultB.source),
resultA.range.start.line - resultB.range.start.line,
resultA.range.start.character - resultB.range.start.character,
compareCode(resultA.code, resultB.code),
resultA.path.join().localeCompare(resultB.path.join()),
].find(diff => diff !== 0) ?? 0); // ?? 0 just in case all previous calls returned 0
Didn't test it at all, but it should work.
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.
@P0lip That should work. But I feel a bit wary about this kind of code. It's usually very painful to debug/troubleshoot when it doesn't behave as expected.
For some reasons that reminds me of the jest exceptArrayContaining
/ObjectContaining
pattern. This is really elegant when you read it, but turns into a gigantic mess when the expectations have changed and you're trying to find what values you forgot to update in the test cases.
As usual, if you feel more at ease with this construct, I'll update the code to meet your expectations.
src/formatters/utils/sortResults.ts
Outdated
return one.toString().localeCompare(another.toString(), undefined, { numeric: true }); | ||
}; | ||
|
||
const compareSource = (one: string | undefined, another: string | undefined): 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.
I like it.
@P0lip Thanks for this very thorough review. I think I've dealt with (or discussed) every comment. Fixup commits have been pushed to make it easier for you to see the introduced changes. Thoughts? |
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.
The very 2 last comments on tests.
Besides these, it's good to go
}); | ||
}); | ||
|
||
describe('resultsComparer', () => { |
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.
describe('resultsComparer', () => { | |
describe('compareResults', () => { |
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.
Fixed
temp = [...shuffled]; | ||
|
||
shuffled = temp.sort((_a, _b) => { | ||
return Math.random() * 10 - 5; |
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 get the idea, but in general tests should be deterministic, so could you execute that and paste the output into some JSON file and load it here in tests?
Alternatively, you could provide an array of indices:
const indices = [-1, 0, 1, 1, -1, 0]; // etc
shuffled = temp.sort((_a, _b, i) => indices[i]);
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.
in general tests should be deterministic,
You're right. But I was really willing to keep the list of results in an sorted way to keep the test as maintainable as possible.
Your indices based proposal is a great idea that allows to do just that. 🎉 (Sadly the sort callback doesn't accept indices. I worked around that)
Fixed.
shuffled = temp.sort((_a, _b) => { | ||
return Math.random() * 10 - 5; | ||
}); | ||
} while (shuffled[0].code === results[0].code); |
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.
Is this loop actually needed?
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.
No longer needed now that the test is deterministic.
return one.localeCompare(another); | ||
}; | ||
|
||
const normalize = (value: number): -1 | 0 | 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.
Nah, I don't really mind. :)
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.
Thank you!
From #681 (comment)