Config reporting with line and error position

Signed-off-by: Waldemar Quevedo <wally@synadia.com>
This commit is contained in:
Waldemar Quevedo
2018-09-30 17:11:40 -07:00
parent fd3e5fdcc9
commit 181b07ebc1
7 changed files with 758 additions and 498 deletions

View File

@@ -101,12 +101,19 @@ type lexer struct {
// Used for processing escapable substrings in double-quoted and raw strings // Used for processing escapable substrings in double-quoted and raw strings
stringParts []string stringParts []string
stringStateFn stateFn stringStateFn stateFn
// lstart is the start position of the current line.
lstart int
// ilstart is the start position of the line from the current item.
ilstart int
} }
type item struct { type item struct {
typ itemType typ itemType
val string val string
line int line int
pos int
} }
func (lx *lexer) nextItem() item { func (lx *lexer) nextItem() item {
@@ -147,8 +154,13 @@ func (lx *lexer) pop() stateFn {
} }
func (lx *lexer) emit(typ itemType) { func (lx *lexer) emit(typ itemType) {
lx.items <- item{typ, strings.Join(lx.stringParts, "") + lx.input[lx.start:lx.pos], lx.line} val := strings.Join(lx.stringParts, "") + lx.input[lx.start:lx.pos]
// Position of item in line where it started.
pos := lx.pos - lx.ilstart - len(val)
lx.items <- item{typ, val, lx.line, pos}
lx.start = lx.pos lx.start = lx.pos
lx.ilstart = lx.lstart
} }
func (lx *lexer) emitString() { func (lx *lexer) emitString() {
@@ -159,8 +171,11 @@ func (lx *lexer) emitString() {
} else { } else {
finalString = lx.input[lx.start:lx.pos] finalString = lx.input[lx.start:lx.pos]
} }
lx.items <- item{itemString, finalString, lx.line} // Position of string in line where it started.
pos := lx.pos - lx.ilstart - len(finalString)
lx.items <- item{itemString, finalString, lx.line, pos}
lx.start = lx.pos lx.start = lx.pos
lx.ilstart = lx.lstart
} }
func (lx *lexer) addCurrentStringPart(offset int) { func (lx *lexer) addCurrentStringPart(offset int) {
@@ -186,15 +201,20 @@ func (lx *lexer) next() (r rune) {
if lx.input[lx.pos] == '\n' { if lx.input[lx.pos] == '\n' {
lx.line++ lx.line++
// Mark start position of current line.
lx.lstart = lx.pos
} }
r, lx.width = utf8.DecodeRuneInString(lx.input[lx.pos:]) r, lx.width = utf8.DecodeRuneInString(lx.input[lx.pos:])
lx.pos += lx.width lx.pos += lx.width
return r return r
} }
// ignore skips over the pending input before this point. // ignore skips over the pending input before this point.
func (lx *lexer) ignore() { func (lx *lexer) ignore() {
lx.start = lx.pos lx.start = lx.pos
lx.ilstart = lx.lstart
} }
// backup steps back one rune. Can be called only once per call of next. // backup steps back one rune. Can be called only once per call of next.
@@ -221,10 +241,14 @@ func (lx *lexer) errorf(format string, values ...interface{}) stateFn {
values[i] = escapeSpecial(v) values[i] = escapeSpecial(v)
} }
} }
// Position of error in current line.
pos := lx.pos - lx.lstart
lx.items <- item{ lx.items <- item{
itemError, itemError,
fmt.Sprintf(format, values...), fmt.Sprintf(format, values...),
lx.line, lx.line,
pos,
} }
return nil return nil
} }
@@ -1131,7 +1155,7 @@ func (itype itemType) String() string {
} }
func (item item) String() string { func (item item) String() string {
return fmt.Sprintf("(%s, '%s', %d)", item.typ.String(), item.val, item.line) return fmt.Sprintf("(%s, '%s', %d, %d)", item.typ.String(), item.val, item.line, item.pos)
} }
func escapeSpecial(c rune) string { func escapeSpecial(c rune) string {

File diff suppressed because it is too large Load Diff

View File

@@ -49,6 +49,9 @@ type parser struct {
// Keys stack // Keys stack
keys []string keys []string
// Keys stack as items
ikeys []item
// The config file path, empty by default. // The config file path, empty by default.
fp string fp string
@@ -118,12 +121,17 @@ func (t *token) SourceFile() string {
return t.sourceFile return t.sourceFile
} }
func (t *token) Position() int {
return t.item.pos
}
func parse(data, fp string, pedantic bool) (p *parser, err error) { func parse(data, fp string, pedantic bool) (p *parser, err error) {
p = &parser{ p = &parser{
mapping: make(map[string]interface{}), mapping: make(map[string]interface{}),
lx: lex(data), lx: lex(data),
ctxs: make([]interface{}, 0, 4), ctxs: make([]interface{}, 0, 4),
keys: make([]string, 0, 4), keys: make([]string, 0, 4),
ikeys: make([]item, 0, 4),
fp: filepath.Dir(fp), fp: filepath.Dir(fp),
pedantic: pedantic, pedantic: pedantic,
} }
@@ -176,6 +184,20 @@ func (p *parser) popKey() string {
return last return last
} }
func (p *parser) pushItemKey(key item) {
p.ikeys = append(p.ikeys, key)
}
func (p *parser) popItemKey() item {
if len(p.ikeys) == 0 {
panic("BUG in parser, item keys stack empty")
}
li := len(p.ikeys) - 1
last := p.ikeys[li]
p.ikeys = p.ikeys[0:li]
return last
}
func (p *parser) processItem(it item, fp string) error { func (p *parser) processItem(it item, fp string) error {
setValue := func(it item, v interface{}) { setValue := func(it item, v interface{}) {
if p.pedantic { if p.pedantic {
@@ -189,7 +211,14 @@ func (p *parser) processItem(it item, fp string) error {
case itemError: case itemError:
return fmt.Errorf("Parse error on line %d: '%s'", it.line, it.val) return fmt.Errorf("Parse error on line %d: '%s'", it.line, it.val)
case itemKey: case itemKey:
// Keep track of the keys as items and strings,
// we do this in order to be able to still support
// includes without many breaking changes.
p.pushKey(it.val) p.pushKey(it.val)
if p.pedantic {
p.pushItemKey(it)
}
case itemMapStart: case itemMapStart:
newCtx := make(map[string]interface{}) newCtx := make(map[string]interface{})
p.pushContext(newCtx) p.pushContext(newCtx)
@@ -297,6 +326,13 @@ func (p *parser) processItem(it item, fp string) error {
} }
for k, v := range m { for k, v := range m {
p.pushKey(k) p.pushKey(k)
if p.pedantic {
switch tk := v.(type) {
case *token:
p.pushItemKey(tk.item)
}
}
p.setValue(v) p.setValue(v)
} }
} }
@@ -356,7 +392,21 @@ func (p *parser) setValue(val interface{}) {
// Map processing // Map processing
if ctx, ok := p.ctx.(map[string]interface{}); ok { if ctx, ok := p.ctx.(map[string]interface{}); ok {
key := p.popKey() key := p.popKey()
// FIXME(dlc), make sure to error if redefining same key?
ctx[key] = val if p.pedantic {
it := p.popItemKey()
// Change the position to the beginning of the key
// since more useful when reporting errors.
switch v := val.(type) {
case *token:
v.item.pos = it.pos
v.item.line = it.line
ctx[key] = v
}
} else {
// FIXME(dlc), make sure to error if redefining same key?
ctx[key] = val
}
} }
} }

View File

@@ -37,6 +37,9 @@ func TestConfigCheck(t *testing.T) {
// errorLine is the location of the error. // errorLine is the location of the error.
errorLine int errorLine int
// errorPos is the position of the error.
errorPos int
// warning errors also include a reason optionally // warning errors also include a reason optionally
reason string reason string
}{ }{
@@ -48,20 +51,20 @@ func TestConfigCheck(t *testing.T) {
defaultErr: nil, defaultErr: nil,
pedanticErr: errors.New(`unknown field "monitor"`), pedanticErr: errors.New(`unknown field "monitor"`),
errorLine: 2, errorLine: 2,
errorPos: 17,
}, },
{ {
name: "when default permissions are used at top level", name: "when default permissions are used at top level",
config: ` config: `
"default_permissions" { "default_permissions" {
publish = ["_SANDBOX.>"] publish = ["_SANDBOX.>"]
subscribe = ["_SANDBOX.>"] subscribe = ["_SANDBOX.>"]
} }
`, `,
defaultErr: nil, defaultErr: nil,
pedanticErr: errors.New(`unknown field "default_permissions"`), pedanticErr: errors.New(`unknown field "default_permissions"`),
errorLine: 2,
// NOTE: line number is '5' because it is where the map definition ends. errorPos: 18,
errorLine: 5,
}, },
{ {
name: "when authorization config is empty", name: "when authorization config is empty",
@@ -82,15 +85,16 @@ func TestConfigCheck(t *testing.T) {
defaultErr: nil, defaultErr: nil,
pedanticErr: errors.New(`unknown field "foo"`), pedanticErr: errors.New(`unknown field "foo"`),
errorLine: 3, errorLine: 3,
errorPos: 5,
}, },
{ {
name: "when authorization config has unknown fields", name: "when authorization config has unknown fields",
config: ` config: `
port = 4222 port = 4222
authorization = { authorization = {
user = "hello" user = "hello"
foo = "bar" foo = "bar"
password = "world" password = "world"
} }
@@ -98,6 +102,7 @@ func TestConfigCheck(t *testing.T) {
defaultErr: nil, defaultErr: nil,
pedanticErr: errors.New(`unknown field "foo"`), pedanticErr: errors.New(`unknown field "foo"`),
errorLine: 6, errorLine: 6,
errorPos: 5,
}, },
{ {
name: "when user authorization config has unknown fields", name: "when user authorization config has unknown fields",
@@ -105,16 +110,17 @@ func TestConfigCheck(t *testing.T) {
authorization = { authorization = {
users = [ users = [
{ {
user = "foo" user = "foo"
pass = "bar" pass = "bar"
token = "quux" token = "quux"
} }
] ]
} }
`, `,
defaultErr: nil, defaultErr: nil,
pedanticErr: errors.New(`unknown field "token"`), pedanticErr: errors.New(`unknown field "token"`),
errorLine: 7, errorLine: 7,
errorPos: 9,
}, },
{ {
name: "when user authorization permissions config has unknown fields", name: "when user authorization permissions config has unknown fields",
@@ -130,6 +136,7 @@ func TestConfigCheck(t *testing.T) {
defaultErr: errors.New(`Unknown field inboxes parsing permissions`), defaultErr: errors.New(`Unknown field inboxes parsing permissions`),
pedanticErr: errors.New(`unknown field "inboxes"`), pedanticErr: errors.New(`unknown field "inboxes"`),
errorLine: 5, errorLine: 5,
errorPos: 7,
}, },
{ {
name: "when user authorization permissions config has unknown fields within allow or deny", name: "when user authorization permissions config has unknown fields within allow or deny",
@@ -137,10 +144,10 @@ func TestConfigCheck(t *testing.T) {
authorization { authorization {
permissions { permissions {
subscribe = { subscribe = {
allow = ["hello", "world"] allow = ["hello", "world"]
deny = ["foo", "bar"] deny = ["foo", "bar"]
denied = "_INBOX.>" denied = "_INBOX.>"
} }
publish = {} publish = {}
} }
} }
@@ -148,6 +155,7 @@ func TestConfigCheck(t *testing.T) {
defaultErr: errors.New(`Unknown field name "denied" parsing subject permissions, only 'allow' or 'deny' are permitted`), defaultErr: errors.New(`Unknown field name "denied" parsing subject permissions, only 'allow' or 'deny' are permitted`),
pedanticErr: errors.New(`unknown field "denied"`), pedanticErr: errors.New(`unknown field "denied"`),
errorLine: 7, errorLine: 7,
errorPos: 9,
}, },
{ {
name: "when user authorization permissions config has unknown fields within allow or deny", name: "when user authorization permissions config has unknown fields within allow or deny",
@@ -155,10 +163,10 @@ func TestConfigCheck(t *testing.T) {
authorization { authorization {
permissions { permissions {
publish = { publish = {
allow = ["hello", "world"] allow = ["hello", "world"]
deny = ["foo", "bar"] deny = ["foo", "bar"]
allowed = "_INBOX.>" allowed = "_INBOX.>"
} }
subscribe = {} subscribe = {}
} }
} }
@@ -166,11 +174,12 @@ func TestConfigCheck(t *testing.T) {
defaultErr: errors.New(`Unknown field name "allowed" parsing subject permissions, only 'allow' or 'deny' are permitted`), defaultErr: errors.New(`Unknown field name "allowed" parsing subject permissions, only 'allow' or 'deny' are permitted`),
pedanticErr: errors.New(`unknown field "allowed"`), pedanticErr: errors.New(`unknown field "allowed"`),
errorLine: 7, errorLine: 7,
errorPos: 9,
}, },
{ {
name: "when user authorization permissions config has unknown fields using arrays", name: "when user authorization permissions config has unknown fields using arrays",
config: ` config: `
authorization { authorization {
default_permissions { default_permissions {
subscribe = ["a"] subscribe = ["a"]
@@ -184,16 +193,17 @@ func TestConfigCheck(t *testing.T) {
pass = "bar" pass = "bar"
} }
] ]
} }
`, `,
defaultErr: errors.New(`Unknown field inboxes parsing permissions`), defaultErr: errors.New(`Unknown field inboxes parsing permissions`),
pedanticErr: errors.New(`unknown field "inboxes"`), pedanticErr: errors.New(`unknown field "inboxes"`),
errorLine: 7, errorLine: 7,
errorPos: 6,
}, },
{ {
name: "when user authorization permissions config has unknown fields using strings", name: "when user authorization permissions config has unknown fields using strings",
config: ` config: `
authorization { authorization {
default_permissions { default_permissions {
subscribe = "a" subscribe = "a"
@@ -207,11 +217,12 @@ func TestConfigCheck(t *testing.T) {
pass = "bar" pass = "bar"
} }
] ]
} }
`, `,
defaultErr: errors.New(`Unknown field requests parsing permissions`), defaultErr: errors.New(`Unknown field requests parsing permissions`),
pedanticErr: errors.New(`unknown field "requests"`), pedanticErr: errors.New(`unknown field "requests"`),
errorLine: 6, errorLine: 6,
errorPos: 6,
}, },
{ {
name: "when user authorization permissions config is empty", name: "when user authorization permissions config is empty",
@@ -244,6 +255,7 @@ func TestConfigCheck(t *testing.T) {
defaultErr: errors.New(`Unknown field inboxes parsing permissions`), defaultErr: errors.New(`Unknown field inboxes parsing permissions`),
pedanticErr: errors.New(`unknown field "inboxes"`), pedanticErr: errors.New(`unknown field "inboxes"`),
errorLine: 6, errorLine: 6,
errorPos: 11,
}, },
{ {
name: "when clustering config is empty", name: "when clustering config is empty",
@@ -258,19 +270,19 @@ func TestConfigCheck(t *testing.T) {
{ {
name: "when unknown option is in clustering config", name: "when unknown option is in clustering config",
config: ` config: `
# NATS Server Configuration # NATS Server Configuration
port = 4222 port = 4222
cluster = { cluster = {
port = 6222 port = 6222
foo = "bar" foo = "bar"
authorization { authorization {
user = "hello" user = "hello"
pass = "world" pass = "world"
} }
} }
`, `,
@@ -278,6 +290,7 @@ func TestConfigCheck(t *testing.T) {
defaultErr: nil, defaultErr: nil,
pedanticErr: errors.New(`unknown field "foo"`), pedanticErr: errors.New(`unknown field "foo"`),
errorLine: 9, errorLine: 9,
errorPos: 5,
}, },
{ {
name: "when unknown option is in clustering authorization config", name: "when unknown option is in clustering authorization config",
@@ -292,6 +305,7 @@ func TestConfigCheck(t *testing.T) {
defaultErr: nil, defaultErr: nil,
pedanticErr: errors.New(`unknown field "foo"`), pedanticErr: errors.New(`unknown field "foo"`),
errorLine: 4, errorLine: 4,
errorPos: 7,
}, },
{ {
name: "when unknown option is in clustering authorization permissions config", name: "when unknown option is in clustering authorization permissions config",
@@ -309,6 +323,7 @@ func TestConfigCheck(t *testing.T) {
defaultErr: errors.New(`Unknown field hello parsing permissions`), defaultErr: errors.New(`Unknown field hello parsing permissions`),
pedanticErr: errors.New(`unknown field "hello"`), pedanticErr: errors.New(`unknown field "hello"`),
errorLine: 7, errorLine: 7,
errorPos: 9,
}, },
{ {
name: "when unknown option is in tls config", name: "when unknown option is in tls config",
@@ -320,6 +335,7 @@ func TestConfigCheck(t *testing.T) {
defaultErr: errors.New(`error parsing tls config, unknown field ["hello"]`), defaultErr: errors.New(`error parsing tls config, unknown field ["hello"]`),
pedanticErr: errors.New(`unknown field "hello"`), pedanticErr: errors.New(`unknown field "hello"`),
errorLine: 3, errorLine: 3,
errorPos: 5,
}, },
{ {
name: "when unknown option is in cluster tls config", name: "when unknown option is in cluster tls config",
@@ -334,6 +350,7 @@ func TestConfigCheck(t *testing.T) {
defaultErr: errors.New(`error parsing tls config, unknown field ["foo"]`), defaultErr: errors.New(`error parsing tls config, unknown field ["foo"]`),
pedanticErr: errors.New(`unknown field "foo"`), pedanticErr: errors.New(`unknown field "foo"`),
errorLine: 4, errorLine: 4,
errorPos: 7,
}, },
{ {
name: "when using cipher suites in the TLS config", name: "when using cipher suites in the TLS config",
@@ -343,12 +360,13 @@ func TestConfigCheck(t *testing.T) {
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
] ]
preferences = [] preferences = []
} }
`, `,
defaultErr: errors.New(`error parsing tls config, unknown field ["preferences"]`), defaultErr: errors.New(`error parsing tls config, unknown field ["preferences"]`),
pedanticErr: errors.New(`unknown field "preferences"`), pedanticErr: errors.New(`unknown field "preferences"`),
errorLine: 7, errorLine: 7,
errorPos: 7,
}, },
{ {
name: "when using curve preferences in the TLS config", name: "when using curve preferences in the TLS config",
@@ -359,27 +377,29 @@ func TestConfigCheck(t *testing.T) {
"CurveP384", "CurveP384",
"CurveP521" "CurveP521"
] ]
suites = [] suites = []
} }
`, `,
defaultErr: errors.New(`error parsing tls config, unknown field ["suites"]`), defaultErr: errors.New(`error parsing tls config, unknown field ["suites"]`),
pedanticErr: errors.New(`unknown field "suites"`), pedanticErr: errors.New(`unknown field "suites"`),
errorLine: 8, errorLine: 8,
errorPos: 7,
}, },
{ {
name: "when unknown option is in cluster config with defined routes", name: "when unknown option is in cluster config with defined routes",
config: ` config: `
cluster { cluster {
port = 6222 port = 6222
routes = [ routes = [
nats://127.0.0.1:6222 nats://127.0.0.1:6222
] ]
peers = [] peers = []
} }
`, `,
defaultErr: nil, defaultErr: nil,
pedanticErr: errors.New(`unknown field "peers"`), pedanticErr: errors.New(`unknown field "peers"`),
errorLine: 7, errorLine: 7,
errorPos: 5,
}, },
{ {
name: "when used as variable in authorization block it should not be considered as unknown field", name: "when used as variable in authorization block it should not be considered as unknown field",
@@ -425,15 +445,16 @@ func TestConfigCheck(t *testing.T) {
defaultErr: nil, defaultErr: nil,
pedanticErr: errors.New(`unknown field "unused"`), pedanticErr: errors.New(`unknown field "unused"`),
errorLine: 27, errorLine: 27,
errorPos: 5,
}, },
{ {
name: "when used as variable in top level config it should not be considered as unknown field", name: "when used as variable in top level config it should not be considered as unknown field",
config: ` config: `
monitoring_port = 8222 monitoring_port = 8222
http_port = $monitoring_port http_port = $monitoring_port
port = 4222 port = 4222
`, `,
defaultErr: nil, defaultErr: nil,
pedanticErr: nil, pedanticErr: nil,
@@ -441,32 +462,33 @@ func TestConfigCheck(t *testing.T) {
{ {
name: "when used as variable in cluster config it should not be considered as unknown field", name: "when used as variable in cluster config it should not be considered as unknown field",
config: ` config: `
cluster { cluster {
clustering_port = 6222 clustering_port = 6222
port = $clustering_port port = $clustering_port
} }
`, `,
defaultErr: nil, defaultErr: nil,
pedanticErr: nil, pedanticErr: nil,
}, },
{ {
name: "when setting permissions within cluster authorization block", name: "when setting permissions within cluster authorization block",
config: ` config: `
cluster { cluster {
authorization { authorization {
permissions = { permissions = {
publish = { allow = ["foo", "bar"] } publish = { allow = ["foo", "bar"] }
} }
} }
permissions = { permissions = {
publish = { deny = ["foo", "bar"] } publish = { deny = ["foo", "bar"] }
} }
} }
`, `,
defaultErr: nil, defaultErr: nil,
pedanticErr: errors.New(`invalid use of field "authorization"`), pedanticErr: errors.New(`invalid use of field "authorization"`),
errorLine: 7, errorLine: 3,
errorPos: 5,
reason: `setting "permissions" within cluster authorization block is deprecated`, reason: `setting "permissions" within cluster authorization block is deprecated`,
}, },
} }
@@ -498,7 +520,7 @@ func TestConfigCheck(t *testing.T) {
err := checkConfig(conf, true) err := checkConfig(conf, true)
expectedErr := test.pedanticErr expectedErr := test.pedanticErr
if err != nil && expectedErr != nil { if err != nil && expectedErr != nil {
msg := fmt.Sprintf("%s in %s:%d", expectedErr.Error(), conf, test.errorLine) msg := fmt.Sprintf("%s in %s:%d:%d", expectedErr.Error(), conf, test.errorLine, test.errorPos)
if test.reason != "" { if test.reason != "" {
msg += ": " + test.reason msg += ": " + test.reason
} }
@@ -528,7 +550,7 @@ func TestConfigCheckIncludes(t *testing.T) {
} }
err := opts.ProcessConfigFile("./configs/include_conf_check_a.conf") err := opts.ProcessConfigFile("./configs/include_conf_check_a.conf")
if err != nil { if err != nil {
t.Errorf("Unexpected error processing include files with configuration check enabled: %s", err) t.Errorf("Unexpected error processing include files with configuration check enabled: %v", err)
} }
opts = &Options{ opts = &Options{
@@ -536,9 +558,9 @@ func TestConfigCheckIncludes(t *testing.T) {
} }
err = opts.ProcessConfigFile("./configs/include_bad_conf_check_a.conf") err = opts.ProcessConfigFile("./configs/include_bad_conf_check_a.conf")
if err == nil { if err == nil {
t.Errorf("Expected error processing include files with configuration check enabled: %s", err) t.Errorf("Expected error processing include files with configuration check enabled: %v", err)
} }
expectedErr := errors.New(`unknown field "monitoring_port" in configs/include_bad_conf_check_b.conf:2`) expectedErr := errors.New(`unknown field "monitoring_port" in configs/include_bad_conf_check_b.conf:10:19`)
if err != nil && expectedErr != nil && err.Error() != expectedErr.Error() { if err != nil && expectedErr != nil && err.Error() != expectedErr.Error() {
t.Errorf("Expected %q, got %q", expectedErr.Error(), err.Error()) t.Errorf("Expected %q, got %q", expectedErr.Error(), err.Error())
} }

View File

@@ -1,4 +1,12 @@
monitoring_port = 8222
monitoring_port = 8222
include "include_conf_check_c.conf" include "include_conf_check_c.conf"

View File

@@ -1,3 +1,4 @@
monitoring_port = 8222 monitoring_port = 8222
include "include_conf_check_c.conf" include "include_conf_check_c.conf"

View File

@@ -196,6 +196,7 @@ type token interface {
Line() int Line() int
IsUsedVariable() bool IsUsedVariable() bool
SourceFile() string SourceFile() string
Position() int
} }
type unknownConfigFieldErr struct { type unknownConfigFieldErr struct {
@@ -207,7 +208,7 @@ type unknownConfigFieldErr struct {
func (e *unknownConfigFieldErr) Error() string { func (e *unknownConfigFieldErr) Error() string {
msg := fmt.Sprintf("unknown field %q", e.field) msg := fmt.Sprintf("unknown field %q", e.field)
if e.token != nil { if e.token != nil {
return msg + fmt.Sprintf(" in %s:%d", e.configFile, e.token.Line()) return msg + fmt.Sprintf(" in %s:%d:%d", e.configFile, e.token.Line(), e.token.Position())
} }
return msg return msg
} }
@@ -222,7 +223,7 @@ type configWarningErr struct {
func (e *configWarningErr) Error() string { func (e *configWarningErr) Error() string {
msg := fmt.Sprintf("invalid use of field %q", e.field) msg := fmt.Sprintf("invalid use of field %q", e.field)
if e.token != nil { if e.token != nil {
msg += fmt.Sprintf(" in %s:%d", e.configFile, e.token.Line()) msg += fmt.Sprintf(" in %s:%d:%d", e.configFile, e.token.Line(), e.token.Position())
} }
msg += ": " + e.reason msg += ": " + e.reason
return msg return msg