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

GetIntrinsic: remove from PlainDate, add monkeypatching test #105

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

jens-ox
Copy link

@jens-ox jens-ox commented Dec 8, 2021

Hi all! As discussed in #103, it would be nice to remove GetIntrinsic and just import things.

This PR removes GetIntrinsic (as of writing this only from plainDate.ts in order to make sure that it makes sense before removing it elsewhere).

I also added a test that monkey-patches Duration (used by PlainDate) to make sure that it doesn't affect the behaviour of PlainDate -- not sure if I did it correctly, as I never used monkey-patching before.

As importing something from a file doesn't import from globalThis anyway (at least that's my understanding), monkey-patches done in userland should not the library. If someone with more knowledge of monkey-patching could verify this, that would probably be great.

Patching stuff in userland also seems the main usecase for ES6 Proxies, so maybe this isn't something to worry about anyway?

@jens-ox jens-ox mentioned this pull request Dec 8, 2021
describe('Monkeypatch', () => {
describe('PlainDate', () => {
it("Monkeypatching Duration doesn't affect PlainDate", () => {
Temporal.Duration.prototype.constructor = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably we also want to check Temporal.Duration = null and delete Temporal.Duration

Copy link
Author

Choose a reason for hiding this comment

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

As the tests are .mjs, imports will be read-only, so JS won't let us do those two things (although monkeypatching the prototype seems to be valid).

@justingrant
Copy link
Contributor

HI @jens-ox - I spent some time digging into what would be involved in getting all of Temporal to compile with strictNullChecks: true and strictPropertyInitialization: true, so that you won't be blocked by trying to understand the most confusing parts of the polyfill codebase which are ecmascript.ts and calendar.ts.

The good news is that once those two files are made TS-strict-friendly, the rest of the codebase requires only a handful of changes to compile with strictNullChecks: true and strictPropertyInitialization: true.

The bad news is that those two files are much larger and harder to understand than the rest of Temporal, which will make it really hard for someone new coming to the project.

So I'd suggest that you start with this intrinsics PR as a good first step.

In parallel, I can help out by making needed changes in ecmascript.ts and calendar.ts. The latter also needs to be fully upgraded to TS types (it still uses a lot of any) and needs some refactoring to be maintainable by developers outside the Temporal champions group, so it's a good excuse to get that done.

I should have a PR done for those changes on Monday. This should make it much easier for you to layer the GetIntrinsic work on top.

Sound like an OK plan?

re: monkeypatching, here's some quick use cases that should continue to work: (I believe that they work in today's polyfill, but it's probably good to test these in a real app to make sure they actually work on top of v0.3.0 of the polyfill)

import { Temporal } from '@js-temporal/polyfill';

// now monkeypatch a Temporal method
Temporal.PlainDate.prototype.equals = () => { return 'not equal!'; }
d = Temporal.PlainDate.from('2020-01-01')

// calling that method should run the patched code
d.equals('2020-01-01')
// => 'not equal!'

// If another Temporal type creates an instance of that method, it should also run the patched code
Temporal.ZonedDateTime.from('2020-01-01[America/Los_Angeles]').toPlainDate().equals('2020-01-01')
// => 'not equal!'

// patching again will change behavior of *existing* instances
Temporal.PlainDate.prototype.equals = () => { return 'actually, equal!'; }
d.equals('2020-01-01')
// => 'actually, equal!'

// also works for static methods
Temporal.PlainDate.compare = (d1, d2) => { return 42; }
Temporal.PlainDate.compare('2020-01-01', '2020-01-01');
// => 42

// can also replace entire types
Temporal.PlainDate = class { constructor() { this.year = 1999 }}
new Temporal.PlainDate().year
// => 1999

// however, those replaced types won't be used by Temporal internal code
// when creating new instances.  It will use intrinsic types.  
Temporal.ZonedDateTime.from('2020-01-01[America/Los_Angeles]').toPlainDate().year
// => 2020

// but those intrinsic types still retain the patches to its prototype
Temporal.ZonedDateTime.from('2020-01-01[America/Los_Angeles]').toPlainDate().equals('2020-01-01')
// => 'actually, equal!'

@ptomato (or @gibson42 who seems to know more about monkeypatching than anyone)

  • First, are the above cases how monkeypatching should work?
  • Second, what are the pitfalls or things that can go wrong when trying to enable monkeypatching, that @jens-ox should be watching out for?

@12wrigja
Copy link
Contributor

@gibson042 (as I think Justin typo'd above)

@ptomato
Copy link
Contributor

ptomato commented Dec 13, 2021

  • First, are the above cases how monkeypatching should work?

I believe so. While checking whether Temporal.PlainDate = ... should be possible, I realized we don't actually specify the property descriptor of Temporal.___, but I assume that those need to be writable=true and configurable=true so that monkeypatching remains possible. (And we actually have test262 tests to verify this already.) I'll open an issue for this in proposal-temporal.

  • Second, what are the pitfalls or things that can go wrong when trying to enable monkeypatching, that @jens-ox should be watching out for?

The main thing I was concerned about when switching from GetIntrinsic to internal imports was your second-to-last example (that instances created internally in Temporal might be instances of the "public" replaced type, which would be bad.)

@jens-ox jens-ox marked this pull request as ready for review December 14, 2021 21:23
@jens-ox
Copy link
Author

jens-ox commented Dec 14, 2021

Hi all, I fully removed intrinsicclass.ts. The failing test262 tests also seem to fail on main.

I'll try to add the monkeypatches from above, but as the tests are .mjs files, the imports are read-only.

@jens-ox
Copy link
Author

jens-ox commented Dec 14, 2021

There was also some funky stuff going on with calendar initialization in ecmascript.ts that made TS angry, so I fixed that (moved the initialization to a ternary as suggested by an inline TODO)

configurable: true
});
if (DEBUG) {
Object.defineProperty(Class.prototype, Symbol.for('nodejs.util.inspect.custom'), {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this functionality (which AFAIK provides a way for Node to show friendlier output in the debugger) moved somewhere else? If not, then it probably should be, unless for some reason there's a better way to handle this.

Copy link
Author

Choose a reason for hiding this comment

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

@ptomato is this currently in use somewhere (as you added this)? Seems like a few other libraries (e.g. Moment.js) use this, but doesn't seem to be super common. If it's needed I would create a debug.ts, add the method there and attach it to the classes explicitly.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is mainly useful when debugging or testing things out interactively. I care mostly that it is present in proposal-temporal, I think it's probably useful here as well, but I'd leave it up to the people who maintain this polyfill more actively than me.

@justingrant
Copy link
Contributor

Hi all, I fully removed intrinsicclass.ts.

Cool! The resulting code is much more readable. Very nice!

The failing test262 tests also seem to fail on main.

The one failure on main was an "unexpected pass" because its PR was merged but we didn't update expected-failures.txt. If you rebase on top of #110 then this failure will go away. There should be no other failures on main. If you're seeing more than one failure on main, could you git pull from upstream and see if that resolves any other failures in your local main?

The other failure does not occur on main, and it's a concerning one:

FAIL built-ins/Temporal/Instant/prototype/builtin.js (strict mode)

What's odd is that when I checked out your PR branch and ran this test in the debugger, it didn't fail. Not sure why. Perhaps because I'm not correctly picking up any changes to Test262 tests that you made (did you make any?)

Could you try rebasing your PR on top of #110 (which updates Test262 to latest) and see what, if any, failures remain?

@justingrant
Copy link
Contributor

Could you try rebasing your PR on top of #110 (which updates Test262 to latest) and see what, if any, failures remain?

Update: #110 was just merged so you can just rebase to main.

justingrant added a commit to justingrant/temporal-polyfill that referenced this pull request Dec 16, 2021
Previously, GetIntrinsic returned a type that could be `undefined`.
That's fixed in this commit. Note that intrinsics.ts may be removed
completely in js-temporal#105; if that PR lands first then we'll remove this
commit.
justingrant added a commit to justingrant/temporal-polyfill that referenced this pull request Dec 17, 2021
Previously, GetIntrinsic returned a type that could be `undefined`.
That's fixed in this commit. Note that intrinsics.ts may be removed
completely in js-temporal#105; if that PR lands first then we'll remove this
commit.
@jens-ox
Copy link
Author

jens-ox commented Dec 17, 2021

Branch is rebased 👍

@jens-ox
Copy link
Author

jens-ox commented Dec 18, 2021

I did some testing today, monkeypatching things will still work in CommonJS. Using the module import, this will not work, as the imports are read-only.

@justingrant
Copy link
Contributor

I did some testing today, monkeypatching things will still work in CommonJS. Using the module import, this will not work, as the imports are read-only.

@jens-ox - Do all the cases in #105 (comment) work when the polyfill is imported in an ESM app (other than modifying properties on exported objects, as noted above) or require-d in a CJS app?

@ptomato - I noticed a subtle issue in monkeypatch behavior: should static methods on Temporal types be patchable? If my understanding of export is correct, then the following polyfill code would not allow patching of static methods because the properties of imported exports are read-only:

// plaindate.mjs
export class PlainDate { ... }

// temporal.mjs
export { PlainDate } from './plaindate.mjs';

But I don't see anything in the spec that describes whether static methods like PlainDate.compare should be writeable or read-only. (Or configurable, or...) See tc39/proposal-temporal#1978 (comment).

@justingrant
Copy link
Contributor

FYI @jens-ox - most of the other maintainers of this polyfill are out for the holidays, so it's unlikely that you'll get more feedback on this PR until January. Just wanted to let you know we weren't ignoring you! 😄

@jens-ox
Copy link
Author

jens-ox commented Dec 20, 2021

That's fine @justingrant 😊 I'm currently digging through the code - if it's ok I'm going to create a discussion post with some questions regarding the codebase.

@justingrant
Copy link
Contributor

I'm currently digging through the code - if it's ok I'm going to create a discussion post with some questions regarding the codebase.

Sounds good. I'll be around over the holidays so can answer questions.

In parallel, I can help out by making needed changes in ecmascript.ts and calendar.ts. The latter also needs to be fully upgraded to TS types (it still uses a lot of any) and needs some refactoring to be maintainable by developers outside the Temporal champions group, so it's a good excuse to get that done.

I should have a PR done for those changes on Monday. This should make it much easier for you to layer the GetIntrinsic work on top.

BTW, this work is here: #109. It turned out that after converting those two files to be strictNullChecks-compatible, there was almost no other changes required so I just included those in the PR too. So there's no need for another PR to enable strictNullChecks project-wide.

@justingrant
Copy link
Contributor

there's no need for another PR to enable strictNullChecks project-wide.

To be clear, this PR #105 is still good (assuming we can figure out the monkeypatching stuff discussed above) but we can close #103 because it's covered by #109 along with a few recent already-merged PRs (#104, #106, #108, #110, #111, #112) that came out of the investigation of #109.

@jens-ox
Copy link
Author

jens-ox commented Dec 20, 2021

Sounds great! Regarding monkeypatching - this seems to work on my end, and the monkeypatching capabilities testable in a .mjs work as expected. When importing the CommonJS bundle and doing some monkeypatching, this also works as expected. Automating this as a proper test is probably not really possible, as all tests are modules.

@jens-ox
Copy link
Author

jens-ox commented Dec 20, 2021

Here is the content of the test.cjs I used:

/* eslint-disable no-console */
const { Temporal } = require('./dist/index.cjs');

Temporal.PlainDate.prototype.equals = () => {
  return 'not equal!';
};
const d = Temporal.PlainDate.from('2020-01-01');

// calling that method should run the patched code
console.log(d.equals('2020-01-01'));
// => 'not equal!'

// If another Temporal type creates an instance of that method, it should also run the patched code
Temporal.ZonedDateTime.from('2020-01-01[America/Los_Angeles]').toPlainDate().equals('2020-01-01');
// => 'not equal!'

// patching again will change behavior of *existing* instances
Temporal.PlainDate.prototype.equals = () => {
  return 'actually, equal!';
};
console.log(d.equals('2020-01-01'));

// also works for static methods
Temporal.PlainDate.compare = (d1, d2) => {
  return 42;
};
console.log(Temporal.PlainDate.compare('2020-01-01', '2020-01-01'));
// => 42

// can also replace entire types
Temporal.PlainDate = class {
  constructor() {
    this.year = 1999;
  }
};
console.log(new Temporal.PlainDate().year);
// => 1999

// however, those replaced types won't be used by Temporal internal code
// when creating new instances.  It will use intrinsic types.
console.log(Temporal.ZonedDateTime.from('2020-01-01[America/Los_Angeles]').toPlainDate().year);
// => 2020

// but those intrinsic types still retain the patches to its prototype
console.log(Temporal.ZonedDateTime.from('2020-01-01[America/Los_Angeles]').toPlainDate().equals('2020-01-01'));
// => 'actually, equal!'

@ptomato
Copy link
Contributor

ptomato commented Jan 7, 2022

I took a look at this, not in detail because most of it is searching and replacing GetIntrinsic and I trust that the TypeScript compiler and the tests are ensuring that that was done correctly. Is there any part of this PR in particular you'd like me to pay attention to?

justingrant added a commit that referenced this pull request Jan 11, 2022
Previously, GetIntrinsic returned a type that could be `undefined`.
That's fixed in this commit. Note that intrinsics.ts may be removed
completely in #105; if that PR lands first then we'll remove this
commit.
@justingrant
Copy link
Contributor

Hi @jens-ox - We're all back from the holiday break and ready to review this. There's been quite a few PRs merged recently so now this PR has a lot of merge conflicts. Could you rebase it? Thanks!

@justingrant
Copy link
Contributor

Hi @jens-ox - We're all back from the holiday break and ready to review this. There's been quite a few PRs merged recently so now this PR has a lot of merge conflicts. Could you rebase it? Thanks!

@jens-ox, are you still interested in completing this PR? Let us know if someone else should pick it up. Thanks!

@jens-ox
Copy link
Author

jens-ox commented Feb 4, 2022

Hi @justingrant, I'll pick this up this weekend 👍

@justingrant
Copy link
Contributor

Hi @justingrant, I'll pick this up this weekend 👍

Hi @jens-ox - any luck getting back to this? Would be good to wrap this up before more merge conflicts are introduced.

@12wrigja 12wrigja mentioned this pull request Feb 18, 2022
@jens-ox
Copy link
Author

jens-ox commented Jul 9, 2022

Hi @justingrant! Sorry, I got a new job and completely forgot about my open source things. I updated the PR - but feel free to close this if this is not relevant anymore.

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