From d946c05fdf3827395d9acfeb9ecd67d86c32f869 Mon Sep 17 00:00:00 2001 From: Zac Peschke Date: Thu, 10 Jul 2025 08:45:48 -0700 Subject: [PATCH] feat: add regex matching and substitution to stdlib --- builtins.go | 52 +++++++++++++++++++++++ linter/internal/types/stdlib.go | 2 + testdata/builtinRegexMatch.golden | 1 + testdata/builtinRegexMatch.jsonnet | 1 + testdata/builtinRegexMatch.linter.golden | 0 testdata/builtinRegexMatch2.golden | 1 + testdata/builtinRegexMatch2.jsonnet | 1 + testdata/builtinRegexMatch2.linter.golden | 0 testdata/builtinRegexSubst.golden | 1 + testdata/builtinRegexSubst.jsonnet | 1 + testdata/builtinRegexSubst.linter.golden | 0 testdata/builtinRegexSubst2.golden | 1 + testdata/builtinRegexSubst2.jsonnet | 1 + testdata/builtinRegexSubst2.linter.golden | 0 testdata/stdlib_smoke_test.golden | 2 + testdata/stdlib_smoke_test.jsonnet | 2 + 16 files changed, 66 insertions(+) create mode 100644 testdata/builtinRegexMatch.golden create mode 100644 testdata/builtinRegexMatch.jsonnet create mode 100644 testdata/builtinRegexMatch.linter.golden create mode 100644 testdata/builtinRegexMatch2.golden create mode 100644 testdata/builtinRegexMatch2.jsonnet create mode 100644 testdata/builtinRegexMatch2.linter.golden create mode 100644 testdata/builtinRegexSubst.golden create mode 100644 testdata/builtinRegexSubst.jsonnet create mode 100644 testdata/builtinRegexSubst.linter.golden create mode 100644 testdata/builtinRegexSubst2.golden create mode 100644 testdata/builtinRegexSubst2.jsonnet create mode 100644 testdata/builtinRegexSubst2.linter.golden diff --git a/builtins.go b/builtins.go index 6ecbd989e..102a76278 100644 --- a/builtins.go +++ b/builtins.go @@ -1428,6 +1428,56 @@ func builtinStrReplace(i *interpreter, strv, fromv, tov value) (value, error) { return makeValueString(strings.Replace(sStr, sFrom, sTo, -1)), nil } +func builtinRegexMatch(i *interpreter, regexv, strv value) (value, error) { + regex, err := i.getString(regexv) + if err != nil { + return nil, err + } + + str, err := i.getString(strv) + if err != nil { + return nil, err + } + + sRegex := regex.getGoString() + sStr := str.getGoString() + + match, err := regexp.MatchString(sRegex, sStr) + if err != nil { + return nil, err + } + + return makeValueBoolean(match), nil +} + +func builtinRegexSubst(i *interpreter, regexv, srcv, replv value) (value, error) { + regex, err := i.getString(regexv) + if err != nil { + return nil, err + } + + src, err := i.getString(srcv) + if err != nil { + return nil, err + } + + repl, err := i.getString(replv) + if err != nil { + return nil, err + } + + sRegex := regex.getGoString() + sSrc := src.getGoString() + sRepl := repl.getGoString() + + r, err := regexp.Compile(sRegex) + if err != nil { + return nil, err + } + + return makeValueString(r.ReplaceAllString(sSrc, sRepl)), nil +} + func builtinIsEmpty(i *interpreter, strv value) (value, error) { str, err := i.getString(strv) if err != nil { @@ -2865,6 +2915,8 @@ var funcBuiltins = buildBuiltinMap([]builtin{ &ternaryBuiltin{name: "splitLimit", function: builtinSplitLimit, params: ast.Identifiers{"str", "c", "maxsplits"}}, &ternaryBuiltin{name: "splitLimitR", function: builtinSplitLimitR, params: ast.Identifiers{"str", "c", "maxsplits"}}, &ternaryBuiltin{name: "strReplace", function: builtinStrReplace, params: ast.Identifiers{"str", "from", "to"}}, + &binaryBuiltin{name: "regexMatch", function: builtinRegexMatch, params: ast.Identifiers{"regex", "str"}}, + &ternaryBuiltin{name: "regexSubst", function: builtinRegexSubst, params: ast.Identifiers{"regex", "src", "repl"}}, &unaryBuiltin{name: "isEmpty", function: builtinIsEmpty, params: ast.Identifiers{"str"}}, &binaryBuiltin{name: "equalsIgnoreCase", function: builtinEqualsIgnoreCase, params: ast.Identifiers{"str1", "str2"}}, &unaryBuiltin{name: "trim", function: builtinTrim, params: ast.Identifiers{"str"}}, diff --git a/linter/internal/types/stdlib.go b/linter/internal/types/stdlib.go index b1afb2d02..a39940327 100644 --- a/linter/internal/types/stdlib.go +++ b/linter/internal/types/stdlib.go @@ -92,6 +92,8 @@ func prepareStdlib(g *typeGraph) { "splitLimit": g.newSimpleFuncType(arrayOfString, "str", "c", "maxsplits"), "splitLimitR": g.newSimpleFuncType(arrayOfString, "str", "c", "maxsplits"), "strReplace": g.newSimpleFuncType(stringType, "str", "from", "to"), + "regexMatch": g.newSimpleFuncType(boolType, "regex", "str"), + "regexSubst": g.newSimpleFuncType(stringType, "regex", "src", "repl"), "asciiUpper": g.newSimpleFuncType(stringType, "str"), "asciiLower": g.newSimpleFuncType(stringType, "str"), "stringChars": g.newSimpleFuncType(stringType, "str"), diff --git a/testdata/builtinRegexMatch.golden b/testdata/builtinRegexMatch.golden new file mode 100644 index 000000000..27ba77dda --- /dev/null +++ b/testdata/builtinRegexMatch.golden @@ -0,0 +1 @@ +true diff --git a/testdata/builtinRegexMatch.jsonnet b/testdata/builtinRegexMatch.jsonnet new file mode 100644 index 000000000..fa48582d4 --- /dev/null +++ b/testdata/builtinRegexMatch.jsonnet @@ -0,0 +1 @@ +std.regexMatch('\\d+', 'abc123def') diff --git a/testdata/builtinRegexMatch.linter.golden b/testdata/builtinRegexMatch.linter.golden new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/builtinRegexMatch2.golden b/testdata/builtinRegexMatch2.golden new file mode 100644 index 000000000..c508d5366 --- /dev/null +++ b/testdata/builtinRegexMatch2.golden @@ -0,0 +1 @@ +false diff --git a/testdata/builtinRegexMatch2.jsonnet b/testdata/builtinRegexMatch2.jsonnet new file mode 100644 index 000000000..9a0e00e1f --- /dev/null +++ b/testdata/builtinRegexMatch2.jsonnet @@ -0,0 +1 @@ +std.regexMatch('\\d+', 'abcdef') diff --git a/testdata/builtinRegexMatch2.linter.golden b/testdata/builtinRegexMatch2.linter.golden new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/builtinRegexSubst.golden b/testdata/builtinRegexSubst.golden new file mode 100644 index 000000000..1b00ed287 --- /dev/null +++ b/testdata/builtinRegexSubst.golden @@ -0,0 +1 @@ +"abc[123]def[456]ghi" diff --git a/testdata/builtinRegexSubst.jsonnet b/testdata/builtinRegexSubst.jsonnet new file mode 100644 index 000000000..08856105b --- /dev/null +++ b/testdata/builtinRegexSubst.jsonnet @@ -0,0 +1 @@ +std.regexSubst('([0-9]+)', 'abc123def456ghi', '[$1]') diff --git a/testdata/builtinRegexSubst.linter.golden b/testdata/builtinRegexSubst.linter.golden new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/builtinRegexSubst2.golden b/testdata/builtinRegexSubst2.golden new file mode 100644 index 000000000..cd4bc1ab6 --- /dev/null +++ b/testdata/builtinRegexSubst2.golden @@ -0,0 +1 @@ +"hello world" diff --git a/testdata/builtinRegexSubst2.jsonnet b/testdata/builtinRegexSubst2.jsonnet new file mode 100644 index 000000000..57a2764d4 --- /dev/null +++ b/testdata/builtinRegexSubst2.jsonnet @@ -0,0 +1 @@ +std.regexSubst('Hello', 'hello world', 'Hi') diff --git a/testdata/builtinRegexSubst2.linter.golden b/testdata/builtinRegexSubst2.linter.golden new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/stdlib_smoke_test.golden b/testdata/stdlib_smoke_test.golden index 3b2e86390..aa90e4aca 100644 --- a/testdata/stdlib_smoke_test.golden +++ b/testdata/stdlib_smoke_test.golden @@ -165,6 +165,8 @@ 4, 5 ], + "regexMatch": true, + "regexSubst": "abc[123]def[456]ghi", "repeat": "foofoofoo", "reverse": [ "a", diff --git a/testdata/stdlib_smoke_test.jsonnet b/testdata/stdlib_smoke_test.jsonnet index ca9ccf8bf..8334c7c13 100644 --- a/testdata/stdlib_smoke_test.jsonnet +++ b/testdata/stdlib_smoke_test.jsonnet @@ -76,6 +76,8 @@ splitLimit: std.splitLimit(str="a,b,c", c=",", maxsplits=1), splitLimitR: std.splitLimitR(str="a,b,c", c=",", maxsplits=1), strReplace: std.strReplace(str="aaa", from="aa", to="bb"), + regexMatch: std.regexMatch(regex="\\d+", str="abc123def"), + regexSubst: std.regexSubst(regex="([0-9]+)", src="abc123def456ghi", repl="[$1]"), asciiUpper: std.asciiUpper(str="Blah"), asciiLower: std.asciiLower(str="Blah"), stringChars: std.stringChars(str="blah"),