Skip to content

Commit a75cba2

Browse files
NetaNirCurtis Eppel
authored and
Curtis Eppel
committed
feat(core): warn if an aspect was added via another aspect (aws#8639)
### Updated PR Since we introduced [`stages`](aws#8423) which had the unintended side effect of the CDK not supporting adding an Aspect via another Aspect, we have not seen any issues reported beside aws#8536, that was resolved without requiring this capability. Given that, and the fact this features has many sharp edges we decided to leave it unsupported, and add a warning in the meantime. ----------- Following up on aws#8536 If an aspect is added via another aspect, the inner aspect will not be invoked. Take for example this code: ```typescript const app = new cdk.App(); app.node.applyAspect({ visit(construct: cdk.IConstruct) { construct.node.applyAspect({ visit(construct: cdk.IConstruct) { console.info("Invoking aspect on construct: " + construct.node.id); // This will not be called } }) } }); ``` Since aspects are added only on the top level node, if an aspect is added while `InvokeAspects` is called on that node, it will be ignored since it will not be added to list of aspects to invoke (`allAspectsHere` in the bellow code): ```typescript function invokeAspects(root: IConstruct) { recurse(root, []); function recurse(construct: IConstruct, inheritedAspects: constructs.IAspect[]) { // hackery to be able to access some private members with strong types (yack!) const node: NodeWithAspectPrivatesHangingOut = construct.node._actualNode as any; const allAspectsHere = [...inheritedAspects ?? [], ...node._aspects]; for (const aspect of allAspectsHere) { if (node.invokedAspects.includes(aspect)) { continue; } aspect.visit(construct); <-- an aspect that was added here will not be added to `allAspectsHere` and will be ignored node.invokedAspects.push(aspect); } for (const child of construct.node.children) { if (!Stage.isStage(child)) { recurse(child, allAspectsHere); } } } } ``` Assuming this is not something we want to support**, we can detect it by comparing the size of `node._aspects` before and after the call to `aspect.visit`, and emit a warning if there has been a change. Note that while the aspect will not be invoked it will be added to every child construct during the recursive visit. Emitting a warning for each child construct will result in a noisy console, to prevent this I have added a flag that will only allow adding **one warning per application**, given this limitation I'm not sure there is a lot of value in adding the warning, thoughts? If we decide to add it I will add tests. (** theoretically we could support it by adding the aspects to `allAspectsHere` during the loop, but this will require a non trivial implementation in order to avoid infinite recursion) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 4b9011f commit a75cba2

File tree

2 files changed

+55
-1
lines changed

2 files changed

+55
-1
lines changed

packages/@aws-cdk/core/lib/private/synthesis.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,25 @@ function synthNestedAssemblies(root: IConstruct, options: StageSynthesisOptions)
5858
* twice for the same construct.
5959
*/
6060
function invokeAspects(root: IConstruct) {
61+
let nestedAspectWarning = false;
6162
recurse(root, []);
6263

6364
function recurse(construct: IConstruct, inheritedAspects: constructs.IAspect[]) {
6465
// hackery to be able to access some private members with strong types (yack!)
6566
const node: NodeWithAspectPrivatesHangingOut = construct.node._actualNode as any;
6667

6768
const allAspectsHere = [...inheritedAspects ?? [], ...node._aspects];
68-
69+
const nodeAspectsCount = node._aspects.length;
6970
for (const aspect of allAspectsHere) {
7071
if (node.invokedAspects.includes(aspect)) { continue; }
72+
7173
aspect.visit(construct);
74+
// if an aspect was added to the node while invoking another aspect it will not be invoked, emit a warning
75+
// the `nestedAspectWarning` flag is used to prevent the warning from being emitted for every child
76+
if (!nestedAspectWarning && nodeAspectsCount !== node._aspects.length) {
77+
construct.node.addWarning('We detected an Aspect was added via another Aspect, and will not be applied');
78+
nestedAspectWarning = true;
79+
}
7280
node.invokedAspects.push(aspect);
7381
}
7482

packages/@aws-cdk/core/test/test.aspect.ts

+46
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
12
import { Test } from 'nodeunit';
23
import { App } from '../lib';
34
import { IAspect } from '../lib/aspect';
@@ -17,6 +18,13 @@ class VisitOnce implements IAspect {
1718
}
1819
}
1920
}
21+
22+
class MyAspect implements IAspect {
23+
public visit(node: IConstruct): void {
24+
node.node.addMetadata('foo', 'bar');
25+
}
26+
}
27+
2028
export = {
2129
'Aspects are invoked only once'(test: Test) {
2230
const app = new App();
@@ -28,4 +36,42 @@ export = {
2836
test.deepEqual(root.visitCounter, 1);
2937
test.done();
3038
},
39+
40+
'Warn if an Aspect is added via another Aspect'(test: Test) {
41+
const app = new App();
42+
const root = new MyConstruct(app, 'MyConstruct');
43+
const child = new MyConstruct(root, 'ChildConstruct');
44+
root.node.applyAspect({
45+
visit(construct: IConstruct) {
46+
construct.node.applyAspect({
47+
visit(inner: IConstruct) {
48+
inner.node.addMetadata('test', 'would-be-ignored');
49+
},
50+
});
51+
},
52+
});
53+
app.synth();
54+
test.deepEqual(root.node.metadata[0].type, cxschema.ArtifactMetadataEntryType.WARN);
55+
test.deepEqual(root.node.metadata[0].data, 'We detected an Aspect was added via another Aspect, and will not be applied');
56+
// warning is not added to child construct
57+
test.equal(child.node.metadata.length, 0);
58+
test.done();
59+
},
60+
61+
'Do not warn if an Aspect is added directly (not by another aspect)'(test: Test) {
62+
const app = new App();
63+
const root = new MyConstruct(app, 'Construct');
64+
const child = new MyConstruct(root, 'ChildConstruct');
65+
root.node.applyAspect(new MyAspect());
66+
app.synth();
67+
test.deepEqual(root.node.metadata[0].type, 'foo');
68+
test.deepEqual(root.node.metadata[0].data, 'bar');
69+
test.deepEqual(root.node.metadata[0].type, 'foo');
70+
test.deepEqual(child.node.metadata[0].data, 'bar');
71+
// no warning is added
72+
test.equal(root.node.metadata.length, 1);
73+
test.equal(child.node.metadata.length, 1);
74+
test.done();
75+
},
76+
3177
};

0 commit comments

Comments
 (0)