Ignoring fields with struct tag expr:"-" and make "in" return false for unexported fields (#806)

* allow ignoring struct member using a - in expr struct tag

* fix in operator

* simplify logic

* add regression test for #807

* fixes #807: make in operator return false for unexported struct fields

---------

Co-authored-by: Anton Medvedev <[email protected]>
This commit is contained in:
Diego Augusto Molina
2025-06-30 20:14:08 +02:00
committed by GitHub
co-authored by Anton Medvedev
parent fefc347053
commit 13992b0526
4 changed files with 130 additions and 13 deletions
+6 -2
View File
@@ -421,10 +421,14 @@ func get(params ...any) (out any, err error) {
fieldName := i.(string)
value := v.FieldByNameFunc(func(name string) bool {
field, _ := v.Type().FieldByName(name)
if field.Tag.Get("expr") == fieldName {
switch field.Tag.Get("expr") {
case "-":
return false
case fieldName:
return true
default:
return name == fieldName
}
return name == fieldName
})
if value.IsValid() {
return value.Interface(), nil
+14 -6
View File
@@ -6,11 +6,15 @@ import (
"github.com/expr-lang/expr/internal/deref"
)
func fieldName(field reflect.StructField) string {
if taggedName := field.Tag.Get("expr"); taggedName != "" {
return taggedName
func fieldName(field reflect.StructField) (string, bool) {
switch taggedName := field.Tag.Get("expr"); taggedName {
case "-":
return "", false
case "":
return field.Name, true
default:
return taggedName, true
}
return field.Name
}
func fetchField(t reflect.Type, name string) (reflect.StructField, bool) {
@@ -23,7 +27,7 @@ func fetchField(t reflect.Type, name string) (reflect.StructField, bool) {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Search all fields, even embedded structs.
if fieldName(field) == name {
if n, ok := fieldName(field); ok && n == name {
return field, true
}
}
@@ -69,7 +73,11 @@ func StructFields(t reflect.Type) map[string]Nature {
}
}
table[fieldName(f)] = Nature{
name, ok := fieldName(f)
if !ok {
continue
}
table[name] = Nature{
Type: f.Type,
FieldIndex: f.Index,
}
+99 -2
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"reflect"
"strings"
"sync"
"testing"
"time"
@@ -139,7 +140,86 @@ func ExampleEnv_tagged_field_names() {
fmt.Printf("%v", output)
// Output : Hello World
// Output: Hello World
}
func ExampleEnv_hidden_tagged_field_names() {
type Internal struct {
Visible string
Hidden string `expr:"-"`
}
type environment struct {
Visible string
Hidden string `expr:"-"`
HiddenInternal Internal `expr:"-"`
VisibleInternal Internal
}
env := environment{
Hidden: "First level secret",
HiddenInternal: Internal{
Visible: "Second level secret",
Hidden: "Also hidden",
},
VisibleInternal: Internal{
Visible: "Not a secret",
Hidden: "Hidden too",
},
}
hiddenValues := []string{
`Hidden`,
`HiddenInternal`,
`HiddenInternal.Visible`,
`HiddenInternal.Hidden`,
`VisibleInternal["Hidden"]`,
}
for _, expression := range hiddenValues {
output, err := expr.Eval(expression, env)
if err == nil || !strings.Contains(err.Error(), "cannot fetch") {
fmt.Printf("unexpected output: %v; err: %v\n", output, err)
return
}
fmt.Printf("%q is hidden as expected\n", expression)
}
visibleValues := []string{
`Visible`,
`VisibleInternal`,
`VisibleInternal["Visible"]`,
}
for _, expression := range visibleValues {
_, err := expr.Eval(expression, env)
if err != nil {
fmt.Printf("unexpected error: %v\n", err)
return
}
fmt.Printf("%q is visible as expected\n", expression)
}
testWithIn := []string{
`not ("Hidden" in $env)`,
`"Visible" in $env`,
`not ("Hidden" in VisibleInternal)`,
`"Visible" in VisibleInternal`,
}
for _, expression := range testWithIn {
val, err := expr.Eval(expression, env)
shouldBeTrue, ok := val.(bool)
if err != nil || !ok || !shouldBeTrue {
fmt.Printf("unexpected result; value: %v; error: %v\n", val, err)
return
}
}
// Output: "Hidden" is hidden as expected
// "HiddenInternal" is hidden as expected
// "HiddenInternal.Visible" is hidden as expected
// "HiddenInternal.Hidden" is hidden as expected
// "VisibleInternal[\"Hidden\"]" is hidden as expected
// "Visible" is visible as expected
// "VisibleInternal" is visible as expected
// "VisibleInternal[\"Visible\"]" is visible as expected
}
func ExampleAsKind() {
@@ -529,7 +609,7 @@ func ExamplePatch() {
}
fmt.Printf("%v", output)
// Output : Hello, you, world!
// Output: Hello, you, world!
}
func ExampleWithContext() {
@@ -2765,3 +2845,20 @@ func TestMemoryBudget(t *testing.T) {
})
}
}
func TestIssue807(t *testing.T) {
type MyStruct struct {
nonExported string
}
out, err := expr.Eval(` "nonExported" in $env `, MyStruct{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
b, ok := out.(bool)
if !ok {
t.Fatalf("expected boolean type, got %T: %v", b, b)
}
if b {
t.Fatalf("expected 'in' operator to return false for unexported field")
}
}
+11 -3
View File
@@ -65,10 +65,14 @@ func Fetch(from, i any) any {
fieldName := i.(string)
value := v.FieldByNameFunc(func(name string) bool {
field, _ := v.Type().FieldByName(name)
if field.Tag.Get("expr") == fieldName {
switch field.Tag.Get("expr") {
case "-":
return false
case fieldName:
return true
default:
return name == fieldName
}
return name == fieldName
})
if value.IsValid() {
return value.Interface()
@@ -213,7 +217,11 @@ func In(needle any, array any) bool {
if !n.IsValid() || n.Kind() != reflect.String {
panic(fmt.Sprintf("cannot use %T as field name of %T", needle, array))
}
value := v.FieldByName(n.String())
field, ok := v.Type().FieldByName(n.String())
if !ok || !field.IsExported() || field.Tag.Get("expr") == "-" {
return false
}
value := v.FieldByIndex(field.Index)
if value.IsValid() {
return true
}