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

Add strict null checks to the codebase #233

Closed

Conversation

petebacondarwin
Copy link
Contributor

Also converts incorrect use of void to undefined and adds some words to the CSpell dictionary.

@changeset-bot
Copy link

changeset-bot bot commented Jan 11, 2022

⚠️ No Changeset found

Latest commit: 4b62761

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

const cwd = searchPaths.shift();
// The `searchPaths.length` check above ensures that `searchPaths.shift()` is defined
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const cwd = searchPaths.shift()!;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is a shame that TS cannot infer this.

Copy link
Contributor

Choose a reason for hiding this comment

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

could include in the while check a && Array.isArray(searchPaths) it could be not inferring because .length can also be on a string.

Copy link
Contributor

Choose a reason for hiding this comment

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

or alternatively

Suggested change
const cwd = searchPaths.shift()!;
const cwd = Array.isArray(searchPaths) && searchPaths.shift()!;

Copy link
Contributor

Choose a reason for hiding this comment

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

If we run across a lot of these we can make a custom type-guard, this example isn't inferring anything from inputs or using generics.

function isNonEmptyArrStrings(value: any): boolean {
    return Array.isArray(value) && value.length && value.every(item => typeof item === "string");
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm going to add the guard because that is quite cool

@@ -110,29 +110,23 @@ export default {
}

const { value } = handlerIterator.next();
if (value) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no need to check the value (according to the types) since it will always be defined.

Copy link
Member

Choose a reason for hiding this comment

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

Always present, but not always truthy. Once the iterator is exhausted, value will be undefined.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doh! I should have remembered.

So the "proper" way should be:

const {value, done} = handleIterator.next();
if (!done) {
 ...
}

But then that would mean that we don't handle the final return statement from the executeRequest() generator.

So I can see that this is the most appropriate approach. Thanks for catching this.
(If only we had unit tests for this...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or we could do:

      const context: EventContext<unknown, string, Record<string, unknown>> = {
        request: new Request(request.clone()),
        next: done ? () => {} : next,
        params,
        data,
        env,
        waitUntil: workerContext.waitUntil.bind(workerContext),
      };

But that is a bit less easy to grok, perhaps?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh actually, I remember why we needed to remove this.
If we have the if (value) then we must also have an else clause that returns "something". Otherwise the return type of next() becomes Promise<Response|undefined> which is not compatible with the EventContext type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@GregBrimble - can you take a new look at this file now? I have refactored the code a bit.

@@ -212,7 +212,7 @@ describe("wrangler", () => {

it("should make multiple requests for paginated results", async () => {
// Create a lot of mock namespaces, so that the fetch requests will be paginated
const KVNamespaces = [];
const KVNamespaces: { title: string; id: string }[] = [];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should this be a named type?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would clarify the API if it was

}));
setBundle(
(previousBundle) =>
previousBundle && {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is possible that the previousBundle is undefined. In this case I have chosen to just return it unchanged. Is that a good idea? cc @threepointone

Copy link
Contributor

Choose a reason for hiding this comment

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

I would rather assert that it is defined, and throw if not. (In practice it will never be undefined, because this will be called only after the first build) Alternately, a non-null assertion?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll add an error.
But I don't actually understand what the point of this handler is. Is the idea that updating bundle by calling setBundle() causes the Dev component to be re-rendered?

wsServerRef.current.on("connection", (ws: WebSocket) => {
if (wsServerRef.current.clients.size > 1) {
wsServerRef.current.on("connection", function (ws: WebSocket) {
if (this.clients.size > 1) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is set to wsServerRef.current but not if we use a fat arrow...

Copy link
Contributor

Choose a reason for hiding this comment

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

that's strange.

Copy link
Contributor

@threepointone threepointone left a comment

Choose a reason for hiding this comment

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

Left a first round of comments

}));
setBundle(
(previousBundle) =>
previousBundle && {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would rather assert that it is defined, and throw if not. (In practice it will never be undefined, because this will be called only after the first build) Alternately, a non-null assertion?

const environments = config.env ?? {};
Object.values(environments).forEach((env) => {
if (env === undefined) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we want to return here, instead we'll have to create the object and copy the inherited fields on to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The previous code would crash if the value of config.env[key] was undefined, since it had the following unguarded expression: config.env[env] (where in that case env is the key.

Object.keys(config.env || {}).forEach((env) => {
Object.entries(environments).forEach(([envName, envValue]) => {
if (envValue === undefined) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

same here, I think we don't want to return early

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Similar to #233 (comment) - the previous code would crash in this case.

Copy link
Contributor

Choose a reason for hiding this comment

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

should it be an error in both cases then?
throw new Error('Missing ${something} in environments)

packages/wrangler/src/index.tsx Outdated Show resolved Hide resolved
console.log("Did not login, quitting...");
return;
}
const loggedIn = await loginOrRefreshIfRequired();
Copy link
Contributor

Choose a reason for hiding this comment

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

why did you remove the args.local checks here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Two reasons:

  • they are redundant due to the error being thrown earlier in the function if args.local is true
  • if this block is wrapped in an if statement then TS is unable to tell that config.account_id is not undefined in the fetchResult() call below.

.positional("filename", {
describe: "entry point",
type: "string",
demandOption: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

using <filename> (i.e the pointy brackets) implies demandOption:true, but ok.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is true for "runtime" but for type checking the typings are only able to indicate that args.filename must not be undefined if the demandOption property is true.

Copy link
Contributor

Choose a reason for hiding this comment

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

Didn't we recently add the PR from James that allowed for empty filename to attempt to run on a root index? This would override that.

#79

Copy link
Contributor Author

@petebacondarwin petebacondarwin Jan 13, 2022

Choose a reason for hiding this comment

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

The PR you link to was replaced by #196.
Even in this new PR the filename option is still required, because that is passed to esbuild as its entry point.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm, James had mentioned it defaulting to index the 196 PR will be merged soon and we can check the behavior with this change too

wsServerRef.current.clients.forEach((ws) => ws.close());
wsServerRef.current.close();
};
if (serverRef.current) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should assert here. It's a big problem if it's not defined, so we should probably throw.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

An alternative is that we capture these values in a const outside the callback scope...

      const serverInst = serverRef.current;
      const wsServerInst = wsServerRef.current;
      return () => {
        serverInst.close();
        // Also disconnect any open websockets/devtools connections
        wsServerInst.clients.forEach((ws) => ws.close());
        wsServerInst.close();
      };

But I wasn't sure if we could be confident that this would never change between calls to useEffect() and its cleanup function...

// We only define `proxyPort`, above, when there is no `directory` defined.
const assetsFetch =
directory !== undefined
? await generateAssetsFetch(directory)
Copy link
Contributor

Choose a reason for hiding this comment

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

seems like we can remove import assert from "assert"; with current changes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍


const invalidAssetsFetch: typeof fetch = () => {
throw new Error(
"Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
"Trying to fetch assets directly when there is no `directory` option specified."

@GregBrimble
Copy link
Member

GregBrimble commented Jan 13, 2022

Tried to add a couple of commits, but realized this is on your fork, @petebacondarwin .

So, two tiny change requests:

--- a/packages/wrangler/src/pages.tsx
+++ b/packages/wrangler/src/pages.tsx
@@ -1,6 +1,5 @@
 /* eslint-disable no-shadow */
 
-import assert from "assert";
 import type { BuilderCallback } from "yargs";
 import { join } from "path";
 import { tmpdir } from "os";
@@ -122,7 +121,9 @@ async function spawnProxyProcess({
       },
     }
   );
-  EXIT_CALLBACKS.push(() => proxy.kill());
+  EXIT_CALLBACKS.push(() => {
+    proxy.kill();
+  });

Other than that, the Pages pieces look good to me! Happy to merge.

@petebacondarwin
Copy link
Contributor Author

@GregBrimble as a "collaborator" on this repo, you have the rights to push to this branch on my fork.

Screenshot 2022-01-13 at 14 57 01

`void` should only be used for return types of functions that do not return anything (not even undefined).

See https://www.typescriptlang.org/docs/handbook/2/functions.html#void
The previous fix (removing the `if(value)` check actually broke the behaviour of the code.
@petebacondarwin petebacondarwin deleted the strict-null-checks branch January 19, 2022 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants