Skip to content

Commit 332e85c

Browse files
authored
Merge branch 'main' into huijbers/skip-subprocess
2 parents 6f27688 + 7c1a2bc commit 332e85c

36 files changed

Lines changed: 3080 additions & 179 deletions

File tree

‎packages/@aws-cdk/cloud-assembly-api/lib/assembly-index.ts‎

Lines changed: 452 additions & 0 deletions
Large diffs are not rendered by default.

‎packages/@aws-cdk/cloud-assembly-api/lib/cloud-assembly.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ export class CloudAssembly implements ICloudAssembly {
9292
*/
9393
public readonly artifacts: CloudArtifact[];
9494

95+
/**
96+
* Modified time
97+
*/
98+
public readonly mtimeMs: number;
99+
95100
/**
96101
* Runtime information such as module versions used to synthesize this assembly.
97102
*/
@@ -115,11 +120,19 @@ export class CloudAssembly implements ICloudAssembly {
115120
this.directory = directory;
116121
this.loadOptions = loadOptions;
117122

118-
this.manifest = cxschema.Manifest.loadAssemblyManifest(path.join(directory, MANIFEST_FILE), this.loadOptions);
123+
const manifestFile = path.join(directory, MANIFEST_FILE);
124+
this.manifest = cxschema.Manifest.loadAssemblyManifest(manifestFile, this.loadOptions);
119125
this.version = this.manifest.version;
120126
this.artifacts = this.renderArtifacts(this.loadOptions?.topoSort ?? true);
121127
this.runtime = this.manifest.runtime || { libraries: { } };
122128

129+
try {
130+
this.mtimeMs = fs.statSync(manifestFile).mtimeMs;
131+
} catch (e) {
132+
// Unlikely to happen but the file could have disappeared due to a race condition since reading it above.
133+
this.mtimeMs = Date.now();
134+
}
135+
123136
Object.defineProperty(this, CLOUD_ASSEMBLY_SYMBOL, { value: true });
124137

125138
// force validation of deps by accessing 'depends' on all artifacts

‎packages/@aws-cdk/cloud-assembly-api/lib/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ export * from './stack-trace';
2525
export * from './validation/formatting';
2626
export * from './validation/parsing';
2727
export * from './validation/loading';
28+
export * from './assembly-index';
29+
export { type ConstructInfo } from './tree-json-types';

‎packages/@aws-cdk/cloud-assembly-api/lib/stack-trace.ts‎

Lines changed: 149 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ export class StackTrace {
4343
}
4444
return undefined;
4545
}
46+
47+
public findFirstUserFrame(): StackFrame | undefined {
48+
return this.findAndParse(DEFAULT_USER_CODE_FINDER);
49+
}
50+
51+
public renderJustMyCode(indent = true): string[] {
52+
return renderCallStackJustMyCode(this.frames.map(f => StackFrame.parse(f)), indent);
53+
}
4654
}
4755

4856
export class StackFrame {
@@ -99,32 +107,154 @@ export class StackFrame {
99107
fileName = m ? frame.slice(0, -m[0].length) : frame;
100108
sourceLocation = m ? m[0].slice(1) : '';
101109

102-
return {
110+
return new StackFrame({
103111
fileName,
104112
functionName,
105113
sourceLocation,
114+
});
115+
}
116+
117+
/**
118+
* The file name this call frame is in
119+
*/
120+
public readonly fileName: string;
121+
122+
/**
123+
* Name of the function this call frame is in
124+
*/
125+
public readonly functionName: string;
126+
127+
/**
128+
* The line and optionally column number this call frame is in
129+
*
130+
* Formatted as `<line> [':' <column>]`.
131+
*/
132+
public readonly sourceLocation: string;
133+
134+
constructor(props: {
135+
fileName: string;
136+
functionName: string;
137+
sourceLocation: string;
138+
}) {
139+
this.fileName = props.fileName;
140+
this.functionName = props.functionName;
141+
this.sourceLocation = props.sourceLocation;
142+
}
143+
144+
public get lineLocation() {
145+
if (!this.sourceLocation) {
146+
return undefined;
147+
}
148+
const parts = this.sourceLocation.split(':');
149+
const line = parseInt(parts[0], 10);
150+
const column = parseInt(parts[1], 10);
151+
return {
152+
line: isNaN(line) ? undefined : line,
153+
column: isNaN(column) ? undefined : column,
106154
};
107155
}
108156

109-
constructor(
110-
/**
111-
* Name of the function this call frame is in
112-
*/
113-
public readonly functionName: string,
114-
115-
/**
116-
* The file name this call frame is in
117-
*/
118-
public readonly fileName: string,
119-
120-
/**
121-
* The line and optionally column number this call frame is in
122-
*
123-
* Formatted as `<line> [':' <column>]`.
124-
*/
125-
public readonly sourceLocation: string,
126-
) {
157+
public get lineNumber(): number | undefined {
158+
return this.lineLocation?.line;
159+
}
160+
161+
public get columnNumber(): number | undefined {
162+
return this.lineLocation?.column;
163+
}
164+
}
165+
166+
// Look for `/node_modules/` followed by either
167+
// - An @ sign, and 2 path segments
168+
// - No @ sign, and 1 path segment
169+
const MODULE_RE = /(\/|\\)node_modules(\/|\\)(@[^/\\]+[/\\][^/\\]+|[^@][^/\\]*)/;
170+
171+
const DECORATOR_RE = /(\/|\\)(prop-injectable|no-box-stack-traces)\./;
172+
173+
/**
174+
* Renders an array of CallSites nicely, focusing on the user application code
175+
*
176+
* We detect "Not My Code" using the following heuristics:
177+
*
178+
* - If there is '/node_modules/' in the file path, we assume the call stack is a library and we skip it.
179+
* - If there is 'node:' in the file path, we assume it is NodeJS internals and we skip it.
180+
* - We skip (and hide) 'prop-injectable' or 'no-box-stack-traces' in the trace, because those are
181+
* constructor decorators that hide what's interesting (and look weird).
182+
*
183+
* If `indent` is enabled, we will prefix stack frames of actual files with " at ", just like
184+
* Node does for its stack frames.
185+
*/
186+
function renderCallStackJustMyCode(stack: StackFrame[], indent = true): string[] {
187+
const lines = [];
188+
let skipped = new Array<{ functionName?: string; fileName: string }>();
189+
190+
let sawMyCode = false;
191+
let i = 0;
192+
while (i < stack.length) {
193+
const frame = stack[i++];
194+
195+
if (frame.fileName.match(DECORATOR_RE)) {
196+
continue;
197+
}
198+
199+
const pat = frame.fileName.match(MODULE_RE);
200+
if (pat) {
201+
while (i < stack.length && stack[i].fileName.includes(pat[0])) {
202+
i++;
203+
}
204+
// The last stack frame has the function that user code call into.
205+
skip({ functionName: stack[i - 1].functionName, fileName: pat[3] });
206+
} else if (frame.fileName.includes('node:')) {
207+
skip({ fileName: 'node internals' });
208+
while (i < stack.length && stack[i].fileName.includes('node:')) {
209+
i++;
210+
}
211+
} else if (isHostInternalFrame(frame)) {
212+
skip({ fileName: 'jsii runtime' });
213+
while (i < stack.length && isHostInternalFrame(stack[i])) {
214+
i++;
215+
}
216+
} else {
217+
reportSkipped(true);
218+
const prefix = indent ? ' at ' : '';
219+
lines.push(`${prefix}${frame.functionName} (${frame.fileName}:${frame.sourceLocation})`);
220+
sawMyCode = true;
221+
}
222+
}
223+
reportSkipped(false);
224+
225+
if (!sawMyCode) {
226+
lines.push(`${indent ? ' ' : ''}(no user code in ${Error.stackTraceLimit} frames, use --stack-trace-limit to capture more)`);
227+
}
228+
229+
return lines;
230+
231+
function skip(what: typeof skipped[number]) {
232+
if (!skipped.find(x => x.fileName === what.fileName && x.functionName === what.functionName)) {
233+
skipped.push(what);
234+
}
235+
}
236+
237+
function reportSkipped(includeFunction: boolean) {
238+
if (skipped.length > 0) {
239+
const rendered = skipped.map((s, j) =>
240+
// Only render the function name of the last frame before user code
241+
j === skipped.length - 1 && s.functionName && includeFunction ?
242+
`${s.functionName} in ${s.fileName}` : s.fileName);
127243

244+
const prefix = indent ? ' ' : '';
245+
lines.push(`${prefix}...${rendered.join(', ')}...`);
246+
}
247+
skipped = [];
128248
}
129249
}
130250

251+
const JSII_HOST_DIR_NAME_SYM = Symbol.for('jsii.context.hostDirName');
252+
253+
/**
254+
* Whether the call site comes from the internals of the host that sent us the stack trace
255+
*/
256+
function isHostInternalFrame(frame: StackFrame): boolean {
257+
const hostDirName = (global as any)[JSII_HOST_DIR_NAME_SYM];
258+
return hostDirName && frame.fileName.includes(hostDirName);
259+
}
260+
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Serializable representation of a construct
3+
*/
4+
export interface Node<Child> {
5+
/**
6+
* The construct's ID
7+
*
8+
* Even though this ID is already in the `children` map of the containing node,
9+
* we repeat it here.
10+
*/
11+
readonly id: string;
12+
13+
/**
14+
* The construct's path
15+
*
16+
* Even though this path can be constructed from the construct IDs of constructs
17+
* on the root path to this construct, we still repeat it here.
18+
*
19+
* FIXME: In a sizeable file (tested on 136MB) this takes about 20% of the
20+
* total size without adding any value. We should probably remove this at some
21+
* point.
22+
*/
23+
readonly path: string;
24+
children?: { [key: string]: Child };
25+
attributes?: { [key: string]: unknown };
26+
27+
/**
28+
* Information on the construct class that led to this node, if available
29+
*/
30+
constructInfo?: ConstructInfo;
31+
}
32+
33+
export interface TreeFile {
34+
version: 'tree-0.1';
35+
tree: TreeFileNode;
36+
}
37+
38+
export interface ForestFile {
39+
version: 'forest-0.1';
40+
forest: Record<string, TreeFileNode>;
41+
}
42+
43+
export type TreeFileNode = Node<TreeFileNode> | SubTreeReference;
44+
45+
export type TreeNode = Node<TreeNode>;
46+
47+
/**
48+
* A reference to a node that is stored in an entirely different tree.json file
49+
*/
50+
export interface SubTreeReference {
51+
readonly id: string;
52+
readonly path: string;
53+
fileName: string;
54+
55+
/**
56+
* If set, indicates the subtree in the forest file
57+
*
58+
* If this is set then `fileName` must point to a ForestFile, and this indicates
59+
* the tree inside the forest.
60+
*/
61+
treeId: string;
62+
}
63+
64+
/**
65+
* Source information on a construct (class fqn and version)
66+
*/
67+
export interface ConstructInfo {
68+
// !!! Important !!!
69+
// This is the public contract of what shows up in tree.json
70+
// We are very unlikely to extend this in future
71+
// I you need additional details, add them on top of this.
72+
73+
/**
74+
* The FQN of the construct.
75+
*/
76+
readonly fqn: string;
77+
/**
78+
* The version of the package the construct belongs to.
79+
*/
80+
readonly version: string;
81+
}
82+
83+
export function isSubtreeReference(x: TreeFileNode): x is SubTreeReference {
84+
return (x as SubTreeReference).fileName !== undefined && !!(x as SubTreeReference).treeId;
85+
}

‎packages/@aws-cdk/cloud-assembly-api/lib/validation/loading.ts‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type * as cxschema from '@aws-cdk/cloud-assembly-schema';
55
import type { CloudFormationStackArtifact } from '../artifacts/cloudformation-artifact';
66
import { SynthesisMessageLevel } from '../metadata';
77
import { ANNOTATION_PLUGIN_NAME } from './parsing';
8+
import { StackTrace } from '../stack-trace';
89

910
const VALIDATION_REPORT_FILE = 'validation-report.json';
1011

@@ -131,14 +132,11 @@ function pluginReportFromAnnotations(stacks: CloudFormationStackArtifact[]): cxs
131132
violation.violatingConstructs.push({
132133
constructPath: entry.id.replace(/^\//, ''), // remove leading slash
133134

134-
// TODO: see if this information can be obtained from tree.json
135-
// cloudFormationResource
136-
// constructFqn:
137-
// libraryVersion
138-
139-
// TODO: see if we can get this from metadata stack traces. We may need to re-enable them for
140-
// annotations in the core library. Otherwise we should probably get a stack trace to the resource itself.
141-
// stackTraces
135+
// If we get this stack trace here, it was recorded by the `constructs` library,
136+
// which means it hasn't been JustMyCode-stripped yet.
137+
stackTraces: entry.entry.trace ? [
138+
new StackTrace(entry.entry.trace).renderJustMyCode().join('\n'),
139+
] : [],
142140
});
143141
}
144142
}

0 commit comments

Comments
 (0)