From 4df6307b1c9be4bd49e291308bfad9496dc8e400 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Sat, 2 May 2026 21:35:46 -0400 Subject: [PATCH 1/4] key: apply ValueMapper to substituted reference values When a key's value contains a %(...)s reference, the substituted text was inserted verbatim from the referenced key's raw value, bypassing ValueMapper. As a result, placeholders like ${ENV_VAR} embedded in a referenced key were never expanded. Substitute via nk.String() so the referenced key goes through its own transformValue (ValueMapper + further reference expansion) before being inlined. Co-Authored-By: Claude Opus 4.7 (1M context) --- key.go | 2 +- key_test.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/key.go b/key.go index 1a7767a..b1ef5c9 100644 --- a/key.go +++ b/key.go @@ -170,7 +170,7 @@ func (k *Key) transformValue(val string) string { } // Substitute by new value and take off leading '%(' and trailing ')s'. - val = strings.ReplaceAll(val, vr, nk.value) + val = strings.ReplaceAll(val, vr, nk.String()) } return val } diff --git a/key_test.go b/key_test.go index cbd9bed..e57a7bd 100644 --- a/key_test.go +++ b/key_test.go @@ -183,6 +183,7 @@ func TestKey_Helpers(t *testing.T) { } return in } + t.Cleanup(func() { f.ValueMapper = nil }) assert.Equal(t, "github.com/go-ini/ini", sec.Key("IMPORT_PATH").String()) }) }) @@ -635,4 +636,22 @@ bar = %(missing)s assert.Equal(t, "%(missing)s", f.Section("foo").Key("bar").String()) }) + + t.Run("ValueMapper applies to substituted reference value", func(t *testing.T) { + f, err := Load([]byte(` +ACCOUNT_ID = ${LFSD_R2_ACCOUNT_ID} +ENDPOINT = https://%(ACCOUNT_ID)s.r2.cloudflarestorage.com +`)) + require.NoError(t, err) + require.NotNil(t, f) + + f.ValueMapper = func(in string) string { + if in == "${LFSD_R2_ACCOUNT_ID}" { + return "abc123" + } + return in + } + + assert.Equal(t, "https://abc123.r2.cloudflarestorage.com", f.Section("").Key("ENDPOINT").String()) + }) } From 733f98c7039ac86b395fbbcec151af13759277c2 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Sat, 2 May 2026 21:40:17 -0400 Subject: [PATCH 2/4] chore: fix golangci-lint findings - Replace deprecated io/ioutil with io and os equivalents. - Replace deprecated reflect.Ptr alias with reflect.Pointer. - Use fmt.Fprint(&buf, ...) instead of buf.WriteString(fmt.Sprint(...)). Co-Authored-By: Claude Opus 4.7 (1M context) --- data_source.go | 5 ++--- file.go | 3 +-- file_test.go | 6 +++--- ini_test.go | 4 ++-- struct.go | 38 +++++++++++++++++++------------------- 5 files changed, 27 insertions(+), 29 deletions(-) diff --git a/data_source.go b/data_source.go index c3a541f..6e2572b 100644 --- a/data_source.go +++ b/data_source.go @@ -18,7 +18,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "os" ) @@ -48,7 +47,7 @@ type sourceData struct { } func (s *sourceData) ReadCloser() (io.ReadCloser, error) { - return ioutil.NopCloser(bytes.NewReader(s.data)), nil + return io.NopCloser(bytes.NewReader(s.data)), nil } // sourceReadCloser represents an input stream with Close method. @@ -69,7 +68,7 @@ func parseDataSource(source interface{}) (dataSource, error) { case io.ReadCloser: return &sourceReadCloser{s}, nil case io.Reader: - return &sourceReadCloser{ioutil.NopCloser(s)}, nil + return &sourceReadCloser{io.NopCloser(s)}, nil default: return nil, fmt.Errorf("error parsing data source: unknown type %q", s) } diff --git a/file.go b/file.go index f8b2240..58beabc 100644 --- a/file.go +++ b/file.go @@ -19,7 +19,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "strings" "sync" @@ -532,7 +531,7 @@ func (f *File) SaveToIndent(filename, indent string) error { return err } - return ioutil.WriteFile(filename, buf.Bytes(), 0666) + return os.WriteFile(filename, buf.Bytes(), 0666) } // SaveTo writes content to file system. diff --git a/file_test.go b/file_test.go index c9914b5..306c0c8 100644 --- a/file_test.go +++ b/file_test.go @@ -16,7 +16,7 @@ package ini import ( "bytes" - "io/ioutil" + "os" "runtime" "sort" "testing" @@ -421,10 +421,10 @@ func TestFile_WriteTo(t *testing.T) { golden := "testdata/TestFile_WriteTo.golden" if *update { - require.NoError(t, ioutil.WriteFile(golden, buf.Bytes(), 0644)) + require.NoError(t, os.WriteFile(golden, buf.Bytes(), 0644)) } - expected, err := ioutil.ReadFile(golden) + expected, err := os.ReadFile(golden) require.NoError(t, err) assert.Equal(t, string(expected), buf.String()) }) diff --git a/ini_test.go b/ini_test.go index aaad1a0..a3d3b4c 100644 --- a/ini_test.go +++ b/ini_test.go @@ -17,7 +17,7 @@ package ini import ( "bytes" "flag" - "io/ioutil" + "io" "path/filepath" "runtime" "testing" @@ -58,7 +58,7 @@ func TestLoad(t *testing.T) { "testdata/minimal.ini", []byte("NAME = ini\nIMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s"), bytes.NewReader([]byte(`VERSION = v1`)), - ioutil.NopCloser(bytes.NewReader([]byte("[author]\nNAME = Unknwon"))), + io.NopCloser(bytes.NewReader([]byte("[author]\nNAME = Unknwon"))), ) require.NoError(t, err) require.NotNil(t, f) diff --git a/struct.go b/struct.go index a486b2f..819a6e5 100644 --- a/struct.go +++ b/struct.go @@ -156,7 +156,7 @@ func wrapStrictError(err error, isStrict bool) error { // because we want to use default value that is already assigned to struct. func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error { vt := t - isPtr := t.Kind() == reflect.Ptr + isPtr := t.Kind() == reflect.Pointer if isPtr { vt = t.Elem() } @@ -278,7 +278,7 @@ func parseTagOptions(tag string) (rawName string, omitEmpty bool, allowShadow bo // mapToField maps the given value to the matching field of the given section. // The sectionIndex is the index (if non unique sections are enabled) to which the value should be added. func (s *Section) mapToField(val reflect.Value, isStrict bool, sectionIndex int, sectionName string) error { - if val.Kind() == reflect.Ptr { + if val.Kind() == reflect.Pointer { val = val.Elem() } typ := val.Type() @@ -299,8 +299,8 @@ func (s *Section) mapToField(val reflect.Value, isStrict bool, sectionIndex int, } isStruct := tpField.Type.Kind() == reflect.Struct - isStructPtr := tpField.Type.Kind() == reflect.Ptr && tpField.Type.Elem().Kind() == reflect.Struct - isAnonymousPtr := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous + isStructPtr := tpField.Type.Kind() == reflect.Pointer && tpField.Type.Elem().Kind() == reflect.Struct + isAnonymousPtr := tpField.Type.Kind() == reflect.Pointer && tpField.Anonymous if isAnonymousPtr { field.Set(reflect.New(tpField.Type.Elem())) } @@ -381,7 +381,7 @@ func (s *Section) mapToSlice(secName string, val reflect.Value, isStrict bool) ( func (s *Section) mapTo(v interface{}, isStrict bool) error { typ := reflect.TypeOf(v) val := reflect.ValueOf(v) - if typ.Kind() == reflect.Ptr { + if typ.Kind() == reflect.Pointer { typ = typ.Elem() val = val.Elem() } else { @@ -500,13 +500,13 @@ func reflectSliceWithProperType(key *Key, field reflect.Value, delim string, all case reflect.String: buf.WriteString(slice.Index(i).String()) case reflect.Int, reflect.Int64: - buf.WriteString(fmt.Sprint(slice.Index(i).Int())) + fmt.Fprint(&buf, slice.Index(i).Int()) case reflect.Uint, reflect.Uint64: - buf.WriteString(fmt.Sprint(slice.Index(i).Uint())) + fmt.Fprint(&buf, slice.Index(i).Uint()) case reflect.Float64: - buf.WriteString(fmt.Sprint(slice.Index(i).Float())) + fmt.Fprint(&buf, slice.Index(i).Float()) case reflect.Bool: - buf.WriteString(fmt.Sprint(slice.Index(i).Bool())) + fmt.Fprint(&buf, slice.Index(i).Bool()) case reflectTime: buf.WriteString(slice.Index(i).Interface().(time.Time).Format(time.RFC3339)) default: @@ -535,7 +535,7 @@ func reflectWithProperType(t reflect.Type, key *Key, field reflect.Value, delim key.SetValue(fmt.Sprint(field.Interface().(time.Time).Format(time.RFC3339))) case reflect.Slice: return reflectSliceWithProperType(key, field, delim, allowShadow) - case reflect.Ptr: + case reflect.Pointer: if !field.IsNil() { return reflectWithProperType(t.Elem(), key, field.Elem(), delim, allowShadow) } @@ -559,7 +559,7 @@ func isEmptyValue(v reflect.Value) bool { return v.Uint() == 0 case reflect.Float32, reflect.Float64: return v.Float() == 0 - case reflect.Interface, reflect.Ptr: + case reflect.Interface, reflect.Pointer: return v.IsNil() case reflectTime: t, ok := v.Interface().(time.Time) @@ -574,7 +574,7 @@ type StructReflector interface { } func (s *Section) reflectFrom(val reflect.Value) error { - if val.Kind() == reflect.Ptr { + if val.Kind() == reflect.Pointer { val = val.Elem() } typ := val.Type() @@ -606,14 +606,14 @@ func (s *Section) reflectFrom(val reflect.Value) error { continue } - if extends && tpField.Anonymous && (tpField.Type.Kind() == reflect.Ptr || tpField.Type.Kind() == reflect.Struct) { + if extends && tpField.Anonymous && (tpField.Type.Kind() == reflect.Pointer || tpField.Type.Kind() == reflect.Struct) { if err := s.reflectFrom(field); err != nil { return fmt.Errorf("reflect from field %q: %v", fieldName, err) } continue } - if (tpField.Type.Kind() == reflect.Ptr && tpField.Type.Elem().Kind() == reflect.Struct) || + if (tpField.Type.Kind() == reflect.Pointer && tpField.Type.Elem().Kind() == reflect.Struct) || (tpField.Type.Kind() == reflect.Struct && tpField.Type.Name() != "Time") { // Note: The only error here is section doesn't exist. sec, err := s.f.GetSection(fieldName) @@ -641,7 +641,7 @@ func (s *Section) reflectFrom(val reflect.Value) error { sliceOf := field.Type().Elem().Kind() for i := 0; i < field.Len(); i++ { - if sliceOf != reflect.Struct && sliceOf != reflect.Ptr { + if sliceOf != reflect.Struct && sliceOf != reflect.Pointer { return fmt.Errorf("field %q is not a slice of pointer or struct", fieldName) } @@ -688,11 +688,11 @@ func (s *Section) ReflectFrom(v interface{}) error { val := reflect.ValueOf(v) if s.name != DefaultSection && s.f.options.AllowNonUniqueSections && - (typ.Kind() == reflect.Slice || typ.Kind() == reflect.Ptr) { + (typ.Kind() == reflect.Slice || typ.Kind() == reflect.Pointer) { // Clear sections to make sure none exists before adding the new ones s.f.DeleteSection(s.name) - if typ.Kind() == reflect.Ptr { + if typ.Kind() == reflect.Pointer { sec, err := s.f.NewSection(s.name) if err != nil { return err @@ -702,7 +702,7 @@ func (s *Section) ReflectFrom(v interface{}) error { slice := val.Slice(0, val.Len()) sliceOf := val.Type().Elem().Kind() - if sliceOf != reflect.Ptr { + if sliceOf != reflect.Pointer { return fmt.Errorf("not a slice of pointers") } @@ -721,7 +721,7 @@ func (s *Section) ReflectFrom(v interface{}) error { return nil } - if typ.Kind() == reflect.Ptr { + if typ.Kind() == reflect.Pointer { val = val.Elem() } else { return errors.New("not a pointer to a struct") From 6d43f16c237151adcba200e3126310741a3125ac Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Sat, 2 May 2026 21:41:36 -0400 Subject: [PATCH 3/4] ci: only test on Go 1.26 Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b698f40..2e2b9fe 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -37,7 +37,7 @@ jobs: name: Test strategy: matrix: - go-version: [1.24.x, 1.25.x] + go-version: [1.26.x] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: From c2531d281d551861049a8e42934298325de7e09c Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Sat, 2 May 2026 21:42:35 -0400 Subject: [PATCH 4/4] 123 --- .golangci.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index fabbdb6..6937629 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -6,11 +6,6 @@ linters: - unconvert - unparam settings: - govet: - disable: - # printf: non-constant format string in call to fmt.Errorf (govet) - # showing up since golangci-lint version 1.60.1 - - printf nakedret: max-func-lines: 0 # Disallow any unnamed return statement exclusions: