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

fix(jsii-diff): external structs returned from methods cannot be changed #2070

Merged
merged 3 commits into from
Oct 7, 2020

Conversation

rix0rrr
Copy link
Contributor

@rix0rrr rix0rrr commented Oct 2, 2020

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 (potentially recursively) 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.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

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.
@rix0rrr rix0rrr requested review from RomainMuller, njlynch, shivlaks and a team October 2, 2020 15:38
@rix0rrr rix0rrr self-assigned this Oct 2, 2020
@mergify mergify bot added the contribution/core This is a PR that came from AWS. label Oct 2, 2020
@rix0rrr rix0rrr changed the title fix(jsii-diff): allow modification of external structs fix(jsii-diff): external structs returned from methods cannot be changed Oct 2, 2020
@rix0rrr
Copy link
Contributor Author

rix0rrr commented Oct 2, 2020

Implementation-wise:

old what new
index.ts used to contain collection logic moved to type-comparison.ts, is now OO
classes-ifaces.ts used to contain rules logic moved to type-comparison.ts, is now OO
" and validation logic moved to validations.ts
enums.ts used to contain validation logic moved to validations.ts
type-analysis.ts used to do structural checking structural checking moved to type-comparison.ts for structs

The rules inheritance looks a bit complicated still (and is made worse by the short line lengths imposed by prettier), I'm open to splitting those out more if the audience finds it bothersome in it's current form.

Copy link
Contributor

@RomainMuller RomainMuller left a 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...

Comment on lines 362 to 383
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) =>
Copy link
Contributor

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).

Suggested change
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.

Copy link
Contributor Author

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.

packages/jsii-diff/test/util.ts Outdated Show resolved Hide resolved
Comment on lines +20 to +21
await expectNoError(original, updated);
return;
Copy link
Contributor

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):

Suggested change
await expectNoError(original, updated);
return;
return expectNoError(original, updated);

Copy link
Contributor Author

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 :).

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense!

@RomainMuller RomainMuller added this to the October Release milestone Oct 7, 2020
@mergify
Copy link
Contributor

mergify bot commented Oct 7, 2020

Thank you for contributing! ❤️ I will now look into making sure the PR is up-to-date, then proceed to try and merge it!

@mergify mergify bot added the pr/ready-to-merge This PR is ready to be merged. label Oct 7, 2020
@mergify
Copy link
Contributor

mergify bot commented Oct 7, 2020

Merging (with squash)...

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-Blkkw9bQFn8A
  • Commit ID: 6089cb9
  • Result: SUCCEEDED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@mergify mergify bot merged commit 11e9389 into master Oct 7, 2020
@mergify mergify bot deleted the huijbers/diff-refactor branch October 7, 2020 15:56
@mergify
Copy link
Contributor

mergify bot commented Oct 7, 2020

Merging (with squash)...

@mergify mergify bot removed the pr/ready-to-merge This PR is ready to be merged. label Oct 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
contribution/core This is a PR that came from AWS.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

jsii-diff: errors caused by L1 props types returned from render() methods
3 participants