Skip to content

Commit 54c7a1d

Browse files
committed
feat(objc): add support for class properties in protocols
1 parent 38d8489 commit 54c7a1d

File tree

3 files changed

+115
-1
lines changed

3 files changed

+115
-1
lines changed

Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ dev-deps: ## Install the dev dependencies
1313
@go install github.com/goreleaser/chglog/cmd/chglog@latest
1414
@go install github.com/caarlos0/svu@v1.4.1
1515

16+
OBJC_SRC := internal/testdata/test.m
17+
OBJC_BIN := internal/testdata/objc_fixture
18+
CLANG := $(shell xcrun -f clang 2>/dev/null)
19+
SDK := $(shell xcrun --sdk macosx --show-sdk-path 2>/dev/null)
20+
21+
.PHONY: objc-fixture
22+
objc-fixture: ## Build the ObjC demo binary with protocol class properties (requires Xcode CLT)
23+
@if [ "$(CLANG)" = "" ]; then echo "xcrun clang not found; install Xcode Command Line Tools"; exit 1; fi
24+
@echo "Compiling $(OBJC_SRC) -> $(OBJC_BIN)"
25+
@$(CLANG) -fobjc-arc -isysroot "$(SDK)" -framework Foundation -o "$(OBJC_BIN)" "$(OBJC_SRC)"
26+
@echo "Built $(OBJC_BIN)"
27+
1628
.PHONY: bump
1729
bump: ## Incriment version patch number
1830
@echo " > Bumping VERSION"

types/objc/protocol.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type Protocol struct {
5656

5757
func (p *Protocol) dump(verbose, addrs bool) string {
5858
var props string
59+
var cProps string
5960
var optProps string
6061
var cMethods string
6162
var iMethods string
@@ -86,6 +87,34 @@ func (p *Protocol) dump(verbose, addrs bool) string {
8687
props += "\n"
8788
}
8889
}
90+
if len(p.ClassProperties) > 0 {
91+
for _, prop := range p.ClassProperties {
92+
if verbose {
93+
if attrs, optional := prop.Attributes(); !optional {
94+
// Ensure "class" appears in the attribute list and retain trailing space
95+
if attrs == "" {
96+
attrs = "(class) "
97+
} else {
98+
inner := strings.TrimSuffix(strings.TrimPrefix(attrs, "("), ") ")
99+
if inner == attrs { // fallback if format differs
100+
inner = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(attrs, "("), ")"))
101+
}
102+
attrs = fmt.Sprintf("(class, %s) ", inner)
103+
}
104+
cProps += fmt.Sprintf("@property %s%s%s;\n", attrs, prop.Type(), prop.Name)
105+
}
106+
} else {
107+
if prop.EncodedAttributes != "" {
108+
cProps += fmt.Sprintf("@property (class, %s) %s;\n", prop.EncodedAttributes, prop.Name)
109+
} else {
110+
cProps += fmt.Sprintf("@property (class) %s;\n", prop.Name)
111+
}
112+
}
113+
}
114+
if cProps != "" {
115+
cProps += "\n"
116+
}
117+
}
89118
if len(p.ClassMethods) > 0 {
90119
for _, meth := range p.ClassMethods {
91120
if verbose {
@@ -134,6 +163,29 @@ func (p *Protocol) dump(verbose, addrs bool) string {
134163
optProps += "\n"
135164
}
136165
}
166+
if len(p.ClassProperties) > 0 {
167+
for _, prop := range p.ClassProperties {
168+
if verbose {
169+
if attrs, optional := prop.Attributes(); optional {
170+
if attrs == "" {
171+
attrs = "(class) "
172+
} else {
173+
inner := strings.TrimSuffix(strings.TrimPrefix(attrs, "("), ") ")
174+
if inner == attrs {
175+
inner = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(attrs, "("), ")"))
176+
}
177+
attrs = fmt.Sprintf("(class, %s) ", inner)
178+
}
179+
optProps += fmt.Sprintf("@property %s%s%s;\n", attrs, prop.Type(), prop.Name)
180+
}
181+
} else {
182+
// optProps += fmt.Sprintf("@property (%s) %s;\n", prop.EncodedAttributes, prop.Name)
183+
}
184+
}
185+
if optProps != "" {
186+
// leave trailing newline managed above
187+
}
188+
}
137189
if len(p.OptionalInstanceMethods) > 0 {
138190
for _, meth := range p.OptionalInstanceMethods {
139191
if verbose {
@@ -154,7 +206,8 @@ func (p *Protocol) dump(verbose, addrs bool) string {
154206
return fmt.Sprintf(
155207
"%s\n\n"+
156208
"@required\n\n"+
157-
"%s"+
209+
"%s"+ // instance properties (required)
210+
"%s"+ // class properties (required)
158211
"%s"+
159212
"%s"+
160213
"@optional\n\n"+
@@ -163,6 +216,7 @@ func (p *Protocol) dump(verbose, addrs bool) string {
163216
"@end\n",
164217
protocol,
165218
props,
219+
cProps,
166220
cMethods,
167221
iMethods,
168222
optProps,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package objc
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestProtocolDump_ClassProperties(t *testing.T) {
9+
p := &Protocol{
10+
Name: "P",
11+
ClassProperties: []Property{
12+
// required class property
13+
{
14+
PropertyT: PropertyT{},
15+
Name: "classProp",
16+
EncodedAttributes: "T@,N", // id, nonatomic
17+
},
18+
// optional class property
19+
{
20+
PropertyT: PropertyT{},
21+
Name: "optProp",
22+
EncodedAttributes: "Tq,N,?", // long long, nonatomic, optional
23+
},
24+
},
25+
}
26+
27+
out := p.Verbose()
28+
29+
// Required class property should appear under @required and be marked as class
30+
reqIdx := strings.Index(out, "@required")
31+
if reqIdx < 0 {
32+
t.Fatalf("expected @required section in protocol dump:\n%s", out)
33+
}
34+
wantReq := "@property (class, nonatomic) id classProp;"
35+
if !strings.Contains(out[reqIdx:], wantReq) {
36+
t.Fatalf("expected required class property in @required section:\nwant: %q\nhave:\n%s", wantReq, out[reqIdx:])
37+
}
38+
39+
// Optional class property should appear under @optional and be marked as class
40+
optIdx := strings.Index(out, "@optional")
41+
if optIdx < 0 {
42+
t.Fatalf("expected @optional section in protocol dump:\n%s", out)
43+
}
44+
wantOpt := "@property (class, nonatomic) long long optProp;"
45+
if !strings.Contains(out[optIdx:], wantOpt) {
46+
t.Fatalf("expected optional class property in @optional section:\nwant: %q\nhave:\n%s", wantOpt, out[optIdx:])
47+
}
48+
}

0 commit comments

Comments
 (0)