diff --git a/assertion/function/assertiontree/backprop.go b/assertion/function/assertiontree/backprop.go index 07f3c2a0..a4c1e4d9 100644 --- a/assertion/function/assertiontree/backprop.go +++ b/assertion/function/assertiontree/backprop.go @@ -486,6 +486,18 @@ func backpropAcrossRange(rootNode *RootAssertionNode, lhs []ast.Expr, rhs ast.Ex if typeIsString(rhsType) { // This checks if we are ranging over a string produceAsIndex(1) // If we are ranging over a string, then the second lhs operand is also non-nil } else { + // Ranging over a pointer to an array with a non-blank second iteration variable + // implicitly dereferences the pointer to read the elements, so it must be non-nil. + // All other range forms over a pointer to an array are nil-safe: with at most one + // (possibly blank) iteration variable the range expression is not even evaluated, + // since its length is a constant. + if typeshelper.IsDeeplyPtr(rhsType) && !asthelper.IsEmptyExpr(lhs[1]) { + rootNode.AddConsumption(&annotation.ConsumeTrigger{ + Annotation: &annotation.PtrLoad{ConsumeTriggerTautology: &annotation.ConsumeTriggerTautology{}}, + Expr: rhs, + Guards: guard.NoGuards(), + }) + } produceAsDeepRHS(1) // If we are not ranging over a string, then we cannot assume basic type } case 1: diff --git a/assertion/function/assertiontree/root_assertion_node.go b/assertion/function/assertiontree/root_assertion_node.go index d6da6e4b..303df955 100644 --- a/assertion/function/assertiontree/root_assertion_node.go +++ b/assertion/function/assertiontree/root_assertion_node.go @@ -531,12 +531,21 @@ func (r *RootAssertionNode) AddGuardMatch(expr ast.Expr, behavior GuardMatchBeha func (r *RootAssertionNode) consumeIndexExpr(expr ast.Expr) { t := r.Pass().TypesInfo.Types[expr].Type - if typeshelper.IsDeeplySlice(t) { + switch { + case typeshelper.IsDeeplySlice(t): r.AddConsumption(&annotation.ConsumeTrigger{ Annotation: &annotation.SliceAccess{ConsumeTriggerTautology: &annotation.ConsumeTriggerTautology{}}, Expr: expr, Guards: guard.NoGuards(), }) + case typeshelper.IsDeeplyPtr(t): + // The only pointer type that can be indexed is a pointer to an array, which implicitly + // dereferences the pointer: p[i] is shorthand for (*p)[i]. + r.AddConsumption(&annotation.ConsumeTrigger{ + Annotation: &annotation.PtrLoad{ConsumeTriggerTautology: &annotation.ConsumeTriggerTautology{}}, + Expr: expr, + Guards: guard.NoGuards(), + }) } } @@ -846,12 +855,20 @@ func (r *RootAssertionNode) AddComputation(expr ast.Expr) { case *ast.SliceExpr: // similar to index case - // safe slicing contains b[:0] b[0:0] b[0:] b[:] b[:0:0] b[0:0:0] and length-bounded forms such - // as b[:len(b)], which are safe even when b is nil, so we do not create consumer triggers for - // those slicing. - if !r.isSafeSlicing(expr) { - // For all the other slicing, the slice must be nonnil, so we create a consumer - // trigger. + if typeshelper.IsDeeplyPtr(r.Pass().TypesInfo.TypeOf(expr.X)) { + // Slicing a pointer to an array implicitly dereferences the pointer (p[low:high] is + // shorthand for (*p)[low:high]), so the pointer must be non-nil regardless of the + // indices -- even for forms like p[:0] that are safe on a nil slice. + r.AddConsumption(&annotation.ConsumeTrigger{ + Annotation: &annotation.PtrLoad{ConsumeTriggerTautology: &annotation.ConsumeTriggerTautology{}}, + Expr: expr.X, + Guards: guard.NoGuards(), + }) + } else if !r.isSafeSlicing(expr) { + // safe slicing contains b[:0] b[0:0] b[0:] b[:] b[:0:0] b[0:0:0] and length-bounded + // forms such as b[:len(b)], which are safe even when b is nil, so we do not create + // consumer triggers for those slicing. For all the other slicing, the slice must be + // nonnil, so we create a consumer trigger. r.AddConsumption(&annotation.ConsumeTrigger{ Annotation: &annotation.SliceAccess{ConsumeTriggerTautology: &annotation.ConsumeTriggerTautology{}}, Expr: expr.X, diff --git a/testdata/src/go.uber.org/slices/inference/arrayptr.go b/testdata/src/go.uber.org/slices/inference/arrayptr.go new file mode 100644 index 00000000..da2d1e4b --- /dev/null +++ b/testdata/src/go.uber.org/slices/inference/arrayptr.go @@ -0,0 +1,107 @@ +// Copyright (c) 2026 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package inference + +// Implicit uses of a pointer to an array dereference the pointer and panic if it is nil: +// slicing (p[low:high] is shorthand for (*p)[low:high], even for forms like p[:0] that are safe +// on a nil slice), indexing (p[i] is (*p)[i], both reads and writes), and ranging with a +// non-blank second iteration variable (which reads the elements). Forms that never read the +// elements are nil-safe: len(p) and cap(p) are constants, and range loops with at most one +// (possibly blank) iteration variable do not even evaluate the range expression. + +var arrayPtrDummy bool + +func nilArrayPtr() *[4]int { + if arrayPtrDummy { + return nil + } + return &[4]int{} +} + +func testArrayPtrSliceSafeForm() []int { + p := nilArrayPtr() + return p[:0] //want "dereferenced" +} + +func testArrayPtrSliceFull() []int { + p := nilArrayPtr() + return p[:] //want "dereferenced" +} + +func testArrayPtrSliceBounds() []int { + p := nilArrayPtr() + return p[1:3] //want "dereferenced" +} + +func testArrayPtrIndexRead() int { + p := nilArrayPtr() + return p[0] //want "dereferenced" +} + +func testArrayPtrIndexWrite() { + p := nilArrayPtr() + p[0] = 1 //want "dereferenced" +} + +func testArrayPtrRangeSecondVar() int { + p := nilArrayPtr() + sum := 0 + for _, v := range p { //want "dereferenced" + sum += v + } + return sum +} + +// Named pointer-to-array types behave just like *[4]int (their core type is what gets sliced). +type namedArrayPtr *[4]int + +func nilNamedArrayPtr() namedArrayPtr { + if arrayPtrDummy { + return nil + } + return &[4]int{} +} + +func testNamedArrayPtrSlice() []int { + p := nilNamedArrayPtr() + return p[:] //want "dereferenced" +} + +func testArrayPtrSafeUses() int { + p := nilArrayPtr() + n := len(p) // len of a pointer to an array is a constant; no dereference happens + for i := range p { + n += i + } + for i, _ := range p { // a blank second variable is equivalent to the one-variable form + n += i + } + for range p { + n++ + } + return n +} + +func testArrayPtrNilChecked() int { + p := nilArrayPtr() + if p == nil { + return 0 + } + for _, v := range p { + _ = v + } + p[0] = 1 + return p[0] + len(p[:]) + len(p[1:3]) +}