-
Notifications
You must be signed in to change notification settings - Fork 4k
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
feat(core): present reason for cyclic references #2061
Conversation
To help people debugging cyclic references, we now trace the "reason" a cyclic reference got added, so that we can present the conflicting references instead of just presenting an error. Fix the order of the error message, which was the wrong way around. Clean up references a little: - Split out `Reference` and `CfnReference`, which got conflated in an undesirable way. `Reference` is now officially the base class for all references, and `CfnReference` is only one implementation of it for CloudFormation references. - Make 'scope' required for references, the only place where it was potentially empty was for CFN Pseudo Parameters, refactored those to not use classes anymore (because there's no need to). - Get rid of 'Ref', the class wasn't being very useful. - Make a dependency Construct => Stack lazy (it was conflicting at load time with the Stack => Construct dependency which is more important).
* Check whether this is actually a Reference | ||
*/ | ||
public static isCfnReferenceToken(x: Token): x is CfnReference { | ||
return (x as any).consumeFromStack !== 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.
I think you could instead use a private symbol to make this 100% reliable:
// don't export
const isReference = Symbol.for('CfnReference');
export class CfnReference extends Reference {
// private shouldn't bother JSII
private [isReference] = true;
public static isCfnReferenceToken(x: Token): x is CfnReference {
return (x as any)[isReference] === true;
}
}
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.
private
doesn't bother JSII but it does bother TypeScript, which complains about unused private members.
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 am also not 100% sure that a private symbol will work across two different versions of this module (which is the reason we can't use instanceof
).
My favorite (and very ugly still) way to do this is to add the marker at runtime:
const MARKER_KEY = '$type-marker';
const MARKER = '<uuid>';
export class MyClass {
public static isMyClass(obj: any): obj is MyClass {
return obj[MARKER_KEY] === MARKER;
}
constructor() {
Object.defineProperty(this, MARKER_KEY, {
value: MARKER,
enumerable: false
});
}
}
Yeah, horrible. TypeScript, can you make this easier?
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.
It will work across two different versions because Symbol.for('key')
stores the symbol in the global symbol registry by name. It would not work if you were to just call Symbol()
, which is unique for every call. By private, I mean hiding it from JSII.
Your UUID approach is an emulation of the global Symbol registry which was designed to solve this problem.
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.
We can use this to avoid some of the ugly duck typing we do - mark up the core constructs with symbols and maybe even generate some for L1. Module augmentation may even allow us to generate for our L2 later.
Create a symbol for each type that would be checked by name in a nominal type system.
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.
Yes, Symbol.for()
will fix it. Although I do wonder what the point of making it a Symbol is, then, since we lose the uniqueness value of it. I guess that it can't conflict with a naive string.
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.
On top of avoiding accidental conflicts, Symbols are not considered properties so it won't show up when enumerating an object's keys. Basically, it's cleaner and simpler.
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.
Lots of goodness!
* Check whether this is actually a Reference | ||
*/ | ||
public static isCfnReferenceToken(x: Token): x is CfnReference { | ||
return (x as any).consumeFromStack !== 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.
I am also not 100% sure that a private symbol will work across two different versions of this module (which is the reason we can't use instanceof
).
My favorite (and very ugly still) way to do this is to add the marker at runtime:
const MARKER_KEY = '$type-marker';
const MARKER = '<uuid>';
export class MyClass {
public static isMyClass(obj: any): obj is MyClass {
return obj[MARKER_KEY] === MARKER;
}
constructor() {
Object.defineProperty(this, MARKER_KEY, {
value: MARKER,
enumerable: false
});
}
}
Yeah, horrible. TypeScript, can you make this easier?
import { Token } from './token'; | ||
|
||
const AWS_ACCOUNTID = 'AWS::AccountId'; |
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 are these consts needed? Aren't they used exactly once?
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.
Once for the scoped, once for the unscoped variant.
* This magic happens in the prepare() phase, where consuming stacks will call | ||
* `consumeFromStack` on these Tokens and if they happen to be exported by a different | ||
* Stack, we'll register the dependency. | ||
* References are recorded. | ||
*/ | ||
export class Reference extends Token { | ||
/** | ||
* Check whether this is actually a Reference | ||
*/ | ||
public static isReferenceToken(x: Token): x is Reference { |
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.
isReference
?
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 all for it, but the previous implementor (you?) picked this name.
if (displayName && scope) { | ||
displayName = `${scope.node.path}.${displayName}`; | ||
} | ||
constructor(value: any, displayName: string, target: Construct) { |
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 still feel we could automatically let Reference
tokens know that they are being used instead of explicit consumeFromStack
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.
To me, that's the essence of a reference token. Then CfnReference
utilizes this capability to do the cross-stack magic, but the behavior is polymorphic. Otherwise, what's the added value in actually modeling the concept of a reference?
To help people debugging cyclic references, we now trace the "reason"
a cyclic reference got added, so that we can present the conflicting
references instead of just presenting an error. Fix the order of
the error message, which was the wrong way around.
Clean up references a little:
Reference
andCfnReference
, which got conflated in anundesirable way.
Reference
is now officially the base class for allreferences, and
CfnReference
is only one implementation of it forCloudFormation references.
potentially empty was for CFN Pseudo Parameters, refactored those to
not use classes anymore (because there's no need to).
load time with the Stack => Construct dependency which is more
important).
Pull Request Checklist
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license.