diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index 27273bd..7165e0f 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -21,7 +21,7 @@ jobs: check-latest: true cache: true - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v8 with: version: latest only-new-issues: true @@ -37,22 +37,21 @@ jobs: go_version: ['oldstable', 'stable' ] steps: - - name: Run unit tests - uses: actions/setup-go@v5 + - uses: actions/setup-go@v5 with: go-version: '${{ matrix.go_version }}' check-latest: true cache: true - uses: actions/checkout@v4 - - - run: go test -v -race -coverprofile="coverage-${{ matrix.os }}.${{ matrix.go_version }}.out" -covermode=atomic -coverpkg=$(go list)/... ./... + - name: Run unit tests + shell: bash + run: go test -v -race -coverprofile="coverage-${{ matrix.os }}.${{ matrix.go_version }}.out" -covermode=atomic -coverpkg=$(go list)/... ./... - name: Upload coverage to codecov uses: codecov/codecov-action@v5 with: files: './coverage-${{ matrix.os }}.${{ matrix.go_version }}.out' - flags: '${{ matrix.go_version }}' - os: '${{ matrix.os }}' + flags: '${{ matrix.go_version }}-${{ matrix.os }}' fail_ci_if_error: false verbose: true diff --git a/.golangci.yml b/.golangci.yml index d2fafb8..5006306 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,56 +1,62 @@ -linters-settings: - gocyclo: - min-complexity: 45 - dupl: - threshold: 200 - goconst: - min-len: 2 - min-occurrences: 3 - +version: "2" linters: - enable-all: true + default: all disable: - - recvcheck - - unparam - - lll - - gochecknoinits - - gochecknoglobals + - cyclop + - depguard + - errchkjson + - errorlint + - exhaustruct + - forcetypeassert - funlen - - godox + - gochecknoglobals + - gochecknoinits - gocognit - - whitespace - - wsl - - wrapcheck - - testpackage - - nlreturn - - errorlint - - nestif - godot - - gofumpt + - godox + - gosmopolitan + - inamedparam + - ireturn + - lll + - musttag + - nestif + - nlreturn + - nonamedreturns - paralleltest - - tparallel + - testpackage - thelper - - exhaustruct + - tparallel + - unparam - varnamelen - - gci - - depguard - - errchkjson - - inamedparam - - nonamedreturns - - musttag - - ireturn - - forcetypeassert - - cyclop - # deprecated linters - #- deadcode - #- interfacer - #- scopelint - #- varcheck - #- structcheck - #- golint - #- nosnakecase - #- maligned - #- goerr113 - #- ifshort - #- gomnd - #- exhaustivestruct + - whitespace + - wrapcheck + - wsl + settings: + dupl: + threshold: 200 + goconst: + min-len: 2 + min-occurrences: 3 + gocyclo: + min-complexity: 45 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/pointer.go b/pointer.go index a08cd68..6136210 100644 --- a/pointer.go +++ b/pointer.go @@ -179,6 +179,11 @@ func getSingleImpl(node any, decodedToken string, nameProvider *swag.NameProvide func setSingleImpl(node, data any, decodedToken string, nameProvider *swag.NameProvider) error { rValue := reflect.Indirect(reflect.ValueOf(node)) + // Check for nil to prevent panic when calling rValue.Type() + if isNil(node) { + return fmt.Errorf("cannot set field %q on nil value: %w", decodedToken, ErrPointer) + } + if ns, ok := node.(JSONSetable); ok { // pointer impl return ns.JSONSet(decodedToken, data) } @@ -285,6 +290,11 @@ func (p *Pointer) set(node, data any, nameProvider *swag.NameProvider) error { return setSingleImpl(node, data, decodedToken, nameProvider) } + // Check for nil during traversal + if isNil(node) { + return fmt.Errorf("cannot traverse through nil value at %q: %w", decodedToken, ErrPointer) + } + rValue := reflect.Indirect(reflect.ValueOf(node)) kind := rValue.Kind() diff --git a/pointer_test.go b/pointer_test.go index 8cac301..b1b382b 100644 --- a/pointer_test.go +++ b/pointer_test.go @@ -644,6 +644,94 @@ func TestSetNode(t *testing.T) { assert.Equal(t, 999, setDoc.Coll.Items[0].C) }) }) + + t.Run("with nil traversal panic", func(t *testing.T) { + // This test exposes the panic that occurs when trying to set a value + // through a path that contains nil intermediate values + data := map[string]any{ + "level1": map[string]any{ + "level2": map[string]any{ + "level3": nil, // This nil causes the panic + }, + }, + } + + ptr, err := New("/level1/level2/level3/value") + require.NoError(t, err) + + // This should return an error, not panic + _, err = ptr.Set(data, "test-value") + + // The library should handle this gracefully and return an error + // instead of panicking + require.Error(t, err, "Setting value through nil intermediate path should return an error, not panic") + }) + + t.Run("with direct nil map value", func(t *testing.T) { + // Simpler test case that directly tests nil traversal + data := map[string]any{ + "container": nil, + } + + ptr, err := New("/container/nested/value") + require.NoError(t, err) + + // Attempting to traverse through nil should return an error, not panic + _, err = ptr.Set(data, "test") + require.Error(t, err, "Cannot traverse through nil intermediate values") + }) + + t.Run("with nil in nested structure", func(t *testing.T) { + // Test case with multiple nil values in nested structure + data := map[string]any{ + "config": map[string]any{ + "settings": nil, + }, + "data": map[string]any{ + "nested": map[string]any{ + "properties": map[string]any{ + "attributes": nil, // Nil intermediate value + }, + }, + }, + } + + ptr, err := New("/data/nested/properties/attributes/name") + require.NoError(t, err) + + // Should return error, not panic + _, err = ptr.Set(data, "test-name") + require.Error(t, err, "Setting through nil intermediate path should return error") + }) + + t.Run("with path creation through nil intermediate", func(t *testing.T) { + // Test case that simulates path creation functions encountering nil + // This happens when tools try to create missing paths but encounter nil intermediate values + data := map[string]any{ + "spec": map[string]any{ + "template": nil, // This blocks path creation attempts + }, + } + + // Attempting to create a path like /spec/template/metadata/labels should fail gracefully + ptr, err := New("/spec/template/metadata") + require.NoError(t, err) + + // Should return error when trying to set on nil intermediate during path creation + _, err = ptr.Set(data, map[string]any{"labels": map[string]any{}}) + require.Error(t, err, "Setting on nil intermediate during path creation should return error") + }) + + t.Run("with SetForToken on nil", func(t *testing.T) { + // Test the single-level SetForToken function with nil + data := map[string]any{ + "container": nil, + } + + // Should handle nil gracefully at single token level + _, err := SetForToken(data["container"], "nested", "value") + require.Error(t, err, "SetForToken on nil should return error, not panic") + }) } func TestOffset(t *testing.T) {