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

Agent: handle null values where we previously only handled undefined #4286

Merged
merged 1 commit into from
May 25, 2024

Conversation

sqs
Copy link
Member

@sqs sqs commented May 24, 2024

To prevent bugs when communicating with peers implemented in different programming languages cross-process RPC protocols must not differentiate between undefined, ECMAScript property presence (hasOwnProperty), and null. Anywhere that a value can be undefined, our code needs to handle null values or no such property existing, and all must be handled in the same way (and vice versa).

If a property's TypeScript type includes any of null, undefined, or ? (optional field), then it must use all of them in its TypeScript type. All of those possible TypeScript types must be handled.

This makes it impossible to accidentally only check if (message.foo === undefined) { ... in cases where message came from a peer that may have sent message.foo === null.

Also adds tests that parse all of the protocol .ts files to report any regressions.

Closes https://linear.app/sourcegraph/issue/CODY-1829/handle-null-values-in-the-protocol.

Test plan

CI

@sqs sqs requested review from olafurpg and a team May 24, 2024 11:29
@sqs
Copy link
Member Author

sqs commented May 24, 2024

I don't see any obvious bugs this fixes, but it would have prevented the bugs fixed in #4279, and it will hopefully prevent new bugs like that.

@@ -86,20 +86,6 @@ export class AgentWorkspaceDocuments implements vscode_shim.WorkspaceDocuments {
document.underlying.visibleRange = fromCache.protocolDocument.visibleRange
}

// The client may send null values that we convert to undefined here.
Copy link
Member Author

Choose a reason for hiding this comment

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

These if-statements are now guaranteed to be false, but I understand if you'd like to keep them in, @olafurpg, from the battle scars.

Copy link
Member

Choose a reason for hiding this comment

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

These if statements should be removed, I added them only out of paranoia

@@ -66,7 +66,7 @@ export class AgentWorkspaceDocuments implements vscode_shim.WorkspaceDocuments {
const changes = applyContentChanges(fromCache, document.contentChanges)
contentChanges.push(...changes.contentChanges)
document.underlying.content = changes.newText
} else if (typeof document.content === 'string') {
} else if (document.content !== undefined) {
Copy link
Member

Choose a reason for hiding this comment

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

Hmmm, this looks like a behavioral change because document.content can be be null. I need to review this diff more closely, only have ~40 more minutes today and need to land a PR in JB. Will come back to this PR on Monday!

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
} else if (document.content !== undefined) {
} else if (document.content) {

Would it work if we just check if document.content is true or not?

Copy link
Member Author

Choose a reason for hiding this comment

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

The protocol type changes I made forced this change in ProtocolTextDocumentWithUri.content: https://github.com/sourcegraph/cody/pull/4286/files#diff-9a8aa7b97ec7b062f534e08aac6f2964faf42c55cfa790267eba4f81387d3b1bR51

So it's now:

    public get content(): string | undefined {
        return this.underlying.content ?? undefined
    }

So, yes, we can certainly check against null as well to be safe, but in this case it made more sense for the code in ProtocolTextDocumentWithUri.content that sits between AgentWorkspaceDocuments and the prototol typings to perform that ?? undefined and make it so it can never be null.

@abeatrix I'm not sure if that would break when files are the empty string. That would work, but it's unnecessary.

But if you both have battle scars from issues and want to keep more extra checks in, that's fine!

Copy link
Contributor

Choose a reason for hiding this comment

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

@sqs oh no, no battle scars for me YET. I was simply asking if if (document.content) { would work here to address @olafurpg 's behavioral change comment where document.content could be null 😄

Good point on the empty string. Maybe we can check the length so that it will catch cases where content is undefined | null | "" ?

Suggested change
} else if (document.content !== undefined) {
} else if (document.content?.length) {

Just a thought though! Will defer to @olafurpg since he's more familiar with agent!

Copy link
Member Author

Choose a reason for hiding this comment

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

:)

It should never be able to be null anymore, though, and that is now well enforced at runtime (not just by assuming the peer follows the protocol types correctly). So, it's not that checking for null would hurt, it's just that it's unnecessary here because:

  1. I made the protocol types (that describe the values received from the JetBrains Kotlin code) basically have this field be typed as content?: string | undefined | null
  2. This immediately made ProtocolTextDocumentWithUri.content have typechecker errors because now it was trying to return a value string | undefined | null as string | undefined...
  3. Which meant I added (and needed to do so or else the typechecker would fail) the ?? undefined to ensure its value is never null.

And if you hover over that value in your editor (the content in if (document.content !== undefined) {, you'll see that it can't be null:

image

And we can trust this more because of the protocol type changes I made.

Copy link
Member

Choose a reason for hiding this comment

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

@sqs You're right here! I only skimmed the diff super quickly on Friday since I was deep working on other stuff.

To prevent bugs when communicating with peers implemented in different programming languages cross-process RPC protocols must not differentiate between `undefined`, ECMAScript property presence (`hasOwnProperty`), and `null`. Anywhere that a value can be `undefined`, our code needs to handle `null` values or no such property existing, and all must be handled in the same way (and vice versa).

If a property's TypeScript type includes any of `null`, `undefined`, or `?` (optional field), then it must use *all* of them in its TypeScript type. All of those possible TypeScript types must be handled.

This makes it impossible to accidentally only check `if (message.foo === undefined) { ...` in cases where `message` came from a peer that may have sent `message.foo === null`.

Also adds tests that parse all of the protocol `.ts` files to report any regressions.

Closes https://linear.app/sourcegraph/issue/CODY-1829/handle-null-values-in-the-protocol.
Copy link
Member

@olafurpg olafurpg left a comment

Choose a reason for hiding this comment

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

LGTM 👍🏻 👍🏻 Awesome work @sqs this is great!

@@ -86,20 +86,6 @@ export class AgentWorkspaceDocuments implements vscode_shim.WorkspaceDocuments {
document.underlying.visibleRange = fromCache.protocolDocument.visibleRange
}

// The client may send null values that we convert to undefined here.
Copy link
Member

Choose a reason for hiding this comment

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

These if statements should be removed, I added them only out of paranoia

@@ -50,7 +50,7 @@ export type WebviewMessage =
*/
command: 'event'
eventName: string
properties: TelemetryEventProperties | undefined
properties?: TelemetryEventProperties | undefined | null
Copy link
Member

Choose a reason for hiding this comment

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

Is | undefined needed when using ?:?

I'm fine keeping it, but I don't think I would have added them myself.

Copy link
Member

Choose a reason for hiding this comment

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

If | undefined is only added for the linter, then it's possible to check for .questionToken on ts.PropertyDeclaration like here https://github.com/sourcegraph/scip-typescript/blob/olafurpg/signatures/src/FileIndexer.ts#L460C1-L462C75

import * as ts from 'typescript'
import { describe, expect, test } from 'vitest'

describe('protocol null/undefined robustness', () => {
Copy link
Member

Choose a reason for hiding this comment

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

😍 Love this!

I was reviewing the diff thinking we need a linter to prevent future regressions, only to scroll down to the bottom of the diff and see you already hooked into tsc to do the static analysis. Well done!

Double-checked that this catches unwanted undefined properties that don't accept | undefined

CleanShot 2024-05-25 at 20 38 05@2x

@olafurpg olafurpg changed the title make protocol types robust to ?|undefined|null Agent: handle null values where we previously only handled undefined May 25, 2024
@olafurpg olafurpg merged commit 8d50564 into main May 25, 2024
20 of 21 checks passed
@olafurpg olafurpg deleted the sqs/protocol-null branch May 25, 2024 18:41
@@ -48,18 +48,18 @@ export class ProtocolTextDocumentWithUri {
}

public get content(): string | undefined {
return this.underlying.content
return this.underlying.content ?? undefined
Copy link
Member

Choose a reason for hiding this comment

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

is this value being the empty string meaningful?

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.

None yet

4 participants