-
Notifications
You must be signed in to change notification settings - Fork 396
Enable 'strictNullChecks' ts compiler option. #676
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
Conversation
c7c9b14
to
2605cf1
Compare
db95eec
to
9c009a4
Compare
2605cf1
to
d1e7e84
Compare
28767f7
to
0daa443
Compare
In the majority of cases, I simply turned stuff like this: ``` function fname(param: T) { if (!param) throw Error("invalid param; must not be nullish"); ... } ``` into this: ``` function fname(param: T|null|undefined) { if (!param) throw Error("invalid param; must not be nullish"); ... } ``` A more sensible approach would be eliminate the possibility of null/undef and eliminate the checks too, but that's a bit more involved and this patch is already enormous. But I think these sorts of improvements could be taken advantage of opportunistically as the code is worked on in the future.
And also update @firebase/auth to most recent to match.
0daa443
to
ad16d69
Compare
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.
Thanks for doing this @rsgowman. Much overdue improvements. It looks mostly good. I just added a few suggestions and questions for clarity.
@@ -683,7 +683,7 @@ const LIST_INBOUND_SAML_CONFIGS = new ApiSettings('/inboundSamlConfigs', 'GET') | |||
* Class that provides the mechanism to send requests to the Firebase Auth backend endpoints. | |||
*/ | |||
export abstract class AbstractAuthRequestHandler { | |||
protected readonly projectId: string; | |||
protected readonly projectId: string | 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.
Should never be null I 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.
It's set via utils.getProjectId (src/utils/index.ts:66), which returns null if all other options fail. (And is documented as such too, so it seems deliberate.)
firebase-admin-node/src/utils/index.ts
Line 66 in 01a01a9
export function getProjectId(app: FirebaseApp): string { |
So I think there's three possibilities:
a) This is a (legitimately) nullable variable.
b) utils.getProjectId() returns a nullable project id, which is correct in general, but we have reason to believe here in this specific case that it should be impossible/illegal. In which case, we can check the return value and throw if null.
c) utils.getProjectId() should never return null. In which case, we can change it to throw if all possibilities are exhausted.
I don't know which is correct though. Tracing through how it's called, it's clear that we're going to send some silly urls if it's not present, so (b) sounds like the most likely. However, I suppose if this is never actually used, then we could defer that error check?
Is it possible to use admin.auth without a valid projectId? If yes, then I suppose the projectId variables should be left as nullable until the cases where they're definitely required. If no, then we could move the check to the auth ctor and then drop the nullable-ness from these variables. (Which would be a behaviour change, but if it's actually required, then it's not a particularly meaningful behaviour 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.
There are some admin.auth()
APIs that doesn't require project ID (custom token creation for example). Option (b) with deferred validation sounds like the right thing to do. We do a similar thing in FirebaseTokenVerifier
. But that may be a larger refactor than we wish to do right now. I'm ok with keeping this as is, and changing it later.
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.
sg; I've left it as is for now. I think AuthResourceUrlBuilder::getUrl() would need to change to check the projectId before using it. We'd also need to trace through to find other potential problems.
I've added a TODO to remind us.
test/unit/auth/auth.spec.ts
Outdated
@@ -1236,7 +1242,7 @@ AUTH_CONFIGS.forEach((testConfig) => { | |||
}); | |||
|
|||
it('should be rejected given invalid properties', () => { | |||
return auth.createUser(null) | |||
return auth.createUser(null as unknown as CreateRequest) |
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 we null as any
?
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 can. But this: null as CreateRequest
generated the following error message:
error TS2352: Conversion of type 'null' to type 'CreateRequest' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
So I've assumed that's a typescript convention and used 'unknown'. But I have no reason other than that error message to prefer 'unknown' here. If you still prefer 'any', let me know, and I'll switch it.
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 a slight preference towards as any
since it's less things to type. But this ok 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.
Switched to as any
.
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.
Thanks @rsgowman. This looks great. Just pointed out a couple of things to consider. But I think we are almost there.
test/unit/auth/auth.spec.ts
Outdated
.should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); | ||
expect(() => { | ||
(auth as any)[emailActionFlow.api](email, undefined); | ||
}).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); |
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 looks like a breaking change. Instead of rejecting the API now throws. We probably don't want 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 wouldn't call it a breaking change; I'd call it a bug fix. :) If you violate the api contract, you expect it to fail. (See also https://g3doc.corp.google.com/firebase/jscore/g3doc/contributing/styleguide.md#public-apis-should-sanitize-their-inputs-aggressively which I'm definitely in favour of.)
But you're right that this is a behaviour change, and I'm trying to minimize those in this PR. I've reverted this for now, but I think this is worth discussing:
a) whether we should change this throughout (though obviously not during this PR)
b) if so, whether we consider it a breaking change
src/utils/api-request.ts
Outdated
@@ -459,6 +470,11 @@ class AsyncHttpCall { | |||
try { | |||
this.config = new HttpRequestConfigImpl(config); | |||
this.options = this.config.buildRequestOptions(); | |||
if (!this.options.headers) { |
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. The method buildEntity()
also has code that modifies headers in-place. I think we can just do this:
this.entity = this.config.buildEntity(this.options.headers!);
If buildRequestOptions
did not set headers, this will trigger a runtime error in buildEntity()
when it attempts to modify headers. That sounds like an ok position for us to be in. WDYT?
test/unit/auth/auth.spec.ts
Outdated
@@ -1236,7 +1242,7 @@ AUTH_CONFIGS.forEach((testConfig) => { | |||
}); | |||
|
|||
it('should be rejected given invalid properties', () => { | |||
return auth.createUser(null) | |||
return auth.createUser(null as unknown as CreateRequest) |
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 a slight preference towards as any
since it's less things to type. But this ok 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.
LGTM with a couple of nits.
test/unit/auth/auth.spec.ts
Outdated
@@ -1243,7 +1243,7 @@ AUTH_CONFIGS.forEach((testConfig) => { | |||
}); | |||
|
|||
it('should be rejected given invalid properties', () => { | |||
return auth.createUser(null as unknown as CreateRequest) | |||
return auth.createUser(null as any as CreateRequest) |
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.
null as any
should be sufficient here 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.
Yup. I've s/as any as/as any/
throughout this file (and the other files you've tagged.)
@@ -401,7 +401,7 @@ describe('TenantManager', () => { | |||
}); | |||
|
|||
it('should be rejected given invalid TenantOptions', () => { | |||
return tenantManager.createTenant(null as unknown as TenantOptions) | |||
return tenantManager.createTenant(null as any as TenantOptions) |
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.
Again null as any
should be sufficient?
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.
@@ -398,10 +398,10 @@ describe('Messaging', () => { | |||
describe('send()', () => { | |||
it('should throw given no message', () => { | |||
expect(() => { | |||
messaging.send(undefined as unknown as Message); | |||
messaging.send(undefined as any as Message); |
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'd expect just undefined as any
to be enough here.
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.
In the majority of cases, I simply turned stuff like this:
into this:
A more sensible approach would be eliminate the possibility of
null/undef and eliminate the checks too, but that's a bit more involved
and this patch is already enormous. But I think these sorts of
improvements could be taken advantage of opportunistically as the code
is worked on in the future.