-
Notifications
You must be signed in to change notification settings - Fork 570
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
SemVer handling is now through opaque types #961
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if I agree with this in general. I'd prefer if we could implement this in a way with template literal types, i.e., ${string}.${string}.${string}
for proper version types. This avoids the need for type casts as well.
@@ -92,7 +93,7 @@ describe('CronjobController', () => { | |||
|
|||
const callActionMock = jest | |||
.spyOn(controllerMessenger, 'call') | |||
.mockImplementation((method) => { | |||
.mockImplementation((method, ..._) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's this for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Visual Studio Code is using new version of TS, and it's complaining that we're not accepting all the parameters of the call
. This silences TypeScript in VSC, and will be useful when we're going to upgrade to newer version.
The whole point is to have explicit type casts. Otherwise TypeScript will happily duck-type a version between range and we'll not find it during testing. For example Additionally I've worked with Opaque Types before in TypeScript, and believe me, it's the only way to properly disable duck typing. |
b2f5207
to
90a8f1f
Compare
* @see {@link assertIsSemVerRange} | ||
* @see {@link isValidSemVerRange} | ||
*/ | ||
export type SemVerRange = Opaque<string, typeof semVerRange>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL - interfaces can be self recursive but types can't.
// works
interface A {
b: Opaque<string, A>;
}
// errors
type B = Opaque<string, B>;
@@ -27,6 +28,16 @@ export const MOCK_SNAP_ID = 'npm:@metamask/example-snap'; | |||
export const MOCK_LOCAL_SNAP_ID = 'local:@metamask/example-snap'; | |||
export const MOCK_ORIGIN = 'example.com'; | |||
|
|||
type GetPersistedSnapObjectOptions = Partial<Omit<PersistedSnap, 'version'>> & { | |||
version?: string | SemVerVersion; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use omit here (and in the other instances that you do this) when you can just change the original type to be SemVerVersion? Also I don't understand why we would want it to be union type w/ string? I thought the whole point of the opaque types was to narrow types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you need to not change original types for some reason, here's a utility type to make things a tiny bit less redundant and clearer to understand:
type MakeSemVer<T> = { [K in keyof T]: K extends 'version' ? SemVerVersion | string : T[K]; }
// then you can do something like below
type GetPersistedSnapObjectOptions = Partial<MakeSemVer<PersistedSnap>>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's tens if not hundreds of tests that use those functions, and we would have to update all of them to have a type cast.
This is for backwards compatibility because of that.
The whole source code (not tests) is type cast though.
packages/snaps-controllers/src/cronjob/CronjobController.test.ts
Outdated
Show resolved
Hide resolved
@@ -2593,7 +2595,7 @@ describe('SnapController', () => { | |||
.mockImplementationOnce(async () => { | |||
const manifest: SnapManifest = { | |||
...getSnapManifest(), | |||
version: '1.1.0', | |||
version: '1.1.0' as SemVerVersion, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be moved to a constant on top of the file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not possible in this case. Tests that use specific versions instead of creation utils, do it because they're testing version/updates functionality.
@@ -27,6 +28,16 @@ export const MOCK_SNAP_ID = 'npm:@metamask/example-snap'; | |||
export const MOCK_LOCAL_SNAP_ID = 'local:@metamask/example-snap'; | |||
export const MOCK_ORIGIN = 'example.com'; | |||
|
|||
type GetPersistedSnapObjectOptions = Partial<Omit<PersistedSnap, 'version'>> & { | |||
version?: string | SemVerVersion; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we accept string
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
90a8f1f
to
c20ab47
Compare
c20ab47
to
eb04a9b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just some small comments.
packages/snaps-utils/package.json
Outdated
@@ -62,7 +62,8 @@ | |||
"rfdc": "^1.3.0", | |||
"semver": "^7.3.7", | |||
"ses": "^0.17.0", | |||
"superstruct": "^0.16.7" | |||
"superstruct": "^0.16.7", | |||
"ts-opaque": "^3.0.1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove.
packages/snaps-utils/src/versions.ts
Outdated
export function resolveVersion(version?: Json): Json { | ||
export function resolveVersionRange( | ||
version?: Json, | ||
): [undefined, SemVerRange] | [Error, undefined] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
): [undefined, SemVerRange] | [Error, undefined] { | |
): [error: undefined, range: SemVerRange] | [error: Error, range: undefined] { |
Codecov Report
@@ Coverage Diff @@
## main #961 +/- ##
==========================================
+ Coverage 92.58% 92.67% +0.08%
==========================================
Files 93 93
Lines 9116 9226 +110
Branches 890 894 +4
==========================================
+ Hits 8440 8550 +110
Misses 676 676
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
Manifest handling is very sensitive part of Snaps. It's loaded inside the main extension process and contains a list of permissions.
Any tools that can help us make handling manifest related operations more secure are deeply appreciated.
This PR introduces two new types
SemVerRange
andSemVerVersion
, both of which are strings, but behave as opaque types. Meaning that any type casting between those types needs to be explicit and disallows duck typing from and to string.This provides not only better intention handling in the code, it increases security by requiring validation before casting.