Skip to content

Commit 34ac760

Browse files
authored
Merge pull request #833 from jakobmoellerdev/optional-checking
fix: various fixes on static type analysis in our structural type checking
2 parents a472f6b + a358f88 commit 34ac760

File tree

6 files changed

+567
-29
lines changed

6 files changed

+567
-29
lines changed

.golangci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ linters:
3434
- builtin$
3535
- examples$
3636
settings:
37+
goconst:
38+
ignore-string-values: ["true","false"]
3739
staticcheck:
3840
checks:
3941
- -QF1008

pkg/cel/compatibility.go

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ const (
2929
TypeNamePrefix = "__type_"
3030
)
3131

32-
// AreTypesStructurallyCompatible checks if two CEL types are structurally compatible,
33-
// ignoring type names and using duck-typing semantics.
32+
// AreTypesStructurallyCompatible checks if an output type from an expected or executed CEL expression is compatible
33+
// with an expected type.
3434
//
3535
// This performs deep structural comparison:
3636
// - For primitives: checks kind equality
3737
// - For lists: recursively checks element type compatibility
3838
// - For maps: recursively checks key and value type compatibility
3939
// - For structs: uses DeclTypeProvider to introspect fields and check all required fields exist with compatible types
40+
// - For map → struct and struct → map compatibility if fields/keys are structurally compatible
4041
//
4142
// The provider is required for introspecting struct field information.
4243
// Returns true if types are compatible, false otherwise. If false, the error describes why.
@@ -50,20 +51,28 @@ func AreTypesStructurallyCompatible(output, expected *cel.Type, provider *DeclTy
5051
return true, nil
5152
}
5253

53-
// Kinds must match
54-
if output.Kind() != expected.Kind() {
55-
return false, fmt.Errorf("type kind mismatch: got %q, expected %q", output.String(), expected.String())
54+
// Unwrap optional output if available
55+
if output.Kind() == cel.OpaqueKind && output.TypeName() == "optional_type" {
56+
return AreTypesStructurallyCompatible(output.Parameters()[0], expected, provider)
5657
}
5758

58-
switch expected.Kind() {
59-
case cel.ListKind:
59+
switch {
60+
case expected.Kind() == cel.StructKind && output.Kind() == cel.MapKind:
61+
return areMapTypesAssignableToStruct(output, expected, provider)
62+
case expected.Kind() == cel.MapKind && output.Kind() == cel.StructKind:
63+
return areStructTypesAssignableToMap(output, expected, provider)
64+
case expected.Kind() == cel.ListKind:
6065
return areListTypesCompatible(output, expected, provider)
61-
case cel.MapKind:
66+
case expected.Kind() == cel.MapKind:
6267
return areMapTypesCompatible(output, expected, provider)
63-
case cel.StructKind:
68+
case expected.Kind() == cel.StructKind:
6469
return areStructTypesCompatible(output, expected, provider)
6570
default:
66-
// For primitives (int, string, bool, etc.), kind equality is enough
71+
// Kinds must match otherwise
72+
if output.Kind() != expected.Kind() {
73+
return false, fmt.Errorf("type kind mismatch: got %q, expected %q", output.String(), expected.String())
74+
}
75+
// primitives: kind equality already checked
6776
return true, nil
6877
}
6978
}
@@ -219,6 +228,10 @@ func areStructFieldsCompatible(output, expected *apiservercel.DeclType, provider
219228
if expected == nil {
220229
return true, nil
221230
}
231+
// PreserveUnknownFields is set on the expected type, so everything we would pass from output would be okay
232+
if expected.Metadata[XKubernetesPreserveUnknownFields] == "true" {
233+
return true, nil
234+
}
222235

223236
if output == nil {
224237
return false, fmt.Errorf("output type is nil")
@@ -268,3 +281,70 @@ func areStructFieldsCompatible(output, expected *apiservercel.DeclType, provider
268281

269282
return true, nil
270283
}
284+
285+
func areMapTypesAssignableToStruct(outputMap, expectedStruct *cel.Type, provider *DeclTypeProvider) (bool, error) {
286+
expectedDecl := resolveDeclTypeFromPath(expectedStruct.String(), provider)
287+
if expectedDecl == nil || expectedDecl.Fields == nil {
288+
return true, nil
289+
}
290+
291+
// map parameters are [keyType, valueType]
292+
params := outputMap.Parameters()
293+
if len(params) < 2 {
294+
return false, fmt.Errorf("map must have key and value types")
295+
}
296+
297+
keyType := params[0]
298+
valType := params[1]
299+
300+
// keys must be strings to match struct field names
301+
if keyType.Kind() != cel.StringKind {
302+
return false, fmt.Errorf("map keys must be strings to assign to struct")
303+
}
304+
305+
for fieldName, expectedField := range expectedDecl.Fields {
306+
expectedFieldCEL := expectedField.Type.CelType()
307+
if expectedFieldCEL == nil {
308+
continue
309+
}
310+
compatible, err := AreTypesStructurallyCompatible(valType, expectedFieldCEL, provider)
311+
if !compatible {
312+
return false, fmt.Errorf("map value incompatible with struct field %q: %w", fieldName, err)
313+
}
314+
}
315+
316+
return true, nil
317+
}
318+
319+
func areStructTypesAssignableToMap(outputStruct, expectedMap *cel.Type, provider *DeclTypeProvider) (bool, error) {
320+
outputDecl := resolveDeclTypeFromPath(outputStruct.String(), provider)
321+
if outputDecl == nil || outputDecl.Fields == nil {
322+
return true, nil
323+
}
324+
325+
params := expectedMap.Parameters()
326+
if len(params) < 2 {
327+
return false, fmt.Errorf("expected map must have key and value types")
328+
}
329+
keyType := params[0]
330+
valType := params[1]
331+
332+
// struct field names map to string keys
333+
if keyType.Kind() != cel.StringKind {
334+
return false, fmt.Errorf("map key type must be string when assigning struct → map")
335+
}
336+
337+
for fieldName, outputField := range outputDecl.Fields {
338+
outputCEL := outputField.Type.CelType()
339+
if outputCEL == nil {
340+
continue
341+
}
342+
343+
compatible, err := AreTypesStructurallyCompatible(outputCEL, valType, provider)
344+
if !compatible {
345+
return false, fmt.Errorf("struct field %q incompatible with map value type: %w", fieldName, err)
346+
}
347+
}
348+
349+
return true, nil
350+
}

0 commit comments

Comments
 (0)