Skip to content

Commit f6078ef

Browse files
authored
fix(rosetta): fails on "Debug Failure" (#2917)
In cases where a literate source file was missing, the substitution value was not valid TypeScript, which could cause the comipler to fail on an opaque error (`Debug Failure`). This falls back to the visible code for the snippet instead of inserting a placeholder, and also makes sure to rescue the `Debug Failure` where it is emitted from, that is in the call to `program.getDeclarationDiagnostics`. Both of this changes result in greater chances of transliteration success. Related to cdklabs/jsii-docgen#369 --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
1 parent 9d554ed commit f6078ef

File tree

5 files changed

+239
-58
lines changed

5 files changed

+239
-58
lines changed

packages/jsii-rosetta/lib/commands/transliterate.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,17 @@ type Mutable<T> = { -readonly [K in keyof T]: Mutable<T[K]> };
137137
type AssemblyLoader = () => Promise<Mutable<Assembly>>;
138138

139139
function prefixDisclaimer(translation: Translation): string {
140-
const message = translation.didCompile
141-
? 'Example automatically generated. See https://github.com/aws/jsii/issues/826'
142-
: 'Example automatically generated without compilation. See https://github.com/aws/jsii/issues/826';
143-
return `${commentToken()} ${message}\n${translation.source}`;
140+
const comment = commentToken();
141+
const disclaimer = translation.didCompile
142+
? 'This example was automatically transliterated.'
143+
: 'This example was automatically transliterated with incomplete type information. It may not work as-is.';
144+
145+
return [
146+
`${comment} ${disclaimer}`,
147+
`${comment} See https://github.com/aws/jsii/issues/826 for more information.`,
148+
'',
149+
translation.source,
150+
].join('\n');
144151

145152
function commentToken() {
146153
// This is future-proofed a bit, but don't read too much in this...

packages/jsii-rosetta/lib/fixtures.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ export function fixturize(
2828
if (literateSource) {
2929
// Compatibility with the "old school" example inclusion mechanism.
3030
// Completely load this file and attach a parameter with its directory.
31-
source = loadLiterateSource(directory, literateSource, loose);
31+
try {
32+
source = loadLiterateSource(directory, literateSource);
33+
} catch (ex) {
34+
// In loose mode, we ignore this failure and stick to the visible source.
35+
if (!loose) {
36+
throw ex;
37+
}
38+
}
3239
parameters[SnippetParameters.$COMPILATION_DIRECTORY] = path.join(
3340
directory,
3441
path.dirname(literateSource),
@@ -48,22 +55,10 @@ export function fixturize(
4855
};
4956
}
5057

51-
function loadLiterateSource(
52-
directory: string,
53-
literateFileName: string,
54-
loose = false,
55-
) {
58+
function loadLiterateSource(directory: string, literateFileName: string) {
5659
const fullPath = path.join(directory, literateFileName);
5760
const exists = fs.existsSync(fullPath);
5861
if (!exists) {
59-
if (loose) {
60-
// In loose mode, we'll fall back to the `.js` file if it exists...
61-
const jsFile = fullPath.replace(/\.ts(x?)$/, '.js$1');
62-
if (fs.existsSync(jsFile)) {
63-
return fs.readFileSync(jsFile, { encoding: 'utf-8' });
64-
}
65-
return `Missing literate source file ${literateFileName}`;
66-
}
6762
// This couldn't really happen in practice, but do the check anyway
6863
throw new Error(
6964
`Sample uses literate source ${literateFileName}, but not found: ${fullPath}`,

packages/jsii-rosetta/lib/translate.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,17 +149,42 @@ export class SnippetTranslator {
149149
if (options.includeCompilerDiagnostics || snippet.strict) {
150150
const program = this.compilation.program;
151151
const diagnostics = [
152-
...program.getGlobalDiagnostics(),
153-
...program.getSyntacticDiagnostics(this.compilation.rootFile),
154-
...program.getDeclarationDiagnostics(this.compilation.rootFile),
155-
...program.getSemanticDiagnostics(this.compilation.rootFile),
152+
...neverThrowing(program.getGlobalDiagnostics)(),
153+
...neverThrowing(program.getSyntacticDiagnostics)(
154+
this.compilation.rootFile,
155+
),
156+
...neverThrowing(program.getDeclarationDiagnostics)(
157+
this.compilation.rootFile,
158+
),
159+
...neverThrowing(program.getSemanticDiagnostics)(
160+
this.compilation.rootFile,
161+
),
156162
];
157163
if (snippet.strict) {
158164
// In a strict assembly, so we'll need to brand all diagnostics here...
159165
diagnostics.forEach(annotateStrictDiagnostic);
160166
}
161167
this.compileDiagnostics.push(...diagnostics);
162168
}
169+
170+
/**
171+
* Intercepts all exceptions thrown by the wrapped call, and logs them to
172+
* console.error instead of re-throwing, then returns an empty array. This
173+
* is here to avoid compiler crashes due to broken code examples that cause
174+
* the TypeScript compiler to hit a "Debug Failure".
175+
*/
176+
function neverThrowing<A extends unknown[], R>(
177+
call: (...args: A) => readonly R[],
178+
): (...args: A) => readonly R[] {
179+
return (...args: A) => {
180+
try {
181+
return call(...args);
182+
} catch (err) {
183+
console.error(`Failed to execute ${call.name}: ${err}`);
184+
return [];
185+
}
186+
};
187+
}
163188
}
164189

165190
public renderUsing(visitor: AstHandler<any>) {

0 commit comments

Comments
 (0)