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

RFC: Fix null-check stuff #103

Closed
wants to merge 3 commits into from

Conversation

jens-ox
Copy link

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

Hi all!

As mentioned in #98, there are currently quite a few alerts when enabling strict null checks etc in the tsconfig. If it makes sense I'd really like to help, but I'm not sure what the preferred way of handling undefined stuff is.

This PR fixes all TS issues in plainDate.ts when turning on the all the currently commented-out flags in the tsconfig. In most cases, the only change is to allow methods that are called with possibly undefined values to handle that gracefully. Or do you prefer to throw more often if something is undefined?

If this looks ok to you I can start fixing the remaining TS issues.

Copy link
Contributor

@ptomato ptomato left a comment

Choose a reason for hiding this comment

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

How exciting to receive a PR from someone new to the project and eager to help, thanks very much!

I gave it a quick look and commented on two things that stood out to me. I can take a closer look again later, but it seems like adjusting the GetIntrinsic thing might change a lot of other stuff.

lib/plaindate.ts Outdated
const Duration = GetIntrinsic('%Temporal.Duration%');
return new Duration(years, months, weeks, days, 0, 0, 0, 0, 0, 0);
// const Duration = GetIntrinsic('%Temporal.Duration%')!;
return new Temporal.Duration(years, months, weeks, days, 0, 0, 0, 0, 0, 0);
Copy link
Contributor

Choose a reason for hiding this comment

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

This we can't do, because it would break if user code monkey-patched globalThis.Temporal.Duration. (That's the reason for the GetIntrinsic mechanism appearing all over the place, so that we can continue to use the original values internally even if they are replaced in user code.) Sorry, this should've been documented somewhere more clearly! Probably in intrinsic.ts.

I think the best solution here (and elsewhere in the file) would be to type GetIntrinsic such that TypeScript knows that it returns Temporal.Duration for an argument of %Temporal.Duration%. Unfortunately I don't immediately have an idea of how to do that properly 😄

Copy link
Author

Choose a reason for hiding this comment

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

Could this be avoided by just doing import { Duration } from './duration'? Or am I missing something? 😅
Makes sense otherwise, if importing is not sensible here I'll try to teach TypeScript how GetIntrinsic works 😊

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 can import as it causes circular value dependencies between the different files? If that would work that could be one potential fix.

If we need to stick with GetIntrinsic it should already be typed such that a key of %Temporal.Duration% returns typeof Temporal.Duration, but what probably needs to change is that the return type for GetIntrinsic needs to be NonNullable<typeof INTRINSICS[key]> here.

Copy link
Author

Choose a reason for hiding this comment

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

Circular dependencies should be fine as there's already ecmascript -> calendar -> ecmascript :D
If just importing is an option that might be cleaner, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

How does TypeScript usually handle circular module dependencies?

lib/slots.ts Outdated
@@ -275,9 +275,10 @@ export function HasSlot(container: unknown, ...ids: (keyof Slots)[]): boolean {
return !!myslots && ids.reduce((all: boolean, id) => all && id in myslots, true);
}
export function GetSlot<KeyT extends keyof Slots>(
container: Slots[typeof id]['usedBy'],
container: Slots[typeof id]['usedBy'] | undefined,
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 signifies a bug in the polyfill if we end up calling GetSlot(undefined, ...) so I'd be happy if TS would complain at compile time rather than resorting to a runtime error. I wonder if there's any way to do that?

Copy link
Author

Choose a reason for hiding this comment

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

In this case it can be undefined if an incorrect disambiguation is given to BuiltinTimeZoneGetInstantFor, so maybe it would be best to just throw?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we can tighten the types in BuiltinTimeZoneGetInstantFor to ensure at compile time that no incorrect disambiguation can be given there? Or otherwise throw earlier if that happens?

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 there may be a legit issue with DisambiguatePossibleInstants because the spec says there should be an assertion that the disambiguation value is valid but the polyfill lacks this assertion. We could add a line at the end of that method e.g.

    throw new Error(`assertion failed: invalid disambiguation value ${disambiguation}`);

@12wrigja
Copy link
Contributor

12wrigja commented Dec 7, 2021

A quick drive-by comment: it is probably easiest to fix these flags by starting at the "root-most" files and working outwards. In this project, that would be in slots.ts and ecmascript.ts. Even small changes here will "ripple" through the rest of the files and might mean other fixes are necessary or obsolete.

@jens-ox
Copy link
Author

jens-ox commented Dec 7, 2021

A quick drive-by comment: it is probably easiest to fix these flags by starting at the "root-most" files and working outwards. In this project, that would be in slots.ts and ecmascript.ts. Even small changes here will "ripple" through the rest of the files and might mean other fixes are necessary or obsolete.

Yeah, that makes sense. plaindate just seemed like the easiest to understand for a newcomer like me 😊
Once it's clear how all of this can best be done I'll close this PR and work on a "proper" one.

@12wrigja
Copy link
Contributor

12wrigja commented Dec 7, 2021

On second thought, I think probably the two hardest files to change (but definitely most useful...) would be slots.ts and intrinsicclass.ts, as that is where a good chunk of the "type magic" is needed (places where TS can't easily infer the right typing). I'd suggest ecmascript.ts as third.

@jens-ox
Copy link
Author

jens-ox commented Dec 7, 2021

I tried removing GetIntrinsic - this doesn't seem to break anything and also has the nice side-effect to clear up some internal naming inconsistencies.

npm run test runs through fine, npm run test262 has problems resolving __ on my machine...

@ptomato
Copy link
Contributor

ptomato commented Dec 7, 2021

If GetIntrinsic works, that might indeed be cleaner! But I'd be concerned, does it still work when bundling for a platform that doesn't have ES6 modules? I'd also suggest verifying that it really is robust in the face of monkey-patching.

@jens-ox
Copy link
Author

jens-ox commented Dec 7, 2021

does it still work when bundling for a platform that doesn't have ES6 modules?

I haven't looked that deeply into the Rollup config used here, but as long as Rollup is properly configured, yes (and there seems to be a CommonJS export generated here, so we should be good to go).

I'd also suggest verifying that it really is robust in the face of monkey-patching

To be honest I never heard of monkey-patching before, so I might have to do a bit of reading first 🙈
But as far as I can see monkey-patching is not possible here.

@@ -5,3 +5,4 @@ tsc-out/
.vscode/*
!.vscode/launch.json
*.tgz
.DS_STORE
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs a newline at the end of this line. I think npm run fix should automatically add it. If it doesn't, then you might want to tweak the npm run fix command line args. 😄

@@ -117,7 +118,7 @@ export class Calendar implements Temporal.Calendar {
return ES.ToString(this);
}
dateFromFields(
fields: Params['dateFromFields'][0],
fields?: Params['dateFromFields'][0],
Copy link
Contributor

Choose a reason for hiding this comment

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

fields should never be undefined.

Also, if undefined were acceptable, then for any public API (like this one) it's intentional to leave it as Params... because that enforces alignment between the implementation and index.d.ts. So let's imagine that undefined were OK for this API, then the right fix would be to change index.d.ts, not here.

@@ -178,7 +179,7 @@ export class Calendar implements Temporal.Calendar {
if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver');
const date = ES.ToTemporalDate(dateParam);
const duration = ES.ToTemporalDuration(durationParam);
const options = ES.GetOptionsObject(optionsParam);
const options = ES.GetOptionsObject<Temporal.ArithmeticOptions>(optionsParam);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this explicit type parameter needed? Can't the type be inferred by TS from the type of options ?

@@ -26,6 +25,7 @@ import {
PlainTimeParams,
PlainYearMonthParams
} from './internaltypes';
import { PlainDateTime } from './plaindatetime';
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this relative path need to be './plaindatetime.js' because of TS's relatively recent support for Node.js Experimental Modules? From https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions:

relative import paths need full extensions (we have to write import "./foo.js" instead of import "./foo")

Same applies to all use of imports like this.

@@ -1069,8 +1077,8 @@ export function ToPartialRecord<B extends AnyTemporalLikeType>(
}

export function PrepareTemporalFields<B extends AnyTemporalLikeType>(
bag: B,
fields: ReadonlyArray<FieldRecord<B>>
bag?: B,
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 probably the wrong solution. See tc39/proposal-temporal#1963.

}

export function ComparisonResult(value: number) {
return value < 0 ? -1 : value > 0 ? 1 : (value as 0);
}

export function GetOptionsObject<T>(options: T) {
export function GetOptionsObject<T>(options: T | undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you explain why this change is needed? If undefined is missing from the caller's type, then the fix is probably needed in the caller, not here. By adding undefined here we're probably hiding a type bug in the caller.

@@ -134,6 +134,3 @@ export function DefineIntrinsic<KeyT extends IntrinsicDefinitionKeys>(name: KeyT
if (INTRINSICS[key] !== undefined) throw new Error(`intrinsic ${name} already exists`);
INTRINSICS[key] = value;
}
export function GetIntrinsic<KeyT extends keyof typeof INTRINSICS>(intrinsic: KeyT): typeof INTRINSICS[KeyT] {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure this can be completely removed, even if the imports work, because there are some objects and methods (e.g. from methods, maybe intrinsic prototypes) that still need to be accessed.

lib/plaindate.ts Show resolved Hide resolved
@@ -351,6 +350,7 @@ export class PlainDate implements Temporal.PlainDate {
calendar
);
const instant = ES.BuiltinTimeZoneGetInstantFor(timeZone, dt, 'compatible');
if (!instant) throw new TypeError(`Could not create instant for timezone "${timeZone}" and dateTime "${dt}"`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can instant actually be undefined here? I don't think it can be. The type problem is upstream. For example, DisambiguatePossibleInstants needs an assertion at the end to prevent undefined from being returned, e.g.

    throw new Error(`assertion failed: invalid disambiguation value ${disambiguation}`);

lib/slots.ts Outdated
@@ -275,9 +275,10 @@ export function HasSlot(container: unknown, ...ids: (keyof Slots)[]): boolean {
return !!myslots && ids.reduce((all: boolean, id) => all && id in myslots, true);
}
export function GetSlot<KeyT extends keyof Slots>(
container: Slots[typeof id]['usedBy'],
container: Slots[typeof id]['usedBy'] | undefined,
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 there may be a legit issue with DisambiguatePossibleInstants because the spec says there should be an assertion that the disambiguation value is valid but the polyfill lacks this assertion. We could add a line at the end of that method e.g.

    throw new Error(`assertion failed: invalid disambiguation value ${disambiguation}`);

@justingrant
Copy link
Contributor

I'd also suggest verifying that it really is robust in the face of monkey-patching

To be honest I never heard of monkey-patching before, so I might have to do a bit of reading first 🙈 But as far as I can see monkey-patching is not possible here.

Yep, there are a few possible issues. I'm probably missing some, but here's a few things to look at:

  • Does TS allow the prototype of TS-created classes to be changed? Currently, the polyfill allows calling setPrototypeOf on Temporal types, and if that's the correct behavior then the TS polyfill can't behave differently.
  • When we create new instances internally in ecmascript.ts by manually using the prototype object, do we use the intrinsic prototype or the monkeypatchable one?
  • What about static methods like Calendar.from which AFAIK are used internally? Those need to be still accessible internally even if the user monkeypatches them.

The first two bullet points above are covered here: tc39/proposal-temporal#1965

@jens-ox thanks so much for your work on this PR and in uncovering questions like this!

Copy link
Contributor

@justingrant justingrant left a comment

Choose a reason for hiding this comment

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

Thanks @jens-ox! Lots of notes are inline. You've already helped us find a few issues which is great. Some general feedback:

@jens-ox
Copy link
Author

jens-ox commented Dec 8, 2021

Hi @justingrant! Thanks for your comments. I opened #105 for the GetIntrinsic things. I agree that all the undefined things should be handled upstream (and then probably throw as early as possible).
I'll open a new PR and will start working from the outermost files, avoiding method signature changes.

@justingrant
Copy link
Contributor

I think this PR has been replaced by others, so I'm going to close it now. If that's a mistake, feel free to re-open.

@justingrant justingrant closed this Mar 4, 2022
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