-
Notifications
You must be signed in to change notification settings - Fork 245
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
fix(jsii-diff): external structs returned from methods cannot be changed #2070
Conversation
We used to be checking the compatibility of structs in-line, as a part of checking whether the call signature of a function is compatible with its previous signature (i.e., checking that a return type is only strengthened and an input type is only weakened). This had a couple of downsides: - Performance: we would check the same type multiple times if it occurred (deeply) in multiple call signatures. - Locality: we would report the mismatch against the function signature instead of the changed type. - Locality(2): if the change occurred in an `external` type but the consuming function was `stable`, the error ought to be suppressed but was reported instead. The last reason recently cropped up in real life so that was a good opportunity to rectify this long-standing architectural defect. I'm afraid the result is a rather large refactoring; I apologize in advance, but rest assured that everything will be better in the future, dear reader. New way of working: * First identify all types in the assembly * Crawl all types that have callables (methods, properties, initializers) and identify the types they use and whether they occur in input or output position. * Validate all types, taking extra care to structurally compare structs based on whether they appear in input or output positions. This only checks every struct once, report errors against the struct itself and allows ignoring of structs that are marked `external` or `experimental`. While working on this change, it occurred to me that we were missing a couple of validations that I added at the same time: - In a `@subclassable` type, no method or property types can be changed *at all* (because C# won't allow it). - A mutable property's type cannot be changed at all (it can both not be strengthened because it can be written, and not be weakened because it can be read). Fixes #2064.
Implementation-wise:
The rules inheritance looks a bit complicated still (and is made worse by the short line lengths imposed by |
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 was about to express some concerns with how this focuses solely on the type uses within this assembly, but re-considered a couple of times and right now I'm thinking it makes sense for the purpose of this tool (@subclassable
is how the external usage is gated/protected).
This looks great as always, and I only have pretty minimal comments at this stage (basically - minimal style tuning). I'll be happy to approve once you've had a chance to go through those and decide on which you want to take...
test.each([ | ||
[ | ||
'name: string', | ||
'name?: string', | ||
/type Optional<string> \(formerly string\): output type is now optional/, | ||
], | ||
[ | ||
'name?: string', | ||
'name: string', | ||
undefined, // Strengthening is okay | ||
], | ||
[ | ||
'name: string', | ||
'name: string | number', | ||
/string \| number is not assignable to string/, | ||
], | ||
[ | ||
'name: string | number', | ||
'name: string', | ||
undefined, // Strengthening is okay | ||
], | ||
])('change class property ', (oldDecl, newDecl, error) => |
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 kinda would have preferred using objects rather than tuples for this (since this is string-abundant).
test.each([ | |
[ | |
'name: string', | |
'name?: string', | |
/type Optional<string> \(formerly string\): output type is now optional/, | |
], | |
[ | |
'name?: string', | |
'name: string', | |
undefined, // Strengthening is okay | |
], | |
[ | |
'name: string', | |
'name: string | number', | |
/string \| number is not assignable to string/, | |
], | |
[ | |
'name: string | number', | |
'name: string', | |
undefined, // Strengthening is okay | |
], | |
])('change class property ', (oldDecl, newDecl, error) => | |
test.each([ | |
{ | |
oldDecl: 'name: string', | |
newDecl: 'name?: string', | |
error: /type Optional<string> \(formerly string\): output type is now optional/, | |
}, | |
{ | |
oldDecl: 'name?: string', | |
newDecl: 'name: string', | |
error: undefined, // Strengthening is okay | |
}, | |
{ | |
oldDecl: 'name: string', | |
newDecl: 'name: string | number', | |
error: /string \| number is not assignable to string/, | |
}, | |
{ | |
oldDecl: 'name: string | number', | |
newDecl: 'name: string', | |
error: undefined, // Strengthening is okay | |
}, | |
])('change class property ', ({oldDecl, newDecl, error}) => |
The same applies to subsequent similar blocks.
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.
Cool, never even considered to do this.
await expectNoError(original, updated); | ||
return; |
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 reckon this is equivalent (but saves 1 promise un-wrapping, then re-wrapping):
await expectNoError(original, updated); | |
return; | |
return expectNoError(original, updated); |
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 had that originally, but if I do that then eslint starts complaining about the regular return path:
{
"resource": "/Users/huijbers/Workspaces/PublicCDK/jsii/packages/jsii-diff/test/util.ts",
"owner": "eslint",
"code": {
"value": "consistent-return",
"target": {
"$mid": 1,
"external": "https://eslint.org/docs/rules/consistent-return",
"path": "/docs/rules/consistent-return",
"scheme": "https",
"authority": "eslint.org"
}
},
"severity": 8,
"message": "Expected to return a value at the end of async function 'expectError'.",
"source": "eslint",
"startLineNumber": 14,
"startColumn": 23,
"endLineNumber": 14,
"endColumn": 23
}
And putting an empty return
at the end of the function doesn't fix it.
Rather take the path of least resistance here and it's not like it's in a hot path or anything :).
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.
Makes sense!
Thank you for contributing! ❤️ I will now look into making sure the PR is up-to-date, then proceed to try and merge it! |
Merging (with squash)... |
AWS CodeBuild CI Report
Powered by github-codebuild-logs, available on the AWS Serverless Application Repository |
Merging (with squash)... |
We used to be checking the compatibility of structs in-line, as
a part of checking whether the call signature of a function is
compatible with its previous signature (i.e., checking that a
return type is only strengthened and an input type is only weakened).
This had a couple of downsides:
occurred (potentially recursively) in multiple call signatures.
instead of the changed type.
external
type but theconsuming function was
stable
, the error ought to be suppressed butwas reported instead.
The last reason recently cropped up in real life so that was a good
opportunity to rectify this long-standing architectural defect. I'm
afraid the result is a rather large refactoring; I apologize in advance,
but rest assured that everything will be better in the future, dear
reader.
New way of working:
initializers) and identify the types they use and whether they
occur in input or output position.
structs based on whether they appear in input or output positions.
This only checks every struct once, report errors against the
struct itself and allows ignoring of structs that are marked
external
or
experimental
.While working on this change, it occurred to me that we were missing
a couple of validations that I added at the same time:
@subclassable
type, no method or property types can be changedat all (because C# won't allow it).
both not be strengthened because it can be written, and not be weakened
because it can be read).
Fixes #2064.
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.