-
Notifications
You must be signed in to change notification settings - Fork 9.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
core: add type checking to audit and gatherer base classes #4762
Conversation
* @return {!DetailsRenderer.DetailsJSON} | ||
* @param {Audit.Headings} headings | ||
* @param {Array<Object<string, string>>} results | ||
* @param {Audit.DetailsRenderer.DetailsSummary} summary |
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.
stuck these on Audit
to keep them local since they're going to be pulled in by the larger LHR/lite effort. summary
is defined as void for now to encourage adding types :)
type: string; | ||
url: string; | ||
webSocketDebuggerUrl: string; | ||
declare namespace LH { |
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.
turns out export as namespace
was really only intended for UMD modules. This is the "proper" way to declare a global type namespace (in a d.ts file). The old way will break in the next release of TS so might as well do it correctly.
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.
https://github.com/GoogleChrome/lighthouse/pull/4762/files?w=1#diff-518173330fe66b5974ceeb9a14a7b215 to see actual changes to externs file
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.
nice 👍
*/ | ||
beforePass(options) { } | ||
beforePass(passContext) { } |
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.
😃
|
||
/* eslint-enable no-unused-vars */ | ||
} | ||
|
||
/** | ||
* @typedef {Object} Gatherer.PassContext |
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.
🎉
tsconfig.json
Outdated
@@ -18,6 +18,7 @@ | |||
"lighthouse-cli/**/*.js", | |||
"lighthouse-core/lib/dependency-graph/**/*.js", | |||
"lighthouse-core/gather/connections/**/*.js", | |||
"lighthouse-core/gather/gatherers/gatherer.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.
add audit.js too?
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.
yeah, better to be explicit
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 looking nice!
@@ -65,9 +64,9 @@ class Audit { | |||
} | |||
|
|||
/** | |||
* @param {!Audit} audit | |||
* @param {typeof Audit} audit |
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.
what's this mean btw?
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.
Audit
is the name of the class in js-land, but in type-land it's the name of the type of Audit
instances.
To refer to the type of the class Audit
itself (since we use all static methods) you have to use typeof
, which is kind of unfortunately overloaded in TS but gets the job done and isn't too obscure...
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.
huh! thanks for explaining.
wish it was instanceof
instead.
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.
Fortunately you almost always want the normal thing. It's just here we're not just using all static methods, we want the static methods on subclasses of Audit
, so we have to pass the subclass in to Audit
's static methods... 🤪
lighthouse-core/audits/audit.js
Outdated
|
||
// TODO: placeholder typedefs until Details are typed | ||
/** | ||
* @typedef {void} Audit.DetailsRenderer.DetailsSummary |
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.
we could go with this for now:
wastedMs?: number
wastedKb?: 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.
done
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.
a few questions below.
also, policy-wise how do we want to handle externs vs typedefs within the source file? and can we avoid duplicating the interfaces between the two?
lighthouse-core/audits/audit.js
Outdated
* @property {string} description | ||
* @property {string} helpText | ||
* @property {Array<string>} requiredArtifacts | ||
* @property {string=} failureDescription |
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.
how do you feel about doing foo|undefined
instead of foo=
? i can almost promise i'll forget what foo=
means in another 3 weeks.
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 don't mind the explicitness, especially because it's only allowed in some circumstances, but on the other hand it can get pretty unwieldy and there's no jsdoc equivalent to the nice ?:
on the ts side
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.
k can we do foo|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.
oh but there is! http://usejsdoc.org/tags-property.html :)
/**
* @property {Array<string>} requiredArtifacts
* @property {string} [failureDescription]
*/
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.
that...does not seem better than =
:P
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.
it seems slightly better to me, but not as good as ? or |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.
you guys are crazy, optional is part of the type not the name!
luckily we can all agree on good old boring dependable |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.
you guys are crazy, optional is part of the type not the name!
but the semantics are different!
|undefined
implies the key is always there but just potentially as undefined, the [prop]
syntax more correctly suggests that the property name is optional rather than the type of the value is sometimes 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.
typescript treats {type=}
and [propName]
the same, but the distinction from |undefined
is important...and I was wrong before about |undefined
. tsc isn't going to let us use it anyways with --strict
on.
The difference is erased when you get something that is this type, but not when declaring that something is one of these types...e.g. with @property {boolean|undefined} informative
you better have a property informative
that's one of the two. A missing informative
won't suffice, so the property is no longer optional.
I like this reasoning of [prop]
, it offers a nice way of expressing the defined/not-defined vs value/undefined mixup of JS, so let's go with that.
* @param {!AuditResult} result | ||
* @return {!AuditFullResult} | ||
* @param {typeof Audit} audit | ||
* @param {LH.AuditResult} result |
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.
Something that's been bugging me lately.. In our external LHR docs we want to basically call LH.AuditFullResult
the "Audit Result". So one alternative here is...
LH.AuditResult
=> something.. (like LH.AuditReturnValue) andLH.AuditFullResult
=>LH.AuditResult
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.
yeah, the problem used to be we needed a nice name in each audit and we didn't care so much what came out the other side. I don't mind the switch but we should come up with a good alternative for the first one (and maybe punt on this because there are a lot of AuditResult
s that would need to change
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.
fine with punting for today.
export interface AuditFullResult extends AuditResult { | ||
displayValue: string; | ||
score: number; | ||
scoreDisplayMode: 'numeric' | 'binary'; |
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.
Audit.ScoringModes?
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.
Audit.ScoringModes
is scoped to audit.js
, so the externs would need to import Audit
to have access. I think that's in our future anyways, but not worth it for this (it changes other things when our .d.ts
file imports things because then it too becomes a module...)
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 future plan is to much as much as external facing stuff as possible to a .d.ts file anyway right?
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'm not sure. There's also what we want global vs scoped. What we produce for others to link against our public API may or may not be tied to that :)
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 I was also wrong here. All the typedefs in Audit
appear to be entirely local to the file and aren't exported, even though they're on an object that was, so all of these may need to be in d.ts files unless only used inside this file (that includes subclasses, so right now no audits can see the typedef of Audt.Meta
even though they all import Audit
)
* @return {{score: number, scoreDisplayMode: string}} | ||
* @param {typeof Audit} audit | ||
* @param {LH.AuditResult} result | ||
* @return {{score: number, scoreDisplayMode: Audit.ScoringModeValues}} |
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.
Audit.ScoringModeValues
vs @typedef {Object} Audit.ScoringModes
below?
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.
ScoringModeValues
is one of the string values of the Audit.ScoringModes
object (so just 'numeric' | 'binary'
). We could probably fix this up better, maybe if we pull most of the audit stuff into here and/or an audit.d.ts
file
notApplicable?: boolean; | ||
error?: boolean; | ||
// TODO: define details | ||
details?: object; |
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.
(for when it's time to take on details
...)
i did an initial spike of details
's types in 95e19a5...typechecking#diff-518173330fe66b5974ceeb9a14a7b215
But then https://docs.google.com/document/d/1TumV5t3xtJ_e2b8y9PTwh4j2RaT-lmaQI9lwmF5tD2U/edit#heading=h.y9rlfi2u13u9 has a more future-forward definition, which is even stricter.
export interface Config {} | ||
|
||
export interface AuditResult { | ||
rawValue: boolean | number | null; |
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.
we need null?
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.
yeah, audit error results have rawValue
of null (generateErrorAuditResult
). Scoring seems to not really care, but we have unit tests asserting it so I just left it as is
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.
mmmmmmmmmmmkkkkkkkkkkkkkkkkkkkkayyyyyyyyyyy.
lgtm once foo|undefined |
fixed up optional properties. I'll follow up with |
having typing here helps in a number of places and was easy to pull out since they're relatively isolated. This is just the base classes...all the implementing classes are untouched (and unchecked)