Skip to content

Commit

Permalink
Merge pull request #869 from BitGo/DX-660-introduce-stacktraces
Browse files Browse the repository at this point in the history
refactor: introduce standardized error messages and stacktraces
  • Loading branch information
anshchaturvedi committed Aug 8, 2024
2 parents 2731c48 + 60218da commit aa52270
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 84 deletions.
11 changes: 6 additions & 5 deletions packages/openapi-generator/src/apiSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import { resolveLiteralOrIdentifier } from './resolveInit';
import { parseRoute, type Route } from './route';
import { SourceFile } from './sourceFile';
import { OpenAPIV3 } from 'openapi-types';
import { errorLeft } from './error';

export function parseApiSpec(
project: Project,
sourceFile: SourceFile,
expr: swc.Expression,
): E.Either<string, Route[]> {
if (expr.type !== 'ObjectExpression') {
return E.left(`unimplemented route expression type ${expr.type}`);
return errorLeft(`unimplemented route expression type ${expr.type}`);
}

const result: Route[] = [];
Expand All @@ -34,7 +35,7 @@ export function parseApiSpec(
if (spreadExpr.type === 'CallExpression') {
const arg = spreadExpr.arguments[0];
if (arg === undefined) {
return E.left(`unimplemented spread argument type ${arg}`);
return errorLeft(`unimplemented spread argument type ${arg}`);
}
spreadExpr = arg.expression;
}
Expand All @@ -47,7 +48,7 @@ export function parseApiSpec(
}

if (apiAction.type !== 'KeyValueProperty') {
return E.left(`unimplemented route property type ${apiAction.type}`);
return errorLeft(`unimplemented route property type ${apiAction.type}`);
}
const routes = apiAction.value;
const routesInitE = resolveLiteralOrIdentifier(project, sourceFile, routes);
Expand All @@ -56,11 +57,11 @@ export function parseApiSpec(
}
const [routesSource, routesInit] = routesInitE.right;
if (routesInit.type !== 'ObjectExpression') {
return E.left(`unimplemented routes type ${routes.type}`);
return errorLeft(`unimplemented routes type ${routes.type}`);
}
for (const route of Object.values(routesInit.properties)) {
if (route.type !== 'KeyValueProperty') {
return E.left(`unimplemented route type ${route.type}`);
return errorLeft(`unimplemented route type ${route.type}`);
}
const routeExpr = route.value;
const routeInitE = resolveLiteralOrIdentifier(project, routesSource, routeExpr);
Expand Down
23 changes: 11 additions & 12 deletions packages/openapi-generator/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { KNOWN_IMPORTS } from './knownImports';
import { findSymbolInitializer } from './resolveInit';
import { parseCodecInitializer } from './codec';
import { SourceFile } from './sourceFile';
import { logError, logInfo, logWarn } from './error';

const app = command({
name: 'api-ts',
Expand Down Expand Up @@ -87,7 +88,7 @@ const app = command({
const codecFilePath = p.resolve(codecFile);
const codecModule = await import(codecFilePath);
if (codecModule.default === undefined) {
console.error(`Could not find default export in ${codecFilePath}`);
logError(`Could not find default export in ${codecFilePath}`);
process.exit(1);
}
const customCodecs = codecModule.default(E);
Expand All @@ -96,13 +97,13 @@ const app = command({

const project = await new Project({}, knownImports).parseEntryPoint(filePath);
if (E.isLeft(project)) {
console.error(project.left);
logError(`${project.left}`);
process.exit(1);
}

const entryPoint = project.right.get(filePath);
if (entryPoint === undefined) {
console.error(`Could not find entry point ${filePath}`);
logError(`Could not find entry point ${filePath}`);
process.exit(1);
}

Expand All @@ -119,22 +120,20 @@ const app = command({
symbol.init.callee.type === 'Super' ||
symbol.init.callee.type === 'Import'
) {
console.error(
`Skipping ${symbol.name} because it is a ${symbol.init.callee.type}`,
);
logWarn(`Skipping ${symbol.name} because it is a ${symbol.init.callee.type}`);
continue;
} else if (!isApiSpec(entryPoint, symbol.init.callee)) {
continue;
}
console.error(`Found API spec in ${symbol.name}`);
logInfo(`[INFO] Found API spec in ${symbol.name}`);

const result = parseApiSpec(
project.right,
entryPoint,
symbol.init.arguments[0]!.expression,
);
if (E.isLeft(result)) {
console.error(`Error parsing ${symbol.name}: ${result.left}`);
logError(`Error when parsing ${symbol.name}: ${result.left}`);
process.exit(1);
}

Expand All @@ -145,7 +144,7 @@ const app = command({
apiSpec.push(...result.right);
}
if (apiSpec.length === 0) {
console.error(`Could not find API spec in ${filePath}`);
logError(`Could not find API spec in ${filePath}`);
process.exit(1);
}

Expand All @@ -166,14 +165,14 @@ const app = command({
}
const sourceFile = project.right.get(ref.location);
if (sourceFile === undefined) {
console.error(`Could not find '${ref.name}' from '${ref.location}'`);
logError(`Could not find '${ref.name}' from '${ref.location}'`);
process.exit(1);
}

const initE = findSymbolInitializer(project.right, sourceFile, ref.name);
if (E.isLeft(initE)) {
console.error(
`Could not find symbol '${ref.name}' in '${ref.location}': ${initE.left}`,
`[ERROR] Could not find symbol '${ref.name}' in '${ref.location}': ${initE.left}`,
);
process.exit(1);
}
Expand All @@ -182,7 +181,7 @@ const app = command({
const codecE = parseCodecInitializer(project.right, newSourceFile, init);
if (E.isLeft(codecE)) {
console.error(
`Could not parse codec '${ref.name}' in '${ref.location}': ${codecE.left}`,
`[ERROR] Could not parse codec '${ref.name}' in '${ref.location}': ${codecE.left}`,
);
process.exit(1);
}
Expand Down
47 changes: 25 additions & 22 deletions packages/openapi-generator/src/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { findSymbolInitializer } from './resolveInit';
import type { SourceFile } from './sourceFile';

import type { KnownCodec } from './knownImports';
import { errorLeft } from './error';

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

Expand All @@ -26,9 +27,9 @@ function codecIdentifier(

const imp = source.symbols.imports.find((s) => s.localName === id.value);
if (imp === undefined) {
return E.left(`Unknown identifier ${id.value}`);
return errorLeft(`Unknown identifier ${id.value}`);
} else if (imp.type === 'star') {
return E.left(`Tried to use star import as codec ${id.value}`);
return errorLeft(`Tried to use star import as codec ${id.value}`);
}
const knownImport = project.resolveKnownImport(imp.from, imp.importedName);
if (knownImport !== undefined) {
Expand All @@ -54,10 +55,12 @@ function codecIdentifier(
const object = id.object;
if (object.type !== 'Identifier') {
if (object.type === 'MemberExpression')
return E.left(
`Object ${((object as swc.MemberExpression) && { value: String }).value} is deeply nested, which is unsupported`,
return errorLeft(
`Object ${
((object as swc.MemberExpression) && { value: String }).value
} is deeply nested, which is unsupported`,
);
return E.left(`Unimplemented object type ${object.type}`);
return errorLeft(`Unimplemented object type ${object.type}`);
}

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

const name = id.property.value;
Expand Down Expand Up @@ -96,7 +99,7 @@ function codecIdentifier(
);
if (objectImportSym !== undefined) {
if (id.property.type !== 'Identifier') {
return E.left(`Unimplemented property type ${id.property.type}`);
return errorLeft(`Unimplemented property type ${id.property.type}`);
}
const name = id.property.value;

Expand All @@ -113,9 +116,9 @@ function codecIdentifier(
if (E.isLeft(objectSchemaE)) {
return objectSchemaE;
} else if (objectSchemaE.right.type !== 'object') {
return E.left(`Expected object, got '${objectSchemaE.right.type}'`);
return errorLeft(`Expected object, got '${objectSchemaE.right.type}'`);
} else if (objectSchemaE.right.properties[name] === undefined) {
return E.left(
return errorLeft(
`Unknown property '${name}' in '${objectImportSym.localName}' from '${objectImportSym.from}'`,
);
} else {
Expand All @@ -124,7 +127,7 @@ function codecIdentifier(
}

if (id.property.type !== 'Identifier') {
return E.left(`Unimplemented property type ${id.property.type}`);
return errorLeft(`Unimplemented property type ${id.property.type}`);
}

// Parse locally declared member expressions
Expand All @@ -136,11 +139,11 @@ function codecIdentifier(
if (E.isLeft(schemaE)) {
return schemaE;
} else if (schemaE.right.type !== 'object') {
return E.left(
return errorLeft(
`Expected object, got '${schemaE.right.type}' for '${declarationSym.name}'`,
);
} else if (schemaE.right.properties[id.property.value] === undefined) {
return E.left(
return errorLeft(
`Unknown property '${id.property.value}' in '${declarationSym.name}'`,
);
} else {
Expand All @@ -158,7 +161,7 @@ function codecIdentifier(
}
}

return E.left(`Unimplemented identifier type ${id.type}`);
return errorLeft(`Unimplemented identifier type ${id.type}`);
}

function parseObjectExpression(
Expand Down Expand Up @@ -210,19 +213,19 @@ function parseObjectExpression(
schema = schemaE.right;
}
if (schema.type !== 'object') {
return E.left(`Spread element must be object`);
return errorLeft(`Spread element must be object`);
}
Object.assign(result.properties, schema.properties);
result.required.push(...schema.required);
continue;
} else if (property.type !== 'KeyValueProperty') {
return E.left(`Unimplemented property type ${property.type}`);
return errorLeft(`Unimplemented property type ${property.type}`);
} else if (
property.key.type !== 'Identifier' &&
property.key.type !== 'StringLiteral' &&
property.key.type !== 'NumericLiteral'
) {
return E.left(`Unimplemented property key type ${property.key.type}`);
return errorLeft(`Unimplemented property key type ${property.key.type}`);
}
const commentEndIdx = property.key.span.start;
const comments = leadingComment(
Expand Down Expand Up @@ -254,7 +257,7 @@ function parseArrayExpression(
const result: Schema[] = [];
for (const element of array.elements) {
if (element === undefined) {
return E.left('Undefined array element');
return errorLeft('Undefined array element');
}
const valueE = parsePlainInitializer(project, source, element.expression);
if (E.isLeft(valueE)) {
Expand All @@ -279,7 +282,7 @@ function parseArrayExpression(
init = schemaE.right;
}
if (init.type !== 'tuple') {
return E.left('Spread element must be array literal');
return errorLeft('Spread element must be array literal');
}
result.push(...init.schemas);
} else {
Expand Down Expand Up @@ -342,7 +345,7 @@ export function parseCodecInitializer(
} else if (init.type === 'CallExpression') {
const callee = init.callee;
if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') {
return E.left(`Unimplemented callee type ${init.callee.type}`);
return errorLeft(`Unimplemented callee type ${init.callee.type}`);
}
const identifierE = codecIdentifier(project, source, callee);
if (E.isLeft(identifierE)) {
Expand All @@ -364,10 +367,10 @@ export function parseCodecInitializer(
// schema.location might be a package name -> need to resolve the path from the project types
const path = project.getTypes()[schema.name];
if (path === undefined)
return E.left(`Cannot find module '${schema.location}' in the project`);
return errorLeft(`Cannot find module '${schema.location}' in the project`);
refSource = project.get(path);
if (refSource === undefined) {
return E.left(`Cannot find '${schema.name}' from '${schema.location}'`);
return errorLeft(`Cannot find '${schema.name}' from '${schema.location}'`);
}
}
const initE = findSymbolInitializer(project, refSource, schema.name);
Expand All @@ -394,6 +397,6 @@ export function parseCodecInitializer(
E.chain((args) => identifier.schema(deref, ...args)),
);
} else {
return E.left(`Unimplemented initializer type ${init.type}`);
return errorLeft(`Unimplemented initializer type ${init.type}`);
}
}
35 changes: 35 additions & 0 deletions packages/openapi-generator/src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as E from 'fp-ts/Either';

/**
* A wrapper around `E.left` that includes a stacktrace.
* @param message the error message
* @returns an `E.left` with the error message and a stacktrace
*/
export function errorLeft(message: string): E.Either<string, never> {
const stacktrace = new Error().stack!.split('\n').slice(2).join('\n');
const messageWithStacktrace = message + '\n' + stacktrace;

return E.left(messageWithStacktrace);
}

/**
* Testing utility to strip the stacktrace from errors.
* @param errors the list of errors to strip
* @returns the errors without the stacktrace
*/
export function stripStacktraceOfErrors(errors: string[]) {
return errors.map((e) => e!.split('\n')[0]);
}

// helper functions for logging
export function logError(message: string): void {
console.error(`[ERROR] ${message}`);
}

export function logWarn(message: string): void {
console.error(`[WARN] ${message}`);
}

export function logInfo(message: string): void {
console.error(`[INFO] ${message}`);
}
Loading

0 comments on commit aa52270

Please sign in to comment.