Skip to content

Commit aa52270

Browse files
Merge pull request #869 from BitGo/DX-660-introduce-stacktraces
refactor: introduce standardized error messages and stacktraces
2 parents 2731c48 + 60218da commit aa52270

File tree

11 files changed

+129
-84
lines changed

11 files changed

+129
-84
lines changed

packages/openapi-generator/src/apiSpec.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import { resolveLiteralOrIdentifier } from './resolveInit';
88
import { parseRoute, type Route } from './route';
99
import { SourceFile } from './sourceFile';
1010
import { OpenAPIV3 } from 'openapi-types';
11+
import { errorLeft } from './error';
1112

1213
export function parseApiSpec(
1314
project: Project,
1415
sourceFile: SourceFile,
1516
expr: swc.Expression,
1617
): E.Either<string, Route[]> {
1718
if (expr.type !== 'ObjectExpression') {
18-
return E.left(`unimplemented route expression type ${expr.type}`);
19+
return errorLeft(`unimplemented route expression type ${expr.type}`);
1920
}
2021

2122
const result: Route[] = [];
@@ -34,7 +35,7 @@ export function parseApiSpec(
3435
if (spreadExpr.type === 'CallExpression') {
3536
const arg = spreadExpr.arguments[0];
3637
if (arg === undefined) {
37-
return E.left(`unimplemented spread argument type ${arg}`);
38+
return errorLeft(`unimplemented spread argument type ${arg}`);
3839
}
3940
spreadExpr = arg.expression;
4041
}
@@ -47,7 +48,7 @@ export function parseApiSpec(
4748
}
4849

4950
if (apiAction.type !== 'KeyValueProperty') {
50-
return E.left(`unimplemented route property type ${apiAction.type}`);
51+
return errorLeft(`unimplemented route property type ${apiAction.type}`);
5152
}
5253
const routes = apiAction.value;
5354
const routesInitE = resolveLiteralOrIdentifier(project, sourceFile, routes);
@@ -56,11 +57,11 @@ export function parseApiSpec(
5657
}
5758
const [routesSource, routesInit] = routesInitE.right;
5859
if (routesInit.type !== 'ObjectExpression') {
59-
return E.left(`unimplemented routes type ${routes.type}`);
60+
return errorLeft(`unimplemented routes type ${routes.type}`);
6061
}
6162
for (const route of Object.values(routesInit.properties)) {
6263
if (route.type !== 'KeyValueProperty') {
63-
return E.left(`unimplemented route type ${route.type}`);
64+
return errorLeft(`unimplemented route type ${route.type}`);
6465
}
6566
const routeExpr = route.value;
6667
const routeInitE = resolveLiteralOrIdentifier(project, routesSource, routeExpr);

packages/openapi-generator/src/cli.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { KNOWN_IMPORTS } from './knownImports';
1818
import { findSymbolInitializer } from './resolveInit';
1919
import { parseCodecInitializer } from './codec';
2020
import { SourceFile } from './sourceFile';
21+
import { logError, logInfo, logWarn } from './error';
2122

2223
const app = command({
2324
name: 'api-ts',
@@ -87,7 +88,7 @@ const app = command({
8788
const codecFilePath = p.resolve(codecFile);
8889
const codecModule = await import(codecFilePath);
8990
if (codecModule.default === undefined) {
90-
console.error(`Could not find default export in ${codecFilePath}`);
91+
logError(`Could not find default export in ${codecFilePath}`);
9192
process.exit(1);
9293
}
9394
const customCodecs = codecModule.default(E);
@@ -96,13 +97,13 @@ const app = command({
9697

9798
const project = await new Project({}, knownImports).parseEntryPoint(filePath);
9899
if (E.isLeft(project)) {
99-
console.error(project.left);
100+
logError(`${project.left}`);
100101
process.exit(1);
101102
}
102103

103104
const entryPoint = project.right.get(filePath);
104105
if (entryPoint === undefined) {
105-
console.error(`Could not find entry point ${filePath}`);
106+
logError(`Could not find entry point ${filePath}`);
106107
process.exit(1);
107108
}
108109

@@ -119,22 +120,20 @@ const app = command({
119120
symbol.init.callee.type === 'Super' ||
120121
symbol.init.callee.type === 'Import'
121122
) {
122-
console.error(
123-
`Skipping ${symbol.name} because it is a ${symbol.init.callee.type}`,
124-
);
123+
logWarn(`Skipping ${symbol.name} because it is a ${symbol.init.callee.type}`);
125124
continue;
126125
} else if (!isApiSpec(entryPoint, symbol.init.callee)) {
127126
continue;
128127
}
129-
console.error(`Found API spec in ${symbol.name}`);
128+
logInfo(`[INFO] Found API spec in ${symbol.name}`);
130129

131130
const result = parseApiSpec(
132131
project.right,
133132
entryPoint,
134133
symbol.init.arguments[0]!.expression,
135134
);
136135
if (E.isLeft(result)) {
137-
console.error(`Error parsing ${symbol.name}: ${result.left}`);
136+
logError(`Error when parsing ${symbol.name}: ${result.left}`);
138137
process.exit(1);
139138
}
140139

@@ -145,7 +144,7 @@ const app = command({
145144
apiSpec.push(...result.right);
146145
}
147146
if (apiSpec.length === 0) {
148-
console.error(`Could not find API spec in ${filePath}`);
147+
logError(`Could not find API spec in ${filePath}`);
149148
process.exit(1);
150149
}
151150

@@ -166,14 +165,14 @@ const app = command({
166165
}
167166
const sourceFile = project.right.get(ref.location);
168167
if (sourceFile === undefined) {
169-
console.error(`Could not find '${ref.name}' from '${ref.location}'`);
168+
logError(`Could not find '${ref.name}' from '${ref.location}'`);
170169
process.exit(1);
171170
}
172171

173172
const initE = findSymbolInitializer(project.right, sourceFile, ref.name);
174173
if (E.isLeft(initE)) {
175174
console.error(
176-
`Could not find symbol '${ref.name}' in '${ref.location}': ${initE.left}`,
175+
`[ERROR] Could not find symbol '${ref.name}' in '${ref.location}': ${initE.left}`,
177176
);
178177
process.exit(1);
179178
}
@@ -182,7 +181,7 @@ const app = command({
182181
const codecE = parseCodecInitializer(project.right, newSourceFile, init);
183182
if (E.isLeft(codecE)) {
184183
console.error(
185-
`Could not parse codec '${ref.name}' in '${ref.location}': ${codecE.left}`,
184+
`[ERROR] Could not parse codec '${ref.name}' in '${ref.location}': ${codecE.left}`,
186185
);
187186
process.exit(1);
188187
}

packages/openapi-generator/src/codec.ts

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { findSymbolInitializer } from './resolveInit';
1010
import type { SourceFile } from './sourceFile';
1111

1212
import type { KnownCodec } from './knownImports';
13+
import { errorLeft } from './error';
1314

1415
type ResolvedIdentifier = Schema | { type: 'codec'; schema: KnownCodec };
1516

@@ -26,9 +27,9 @@ function codecIdentifier(
2627

2728
const imp = source.symbols.imports.find((s) => s.localName === id.value);
2829
if (imp === undefined) {
29-
return E.left(`Unknown identifier ${id.value}`);
30+
return errorLeft(`Unknown identifier ${id.value}`);
3031
} else if (imp.type === 'star') {
31-
return E.left(`Tried to use star import as codec ${id.value}`);
32+
return errorLeft(`Tried to use star import as codec ${id.value}`);
3233
}
3334
const knownImport = project.resolveKnownImport(imp.from, imp.importedName);
3435
if (knownImport !== undefined) {
@@ -54,10 +55,12 @@ function codecIdentifier(
5455
const object = id.object;
5556
if (object.type !== 'Identifier') {
5657
if (object.type === 'MemberExpression')
57-
return E.left(
58-
`Object ${((object as swc.MemberExpression) && { value: String }).value} is deeply nested, which is unsupported`,
58+
return errorLeft(
59+
`Object ${
60+
((object as swc.MemberExpression) && { value: String }).value
61+
} is deeply nested, which is unsupported`,
5962
);
60-
return E.left(`Unimplemented object type ${object.type}`);
63+
return errorLeft(`Unimplemented object type ${object.type}`);
6164
}
6265

6366
// Parse member expressions that come from `* as foo` imports
@@ -66,7 +69,7 @@ function codecIdentifier(
6669
);
6770
if (starImportSym !== undefined) {
6871
if (id.property.type !== 'Identifier') {
69-
return E.left(`Unimplemented property type ${id.property.type}`);
72+
return errorLeft(`Unimplemented property type ${id.property.type}`);
7073
}
7174

7275
const name = id.property.value;
@@ -96,7 +99,7 @@ function codecIdentifier(
9699
);
97100
if (objectImportSym !== undefined) {
98101
if (id.property.type !== 'Identifier') {
99-
return E.left(`Unimplemented property type ${id.property.type}`);
102+
return errorLeft(`Unimplemented property type ${id.property.type}`);
100103
}
101104
const name = id.property.value;
102105

@@ -113,9 +116,9 @@ function codecIdentifier(
113116
if (E.isLeft(objectSchemaE)) {
114117
return objectSchemaE;
115118
} else if (objectSchemaE.right.type !== 'object') {
116-
return E.left(`Expected object, got '${objectSchemaE.right.type}'`);
119+
return errorLeft(`Expected object, got '${objectSchemaE.right.type}'`);
117120
} else if (objectSchemaE.right.properties[name] === undefined) {
118-
return E.left(
121+
return errorLeft(
119122
`Unknown property '${name}' in '${objectImportSym.localName}' from '${objectImportSym.from}'`,
120123
);
121124
} else {
@@ -124,7 +127,7 @@ function codecIdentifier(
124127
}
125128

126129
if (id.property.type !== 'Identifier') {
127-
return E.left(`Unimplemented property type ${id.property.type}`);
130+
return errorLeft(`Unimplemented property type ${id.property.type}`);
128131
}
129132

130133
// Parse locally declared member expressions
@@ -136,11 +139,11 @@ function codecIdentifier(
136139
if (E.isLeft(schemaE)) {
137140
return schemaE;
138141
} else if (schemaE.right.type !== 'object') {
139-
return E.left(
142+
return errorLeft(
140143
`Expected object, got '${schemaE.right.type}' for '${declarationSym.name}'`,
141144
);
142145
} else if (schemaE.right.properties[id.property.value] === undefined) {
143-
return E.left(
146+
return errorLeft(
144147
`Unknown property '${id.property.value}' in '${declarationSym.name}'`,
145148
);
146149
} else {
@@ -158,7 +161,7 @@ function codecIdentifier(
158161
}
159162
}
160163

161-
return E.left(`Unimplemented identifier type ${id.type}`);
164+
return errorLeft(`Unimplemented identifier type ${id.type}`);
162165
}
163166

164167
function parseObjectExpression(
@@ -210,19 +213,19 @@ function parseObjectExpression(
210213
schema = schemaE.right;
211214
}
212215
if (schema.type !== 'object') {
213-
return E.left(`Spread element must be object`);
216+
return errorLeft(`Spread element must be object`);
214217
}
215218
Object.assign(result.properties, schema.properties);
216219
result.required.push(...schema.required);
217220
continue;
218221
} else if (property.type !== 'KeyValueProperty') {
219-
return E.left(`Unimplemented property type ${property.type}`);
222+
return errorLeft(`Unimplemented property type ${property.type}`);
220223
} else if (
221224
property.key.type !== 'Identifier' &&
222225
property.key.type !== 'StringLiteral' &&
223226
property.key.type !== 'NumericLiteral'
224227
) {
225-
return E.left(`Unimplemented property key type ${property.key.type}`);
228+
return errorLeft(`Unimplemented property key type ${property.key.type}`);
226229
}
227230
const commentEndIdx = property.key.span.start;
228231
const comments = leadingComment(
@@ -254,7 +257,7 @@ function parseArrayExpression(
254257
const result: Schema[] = [];
255258
for (const element of array.elements) {
256259
if (element === undefined) {
257-
return E.left('Undefined array element');
260+
return errorLeft('Undefined array element');
258261
}
259262
const valueE = parsePlainInitializer(project, source, element.expression);
260263
if (E.isLeft(valueE)) {
@@ -279,7 +282,7 @@ function parseArrayExpression(
279282
init = schemaE.right;
280283
}
281284
if (init.type !== 'tuple') {
282-
return E.left('Spread element must be array literal');
285+
return errorLeft('Spread element must be array literal');
283286
}
284287
result.push(...init.schemas);
285288
} else {
@@ -342,7 +345,7 @@ export function parseCodecInitializer(
342345
} else if (init.type === 'CallExpression') {
343346
const callee = init.callee;
344347
if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') {
345-
return E.left(`Unimplemented callee type ${init.callee.type}`);
348+
return errorLeft(`Unimplemented callee type ${init.callee.type}`);
346349
}
347350
const identifierE = codecIdentifier(project, source, callee);
348351
if (E.isLeft(identifierE)) {
@@ -364,10 +367,10 @@ export function parseCodecInitializer(
364367
// schema.location might be a package name -> need to resolve the path from the project types
365368
const path = project.getTypes()[schema.name];
366369
if (path === undefined)
367-
return E.left(`Cannot find module '${schema.location}' in the project`);
370+
return errorLeft(`Cannot find module '${schema.location}' in the project`);
368371
refSource = project.get(path);
369372
if (refSource === undefined) {
370-
return E.left(`Cannot find '${schema.name}' from '${schema.location}'`);
373+
return errorLeft(`Cannot find '${schema.name}' from '${schema.location}'`);
371374
}
372375
}
373376
const initE = findSymbolInitializer(project, refSource, schema.name);
@@ -394,6 +397,6 @@ export function parseCodecInitializer(
394397
E.chain((args) => identifier.schema(deref, ...args)),
395398
);
396399
} else {
397-
return E.left(`Unimplemented initializer type ${init.type}`);
400+
return errorLeft(`Unimplemented initializer type ${init.type}`);
398401
}
399402
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as E from 'fp-ts/Either';
2+
3+
/**
4+
* A wrapper around `E.left` that includes a stacktrace.
5+
* @param message the error message
6+
* @returns an `E.left` with the error message and a stacktrace
7+
*/
8+
export function errorLeft(message: string): E.Either<string, never> {
9+
const stacktrace = new Error().stack!.split('\n').slice(2).join('\n');
10+
const messageWithStacktrace = message + '\n' + stacktrace;
11+
12+
return E.left(messageWithStacktrace);
13+
}
14+
15+
/**
16+
* Testing utility to strip the stacktrace from errors.
17+
* @param errors the list of errors to strip
18+
* @returns the errors without the stacktrace
19+
*/
20+
export function stripStacktraceOfErrors(errors: string[]) {
21+
return errors.map((e) => e!.split('\n')[0]);
22+
}
23+
24+
// helper functions for logging
25+
export function logError(message: string): void {
26+
console.error(`[ERROR] ${message}`);
27+
}
28+
29+
export function logWarn(message: string): void {
30+
console.error(`[WARN] ${message}`);
31+
}
32+
33+
export function logInfo(message: string): void {
34+
console.error(`[INFO] ${message}`);
35+
}

0 commit comments

Comments
 (0)