Skip to content

Commit 96c3aae

Browse files
authored
Use better string splitting techniques where possible (#8226)
## What changed? This PR aims to avoid usage of [`strings.Split`](https://pkg.go.dev/strings#Split) where possible in favor of better string splitting techniques, speficially: [`strings.SplitN`](https://pkg.go.dev/strings#SplitN) and [`strings.SplitSeq`](https://pkg.go.dev/strings#SplitSeq) where appropriate. There was also a [`strings.Fields`](https://pkg.go.dev/strings#Fields) change I made to use [`strings.FieldsSeq`](https://pkg.go.dev/strings#FieldsSeq) instead, and another for S3 to use the [`path`](https://pkg.go.dev/path) package instead of [`strings.Split`](https://pkg.go.dev/strings#Split). ## Why? [`strings.SplitN`](https://pkg.go.dev/strings#SplitN) and [`strings.SplitSeq`](https://pkg.go.dev/strings#SplitSeq) are often better options in many cases, and can be _partially_ detected using [`modernize`](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize): > `stringsseq`: replace Split in "for range strings.Split(...)" by go1.24's more efficient `SplitSeq`, or `Fields` with `FieldSeq`. ## How did you test it? - [X] built - [X] run locally and tested manually - [x] covered by existing tests - [x] added new unit test(s) - [ ] added new functional test(s) ## Potential risks There are lots of potentially subtle behaviors from the `strings.Split` (and `strings.Fields`) usage that should be accounted for. If our existing tests don't cover those subtleties, there's risk for introducing an unintended bug. More intricate handling/parsing previously using the `strings` package should get extra attention from reviewers. I've attempted to break up my changes into logical commit chunks to aid in review / help spot potentially concerning changes.
1 parent ebcc3fd commit 96c3aae

File tree

22 files changed

+312
-80
lines changed

22 files changed

+312
-80
lines changed

cmd/tools/codegen/helpers.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,14 @@ func CamelCaseToSnakeCase(s string) string {
8181
}
8282

8383
func SnakeCaseToPascalCase(s string) string {
84-
// Split the string by underscores
85-
words := strings.Split(s, "_")
86-
87-
// Capitalize the first letter of each word
88-
for i, word := range words {
84+
var b strings.Builder
85+
// Capitalize the first letter of each word split by underscore
86+
for word := range strings.SplitSeq(s, "_") {
8987
// Convert first rune to upper and the rest to lower case
90-
words[i] = cases.Title(language.AmericanEnglish).String(strings.ToLower(word))
88+
b.WriteString(cases.Title(language.AmericanEnglish).String(strings.ToLower(word)))
9189
}
92-
9390
// Join them back into a single string
94-
return strings.Join(words, "")
91+
return b.String()
9592
}
9693

9794
func IsASCIIUpper(c rune) bool {

cmd/tools/codegen/helpers_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package codegen
2+
3+
import "testing"
4+
5+
func TestSnakeCaseToPascalCase(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
in string
9+
want string
10+
}{
11+
{name: "empty", in: "", want: ""},
12+
{name: "single_lower", in: "a", want: "A"},
13+
{name: "single_upper", in: "A", want: "A"},
14+
{name: "simple", in: "hello", want: "Hello"},
15+
{name: "two_words", in: "hello_world", want: "HelloWorld"},
16+
{name: "all_caps", in: "HELLO_WORLD", want: "HelloWorld"},
17+
{name: "leading_underscore", in: "_leading", want: "Leading"},
18+
{name: "trailing_underscore", in: "trailing_", want: "Trailing"},
19+
{name: "double_underscore", in: "a__b", want: "AB"},
20+
{name: "only_underscores", in: "__", want: ""},
21+
{name: "common_id", in: "user_id", want: "UserId"},
22+
{name: "with_digits", in: "http_server_v2", want: "HttpServerV2"},
23+
}
24+
25+
for _, tt := range tests {
26+
t.Run(tt.name, func(t *testing.T) {
27+
got := SnakeCaseToPascalCase(tt.in)
28+
if got != tt.want {
29+
t.Fatalf("SnakeCaseToPascalCase(%q) = %q, want %q", tt.in, got, tt.want)
30+
}
31+
})
32+
}
33+
}

cmd/tools/getproto/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func findProtoImports() []string {
4141
if d.Type().IsRegular() && strings.HasSuffix(path, ".proto") {
4242
protoFile, err := os.ReadFile(path)
4343
fatalIfErr(err)
44-
for _, line := range strings.Split(string(protoFile), "\n") {
44+
for line := range strings.SplitSeq(string(protoFile), "\n") {
4545
if match := matchImport.FindStringSubmatch(line); len(match) > 0 {
4646
i := match[1]
4747
if strings.HasPrefix(i, "temporal/api/") ||

common/archiver/gcloud/util.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ func newRunIDPrecondition(runID string) connector.Precondition {
152152
}
153153

154154
if strings.Contains(fileName, runID) {
155-
fileNameParts := strings.Split(fileName, "_")
155+
fileNameParts := strings.SplitN(fileName, "_", 5)
156156
if len(fileNameParts) != 5 {
157157
return true
158158
}
@@ -176,7 +176,7 @@ func newWorkflowIDPrecondition(workflowID string) connector.Precondition {
176176
}
177177

178178
if strings.Contains(fileName, workflowID) {
179-
fileNameParts := strings.Split(fileName, "_")
179+
fileNameParts := strings.SplitN(fileName, "_", 5)
180180
if len(fileNameParts) != 5 {
181181
return true
182182
}
@@ -200,7 +200,7 @@ func newWorkflowTypeNamePrecondition(workflowTypeName string) connector.Precondi
200200
}
201201

202202
if strings.Contains(fileName, workflowTypeName) {
203-
fileNameParts := strings.Split(fileName, "_")
203+
fileNameParts := strings.SplitN(fileName, "_", 5)
204204
if len(fileNameParts) != 5 {
205205
return true
206206
}

common/archiver/s3store/visibility_archiver.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package s3store
22

33
import (
44
"context"
5+
"path"
56
"strings"
67
"time"
78

@@ -223,11 +224,17 @@ func (v *visibilityArchiver) queryAll(
223224
nextPageToken: nextPageToken,
224225
parsedQuery: &parsedQuery{},
225226
}, saTypeMap, searchPrefix, func(key string) bool {
226-
// We only want to return entries for the closeTimeout secondary index, which will always be of the form:
227-
// .../closeTimeout/<closeTimeout>/<runID>, so we split the key on "/" and check that the third-to-last
228-
// element is "closeTimeout".
229-
elements := strings.Split(key, "/")
230-
return len(elements) >= 3 && elements[len(elements)-3] == secondaryIndexKeyCloseTimeout
227+
// We only want to return entries for the closeTimeout secondary index. Keys for this
228+
// index are always of the form:
229+
// .../closeTimeout/<timestamp>/<runID>
230+
// Walk from the end instead of splitting the whole string to avoid unnecessary
231+
// allocations and to keep the logic clear:
232+
// - drop <runID>
233+
// - drop <timestamp>
234+
// - check the remaining last segment equals "closeTimeout".
235+
dir := path.Dir(key) // drop runID
236+
dir = path.Dir(dir) // drop <timestamp>
237+
return path.Base(dir) == secondaryIndexKeyCloseTimeout
231238
})
232239
if err != nil {
233240
return nil, err

common/authorization/default_jwt_claim_mapper.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func (a *defaultJWTClaimMapper) extractPermissions(permissions []interface{}, cl
125125
}
126126
parts = []string{match[a.matchNamespaceIndex], match[a.matchRoleIndex]}
127127
} else {
128-
parts = strings.Split(p, ":")
128+
parts = strings.SplitN(p, ":", 2)
129129
if len(parts) != 2 {
130130
a.logger.Warn(fmt.Sprintf("ignoring permission in unexpected format: %v", permission))
131131
continue

common/headers/version_checker.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package headers
22

33
import (
44
"context"
5-
"slices"
65
"strings"
76

87
"github.com/blang/semver/v4"
@@ -152,8 +151,15 @@ func (vc *versionChecker) ClientSupported(ctx context.Context) error {
152151
// given feature (which should be one of the Feature... constants above).
153152
func (vc *versionChecker) ClientSupportsFeature(ctx context.Context, feature string) bool {
154153
headers := GetValues(ctx, SupportedFeaturesHeaderName)
155-
clientFeatures := strings.Split(headers[0], SupportedFeaturesHeaderDelim)
156-
return slices.Contains(clientFeatures, feature)
154+
if len(headers) == 0 {
155+
return false
156+
}
157+
for clientFeature := range strings.SplitSeq(headers[0], SupportedFeaturesHeaderDelim) {
158+
if clientFeature == feature {
159+
return true
160+
}
161+
}
162+
return false
157163
}
158164

159165
func mustParseRanges(ranges map[string]string) map[string]semver.Range {

common/persistence/data_interfaces.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,15 +1385,16 @@ func BuildHistoryGarbageCleanupInfo(namespaceID, workflowID, runID string) strin
13851385

13861386
// SplitHistoryGarbageCleanupInfo returns workflow identity information
13871387
func SplitHistoryGarbageCleanupInfo(info string) (namespaceID, workflowID, runID string, err error) {
1388-
ss := strings.Split(info, ":")
1389-
// workflowID can contain ":" so len(ss) can be greater than 3
1390-
if len(ss) < numItemsInGarbageInfo {
1391-
return "", "", "", fmt.Errorf("not able to split info for %s", info)
1392-
}
1393-
namespaceID = ss[0]
1394-
runID = ss[len(ss)-1]
1395-
workflowEnd := len(info) - len(runID) - 1
1396-
workflowID = info[len(namespaceID)+1 : workflowEnd]
1388+
// Expect format: namespaceID:workflowID:runID, but workflowID may contain ':' so we
1389+
// take everything between the first and last ':' as workflowID.
1390+
first := strings.IndexByte(info, ':')
1391+
last := strings.LastIndexByte(info, ':')
1392+
if first < 0 || first == last { // need at least two ':' to have 3 parts
1393+
return "", "", "", fmt.Errorf("not able to split info for %s", info)
1394+
}
1395+
namespaceID = info[:first]
1396+
workflowID = info[first+1 : last]
1397+
runID = info[last+1:]
13971398
return
13981399
}
13991400

common/persistence/history_task_queue_manager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ func GetHistoryTaskQueueName(
248248
}
249249

250250
func GetHistoryTaskQueueCategoryID(queueName string) (int, error) {
251-
fields := strings.Split(queueName, "_")
251+
fields := strings.SplitN(queueName, "_", 4)
252252
if len(fields) != 4 {
253253
return 0, fmt.Errorf("%w: %s", ErrInvalidQueueName, queueName)
254254
}

common/persistence/nosql/nosqlplugin/cassandra/gocql/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ func ConfigureCassandraCluster(cfg config.Cassandra, cluster *gocql.ClusterConfi
173173
// parseHosts returns parses a list of hosts separated by comma
174174
func parseHosts(input string) []string {
175175
var hosts []string
176-
for _, h := range strings.Split(input, ",") {
176+
for h := range strings.SplitSeq(input, ",") {
177177
if host := strings.TrimSpace(h); len(host) > 0 {
178178
hosts = append(hosts, host)
179179
}

0 commit comments

Comments
 (0)