| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package compile |
| |
| import ( |
| "fmt" |
| "regexp" |
| "strconv" |
| |
| "v.io/v23/i18n" |
| "v.io/v23/vdl" |
| "v.io/x/ref/lib/vdl/parse" |
| ) |
| |
| // ErrorDef represents a user-defined error definition in the compiled results. |
| type ErrorDef struct { |
| NamePos // name, parse position and docs |
| Exported bool // is this error definition exported? |
| ID string // error ID |
| RetryCode vdl.WireRetryCode // retry action to be performed by client |
| Params []*Field // list of positional parameter names and types |
| Formats []LangFmt // list of language / format pairs |
| English string // English format text from Formats |
| File *File // parent file that this error is defined in |
| } |
| |
| // LangFmt represents a language / format string pair. |
| type LangFmt struct { |
| Lang i18n.LangID // IETF language tag |
| Fmt string // i18n format string in the given language. |
| } |
| |
| func (x *ErrorDef) String() string { |
| y := *x |
| y.File = nil |
| return fmt.Sprintf("%+v", y) |
| } |
| |
| // compileErrorDefs fills in pkg with compiled error definitions. |
| func compileErrorDefs(pkg *Package, pfiles []*parse.File, env *Env) { |
| for index := range pkg.Files { |
| file, pfile := pkg.Files[index], pfiles[index] |
| for _, ped := range pfile.ErrorDefs { |
| name, detail := ped.Name, identDetail("error", file, ped.Pos) |
| export, err := validIdent(name, reservedNormal) |
| if err != nil { |
| env.prefixErrorf(file, ped.Pos, err, "error %s invalid name", name) |
| continue |
| } |
| if err := file.DeclareIdent(name, detail); err != nil { |
| env.prefixErrorf(file, ped.Pos, err, "error %s name conflict", name) |
| continue |
| } |
| // The error id should not be changed; the whole point of error defs is |
| // that the id is stable. |
| id := pkg.Path + "." + name |
| ed := &ErrorDef{NamePos: NamePos(ped.NamePos), Exported: export, ID: id, File: file} |
| defineErrorActions(ed, name, ped.Actions, file, env) |
| ed.Params = defineErrorParams(name, ped.Params, export, file, env) |
| ed.Formats = defineErrorFormats(name, ped.Formats, ed.Params, file, env) |
| // We require the "en" base language for at least one of the Formats, and |
| // favor "en-US" if it exists. This requirement is an attempt to ensure |
| // there is at least one common language across all errors. |
| for _, lf := range ed.Formats { |
| if lf.Lang == i18n.LangID("en-US") { |
| ed.English = lf.Fmt |
| break |
| } |
| if ed.English == "" && i18n.BaseLangID(lf.Lang) == i18n.LangID("en") { |
| ed.English = lf.Fmt |
| } |
| } |
| if ed.English == "" { |
| env.Errorf(file, ed.Pos, "error %s invalid (must define at least one English format)", name) |
| continue |
| } |
| addErrorDef(ed, env) |
| } |
| } |
| } |
| |
| // addErrorDef updates our various structures to add a new error def. |
| func addErrorDef(def *ErrorDef, env *Env) { |
| def.File.ErrorDefs = append(def.File.ErrorDefs, def) |
| def.File.Package.errorDefs = append(def.File.Package.errorDefs, def) |
| } |
| |
| func defineErrorActions(ed *ErrorDef, name string, pactions []parse.StringPos, file *File, env *Env) { |
| // We allow multiple actions to be specified in the parser, so that it's easy |
| // to add new actions in the future. |
| seenRetry := false |
| for _, pact := range pactions { |
| if retry, err := vdl.WireRetryCodeFromString(pact.String); err == nil { |
| if seenRetry { |
| env.Errorf(file, pact.Pos, "error %s action %s invalid (retry action specified multiple times)", name, pact.String) |
| continue |
| } |
| seenRetry = true |
| ed.RetryCode = retry |
| continue |
| } |
| env.Errorf(file, pact.Pos, "error %s action %s invalid (unknown action)", name, pact.String) |
| } |
| } |
| |
| func defineErrorParams(name string, pparams []*parse.Field, export bool, file *File, env *Env) []*Field { |
| var params []*Field |
| seen := make(map[string]*parse.Field) |
| for _, pparam := range pparams { |
| pname, pos := pparam.Name, pparam.Pos |
| if pname == "" { |
| env.Errorf(file, pos, "error %s invalid (parameters must be named)", name) |
| return nil |
| } |
| if dup := seen[pname]; dup != nil { |
| env.Errorf(file, pos, "error %s param %s duplicate name (previous at %s)", name, pname, dup.Pos) |
| continue |
| } |
| seen[pname] = pparam |
| if _, err := validIdent(pname, reservedFirstRuneLower); err != nil { |
| env.prefixErrorf(file, pos, err, "error %s param %s invalid", name, pname) |
| continue |
| } |
| var paramType *vdl.Type |
| if export { |
| paramType = compileExportedType(pparam.Type, file, env) |
| } else { |
| paramType = compileType(pparam.Type, file, env) |
| } |
| param := &Field{NamePos(pparam.NamePos), paramType} |
| params = append(params, param) |
| } |
| return params |
| } |
| |
| func defineErrorFormats(name string, plfs []parse.LangFmt, params []*Field, file *File, env *Env) []LangFmt { |
| var lfs []LangFmt |
| seen := make(map[i18n.LangID]parse.LangFmt) |
| for _, plf := range plfs { |
| pos, lang, fmt := plf.Pos(), i18n.LangID(plf.Lang.String), plf.Fmt.String |
| if lang == "" { |
| env.Errorf(file, pos, "error %s has empty language identifier", name) |
| continue |
| } |
| if dup, ok := seen[lang]; ok { |
| env.Errorf(file, pos, "error %s duplicate language %s (previous at %s)", name, lang, dup.Pos()) |
| continue |
| } |
| seen[lang] = plf |
| xfmt, err := xlateErrorFormat(fmt, params) |
| if err != nil { |
| env.prefixErrorf(file, pos, err, "error %s language %s format invalid", name, lang) |
| continue |
| } |
| lfs = append(lfs, LangFmt{lang, xfmt}) |
| } |
| return lfs |
| } |
| |
| // xlateErrorFormat translates the user-supplied format into the format |
| // expected by i18n, mainly translating parameter names into numeric indexes. |
| func xlateErrorFormat(format string, params []*Field) (string, error) { |
| const prefix = "{1:}{2:}" |
| if format == "" { |
| return prefix, nil |
| } |
| // Create a map from param name to index. The index numbering starts at 3, |
| // since the first two params are the component and op name, and i18n formats |
| // use 1-based indices. |
| pmap := make(map[string]string) |
| for ix, param := range params { |
| pmap[param.Name] = strconv.Itoa(ix + 3) |
| } |
| tagRE, err := regexp.Compile(`\{\:?([0-9a-zA-Z_]+)\:?\}`) |
| if err != nil { |
| return "", err |
| } |
| result, pos := prefix+" ", 0 |
| for _, match := range tagRE.FindAllStringSubmatchIndex(format, -1) { |
| // The tag submatch indices are available as match[2], match[3] |
| if len(match) != 4 || match[2] < pos || match[2] > match[3] { |
| return "", fmt.Errorf("internal error: bad regexp indices %v", match) |
| } |
| beg, end := match[2], match[3] |
| tag := format[beg:end] |
| if tag == "_" { |
| continue // Skip underscore tags. |
| } |
| if _, err := strconv.Atoi(tag); err == nil { |
| continue // Skip number tags. |
| } |
| xtag, ok := pmap[tag] |
| if !ok { |
| return "", fmt.Errorf("unknown param %q", tag) |
| } |
| // Replace tag with xtag in the result. |
| result += format[pos:beg] |
| result += xtag |
| pos = end |
| } |
| if end := len(format); pos < end { |
| result += format[pos:end] |
| } |
| return result, nil |
| } |