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

#6739: Incremental strict null checks adoption: Slice 10 #6808

Merged
merged 12 commits into from Nov 9, 2023

Conversation

fregante
Copy link
Collaborator

@fregante fregante commented Nov 2, 2023

What does this PR do?

Checklist

  • Designate a primary reviewer: @grahamlangford
  • I'd like to do some smoke tests before merging

@@ -154,6 +154,7 @@
"slugify": "^1.6.6",
"stemmer": "^2.0.1",
"timezone-mock": "^1.3.6",
"uint8array-extras": "^0.5.0",
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This PR includes some deduplication around base64/data-url parsing, for which we had like 3 functions.

I merged them into one and started using this package for a safe base64/arraybuffer conversion:

Note: In the browser, do not use globalThis.atob() / globalThis.btoa() because they do not support Unicode. This package does.

@@ -62,47 +62,62 @@ const resizerMap = new WeakMap<HTMLElement, IFrameComponent>();
function popoverFactory(initialUrl: URL): HTMLElement {
const $container = $(ensureTooltipsContainer());

Copy link
Collaborator Author

@fregante fregante Nov 2, 2023

Choose a reason for hiding this comment

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

Diffs in this file best seen without whitespace changes:

  1. I flipped a condition to allow for a early return
  2. I replaced some logic; This appeases the null checks and we will have to do more of it:
    • bye .length + .push() + .pop()
    • hello .pop() + null check
  3. I added the html helper to force Parcel to format the HTML snippet. However I don’t think our version is working correctly so I'll add code-tag

src/bricks/transformers/temporaryInfo/popoverUtils.ts Outdated Show resolved Hide resolved
src/starterBricks/helpers.ts Show resolved Hide resolved

if (selectors.length === 0) {
if (!nextSelector) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Again here:

  • bye length + [x, ...rest]
  • hello shift() + null check

label: extension.label,
extensionLabel: extension.label,
label: extension.label ?? undefined,
extensionLabel: extension.label ?? undefined,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the consequence of one type having optional properties ({x?: Type}) and the other using {x: Type | null}

I can look into a better solution but I think it'd require changes to some higher type like ModComponent.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

That plus a linting rule to prefer null could clean a lot of that up.

Copy link
Collaborator Author

@fregante fregante Nov 3, 2023

Choose a reason for hiding this comment

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

I don’t think it's practical to enforce specifying every property on an object with null if they're really meant to be optional. If anything, I'd enforce the opposite: use an optional key instead of T | null, because it's much easier to handle

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's my understanding that enforcing exactOptionalPropertyTypes would still allow optional properties, but would prevent us from setting a property to undefined. null should be reserved for when we want to specify that we "know a value doesn't exist" while undefined should mean that the value was never set.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah yeah I was answering to the second comment, regarding preferring null, which I read as "Use {x: string | null} instead of {x?: string}"

exactOptionalPropertyTypes is unrelated here because {x?: string} still generates string | undefined regardless of exactOptionalPropertyTypes, so it would still conflict with string | null.

This sort of mismatches are why I subscribe to sindresorhus/meta#7, which also cites this problem.

Copy link
Collaborator

Choose a reason for hiding this comment

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

null can be important. Especially with JSON. See JSON.stringify({ foo: undefined }) vs JSON.stringify({ foo: null })

Copy link
Collaborator

Choose a reason for hiding this comment

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

exactOptionalPropertyTypes is unrelated here because {x?: string} still generates string | undefined regardless of exactOptionalPropertyTypes, so it would still conflict with string | null
Is that true? It would definitely still conflict with string | null, but {x: undefined} would be flagged. exactOptionalPropertyTypes would allow { } but error on {x: undefined}.

To allow { x: undefined }, we'd have to specify { x?: string | undefined }, which we could then flag with a linting rule.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

json.foo is still nullish, it's not important if you consider null and undefined the same.

JSON.stringify([undefined, 1]) will generate null in cases where there was undefined, but then again if you treat both the same way it doesn't matter if JSON.parse() generate [null, 1] and the expected type is Array<number | undefined>

Copy link
Collaborator

Choose a reason for hiding this comment

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

JSON.stringify([undefined, 1]) will generate null in cases where there was undefined
TIL

this.cancelListeners.add(() => {
mutationObserver.disconnect();
});
onAbort(this.cancelController, mutationObserver);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This helper also comes from Refined GitHub. AbortController/AbortSignal has bad DX, but this makes it much nicer.

// https://stackoverflow.com/q/52612122/288906
// globalThis.crypto = {
// getRandomValues: (array) => crypto.randomBytes(array.length),
// };
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This was required on Node 16, but it already exists in Node 18. We'll never need it again.

return new Blob([ia], { type: mimeTypeEssence });
}

export { convertDataUrl };
Copy link
Collaborator Author

@fregante fregante Nov 2, 2023

Choose a reason for hiding this comment

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

TODO:

  • Add tests for convertDataUrl

Our parseDataUrl is already tested and so is the 3rd party base64ToUint8Array

src/utils/promiseUtils.ts Show resolved Hide resolved
Copy link

codecov bot commented Nov 3, 2023

Codecov Report

Attention: 23 lines in your changes are missing coverage. Please review.

Comparison is base (05da7ae) 70.39% compared to head (5a18c18) 70.41%.
Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6808      +/-   ##
==========================================
+ Coverage   70.39%   70.41%   +0.02%     
==========================================
  Files        1193     1193              
  Lines       37053    37049       -4     
  Branches     6935     6949      +14     
==========================================
+ Hits        26082    26087       +5     
+ Misses      10971    10962       -9     
Files Coverage Δ
src/auth/authTypes.ts 100.00% <ø> (ø)
src/bricks/effects/clipboard.ts 85.71% <100.00%> (+0.52%) ⬆️
src/components/fields/fieldUtils.ts 83.05% <100.00%> (ø)
src/contentScript/sidebarController.tsx 37.71% <ø> (ø)
src/types/bricks/readerTypes.ts 75.00% <100.00%> (ø)
src/utils/clipboardUtils.ts 72.34% <ø> (-4.03%) ⬇️
src/utils/domUtils.ts 81.39% <ø> (-1.94%) ⬇️
src/bricks/readers/ImageExifReader.ts 29.03% <50.00%> (+8.51%) ⬆️
src/utils/parseDataUrl.ts 86.84% <86.66%> (-1.16%) ⬇️
src/utils/promiseUtils.ts 73.91% <90.00%> (+4.46%) ⬆️
... and 5 more

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Collaborator Author

@fregante fregante left a comment

Choose a reason for hiding this comment

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

I reviewed the code multiple times and tested the changed logic. Should be good to go.

@@ -86,7 +86,7 @@ export class CopyToClipboard extends EffectABC {
}

try {
blob = dataURItoBlob(text);
blob = convertDataUrl(text, "Blob");
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Conversion tested via:

const image = new Image()
image.src = URL.createObjectURL(blob);
document.body.prepend(image);

But the brick is otherwise broken on main:

async function getData(img: HTMLImageElement): Promise<ArrayBuffer> {
// Adapted from https://github.com/exif-js/exif-js/blob/master/exif.js#L384
if (/^data:/i.test(img.src)) {
// Data URI
return base64ToArrayBuffer(img.src);
return convertDataUrl(img.src, "ArrayBuffer");
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Unable to test this brick, I saw this message with two different bricks. What am I doing wrong?

Screenshot 2

In convertDataUrl, arraybuffer comes before blob in the code, so we can assume this also works.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@BLoe or @twschiller, I've not used this brick. Do you have any context?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that's a super old brick, I don't know if anyone uses it. Looks like both image bricks are element-target bricks, where you don't explicitly pass an input, but you need to use them with element target mode. If you ask on Slack someone can probably link you to the PB developer docs on how to use them.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I was able to test it.
Steps:

  1. Navigate to https://github.com/ianare/exif-samples
  2. Create a Context Menu starter brick
  3. Add the EXIF brick
  4. Under Advanced Options, set Target Root Mode to Element
  5. Set Target Element to @input.element.ref

// TODO: replace callback with having caller pass AbortSignal
export function onNodeRemoved(node: Node, callback: () => void): () => void {
const ancestors = getAncestors(node);
export function onNodeRemoved(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Greatly simplified, now uses a single MutationObserver, no sets/weaksets, no nulls.

@@ -237,6 +238,7 @@
"@types/webpack-env": "^1.18.3",
"@types/whatwg-mimetype": "^3.0.1",
"axios-mock-adapter": "^1.22.0",
"blob-polyfill": "^7.0.20220408",
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Jest 🤷‍♂️

jsdom/jsdom#2555 (comment)


private uninstalled = false;

// TODO: Rewrite this logic to use AbortController and to possibly unify the logic with the
// global cancelController. The `onAbort` utility might be useful to link multiple controllers
// together, but it's probably best to skip the duplicate `cancelController`.
private readonly cancelRemovalMonitor: Map<string, () => void>;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This file has a weird setup where there's a main "cancel callbacks" Set and also a per-extension Map:

  • The main Set is currently used, I turned it into an AbortController
  • The Map is currently already commented out, so I left some context for the changes required.

@@ -145,3 +146,89 @@ describe("asyncMapValues", () => {
});
});
});

describe("onAbort", () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

💜

Copy link

github-actions bot commented Nov 8, 2023

No loom links were found in the first post. Please add one there if you'd like to it to appear on Slack.

Do not edit this comment manually.

@fregante fregante merged commit 4f4c027 into main Nov 9, 2023
20 checks passed
@fregante fregante deleted the F/types/slice-10 branch November 9, 2023 01:28
@grahamlangford grahamlangford added this to the 1.8.3 milestone Nov 13, 2023
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.

Incremental strict null checks adoption: Slice 10
3 participants