Skip to content

Commit e831088

Browse files
committed
check JSON value in string literal passed to fromJSON
1 parent fbf47ea commit e831088

File tree

2 files changed

+62
-4
lines changed

2 files changed

+62
-4
lines changed

expr_sema.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package actionlint
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"regexp"
67
"strconv"
@@ -786,15 +787,15 @@ func checkFuncSignature(n *FuncCallNode, sig *FuncSignature, args []ExprType) *E
786787
return nil
787788
}
788789

789-
func (sema *ExprSemanticsChecker) checkBuiltinFunctionCall(n *FuncCallNode, _ *FuncSignature) {
790+
func (sema *ExprSemanticsChecker) checkBuiltinFuncCall(n *FuncCallNode, sig *FuncSignature) ExprType {
790791
sema.checkSpecialFunctionAvailability(n)
791792

792793
// Special checks for specific built-in functions
793794
switch strings.ToLower(n.Callee) {
794795
case "format":
795796
lit, ok := n.Args[0].(*StringNode)
796797
if !ok {
797-
return
798+
return sig.Ret
798799
}
799800
l := len(n.Args) - 1 // -1 means removing first format string argument
800801

@@ -817,7 +818,22 @@ func (sema *ExprSemanticsChecker) checkBuiltinFunctionCall(n *FuncCallNode, _ *F
817818
for i := range holders {
818819
sema.errorf(n, "format string %q contains placeholder {%d} but only %d arguments are given to format", lit.Value, i, l)
819820
}
821+
case "fromjson":
822+
lit, ok := n.Args[0].(*StringNode)
823+
if !ok {
824+
return sig.Ret
825+
}
826+
var v any
827+
err := json.Unmarshal([]byte(lit.Value), &v)
828+
if err == nil {
829+
return typeOfJSONValue(v)
830+
}
831+
if s, ok := err.(*json.SyntaxError); ok {
832+
sema.errorf(lit, "broken JSON string is passed to fromJSON() at offset %d: %s", s.Offset, s)
833+
}
820834
}
835+
836+
return sig.Ret
821837
}
822838

823839
func (sema *ExprSemanticsChecker) checkFuncCall(n *FuncCallNode) ExprType {
@@ -844,8 +860,7 @@ func (sema *ExprSemanticsChecker) checkFuncCall(n *FuncCallNode) ExprType {
844860
err := checkFuncSignature(n, sig, tys)
845861
if err == nil {
846862
// When one of overload pass type check, overload was resolved correctly
847-
sema.checkBuiltinFunctionCall(n, sig)
848-
return sig.Ret
863+
return sema.checkBuiltinFuncCall(n, sig)
849864
}
850865
errs = append(errs, err)
851866
}

expr_type.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,3 +427,46 @@ func (ty *ArrayType) DeepCopy() ExprType {
427427
func EqualTypes(l, r ExprType) bool {
428428
return l.Assignable(r) && r.Assignable(l)
429429
}
430+
431+
func typeOfJSONValue(v any) ExprType {
432+
// https://pkg.go.dev/encoding/json#Unmarshal
433+
// To unmarshal JSON into an interface value, Unmarshal stores one of these in the interface value:
434+
// - bool, for JSON booleans
435+
// - float64, for JSON numbers
436+
// - string, for JSON strings
437+
// - []interface{}, for JSON arrays
438+
// - map[string]interface{}, for JSON objects
439+
// - nil for JSON null
440+
switch v := v.(type) {
441+
case bool:
442+
return BoolType{}
443+
case float64:
444+
return NumberType{}
445+
case string:
446+
return StringType{}
447+
case []any:
448+
var elem ExprType
449+
for _, e := range v {
450+
t := typeOfJSONValue(e)
451+
if elem == nil {
452+
elem = t
453+
} else {
454+
elem = elem.Merge(t)
455+
}
456+
}
457+
if elem == nil {
458+
elem = AnyType{}
459+
}
460+
return &ArrayType{Elem: elem}
461+
case map[string]any:
462+
props := make(map[string]ExprType, len(v))
463+
for k, v := range v {
464+
props[k] = typeOfJSONValue(v)
465+
}
466+
return NewStrictObjectType(props)
467+
case nil:
468+
return NullType{}
469+
default:
470+
panic(v) // Unreachable
471+
}
472+
}

0 commit comments

Comments
 (0)