From 3cc840cc59bc0ec5874a8b99bdc50137bb5f0e36 Mon Sep 17 00:00:00 2001 From: "Randall C. O'Reilly" Date: Sat, 24 Oct 2026 16:39:53 +0200 Subject: [PATCH] fix(time): give ParseError the exported fields of Go's ParseError The time override's ParseError had lowercase fields, a positional constructor and no Error method, so Go code that makes one with a composite literal, as encoding/json/v2 does, built a broken value, and parse errors were wrapped as plain JavaScript errors that errors.As could not match to *time.ParseError. Give it Go's exported fields (Layout, Value, LayoutElem, ValueElem, Message), an init-object constructor, Go's Error formatting, and a registered struct type, and return it directly from the parse functions. Add a compliance test for errors.As, the fields, and composite literals. Signed-off-by: Randall C. O'Reilly --- gs/time/time.ts | 95 +++++++++++++------ .../expected.log | 5 + .../package_import_time_parse_error/index.ts | 0 .../package_import_time_parse_error.go | 21 ++++ .../package_import_time_parse_error.gs.ts | 27 ++++++ .../tsconfig.json | 25 +++++ 6 files changed, 142 insertions(+), 31 deletions(-) create mode 100644 tests/tests/package_import_time_parse_error/expected.log create mode 100644 tests/tests/package_import_time_parse_error/index.ts create mode 100644 tests/tests/package_import_time_parse_error/package_import_time_parse_error.go create mode 100644 tests/tests/package_import_time_parse_error/package_import_time_parse_error.gs.ts create mode 100644 tests/tests/package_import_time_parse_error/tsconfig.json diff --git a/gs/time/time.ts b/gs/time/time.ts index 38a691358..e11788b35 100644 --- a/gs/time/time.ts +++ b/gs/time/time.ts @@ -1259,31 +1259,69 @@ export function Weekday_String(w: Weekday): string { return WeekdayString(w) } -// ParseError describes a problem parsing a time string. -export class ParseError extends Error { - public layout: string - public value: string - public layoutElem: string - public valueElem: string - public message: string +// ParseError describes a problem parsing a time string. It has the exported +// fields of Go's ParseError, so Go code can make one with a composite literal +// and match one with errors.As. +export class ParseError { + public Layout: string + public Value: string + public LayoutElem: string + public ValueElem: string + public Message: string constructor( - layout: string, - value: string, - layoutElem: string, - valueElem: string, - message: string, + init?: Partial<{ + Layout: string + Value: string + LayoutElem: string + ValueElem: string + Message: string + }>, ) { - super(message) - this.layout = layout - this.value = value - this.layoutElem = layoutElem - this.valueElem = valueElem - this.message = message - this.name = 'ParseError' + this.Layout = init?.Layout ?? '' + this.Value = init?.Value ?? '' + this.LayoutElem = init?.LayoutElem ?? '' + this.ValueElem = init?.ValueElem ?? '' + this.Message = init?.Message ?? '' + } + + public clone(): ParseError { + return new ParseError(this) + } + + // Error returns the string representation of a ParseError, as Go does. + public Error(): string { + if (this.Message === '') { + return ( + 'parsing time ' + + quoteParseValue(this.Value) + + ' as ' + + quoteParseValue(this.Layout) + + ': cannot parse ' + + quoteParseValue(this.ValueElem) + + ' as ' + + quoteParseValue(this.LayoutElem) + ) + } + return 'parsing time ' + quoteParseValue(this.Value) + this.Message } + + static __typeInfo: $.StructTypeInfo } +// ParseError registers with the runtime type system, so that *ParseError +// implements error for type assertions and errors.As. +ParseError.__typeInfo = $.registerStructType( + 'time.ParseError', + new ParseError(), + [{ name: 'Error', args: [], returns: [{ type: $.basicType('string') }] }], + ParseError, + () => + ['Layout', 'Value', 'LayoutElem', 'ValueElem', 'Message'].map((name, i) => + $.structField(name, $.basicType('string'), [i], i * 16, true), + ), +) + // Timer represents a single event timer. A Timer made without a duration is // the zero Timer, which does not run until it is Reset. export class Timer { @@ -2185,18 +2223,13 @@ function newParseError( valueElem: string, message: string, ): $.GoError { - const text = - message === '' ? - 'parsing time ' + - quoteParseValue(value) + - ' as ' + - quoteParseValue(layout) + - ': cannot parse ' + - quoteParseValue(valueElem) + - ' as ' + - quoteParseValue(layoutElem) - : 'parsing time ' + quoteParseValue(value) + message - return $.toGoError(new ParseError(layout, value, layoutElem, valueElem, text)) + return new ParseError({ + Layout: layout, + Value: value, + LayoutElem: layoutElem, + ValueElem: valueElem, + Message: message, + }) } function isLeap(year: number): boolean { diff --git a/tests/tests/package_import_time_parse_error/expected.log b/tests/tests/package_import_time_parse_error/expected.log new file mode 100644 index 000000000..c504c4fcc --- /dev/null +++ b/tests/tests/package_import_time_parse_error/expected.log @@ -0,0 +1,5 @@ +as: true +fields: 2006-01-02 2024-13-01 : month out of range +error: parsing time "2024-13-01": month out of range +literal: parsing time "x" as "15:04": cannot parse "x" as "15" +message: parsing time "y": bad diff --git a/tests/tests/package_import_time_parse_error/index.ts b/tests/tests/package_import_time_parse_error/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/tests/tests/package_import_time_parse_error/package_import_time_parse_error.go b/tests/tests/package_import_time_parse_error/package_import_time_parse_error.go new file mode 100644 index 000000000..53a06dfcd --- /dev/null +++ b/tests/tests/package_import_time_parse_error/package_import_time_parse_error.go @@ -0,0 +1,21 @@ +package main + +import ( + "errors" + "time" +) + +func main() { + // A parse failure is a *time.ParseError that errors.As finds. + _, err := time.Parse("2006-01-02", "2024-13-01") + var pe *time.ParseError + println("as:", errors.As(err, &pe)) + println("fields:", pe.Layout, pe.Value, pe.Message) + println("error:", err.Error()) + + // A composite literal sets the exported fields, and Error formats them. + lit := &time.ParseError{Layout: "15:04", Value: "x", LayoutElem: "15", ValueElem: "x"} + println("literal:", lit.Error()) + lit = &time.ParseError{Value: "y", Message: ": bad"} + println("message:", lit.Error()) +} diff --git a/tests/tests/package_import_time_parse_error/package_import_time_parse_error.gs.ts b/tests/tests/package_import_time_parse_error/package_import_time_parse_error.gs.ts new file mode 100644 index 000000000..9b9f3f45b --- /dev/null +++ b/tests/tests/package_import_time_parse_error/package_import_time_parse_error.gs.ts @@ -0,0 +1,27 @@ +// Generated file based on package_import_time_parse_error.go +// Updated when compliance tests are re-run, DO NOT EDIT! + +import * as $ from "@goscript/builtin/index.js" +import * as errors from "@goscript/errors/index.js" +import * as time from "@goscript/time/index.js" + +export async function main(): globalThis.Promise { + // A parse failure is a *time.ParseError that errors.As finds. + let [, err] = time.Parse("2006-01-02", "2024-13-01") + let pe: $.VarRef | null> = $.varRef(null! as time.ParseError | $.VarRef | null) + await $.println("as:", errors.As($.pointerValueOrNil(err)!, $.interfaceValue(pe, "**time.ParseError", /* @__PURE__ */ $.pointerType(/* @__PURE__ */ $.pointerType("time.ParseError"))))) + await $.println("fields:", $.pointerValue(pe.value).Layout, $.pointerValue(pe.value).Value, $.pointerValue(pe.value).Message) + await $.println("error:", (__goscriptAwait = (err !== null && (err as any).__isVarRef !== true && ((err as any).__goValue === undefined || (err as any).__goType?.charCodeAt(0) !== 42) ? (err as Exclude<$.GoError, null>) : $.pointerValue>(err)).Error(), __goscriptAwait instanceof Promise ? await __goscriptAwait : __goscriptAwait)) + + // A composite literal sets the exported fields, and Error formats them. + let lit: time.ParseError | $.VarRef | null = new time.ParseError({Layout: "15:04", Value: "x", LayoutElem: "15", ValueElem: "x"}) + await $.println("literal:", (lit !== null && (lit as any).__isVarRef !== true && (lit as any).__goValue === undefined ? ((lit as any) as time.ParseError).Error() : time.ParseError.prototype.Error.call($.pointerValue(lit)))) + lit = new time.ParseError({Value: "y", Message: ": bad"}) + await $.println("message:", (lit !== null && (lit as any).__isVarRef !== true && (lit as any).__goValue === undefined ? ((lit as any) as time.ParseError).Error() : time.ParseError.prototype.Error.call($.pointerValue(lit)))) +} + +var __goscriptAwait: any + +if ($.isMainScript(import.meta)) { + await main() +} diff --git a/tests/tests/package_import_time_parse_error/tsconfig.json b/tests/tests/package_import_time_parse_error/tsconfig.json new file mode 100644 index 000000000..210a25528 --- /dev/null +++ b/tests/tests/package_import_time_parse_error/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "paths": { + "*": [ + "./*" + ], + "@go/github.com/s4wave/goscript/*": [ + "../../../*" + ], + "@goscript/*": [ + "../../../tests/tests/package_import_time_parse_error/run/output/@goscript/*", + "../../../gs/*", + "../../../tests/deps/*" + ], + "@goscript/github.com/s4wave/goscript/tests/tests/package_import_time_parse_error/*": [ + "./*" + ] + } + }, + "extends": "../../../tests/tsconfig.base.json", + "include": [ + "index.ts", + "package_import_time_parse_error.gs.ts" + ] +}