From b22d32e017cc063092b63711b64f235b61e396de Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 13 Sep 2026 12:49:52 +0200 Subject: [PATCH] Fix parameter spacing when adding a type to a later parameter The injected type node had no leading whitespace, so on any parameter after the first the space that belonged before the variable ended up after the type, producing output like `($a,string $b)`. Move the variable's leading whitespace onto the new type and separate the type from the variable with a single space. This keeps the comma spacing, modifier spacing and multiline indentation intact. --- internal/apply/apply.go | 39 ++++++++++++++++++++++++++++++++++-- internal/apply/apply_test.go | 21 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/internal/apply/apply.go b/internal/apply/apply.go index 63ba93e6..effbfacf 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -8,6 +8,7 @@ import ( "github.com/rectorphp/argtyper/internal/aggregate" "github.com/rectorphp/argtyper/internal/phpast" "github.com/rectorphp/php-parser-in-go/pkg/ast" + "github.com/rectorphp/php-parser-in-go/pkg/token" ) // Source adds parameter types to a single PHP source file. It returns the new @@ -95,14 +96,48 @@ func (a *applier) setType(param *ast.Parameter, resolved aggregate.Resolved) { nullable := resolved.Nullable || hasNullDefault(param) typeText := typeText(resolved.Type) + // Take the whitespace that sat before the variable (indentation, or the + // space after a comma or modifier) and put it in front of the new type, so + // the type slots into the variable's old position. A single space then + // separates the type from the variable. + leading := takeVarLeading(param) + identifier := &ast.Identifier{IdentifierTkn: &token.Token{Value: []byte(typeText)}} + if nullable { - param.Type = &ast.Nullable{Expr: &ast.Identifier{Value: []byte(typeText + " ")}} + param.Type = &ast.Nullable{ + QuestionTkn: &token.Token{Value: []byte("?"), FreeFloating: leading}, + Expr: identifier, + } } else { - param.Type = &ast.Identifier{Value: []byte(typeText + " ")} + identifier.IdentifierTkn.FreeFloating = leading + param.Type = identifier } a.added++ } +// takeVarLeading returns the leading whitespace tokens of the parameter's +// variable and replaces them with a single space. +func takeVarLeading(param *ast.Parameter) []*token.Token { + variable, ok := param.Var.(*ast.ExprVariable) + if !ok { + return nil + } + + leadingToken := variable.DollarTkn + if leadingToken == nil { + if name, ok := variable.Name.(*ast.Identifier); ok { + leadingToken = name.IdentifierTkn + } + } + if leadingToken == nil { + return nil + } + + leading := leadingToken.FreeFloating + leadingToken.FreeFloating = []*token.Token{{Value: []byte(" ")}} + return leading +} + // typeText turns a resolved type into the text written into source. The trailing // space that separates the type from the variable is added by the caller. func typeText(resolved string) string { diff --git a/internal/apply/apply_test.go b/internal/apply/apply_test.go index 8d1f0fd9..1cb8fffd 100644 --- a/internal/apply/apply_test.go +++ b/internal/apply/apply_test.go @@ -90,6 +90,27 @@ func TestApply(t *testing.T) { target: "set(1); }\n}", want: "set(1); }\n}", }, + { + name: "keeps comma spacing when typing a later parameter", + target: "set(\"x\", 1); }\n}", + want: "set(\"x\", 1); }\n}", + }, + { + name: "types promoted constructor parameter with modifier", + target: "