8000 fix(fields)!: Make `Field.SKIP_SETUP` a `Symbol`; remove `class Sentinel` by cpcallen · Pull Request #6919 · google/blockly · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

fix(fields)!: Make Field.SKIP_SETUP a Symbol; remove class Sentinel #6919

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

Merged
merged 1 commit into from
Mar 23, 2023

Conversation

cpcallen
Copy link
Contributor

The basics

  • I branched from develop
  • My pull request is against develop
  • My code follows the style guide
  • I ran npm run format and npm run lint

The details

Resolves

We introduced the SKIP_SETUP sentinel value when converting Field and its subclasses to ES6 class syntax, because super must be called before any other code in a subclass constructor, breaking the previous mechanism where subclasses would set some properties before calling their superclass constructor.

SKIP_SETUP was a singleton value of class Sentinel.

Recently, in PR #6639 @btw17 introduced the isSentinel type predicate to improve the typing of Field. Unfortunately, there were some aspects of this change that were not very elegant:

  • isSentinel was declared as a static method on Field (rather than on Sentinel itself).
  • It only checks against the specific value SKIP_SETUP, rather than checking if the argument is instanceof Sentinel (though perhaps this is for efficiency?)

Additionally—as a result of the migration from ES6 to TS, and predating PR #6639—the signature for the Field constructor's first argument was value: T|Sentinel, with subclass constructors generally being <some types>|Sentinel.

This creates a small problem when attempting to port fields from core to plugins, because Sentinel is not reexported by core/utils.ts (and therefore not from core/blockly.ts either).

Proposed Changes

The latter problem could be solved simply by reexporting Sentinel, or by having plugin constructors not accept SKIP_SETUP (though this potentially makes them more difficult to subclass), but neither is particularly desirable.

Instead, this PR proposes that we:

  • Make Field.SKIP_SETUP a Symbol.
  • Delete Field.isSentinel.
  • Change the value argument to the Field constructor to accept T|typeof Field.SKIP_SETUP—where typeof Field.SKIP_SETUP is (like a literal type) a type that accepts just the single value SKIP_SETUP.
  • Remove the Sentinel class and core/utils/sentinel.ts.

Reason for Changes

To allow plug-in Field subclasses to use the same SKIP_SETUP mechanism as in-core fields.

Test Coverage

Passes npm test. No changes to manual testing expected.

Documentation

No updates beyond automatic updates of generated reference docs.

Additional Information

BREAKING CHANGE:

  • Removes Field.isSentinel

    • But this addition has not yet been published, so it can only break our own as-yet-unreleased code in samples.
  • Changes the type of Field.SKIP_SETUP and the first argument of the Field constructor from Sentinel to typeof SKIP_SETUP (a unique Symbol)

    • But given that Sentinel has never been exported this should not break any actual external code.

For the above-noted reasons these changes should not necessitate a major version bump.

@cpcallen cpcallen added component: fields breaking change Used to mark a PR or issue that changes our public APIs. PR: fix Fixes a bug labels Mar 23, 2023
@cpcallen cpcallen requested a review from a team as a code owner March 23, 2023 16:41
@cpcallen cpcallen requested a review from BeksOmega March 23, 2023 16:41
@conventional-commit-lint-gcf
Copy link
conventional-commit-lint-gcf bot commented Mar 23, 2023

🤖 I detect that the PR title and the commit message differ and there's only one commit. To use the PR title for the commit history, you can use Github's automerge feature with squashing, or use automerge label. Good luck human!

-- conventional-commit-lint bot
https://conventionalcommits.org/

@cpcallen cpcallen requested review from maribethb and removed request for BeksOmega March 23, 2023 16:42
@cpcallen
Copy link
Contributor Author

@btw17: I can't formally request a review from you, but I would like your comments if you have any.

@cpcallen
Copy link
Contributor Author

Addendum: this PR also removes the isMenuGenerator type predicate from core/field_dropdown.ts; this was added in #6550 which has been released, but is unexported and therefore should be safe to remove.

@rachel-fenichel
Copy link
Collaborator

This is a late change but I agree with merging it into the upcoming release:

  • The change is largely about types, and is type-checked by the compiler
  • The runtime behaviour is tested by our extensive field tests, which exercise the sentinel through subclassing
  • This is a good use of Symbols
  • If we don't merge this, we'll need to make a breaking change to undo it in the following release
    • Merging it now is not breaking, because we haven't yet released the current version of the sentinel

@BeksOmega BeksOmega removed their assignment Mar 23, 2023
@btw17
Copy link
Member
btw17 commented Mar 23, 2023

First off, I think making Field.SKIP_SETUP a Symbol is absolutely the desirable option! Using the Sentinel class always seemed strange to me, and I was under the impression previously that it needed to remain as that for legacy reasons.

tldr, though, I believe Field plugin subclasses should be fine as long as they call Field.SKIP_SENTINEL instead of FieldSubclass.SKIP_SENTINEL.

Clarifying my intent in #6639:

  • I believe I made it a static method on Field so that it was delivered with Field wherever it was currently being used. Users were previously supplying Field.SKIP_SENTINEL, so I knew they would have this method in scope instead of having to import Sentinel, which they previously wouldn't have needed.
  • Regarding checking the specific value, this was a deliberate choice. This behavior matched what was there previously (see code snippet below). All subclasses in the core codebase call specifically Field.SKIP_SENTINEL instead of FieldSubclass.SKIP_SENTINEL, which is always that exact single instance of new Sentinel() since Field.SKIP_SENTINEL is set to that value when the class is defined, long before its constructor is used. If calling FieldSubclass.SKIP_SENTINEL is what this is fixing, that would've already been broken.
// Before #6639
class Field<T> {
   static readonly SKIP_SETUP = new Sentinel();
   // ...
   constructor(value: T|Sentinel /*, ... */) {
      // ...
      if (opt_value === Field.SKIP_SETUP) {
        return;
      }
   }
   // ...
}

// After #6639
class Field<T> {
   static readonly SKIP_SETUP = new Sentinel();
   static isSentinel<T>(value: T|Sentinel): value is Sentinel {
      return value === Field.SKIP_SETUP;
   }
   // ...
   constructor(value: T|Sentinel /*, ... */) {
      // ...
      if (Field.isSentinel(opt_value)) return;
   }
   // ...
}

Copy link
Member
@btw17 btw17 left a comment

Choose a reason for hiding this comment

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

Looked through it all - LGTM! (Although I don't have the permissions to give approval)

@cpcallen
Copy link
Contributor Author

I think this is now awaiting formal approval by @rachel-fenichel.

@rachel-fenichel
Copy link
Collaborator

Please fix the PR title (no !) before merging.

@cpcallen
Copy link
Contributor Author
cpcallen commented Mar 23, 2023

First off, I think making Field.SKIP_SETUP a Symbol is absolutely the desirable option! Using the Sentinel class always seemed strange to me, and I was under the impression previously that it needed to remain as that for legacy reasons.

It was introduced during the ES5->ES6 phase of the migration, and it is almost certainly my fault, though I am no longer exactly sure why I suggested creating a Sentinel class rather than using a Symbol; maybe I'd just temporarily forgotten about symbols (I have done vastly more ES5 work than ES6), or perhaps I was concerned about the limitations of the Closure type system which (as far as I am aware) has no equivalent of a literal type or unique symbol type: I think the constructor could have had /** @param {T|Symbol} value */, but I may have been worried that this could cause issues if someone wanted to define a Symbol-valued field. (Not sure why I would have worried about that, though, because a sentinel symbol is no more difficult to pick out using === than a sentinel object is.) Most likely it was just that I had used this exact solution to type sentinel values in the Code City interpreter.

tldr, though, I believe Field plugin subclasses should be fine as long as they call Field.SKIP_SENTINEL instead of FieldSubclass.SKIP_SENTINEL.

Actually, using FieldSubclass.SKIP_SENTINEL should be just fine (unless FieldSubclass redefines SKIP_SENTINEL) since it will inherit the value from the Field constructor. (I actually tested this at one point while preparing this PR as an alternative to adding import {Field} from './field.ts'; in one file that didn't otherwise need to import Field.)

Clarifying my intent in #6639:

  • I believe I made it a static method on Field so that it was delivered with Field wherever it was currently being used. Users were previously supplying Field.SKIP_SENTINEL, so I knew they would have this method in scope instead of having to import Sentinel, which they previously wouldn't have needed.

That's true, though any code that was likely to need isSentinel would probably also need Sentinel (in order to, for example, declare that a method can accept a Sentinel value to test).

  • Regarding checking the specific value, this was a deliberate choice. This behavior matched what was there previously (see code snippet below).

I think this was a very sensible choice on your part, though I also note that it may have made the type checker a little complacent about the possibility of a naughty developer passing in a Sentinel value other than SKIP_SETUP: as a type predicate, isSentinel basically promises that if it returns false then the value wasn't a Sentinel, but that isn't necessarily so. Presumably the reason you introduced isSentinel was that tsc had noticed that we only guarded against one possible sentinel value, not all. (I'm slightly surprised that Closure Compiler had been happy with the original JS version of this code, come to think of it.)

@cpcallen
Copy link
Contributor Author

(Previous comment edited due to being inadvertently submitted before complete. Check hopefully final version here.

@cpcallen cpcallen changed the title fix!(fields): Make Field.SKIP_SETUP a Symbol; remove class Sentinel fix(fields): Make Field.SKIP_SETUP a Symbol; remove class Sentinel Mar 23, 2023
@github-actions github-actions bot added PR: fix Fixes a bug and removed PR: fix Fixes a bug breaking change Used to mark a PR or issue that changes our public APIs. labels Mar 23, 2023
We introduced the SKIP_SETUP sentinel value when converting
Field and its subclasses to ES6 class syntax, because super
must be called before any other code in a subclass
constructor, breaking the previous mechanism where subclasses
would set some properties before calling their superclass
constructor.

SKIP_SETUP was a singleton value of class Sentinel.

Recently, in PR google#6639 @btw17 introduced the isSentinel type
predicate to improve the typing of Field.  Unfortunately, there
were some aspects of this change that were not very elegant:

- isSentinel was declared as a static method on Field (rather
  than on Sentinel itself).
- It only checks against the specific value SKIP_SETUP,
  rather than checking if the argument is instanceof Sentinel
  (though perhaps this is for efficiency?)

Additionally - as a result of the migration from ES6 to TS, and
predating PR google#6639 - The signature for the Field constructor's
first argument was typed T|Sentinel, with subclass constructors
generally being <some type(s)>|Sentinel.

This creates a small problem when attempting to port Fields from
core to plugins, because Sentinel is not reexported by
core/utils.ts (and therefore not from core/blockly.ts either).

The latter problem could be solved simply by reexporting Sentinel,
or by having plugin constructors not accept SKIP_SETUP (though
this potentially makes them more difficult to subclass), but
neither is particularly desirable.

Instead, this PR proposes that we:

- Make Field.SKIP_SETUP a (unique) Symbol.
- Change the value argument to the Field constructor to accept
  T|typeof Field.SKIP_SETUP - where typeof Field.SKIP_SETUP is
  (like a literal type) a type that accepts just the single
  value SKIP_SETUP.
- Remove the Sentinel class and core/utils/sentinel.ts.

BREAKING CHANGE:

- Removes Field.isSentinel - though this addition has not yet
  been published, so it can only break our own as-yet-unreleased
  code in samples.

- Changes the type of Field.SKIP_SETUP and the first argument
  of the Field constructor from Sentinel to typeof SKIP_SETUP
  (a unique Symbol) - but given that Sentinel has never been
  exported this should not break any actual external code.
@cpcallen cpcallen merged commit faa95d0 into google:develop Mar 23, 2023
@cpcallen cpcallen deleted the fix/sentinel branch March 23, 2023 20:32
@cpcallen cpcallen added the breaking change Used to mark a PR or issue that changes our public APIs. label Jun 28, 2023
@cpcallen cpcallen changed the title fix(fields): Make Field.SKIP_SETUP a Symbol; remove class Sentinel fix(fields)!: Make Field.SKIP_SETUP a Symbol; remove class Sentinel Jun 28, 2023
@github-actions github-actions bot added breaking change Used to mark a PR or issue that changes our public APIs. PR: fix Fixes a bug and removed PR: fix Fixes a bug breaking change Used to mark a PR or issue that changes our public APIs. labels Jun 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change Used to mark a PR or issue that changes our public APIs. component: fields PR: fix Fixes a bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants
0