fix(extgen): correctly handle const blocks to declare iota constants (#2086)

While continuing the work on #2011, I realized that constant
declarations have a problem when using `iota`. I mean, it technically
works, but const *blocks* we not supported which means that setting all
constants to `iota` as shown in the documentation was non-sensical, as
`iota` resets every time outside of const blocks.

So, this is between the bug fix and the feature. To me, it's a bug fix
as the behavior wasn't the one intended when creating extgen.
This commit is contained in:
Alexandre Daubois
2026-01-12 15:44:46 +01:00
committed by GitHub
parent ecad5ec0a0
commit c6b2b02277
6 changed files with 301 additions and 32 deletions

View File

@@ -406,12 +406,15 @@ const MAX_CONNECTIONS = 100
const API_VERSION = "1.2.3"
//export_php:const
const STATUS_OK = iota
//export_php:const
const STATUS_ERROR = iota
const (
STATUS_OK = iota
STATUS_ERROR
)
```
> [!NOTE]
> PHP constants will take the name of the Go constant, thus using upper case letters is recommended.
#### Class Constants
Use the `//export_php:classconst ClassName` directive to create constants that belong to a specific PHP class:
@@ -429,15 +432,16 @@ const STATUS_INACTIVE = 0
const ROLE_ADMIN = "admin"
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconst Order
const STATE_COMPLETED = iota
const (
STATE_PENDING = iota
STATE_PROCESSING
STATE_COMPLETED
)
```
> [!NOTE]
> Just like global constants, the class constants will take the name of the Go constant.
Class constants are accessible using the class name scope in PHP:
```php

View File

@@ -402,12 +402,15 @@ const MAX_CONNECTIONS = 100
const API_VERSION = "1.2.3"
//export_php:const
const STATUS_OK = iota
//export_php:const
const STATUS_ERROR = iota
const (
STATUS_OK = iota
STATUS_ERROR
)
```
> [!NOTE]
> Les constantes PHP prennent le nom de la constante Go, d'où l'utilisation de majuscules pour les noms des constants en Go.
#### Constantes de Classe
Utilisez la directive `//export_php:classconst ClassName` pour créer des constantes qui appartiennent à une classe PHP spécifique :
@@ -425,15 +428,16 @@ const STATUS_INACTIVE = 0
const ROLE_ADMIN = "admin"
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconst Order
const STATE_COMPLETED = iota
const (
STATE_PENDING = iota
STATE_PROCESSING
STATE_COMPLETED
)
```
> [!NOTE]
> Comme les constantes globales, les constantes de classe prennent le nom de la constante Go.
Les constantes de classe sont accessibles en utilisant la portée du nom de classe en PHP :
```php

View File

@@ -34,6 +34,10 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e
expectClassConstDecl := false
currentClassName := ""
currentConstantValue := 0
inConstBlock := false
exportAllInBlock := false
lastConstValue := ""
lastConstWasIota := false
for scanner.Scan() {
lineNumber++
@@ -55,7 +59,26 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e
continue
}
if (expectConstDecl || expectClassConstDecl) && strings.HasPrefix(line, "const ") {
if strings.HasPrefix(line, "const (") {
inConstBlock = true
if expectConstDecl || expectClassConstDecl {
exportAllInBlock = true
}
continue
}
if inConstBlock && line == ")" {
inConstBlock = false
exportAllInBlock = false
expectConstDecl = false
expectClassConstDecl = false
currentClassName = ""
lastConstValue = ""
lastConstWasIota = false
continue
}
if (expectConstDecl || expectClassConstDecl) && strings.HasPrefix(line, "const ") && !inConstBlock {
matches := constDeclRegex.FindStringSubmatch(line)
if len(matches) == 3 {
name := matches[1]
@@ -72,10 +95,11 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e
constant.PhpType = determineConstantType(value)
if constant.IsIota {
// affect a default value because user didn't give one
constant.Value = fmt.Sprintf("%d", currentConstantValue)
constant.PhpType = phpInt
currentConstantValue++
lastConstWasIota = true
lastConstValue = constant.Value
}
constants = append(constants, constant)
@@ -84,7 +108,65 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e
}
expectConstDecl = false
expectClassConstDecl = false
} else if (expectConstDecl || expectClassConstDecl) && !strings.HasPrefix(line, "//") && line != "" {
} else if inConstBlock && (expectConstDecl || expectClassConstDecl || exportAllInBlock) {
constBlockDeclRegex := regexp.MustCompile(`^(\w+)\s*=\s*(.+)$`)
if matches := constBlockDeclRegex.FindStringSubmatch(line); len(matches) == 3 {
name := matches[1]
value := strings.TrimSpace(matches[2])
constant := phpConstant{
Name: name,
Value: value,
IsIota: value == "iota",
lineNumber: lineNumber,
ClassName: currentClassName,
}
constant.PhpType = determineConstantType(value)
if constant.IsIota {
constant.Value = fmt.Sprintf("%d", currentConstantValue)
constant.PhpType = phpInt
currentConstantValue++
lastConstWasIota = true
lastConstValue = constant.Value
} else {
lastConstWasIota = false
lastConstValue = value
}
constants = append(constants, constant)
expectConstDecl = false
expectClassConstDecl = false
} else {
constNameRegex := regexp.MustCompile(`^(\w+)$`)
if matches := constNameRegex.FindStringSubmatch(line); len(matches) == 2 {
name := matches[1]
constant := phpConstant{
Name: name,
Value: "",
IsIota: lastConstWasIota,
lineNumber: lineNumber,
ClassName: currentClassName,
}
if lastConstWasIota {
constant.Value = fmt.Sprintf("%d", currentConstantValue)
constant.PhpType = phpInt
currentConstantValue++
lastConstValue = constant.Value
} else {
constant.Value = lastConstValue
constant.PhpType = determineConstantType(lastConstValue)
}
constants = append(constants, constant)
expectConstDecl = false
expectClassConstDecl = false
}
}
} else if (expectConstDecl || expectClassConstDecl) && !strings.HasPrefix(line, "//") && line != "" && !inConstBlock {
// we expected a const declaration but found something else, reset
expectConstDecl = false
expectClassConstDecl = false

View File

@@ -221,7 +221,7 @@ func TestConstantParserIotaSequence(t *testing.T) {
//export_php:const
const FirstIota = iota
//export_php:const
//export_php:const
const SecondIota = iota
//export_php:const
@@ -244,6 +244,179 @@ const ThirdIota = iota`
}
}
func TestConstantParserConstBlock(t *testing.T) {
input := `package main
const (
// export_php:const
STATUS_PENDING = iota
// export_php:const
STATUS_PROCESSING
// export_php:const
STATUS_COMPLETED
)`
tmpDir := t.TempDir()
fileName := filepath.Join(tmpDir, "test.go")
require.NoError(t, os.WriteFile(fileName, []byte(input), 0644))
parser := &ConstantParser{}
constants, err := parser.parse(fileName)
assert.NoError(t, err, "parse() error")
assert.Len(t, constants, 3, "Expected 3 constants")
expectedNames := []string{"STATUS_PENDING", "STATUS_PROCESSING", "STATUS_COMPLETED"}
expectedValues := []string{"0", "1", "2"}
for i, c := range constants {
assert.Equal(t, expectedNames[i], c.Name, "Expected constant %d name to be '%s'", i, expectedNames[i])
assert.True(t, c.IsIota, "Expected constant %d to be iota", i)
assert.Equal(t, expectedValues[i], c.Value, "Expected constant %d value to be '%s'", i, expectedValues[i])
assert.Equal(t, phpInt, c.PhpType, "Expected constant %d to be phpInt type", i)
}
}
func TestConstantParserConstBlockWithBlockLevelDirective(t *testing.T) {
input := `package main
// export_php:const
const (
STATUS_PENDING = iota
STATUS_PROCESSING
STATUS_COMPLETED
)`
tmpDir := t.TempDir()
fileName := filepath.Join(tmpDir, "test.go")
require.NoError(t, os.WriteFile(fileName, []byte(input), 0644))
parser := &ConstantParser{}
constants, err := parser.parse(fileName)
assert.NoError(t, err, "parse() error")
assert.Len(t, constants, 3, "Expected 3 constants")
expectedNames := []string{"STATUS_PENDING", "STATUS_PROCESSING", "STATUS_COMPLETED"}
expectedValues := []string{"0", "1", "2"}
for i, c := range constants {
assert.Equal(t, expectedNames[i], c.Name, "Expected constant %d name to be '%s'", i, expectedNames[i])
assert.True(t, c.IsIota, "Expected constant %d to be iota", i)
assert.Equal(t, expectedValues[i], c.Value, "Expected constant %d value to be '%s'", i, expectedValues[i])
assert.Equal(t, phpInt, c.PhpType, "Expected constant %d to be phpInt type", i)
}
}
func TestConstantParserMixedConstBlockAndIndividual(t *testing.T) {
input := `package main
// export_php:const
const INDIVIDUAL = 42
const (
// export_php:const
BLOCK_ONE = iota
// export_php:const
BLOCK_TWO
)
// export_php:const
const ANOTHER_INDIVIDUAL = "test"`
tmpDir := t.TempDir()
fileName := filepath.Join(tmpDir, "test.go")
require.NoError(t, os.WriteFile(fileName, []byte(input), 0644))
parser := &ConstantParser{}
constants, err := parser.parse(fileName)
assert.NoError(t, err, "parse() error")
assert.Len(t, constants, 4, "Expected 4 constants")
assert.Equal(t, "INDIVIDUAL", constants[0].Name)
assert.Equal(t, "42", constants[0].Value)
assert.Equal(t, phpInt, constants[0].PhpType)
assert.Equal(t, "BLOCK_ONE", constants[1].Name)
assert.Equal(t, "0", constants[1].Value)
assert.True(t, constants[1].IsIota)
assert.Equal(t, "BLOCK_TWO", constants[2].Name)
assert.Equal(t, "1", constants[2].Value)
assert.True(t, constants[2].IsIota)
assert.Equal(t, "ANOTHER_INDIVIDUAL", constants[3].Name)
assert.Equal(t, `"test"`, constants[3].Value)
assert.Equal(t, phpString, constants[3].PhpType)
}
func TestConstantParserClassConstBlock(t *testing.T) {
input := `package main
// export_php:classconst Config
const (
MODE_DEBUG = 1
MODE_PRODUCTION = 2
MODE_TEST = 3
)`
tmpDir := t.TempDir()
fileName := filepath.Join(tmpDir, "test.go")
require.NoError(t, os.WriteFile(fileName, []byte(input), 0644))
parser := &ConstantParser{}
constants, err := parser.parse(fileName)
assert.NoError(t, err, "parse() error")
assert.Len(t, constants, 3, "Expected 3 class constants")
expectedNames := []string{"MODE_DEBUG", "MODE_PRODUCTION", "MODE_TEST"}
expectedValues := []string{"1", "2", "3"}
for i, c := range constants {
assert.Equal(t, expectedNames[i], c.Name, "Expected constant %d name to be '%s'", i, expectedNames[i])
assert.Equal(t, "Config", c.ClassName, "Expected constant %d to belong to Config class", i)
assert.Equal(t, expectedValues[i], c.Value, "Expected constant %d value to be '%s'", i, expectedValues[i])
assert.Equal(t, phpInt, c.PhpType, "Expected constant %d to be phpInt type", i)
}
}
func TestConstantParserClassConstBlockWithIota(t *testing.T) {
input := `package main
// export_php:classconst Status
const (
STATUS_PENDING = iota
STATUS_ACTIVE
STATUS_COMPLETED
)`
tmpDir := t.TempDir()
fileName := filepath.Join(tmpDir, "test.go")
require.NoError(t, os.WriteFile(fileName, []byte(input), 0644))
parser := &ConstantParser{}
constants, err := parser.parse(fileName)
assert.NoError(t, err, "parse() error")
assert.Len(t, constants, 3, "Expected 3 class constants")
expectedNames := []string{"STATUS_PENDING", "STATUS_ACTIVE", "STATUS_COMPLETED"}
expectedValues := []string{"0", "1", "2"}
for i, c := range constants {
assert.Equal(t, expectedNames[i], c.Name, "Expected constant %d name to be '%s'", i, expectedNames[i])
assert.Equal(t, "Status", c.ClassName, "Expected constant %d to belong to Status class", i)
assert.True(t, c.IsIota, "Expected constant %d to be iota", i)
assert.Equal(t, expectedValues[i], c.Value, "Expected constant %d value to be '%s'", i, expectedValues[i])
assert.Equal(t, phpInt, c.PhpType, "Expected constant %d to be phpInt type", i)
}
}
func TestConstantParserTypeDetection(t *testing.T) {
tests := []struct {
name string

View File

@@ -480,6 +480,7 @@ func TestConstants(t *testing.T) {
[]string{
"TEST_MAX_RETRIES", "TEST_API_VERSION", "TEST_ENABLED", "TEST_PI",
"STATUS_PENDING", "STATUS_PROCESSING", "STATUS_COMPLETED",
"ONE", "TWO",
},
)
require.NoError(t, err, "all constants, functions, and classes should be accessible from PHP")

View File

@@ -21,13 +21,18 @@ const TEST_ENABLED = true
const TEST_PI = 3.14159
// export_php:const
const STATUS_PENDING = iota
const (
STATUS_PENDING = iota
STATUS_PROCESSING
STATUS_COMPLETED
)
// export_php:const
const STATUS_PROCESSING = iota
// export_php:const
const STATUS_COMPLETED = iota
const (
// export_php:const
ONE = 1
// export_php:const
TWO = 2
)
// export_php:class Config
type ConfigStruct struct {