diff --git a/packages/malloy/src/lang/ast/expressions/expr-func.ts b/packages/malloy/src/lang/ast/expressions/expr-func.ts index 94d4cb663..52c492f01 100644 --- a/packages/malloy/src/lang/ast/expressions/expr-func.ts +++ b/packages/malloy/src/lang/ast/expressions/expr-func.ts @@ -350,9 +350,9 @@ export class ExprFunc extends ExpressionDef { ? `'.' paths are not yet supported in sql interpolations, found ${unsupportedInterpolations.at( 0 )}` - : `'.' paths are not yet supported in sql interpolations, found [${unsupportedInterpolations.join( + : `'.' paths are not yet supported in sql interpolations, found (${unsupportedInterpolations.join( ', ' - )}]`; + )})`; this.log(unsupportedInterpolationMsg); return errorFor( diff --git a/packages/malloy/src/lang/ast/expressions/pick-when.ts b/packages/malloy/src/lang/ast/expressions/pick-when.ts index 5a0b08363..f32689ac2 100644 --- a/packages/malloy/src/lang/ast/expressions/pick-when.ts +++ b/packages/malloy/src/lang/ast/expressions/pick-when.ts @@ -100,7 +100,9 @@ export class Pick extends ExpressionDef { ); if (returnType && !FT.typeEq(returnType, thenExpr, true)) { const whenType = FT.inspect(thenExpr); - this.log(`pick type '${whenType}', expected '${returnType.dataType}'`); + this.log( + `pick type '${whenType}', expected '${returnType.dataType}'[pick-values-must-match]` + ); return errorFor('pick when type'); } returnType = typeCoalesce(returnType, thenExpr); @@ -114,7 +116,7 @@ export class Pick extends ExpressionDef { this.log( `${errSrc} type '${FT.inspect(elseVal)}', expected '${ returnType.dataType - }'` + }'[pick-values-must-match]` ); return errorFor('pick else type'); } @@ -164,7 +166,9 @@ export class Pick extends ExpressionDef { } if (returnType && !FT.typeEq(returnType, aChoice.pick, true)) { const whenType = FT.inspect(aChoice.pick); - this.log(`pick type '${whenType}', expected '${returnType.dataType}'`); + this.log( + `pick type '${whenType}', expected '${returnType.dataType}'[pick-values-must-match]` + ); return errorFor('pick value type'); } returnType = typeCoalesce(returnType, aChoice.pick); @@ -196,7 +200,9 @@ export class Pick extends ExpressionDef { returnType = typeCoalesce(returnType, defVal); if (!FT.typeEq(returnType, defVal, true)) { this.elsePick.log( - `else type '${FT.inspect(defVal)}', expected '${returnType.dataType}'` + `else type '${FT.inspect(defVal)}', expected '${ + returnType.dataType + }'[pick-values-must-match]` ); return errorFor('pick value type mismatch'); } diff --git a/packages/malloy/src/lang/parse-log.ts b/packages/malloy/src/lang/parse-log.ts index 519d9acd6..644009e2a 100644 --- a/packages/malloy/src/lang/parse-log.ts +++ b/packages/malloy/src/lang/parse-log.ts @@ -32,6 +32,7 @@ export interface LogMessage { message: string; at?: DocumentLocation; severity: LogSeverity; + errorTag?: string; } export interface MessageLogger { @@ -50,7 +51,23 @@ export class MessageLog implements MessageLogger { return this.rawLog; } + /** + * Add a message to the log. + * + * If the messsage ends with '[tag]', the tag is removed and stored in the `errorTag` field. + * @param logMsg Message possibly containing an error tag + */ log(logMsg: LogMessage): void { + const msg = logMsg.message; + // github security is worried about msg.match(/^(.+)\[(.+)\]$/ because if someone + // could craft code with a long varibale name which would blow up that regular expression + if (msg.endsWith(']')) { + const tagStart = msg.lastIndexOf('['); + if (tagStart > 0) { + logMsg.message = msg.slice(0, tagStart); + logMsg.errorTag = msg.slice(tagStart + 1, -1); + } + } this.rawLog.push(logMsg); } diff --git a/packages/malloy/src/lang/test/parse.spec.ts b/packages/malloy/src/lang/test/parse.spec.ts index ebe045b52..e5e9675c4 100644 --- a/packages/malloy/src/lang/test/parse.spec.ts +++ b/packages/malloy/src/lang/test/parse.spec.ts @@ -1026,3 +1026,10 @@ describe('sql_functions', () => { test('non breaking space in source', () => expect('source:\u00a0z\u00a0is\u00a0a').toParse()); + +test('error tagging', () => { + const m = model`run: a -> { select: err is pick 1 when true else 'one' }`; + expect(m).translationToFailWith("else type 'string', expected 'number'"); + const firstErr = m.translator.root.logger.getLog()[0]; + expect(firstErr?.errorTag).toBe('pick-values-must-match'); +}); diff --git a/test/src/databases/all/expr.spec.ts b/test/src/databases/all/expr.spec.ts index 01bea3c02..440e7cf97 100644 --- a/test/src/databases/all/expr.spec.ts +++ b/test/src/databases/all/expr.spec.ts @@ -596,7 +596,7 @@ describe.each(runtimes.runtimeList)('%s', (databaseName, runtime) => { ` ); await expect(query.run()).rejects.toThrow( - "'.' paths are not yet supported in sql interpolations, found [${a.seats}, ${a.seats}, ${a.total_seats}]" + "'.' paths are not yet supported in sql interpolations, found (${a.seats}, ${a.seats}, ${a.total_seats})" ); });