Skip to content

Commit 8f298ab

Browse files
alexandre-dauboisdunglas
authored andcommitted
fix(extgen): constant should be declared under the namespace provided by export_php:namespace
1 parent 41cb2bb commit 8f298ab

4 files changed

Lines changed: 295 additions & 0 deletions

File tree

internal/extgen/cfile.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func (cg *cFileGenerator) buildContent() (string, error) {
5858
func (cg *cFileGenerator) getTemplateContent() (string, error) {
5959
funcMap := sprig.FuncMap()
6060
funcMap["namespacedClassName"] = NamespacedName
61+
funcMap["cString"] = escapeCString
6162

6263
tmpl := template.Must(template.New("cfile").Funcs(funcMap).Parse(cFileContent))
6364

@@ -74,3 +75,8 @@ func (cg *cFileGenerator) getTemplateContent() (string, error) {
7475

7576
return buf.String(), nil
7677
}
78+
79+
// escapeCString escapes backslashes for C string literals
80+
func escapeCString(s string) string {
81+
return strings.ReplaceAll(s, `\`, `\\`)
82+
}

internal/extgen/cfile_namespace_test.go

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,212 @@ func TestCFileGenerationWithoutNamespace(t *testing.T) {
129129
expectedCall := "register_class_MySuperClass()"
130130
require.Contains(t, contentResult, expectedCall, "C file should not contain the standard function call")
131131
}
132+
133+
func TestCFileGenerationWithNamespacedConstants(t *testing.T) {
134+
tests := []struct {
135+
name string
136+
namespace string
137+
constants []phpConstant
138+
contains []string
139+
}{
140+
{
141+
name: "integer constant with namespace",
142+
namespace: `Go\Extension`,
143+
constants: []phpConstant{
144+
{Name: "TEST_INT", Value: "42", PhpType: phpInt},
145+
},
146+
contains: []string{
147+
`REGISTER_NS_LONG_CONSTANT("Go\\Extension", "TEST_INT", 42, CONST_CS | CONST_PERSISTENT);`,
148+
},
149+
},
150+
{
151+
name: "string constant with namespace",
152+
namespace: `Go\Extension`,
153+
constants: []phpConstant{
154+
{Name: "TEST_STRING", Value: `"hello"`, PhpType: phpString},
155+
},
156+
contains: []string{
157+
`REGISTER_NS_STRING_CONSTANT("Go\\Extension", "TEST_STRING", "hello", CONST_CS | CONST_PERSISTENT);`,
158+
},
159+
},
160+
{
161+
name: "float constant with namespace",
162+
namespace: `Go\Extension`,
163+
constants: []phpConstant{
164+
{Name: "TEST_FLOAT", Value: "3.14", PhpType: phpFloat},
165+
},
166+
contains: []string{
167+
`REGISTER_NS_DOUBLE_CONSTANT("Go\\Extension", "TEST_FLOAT", 3.14, CONST_CS | CONST_PERSISTENT);`,
168+
},
169+
},
170+
{
171+
name: "bool constant with namespace",
172+
namespace: `Go\Extension`,
173+
constants: []phpConstant{
174+
{Name: "TEST_BOOL", Value: "true", PhpType: phpBool},
175+
},
176+
contains: []string{
177+
`REGISTER_NS_LONG_CONSTANT("Go\\Extension", "TEST_BOOL", 1, CONST_CS | CONST_PERSISTENT);`,
178+
},
179+
},
180+
{
181+
name: "iota constant with namespace",
182+
namespace: `Go\Extension`,
183+
constants: []phpConstant{
184+
{Name: "STATUS_OK", Value: "STATUS_OK", PhpType: phpInt, IsIota: true},
185+
},
186+
contains: []string{
187+
`REGISTER_NS_LONG_CONSTANT("Go\\Extension", "STATUS_OK", STATUS_OK, CONST_CS | CONST_PERSISTENT);`,
188+
},
189+
},
190+
{
191+
name: "multiple constants with deep namespace",
192+
namespace: `My\Deep\Namespace`,
193+
constants: []phpConstant{
194+
{Name: "CONST_INT", Value: "100", PhpType: phpInt},
195+
{Name: "CONST_STR", Value: `"value"`, PhpType: phpString},
196+
{Name: "CONST_FLOAT", Value: "1.5", PhpType: phpFloat},
197+
},
198+
contains: []string{
199+
`REGISTER_NS_LONG_CONSTANT("My\\Deep\\Namespace", "CONST_INT", 100, CONST_CS | CONST_PERSISTENT);`,
200+
`REGISTER_NS_STRING_CONSTANT("My\\Deep\\Namespace", "CONST_STR", "value", CONST_CS | CONST_PERSISTENT);`,
201+
`REGISTER_NS_DOUBLE_CONSTANT("My\\Deep\\Namespace", "CONST_FLOAT", 1.5, CONST_CS | CONST_PERSISTENT);`,
202+
},
203+
},
204+
{
205+
name: "single level namespace",
206+
namespace: `TestNamespace`,
207+
constants: []phpConstant{
208+
{Name: "MY_CONST", Value: "999", PhpType: phpInt},
209+
},
210+
contains: []string{
211+
`REGISTER_NS_LONG_CONSTANT("TestNamespace", "MY_CONST", 999, CONST_CS | CONST_PERSISTENT);`,
212+
},
213+
},
214+
{
215+
name: "namespace with trailing backslash",
216+
namespace: `TestIntegration\Extension`,
217+
constants: []phpConstant{
218+
{Name: "VERSION", Value: `"1.0.0"`, PhpType: phpString},
219+
},
220+
contains: []string{
221+
`REGISTER_NS_STRING_CONSTANT("TestIntegration\\Extension", "VERSION", "1.0.0", CONST_CS | CONST_PERSISTENT);`,
222+
},
223+
},
224+
}
225+
226+
for _, tt := range tests {
227+
t.Run(tt.name, func(t *testing.T) {
228+
generator := &Generator{
229+
BaseName: "test_ext",
230+
Namespace: tt.namespace,
231+
Constants: tt.constants,
232+
}
233+
234+
cGen := cFileGenerator{generator: generator}
235+
content, err := cGen.buildContent()
236+
require.NoError(t, err, "Failed to build C file content")
237+
238+
for _, expected := range tt.contains {
239+
assert.Contains(t, content, expected, "Generated C content should contain '%s'", expected)
240+
}
241+
})
242+
}
243+
}
244+
245+
func TestCFileGenerationWithoutNamespacedConstants(t *testing.T) {
246+
tests := []struct {
247+
name string
248+
namespace string
249+
constants []phpConstant
250+
contains []string
251+
}{
252+
{
253+
name: "integer constant without namespace",
254+
namespace: "",
255+
constants: []phpConstant{
256+
{Name: "GLOBAL_INT", Value: "42", PhpType: phpInt},
257+
},
258+
contains: []string{
259+
`REGISTER_LONG_CONSTANT("GLOBAL_INT", 42, CONST_CS | CONST_PERSISTENT);`,
260+
},
261+
},
262+
{
263+
name: "string constant without namespace",
264+
namespace: "",
265+
constants: []phpConstant{
266+
{Name: "GLOBAL_STRING", Value: `"test"`, PhpType: phpString},
267+
},
268+
contains: []string{
269+
`REGISTER_STRING_CONSTANT("GLOBAL_STRING", "test", CONST_CS | CONST_PERSISTENT);`,
270+
},
271+
},
272+
{
273+
name: "float constant without namespace",
274+
namespace: "",
275+
constants: []phpConstant{
276+
{Name: "GLOBAL_FLOAT", Value: "2.71", PhpType: phpFloat},
277+
},
278+
contains: []string{
279+
`REGISTER_DOUBLE_CONSTANT("GLOBAL_FLOAT", 2.71, CONST_CS | CONST_PERSISTENT);`,
280+
},
281+
},
282+
{
283+
name: "bool constant without namespace",
284+
namespace: "",
285+
constants: []phpConstant{
286+
{Name: "GLOBAL_BOOL", Value: "false", PhpType: phpBool},
287+
},
288+
contains: []string{
289+
`REGISTER_LONG_CONSTANT("GLOBAL_BOOL", 0, CONST_CS | CONST_PERSISTENT);`,
290+
},
291+
},
292+
{
293+
name: "iota constant without namespace",
294+
namespace: "",
295+
constants: []phpConstant{
296+
{Name: "ERROR_CODE", Value: "ERROR_CODE", PhpType: phpInt, IsIota: true},
297+
},
298+
contains: []string{
299+
`REGISTER_LONG_CONSTANT("ERROR_CODE", ERROR_CODE, CONST_CS | CONST_PERSISTENT);`,
300+
},
301+
},
302+
}
303+
304+
for _, tt := range tests {
305+
t.Run(tt.name, func(t *testing.T) {
306+
generator := &Generator{
307+
BaseName: "test_ext",
308+
Namespace: tt.namespace,
309+
Constants: tt.constants,
310+
}
311+
312+
cGen := cFileGenerator{generator: generator}
313+
content, err := cGen.buildContent()
314+
require.NoError(t, err, "Failed to build C file content")
315+
316+
for _, expected := range tt.contains {
317+
assert.Contains(t, content, expected, "Generated C content should contain '%s'", expected)
318+
}
319+
320+
assert.NotContains(t, content, "REGISTER_NS_", "Content should NOT contain namespaced constant macros when namespace is empty")
321+
})
322+
}
323+
}
324+
325+
func TestCFileTemplateFunctionMapCString(t *testing.T) {
326+
generator := &Generator{
327+
BaseName: "test_ext",
328+
Namespace: `My\Namespace\Test`,
329+
Constants: []phpConstant{
330+
{Name: "MY_CONST", Value: "123", PhpType: phpInt},
331+
},
332+
}
333+
334+
cGen := cFileGenerator{generator: generator}
335+
content, err := cGen.getTemplateContent()
336+
require.NoError(t, err, "Failed to get template content")
337+
338+
assert.Contains(t, content, `"My\\Namespace\\Test"`, "Template should properly escape namespace backslashes using cString filter")
339+
assert.NotContains(t, content, `"My\Namespace\Test"`, "Template should not contain unescaped namespace (single backslashes)")
340+
}

internal/extgen/cfile_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,3 +459,74 @@ func TestCFileTemplateErrorHandling(t *testing.T) {
459459
_, err := cGen.getTemplateContent()
460460
assert.NoError(t, err, "getTemplateContent() should not fail with valid template")
461461
}
462+
463+
func TestEscapeCString(t *testing.T) {
464+
tests := []struct {
465+
name string
466+
input string
467+
expected string
468+
}{
469+
{
470+
name: "simple namespace with single backslash",
471+
input: `Go\Extension`,
472+
expected: `Go\\Extension`,
473+
},
474+
{
475+
name: "namespace with multiple backslashes",
476+
input: `My\Deep\Namespace`,
477+
expected: `My\\Deep\\Namespace`,
478+
},
479+
{
480+
name: "complex nested namespace",
481+
input: `TestIntegration\Extension\Module`,
482+
expected: `TestIntegration\\Extension\\Module`,
483+
},
484+
{
485+
name: "empty string",
486+
input: "",
487+
expected: "",
488+
},
489+
{
490+
name: "single backslash",
491+
input: `\`,
492+
expected: `\\`,
493+
},
494+
{
495+
name: "multiple consecutive backslashes",
496+
input: `\\\`,
497+
expected: `\\\\\\`,
498+
},
499+
{
500+
name: "string without backslashes",
501+
input: "TestNamespace",
502+
expected: "TestNamespace",
503+
},
504+
{
505+
name: "leading backslash",
506+
input: `\Leading`,
507+
expected: `\\Leading`,
508+
},
509+
{
510+
name: "trailing backslash",
511+
input: `Trailing\`,
512+
expected: `Trailing\\`,
513+
},
514+
{
515+
name: "mixed alphanumeric with backslashes",
516+
input: `Path123\To456\File789`,
517+
expected: `Path123\\To456\\File789`,
518+
},
519+
{
520+
name: "unicode characters with backslashes",
521+
input: `Namespace\Über\Test`,
522+
expected: `Namespace\\Über\\Test`,
523+
},
524+
}
525+
526+
for _, tt := range tests {
527+
t.Run(tt.name, func(t *testing.T) {
528+
result := escapeCString(tt.input)
529+
assert.Equal(t, tt.expected, result, "escapeCString(%q) should return %q, got %q", tt.input, tt.expected, result)
530+
})
531+
}
532+
}

internal/extgen/templates/extension.c.tpl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@ PHP_MINIT_FUNCTION({{.BaseName}}) {
159159

160160
{{- range .Constants}}
161161
{{- if eq .ClassName ""}}
162+
{{- if $.Namespace}}
163+
{{if .IsIota}}REGISTER_NS_LONG_CONSTANT("{{cString $.Namespace}}", "{{.Name}}", {{.Name}}, CONST_CS | CONST_PERSISTENT);
164+
{{else if eq .PhpType "string"}}REGISTER_NS_STRING_CONSTANT("{{cString $.Namespace}}", "{{.Name}}", {{.CValue}}, CONST_CS | CONST_PERSISTENT);
165+
{{else if eq .PhpType "bool"}}REGISTER_NS_LONG_CONSTANT("{{cString $.Namespace}}", "{{.Name}}", {{if eq .Value "true"}}1{{else}}0{{end}}, CONST_CS | CONST_PERSISTENT);
166+
{{else if eq .PhpType "float"}}REGISTER_NS_DOUBLE_CONSTANT("{{cString $.Namespace}}", "{{.Name}}", {{.CValue}}, CONST_CS | CONST_PERSISTENT);
167+
{{else}}REGISTER_NS_LONG_CONSTANT("{{cString $.Namespace}}", "{{.Name}}", {{.CValue}}, CONST_CS | CONST_PERSISTENT);
168+
{{- end}}
169+
{{- else}}
162170
{{if .IsIota}}REGISTER_LONG_CONSTANT("{{.Name}}", {{.Name}}, CONST_CS | CONST_PERSISTENT);
163171
{{else if eq .PhpType "string"}}REGISTER_STRING_CONSTANT("{{.Name}}", {{.CValue}}, CONST_CS | CONST_PERSISTENT);
164172
{{else if eq .PhpType "bool"}}REGISTER_LONG_CONSTANT("{{.Name}}", {{if eq .Value "true"}}1{{else}}0{{end}}, CONST_CS | CONST_PERSISTENT);
@@ -167,6 +175,7 @@ PHP_MINIT_FUNCTION({{.BaseName}}) {
167175
{{- end}}
168176
{{- end}}
169177
{{- end}}
178+
{{- end}}
170179
return SUCCESS;
171180
}
172181

0 commit comments

Comments
 (0)