mirror of
https://github.com/php/frankenphp.git
synced 2026-03-24 00:52:11 +01:00
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:
committed by
GitHub
parent
ecad5ec0a0
commit
c6b2b02277
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user