Skip to content

Commit c0d0751

Browse files
fix(config): pluralize names ending in consonant+y as 'ies' (#81)
* fix(config): pluralize names ending in consonant+y as 'ies' EnumLists used a simple suffix rule that handled x/s/z and ch/sh but ignored the y -> ies rule. Type names like Policy or HealthSeverity produced Policys and HealthSeveritys. Apply the standard English rule: consonant + y drops the y and appends ies, vowel + y just appends s. Move the heuristic into a pluralize helper and cover the cases with a unit test. * refactor(config): address review feedback - Drop redundant c := c loop-variable rebind (Go 1.22+ scoping). - Use testify/require.Equal in the table test. - Generalize pluralize godoc to not tie it to EnumLists. - Trim two redundant test cases. * Add testdata --------- Co-authored-by: Steven Masley <stevenmasley@gmail.com>
1 parent e8b786c commit c0d0751

8 files changed

Lines changed: 335 additions & 23 deletions

File tree

bindings/comments_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package bindings_test
2+
3+
import (
4+
"fmt"
5+
"go/token"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/guts/bindings"
11+
)
12+
13+
func TestSyntheticComments(t *testing.T) {
14+
b, err := bindings.New()
15+
require.NoError(t, err)
16+
17+
str := bindings.LiteralKeyword(bindings.KeywordString)
18+
param := &bindings.TypeParameter{
19+
Name: bindings.Identifier{
20+
Name: "testparam",
21+
Package: nil,
22+
Prefix: "",
23+
},
24+
Modifiers: nil,
25+
Type: &str,
26+
DefaultType: nil,
27+
//SupportComments: bindings.SupportComments{},
28+
}
29+
//param.LeadingComment("a type parameter")
30+
31+
exp := &bindings.Interface{
32+
Name: bindings.Identifier{
33+
Name: "TestingInterface",
34+
},
35+
Modifiers: nil,
36+
Fields: nil,
37+
Parameters: []*bindings.TypeParameter{
38+
param,
39+
},
40+
Heritage: nil,
41+
Source: bindings.Source{
42+
File: "test.go",
43+
Position: token.Position{
44+
Filename: "test.go",
45+
Offset: 0,
46+
Line: 5,
47+
Column: 10,
48+
},
49+
},
50+
}
51+
52+
exp.AppendComment(bindings.SyntheticComment{
53+
Leading: true,
54+
SingleLine: true,
55+
Text: "hello world",
56+
TrailingNewLine: false,
57+
})
58+
59+
exp.AppendComment(bindings.SyntheticComment{
60+
Leading: false,
61+
SingleLine: true,
62+
Text: "goodbye world",
63+
TrailingNewLine: false,
64+
})
65+
66+
node, err := b.ToTypescriptNode(exp)
67+
require.NoError(t, err)
68+
69+
ts, err := b.SerializeToTypescript(node)
70+
require.NoError(t, err)
71+
fmt.Println(ts)
72+
}

config/mutations.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,7 @@ func EnumLists(ts *guts.Typescript) {
158158
values = append(values, t)
159159
}
160160

161-
// Pluralize the name
162-
name := key + "s"
163-
switch key[len(key)-1] {
164-
case 'x', 's', 'z':
165-
name = key + "es"
166-
}
167-
if strings.HasSuffix(key, "ch") || strings.HasSuffix(key, "sh") {
168-
name = key + "es"
169-
}
161+
name := pluralize(key)
170162

171163
addNodes[name] = &bindings.VariableStatement{
172164
Modifiers: []bindings.Modifier{},
@@ -384,6 +376,35 @@ func InterfaceToType(ts *guts.Typescript) {
384376
})
385377
}
386378

379+
// pluralize applies a best-effort English pluralization rule to a name
380+
// (for example "Policy" -> "Policies", "Box" -> "Boxes", "User" -> "Users").
381+
//
382+
// The heuristic only handles common regular cases. It does not try to
383+
// recognize already-plural inputs ("Updates" -> "Updateses") or irregular
384+
// plurals ("Person" -> "Persons", not "People").
385+
func pluralize(key string) string {
386+
if key == "" {
387+
return key
388+
}
389+
if strings.HasSuffix(key, "ch") || strings.HasSuffix(key, "sh") {
390+
return key + "es"
391+
}
392+
switch key[len(key)-1] {
393+
case 'x', 's', 'z':
394+
return key + "es"
395+
case 'y':
396+
// consonant + y -> ies, vowel + y -> just s.
397+
if len(key) >= 2 && !isVowel(key[len(key)-2]) {
398+
return key[:len(key)-1] + "ies"
399+
}
400+
}
401+
return key + "s"
402+
}
403+
404+
func isVowel(b byte) bool {
405+
return strings.IndexByte("aeiouAEIOU", b) >= 0
406+
}
407+
387408
func isGoEnum(n bindings.Node) (*bindings.Alias, *bindings.UnionType, bool) {
388409
al, ok := n.(*bindings.Alias)
389410
if !ok {

config/pluralize_internal_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package config
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestPluralize(t *testing.T) {
10+
t.Parallel()
11+
12+
cases := []struct {
13+
in string
14+
want string
15+
}{
16+
// Default: just add "s".
17+
{"User", "Users"},
18+
{"Audience", "Audiences"},
19+
20+
// Ends in x, s, z: add "es".
21+
{"Box", "Boxes"},
22+
{"Bus", "Buses"},
23+
{"Buzz", "Buzzes"},
24+
25+
// Ends in ch, sh: add "es".
26+
{"Church", "Churches"},
27+
{"Bush", "Bushes"},
28+
29+
// Consonant + y: drop "y", add "ies".
30+
{"Policy", "Policies"},
31+
{"Category", "Categories"},
32+
{"Story", "Stories"},
33+
{"City", "Cities"},
34+
{"HealthSeverity", "HealthSeverities"},
35+
36+
// Vowel + y: just add "s".
37+
{"Day", "Days"},
38+
{"Key", "Keys"},
39+
{"Boy", "Boys"},
40+
41+
// Single-character edge cases.
42+
{"", ""},
43+
{"y", "ys"},
44+
{"A", "As"},
45+
}
46+
47+
for _, c := range cases {
48+
t.Run(c.in, func(t *testing.T) {
49+
t.Parallel()
50+
require.Equal(t, c.want, pluralize(c.in))
51+
})
52+
}
53+
}

go.mod

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/coder/guts
22

3-
go 1.24.0
3+
go 1.24.4
44

55
require (
66
github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd
@@ -11,16 +11,18 @@ require (
1111
)
1212

1313
require (
14+
github.com/Masterminds/semver/v3 v3.3.1 // indirect
1415
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
15-
github.com/dlclark/regexp2 v1.11.4 // indirect
16+
github.com/dlclark/regexp2 v1.11.5 // indirect
1617
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
18+
github.com/google/go-cmp v0.7.0 // indirect
1719
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
1820
github.com/kr/pretty v0.3.1 // indirect
1921
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
20-
github.com/rogpeppe/go-internal v1.10.0 // indirect
22+
github.com/rogpeppe/go-internal v1.14.1 // indirect
2123
golang.org/x/mod v0.31.0 // indirect
2224
golang.org/x/sync v0.19.0 // indirect
23-
golang.org/x/text v0.14.0 // indirect
25+
golang.org/x/text v0.32.0 // indirect
2426
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
2527
gopkg.in/yaml.v3 v3.0.1 // indirect
2628
)

go.sum

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
2-
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
1+
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
2+
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
33
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
44
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
55
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6-
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
7-
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
6+
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
7+
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
88
github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd h1:QMSNEh9uQkDjyPwu/J541GgSH+4hw+0skJDIj9HJ3mE=
99
github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
1010
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
1111
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
1212
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
1313
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
14-
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
15-
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
14+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
15+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
1616
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
1717
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
1818
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -26,16 +26,16 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK
2626
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
2727
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2828
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
29-
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
30-
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
29+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
30+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
3131
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
3232
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
3333
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
3434
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
3535
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
3636
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
37-
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
38-
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
37+
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
38+
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
3939
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
4040
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
4141
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=

0 commit comments

Comments
 (0)