From 31dc4c86431285134eac6b26c501ebd2b82c7a8d Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 8 Nov 2026 20:30:33 +0100 Subject: [PATCH 1/2] Add attribute bag to AST nodes Every node embeds AttributeBag, so a resolver can store post-parse metadata (such as an inferred type) on a node via SetAttribute / GetAttribute without widening node constructors. Equal keeps go-cmp node comparisons working past the bag's unexported map. --- internal/php5/node.go | 5 + internal/php7/node.go | 6 ++ internal/php8/node.go | 8 ++ pkg/ast/ast.go | 4 + pkg/ast/attribute_bag.go | 30 ++++++ pkg/ast/attribute_bag_test.go | 50 ++++++++++ pkg/ast/node.go | 166 ++++++++++++++++++++++++++++++++++ 7 files changed, 269 insertions(+) create mode 100644 pkg/ast/attribute_bag.go create mode 100644 pkg/ast/attribute_bag_test.go diff --git a/internal/php5/node.go b/internal/php5/node.go index f81ee09..8f7f501 100644 --- a/internal/php5/node.go +++ b/internal/php5/node.go @@ -7,6 +7,7 @@ import ( ) type ParserBrackets struct { + ast.AttributeBag Position *position.Position OpenBracketTkn *token.Token Child ast.Vertex @@ -22,6 +23,7 @@ func (parserBrackets *ParserBrackets) GetPosition() *position.Position { } type ParserSeparatedList struct { + ast.AttributeBag Position *position.Position Items []ast.Vertex SeparatorTkns []*token.Token @@ -37,6 +39,7 @@ func (parserSeparatedList *ParserSeparatedList) GetPosition() *position.Position // TraitAdaptationList node type TraitAdaptationList struct { + ast.AttributeBag Position *position.Position OpenCurlyBracketTkn *token.Token Adaptations []ast.Vertex @@ -53,6 +56,7 @@ func (traitAdaptationList *TraitAdaptationList) GetPosition() *position.Position // ArgumentList node type ArgumentList struct { + ast.AttributeBag Position *position.Position OpenParenthesisTkn *token.Token Arguments []ast.Vertex @@ -70,6 +74,7 @@ func (argumentList *ArgumentList) GetPosition() *position.Position { // TraitMethodRef node type TraitMethodRef struct { + ast.AttributeBag Position *position.Position Trait ast.Vertex DoubleColonTkn *token.Token diff --git a/internal/php7/node.go b/internal/php7/node.go index 12d190b..a93b451 100644 --- a/internal/php7/node.go +++ b/internal/php7/node.go @@ -7,6 +7,7 @@ import ( ) type ParserBrackets struct { + ast.AttributeBag Position *position.Position OpenBracketTkn *token.Token Child ast.Vertex @@ -22,6 +23,7 @@ func (parserBrackets *ParserBrackets) GetPosition() *position.Position { } type ParserSeparatedList struct { + ast.AttributeBag Position *position.Position Items []ast.Vertex SeparatorTkns []*token.Token @@ -37,6 +39,7 @@ func (parserSeparatedList *ParserSeparatedList) GetPosition() *position.Position // TraitAdaptationList node type TraitAdaptationList struct { + ast.AttributeBag Position *position.Position OpenCurlyBracketTkn *token.Token Adaptations []ast.Vertex @@ -53,6 +56,7 @@ func (traitAdaptationList *TraitAdaptationList) GetPosition() *position.Position // ArgumentList node type ArgumentList struct { + ast.AttributeBag Position *position.Position OpenParenthesisTkn *token.Token Arguments []ast.Vertex @@ -69,6 +73,7 @@ func (argumentList *ArgumentList) GetPosition() *position.Position { } type ReturnType struct { + ast.AttributeBag Position *position.Position ColonTkn *token.Token Type ast.Vertex @@ -84,6 +89,7 @@ func (returnType *ReturnType) GetPosition() *position.Position { // TraitMethodRef node type TraitMethodRef struct { + ast.AttributeBag Position *position.Position Trait ast.Vertex DoubleColonTkn *token.Token diff --git a/internal/php8/node.go b/internal/php8/node.go index fc2dd81..a6392ee 100644 --- a/internal/php8/node.go +++ b/internal/php8/node.go @@ -7,6 +7,7 @@ import ( ) type ParserBrackets struct { + ast.AttributeBag Position *position.Position OpenBracketTkn *token.Token Child ast.Vertex @@ -22,6 +23,7 @@ func (parserBrackets *ParserBrackets) GetPosition() *position.Position { } type ParserSeparatedList struct { + ast.AttributeBag Position *position.Position Items []ast.Vertex SeparatorTkns []*token.Token @@ -37,6 +39,7 @@ func (parserSeparatedList *ParserSeparatedList) GetPosition() *position.Position // TraitAdaptationList node type TraitAdaptationList struct { + ast.AttributeBag Position *position.Position OpenCurlyBracketTkn *token.Token Adaptations []ast.Vertex @@ -53,6 +56,7 @@ func (traitAdaptationList *TraitAdaptationList) GetPosition() *position.Position // ArgumentList node type ArgumentList struct { + ast.AttributeBag Position *position.Position OpenParenthesisTkn *token.Token Arguments []ast.Vertex @@ -69,6 +73,7 @@ func (argumentList *ArgumentList) GetPosition() *position.Position { } type EnumBackingType struct { + ast.AttributeBag Position *position.Position ColonTkn *token.Token Type ast.Vertex @@ -83,6 +88,7 @@ func (enumBackingType *EnumBackingType) GetPosition() *position.Position { } type EnumCaseValue struct { + ast.AttributeBag Position *position.Position EqualTkn *token.Token Expr ast.Vertex @@ -97,6 +103,7 @@ func (enumCaseValue *EnumCaseValue) GetPosition() *position.Position { } type ReturnType struct { + ast.AttributeBag Position *position.Position ColonTkn *token.Token Type ast.Vertex @@ -112,6 +119,7 @@ func (returnType *ReturnType) GetPosition() *position.Position { // TraitMethodRef node type TraitMethodRef struct { + ast.AttributeBag Position *position.Position Trait ast.Vertex DoubleColonTkn *token.Token diff --git a/pkg/ast/ast.go b/pkg/ast/ast.go index 090a5e1..ff47593 100644 --- a/pkg/ast/ast.go +++ b/pkg/ast/ast.go @@ -5,6 +5,10 @@ import "github.com/rectorphp/php-parser-in-go/pkg/position" type Vertex interface { Accept(visitor Visitor) GetPosition() *position.Position + // SetAttribute stores metadata assigned after parsing, such as a resolved type. + SetAttribute(key string, value any) + // GetAttribute returns a stored attribute and whether it was present. + GetAttribute(key string) (any, bool) } type Visitor interface { diff --git a/pkg/ast/attribute_bag.go b/pkg/ast/attribute_bag.go new file mode 100644 index 0000000..eb1b311 --- /dev/null +++ b/pkg/ast/attribute_bag.go @@ -0,0 +1,30 @@ +package ast + +import "reflect" + +// AttributeBag is embedded in every AST node to carry metadata assigned after +// parsing, such as a resolved type. Attributes are keyed by string and allocated +// lazily, so a node that never gets one costs nothing. +type AttributeBag struct { + attributes map[string]any +} + +// Equal lets go-cmp compare nodes that embed a bag without reaching its +// unexported field; two bags are equal when they hold the same attributes. +func (bag AttributeBag) Equal(other AttributeBag) bool { + return reflect.DeepEqual(bag.attributes, other.attributes) +} + +// SetAttribute stores value under key, allocating the backing map on first use. +func (bag *AttributeBag) SetAttribute(key string, value any) { + if bag.attributes == nil { + bag.attributes = map[string]any{} + } + bag.attributes[key] = value +} + +// GetAttribute returns the value stored under key and whether it was present. +func (bag *AttributeBag) GetAttribute(key string) (any, bool) { + value, ok := bag.attributes[key] + return value, ok +} diff --git a/pkg/ast/attribute_bag_test.go b/pkg/ast/attribute_bag_test.go new file mode 100644 index 0000000..c2ec03b --- /dev/null +++ b/pkg/ast/attribute_bag_test.go @@ -0,0 +1,50 @@ +package ast + +import "testing" + +func TestAttributeBagSetGet(test *testing.T) { + var bag AttributeBag + + if _, ok := bag.GetAttribute("type"); ok { + test.Fatal("empty bag reported a value") + } + + bag.SetAttribute("type", "User") + value, ok := bag.GetAttribute("type") + if !ok || value != "User" { + test.Fatalf("got %v, %v; want User, true", value, ok) + } + + bag.SetAttribute("type", "Admin") + if value, _ := bag.GetAttribute("type"); value != "Admin" { + test.Fatalf("overwrite failed: got %v", value) + } +} + +func TestAttributeBagEqual(test *testing.T) { + var empty, other AttributeBag + if !empty.Equal(other) { + test.Fatal("two empty bags should be equal") + } + + set := AttributeBag{} + set.SetAttribute("type", "User") + if empty.Equal(set) { + test.Fatal("empty bag should differ from a populated one") + } + + same := AttributeBag{} + same.SetAttribute("type", "User") + if !set.Equal(same) { + test.Fatal("bags with the same attributes should be equal") + } +} + +// node embeds AttributeBag, so every node satisfies the Vertex attribute methods. +func TestNodeCarriesAttributes(test *testing.T) { + var node Vertex = &ExprVariable{} + node.SetAttribute("type", "string") + if value, ok := node.GetAttribute("type"); !ok || value != "string" { + test.Fatalf("node attribute not stored: got %v, %v", value, ok) + } +} diff --git a/pkg/ast/node.go b/pkg/ast/node.go index 036f08b..9d8a5d7 100644 --- a/pkg/ast/node.go +++ b/pkg/ast/node.go @@ -7,6 +7,7 @@ import ( // Root node type Root struct { + AttributeBag Position *position.Position Stmts []Vertex EndTkn *token.Token @@ -22,6 +23,7 @@ func (node *Root) GetPosition() *position.Position { // Nullable node type Nullable struct { + AttributeBag Position *position.Position QuestionTkn *token.Token Expr Vertex @@ -38,6 +40,7 @@ func (node *Nullable) GetPosition() *position.Position { // Union node holds a union type such as `int|string`, its member types // separated by `|`. type Union struct { + AttributeBag Position *position.Position Types []Vertex SeparatorTkns []*token.Token @@ -55,6 +58,7 @@ func (node *Union) GetPosition() *position.Position { // its member types separated by `&`. In a DNF type the intersection is wrapped // in parentheses, e.g. `(A&B)|C`; the parenthesis tokens are then set. type Intersection struct { + AttributeBag Position *position.Position OpenParenthesisTkn *token.Token Types []Vertex @@ -72,6 +76,7 @@ func (node *Intersection) GetPosition() *position.Position { // Parameter node type Parameter struct { + AttributeBag Position *position.Position AttrGroups []Vertex Modifiers []Vertex @@ -93,6 +98,7 @@ func (node *Parameter) GetPosition() *position.Position { // Identifier node type Identifier struct { + AttributeBag Position *position.Position IdentifierTkn *token.Token Value []byte @@ -108,6 +114,7 @@ func (node *Identifier) GetPosition() *position.Position { // Argument node type Argument struct { + AttributeBag Position *position.Position Name Vertex ColonTkn *token.Token @@ -126,6 +133,7 @@ func (node *Argument) GetPosition() *position.Position { // Attribute node type Attribute struct { + AttributeBag Position *position.Position Name Vertex OpenParenthesisTkn *token.Token @@ -144,6 +152,7 @@ func (node *Attribute) GetPosition() *position.Position { // AttributeGroup node type AttributeGroup struct { + AttributeBag Position *position.Position OpenAttributeTkn *token.Token Attrs []Vertex @@ -161,6 +170,7 @@ func (node *AttributeGroup) GetPosition() *position.Position { // ScalarDnumber node type ScalarDnumber struct { + AttributeBag Position *position.Position NumberTkn *token.Token Value []byte @@ -176,6 +186,7 @@ func (node *ScalarDnumber) GetPosition() *position.Position { // ScalarEncapsed node type ScalarEncapsed struct { + AttributeBag Position *position.Position OpenQuoteTkn *token.Token Parts []Vertex @@ -192,6 +203,7 @@ func (node *ScalarEncapsed) GetPosition() *position.Position { // ScalarEncapsedStringPart node type ScalarEncapsedStringPart struct { + AttributeBag Position *position.Position EncapsedStrTkn *token.Token Value []byte @@ -207,6 +219,7 @@ func (node *ScalarEncapsedStringPart) GetPosition() *position.Position { // ScalarEncapsedStringVar node type ScalarEncapsedStringVar struct { + AttributeBag Position *position.Position DollarOpenCurlyBracketTkn *token.Token Name Vertex @@ -226,6 +239,7 @@ func (node *ScalarEncapsedStringVar) GetPosition() *position.Position { // ScalarEncapsedStringVar node type ScalarEncapsedStringBrackets struct { + AttributeBag Position *position.Position OpenCurlyBracketTkn *token.Token Var Vertex @@ -242,6 +256,7 @@ func (node *ScalarEncapsedStringBrackets) GetPosition() *position.Position { // ScalarHeredoc node type ScalarHeredoc struct { + AttributeBag Position *position.Position OpenHeredocTkn *token.Token Parts []Vertex @@ -258,6 +273,7 @@ func (node *ScalarHeredoc) GetPosition() *position.Position { // ScalarLnumber node type ScalarLnumber struct { + AttributeBag Position *position.Position NumberTkn *token.Token Value []byte @@ -273,6 +289,7 @@ func (node *ScalarLnumber) GetPosition() *position.Position { // ScalarMagicConstant node type ScalarMagicConstant struct { + AttributeBag Position *position.Position MagicConstTkn *token.Token Value []byte @@ -288,6 +305,7 @@ func (node *ScalarMagicConstant) GetPosition() *position.Position { // ScalarString node type ScalarString struct { + AttributeBag Position *position.Position MinusTkn *token.Token StringTkn *token.Token @@ -304,6 +322,7 @@ func (node *ScalarString) GetPosition() *position.Position { // StmtBreak node type StmtBreak struct { + AttributeBag Position *position.Position BreakTkn *token.Token Expr Vertex @@ -320,6 +339,7 @@ func (node *StmtBreak) GetPosition() *position.Position { // StmtCase node type StmtCase struct { + AttributeBag Position *position.Position CaseTkn *token.Token Cond Vertex @@ -337,6 +357,7 @@ func (node *StmtCase) GetPosition() *position.Position { // StmtCatch node type StmtCatch struct { + AttributeBag Position *position.Position CatchTkn *token.Token OpenParenthesisTkn *token.Token @@ -359,6 +380,7 @@ func (node *StmtCatch) GetPosition() *position.Position { // StmtClass node type StmtClass struct { + AttributeBag Position *position.Position AttrGroups []Vertex Modifiers []Vertex @@ -388,6 +410,7 @@ func (node *StmtClass) GetPosition() *position.Position { // StmtClassConstList node type StmtClassConstList struct { + AttributeBag Position *position.Position AttrGroups []Vertex Modifiers []Vertex @@ -408,6 +431,7 @@ func (node *StmtClassConstList) GetPosition() *position.Position { // StmtClassMethod node type StmtClassMethod struct { + AttributeBag Position *position.Position AttrGroups []Vertex Modifiers []Vertex @@ -433,6 +457,7 @@ func (node *StmtClassMethod) GetPosition() *position.Position { // StmtConstList node type StmtConstList struct { + AttributeBag Position *position.Position ConstTkn *token.Token Consts []Vertex @@ -450,6 +475,7 @@ func (node *StmtConstList) GetPosition() *position.Position { // StmtConstant node type StmtConstant struct { + AttributeBag Position *position.Position Name Vertex EqualTkn *token.Token @@ -466,6 +492,7 @@ func (node *StmtConstant) GetPosition() *position.Position { // StmtContinue node type StmtContinue struct { + AttributeBag Position *position.Position ContinueTkn *token.Token Expr Vertex @@ -482,6 +509,7 @@ func (node *StmtContinue) GetPosition() *position.Position { // StmtDeclare node type StmtDeclare struct { + AttributeBag Position *position.Position DeclareTkn *token.Token OpenParenthesisTkn *token.Token @@ -504,6 +532,7 @@ func (node *StmtDeclare) GetPosition() *position.Position { // StmtDefault node type StmtDefault struct { + AttributeBag Position *position.Position DefaultTkn *token.Token CaseSeparatorTkn *token.Token @@ -520,6 +549,7 @@ func (node *StmtDefault) GetPosition() *position.Position { // StmtDo node type StmtDo struct { + AttributeBag Position *position.Position DoTkn *token.Token Stmt Vertex @@ -540,6 +570,7 @@ func (node *StmtDo) GetPosition() *position.Position { // StmtEcho node type StmtEcho struct { + AttributeBag Position *position.Position EchoTkn *token.Token Exprs []Vertex @@ -557,6 +588,7 @@ func (node *StmtEcho) GetPosition() *position.Position { // StmtElse node type StmtElse struct { + AttributeBag Position *position.Position ElseTkn *token.Token ColonTkn *token.Token @@ -573,6 +605,7 @@ func (node *StmtElse) GetPosition() *position.Position { // StmtElseIf node type StmtElseIf struct { + AttributeBag Position *position.Position ElseIfTkn *token.Token OpenParenthesisTkn *token.Token @@ -592,6 +625,7 @@ func (node *StmtElseIf) GetPosition() *position.Position { // StmtExpression node type StmtExpression struct { + AttributeBag Position *position.Position Expr Vertex SemiColonTkn *token.Token @@ -607,6 +641,7 @@ func (node *StmtExpression) GetPosition() *position.Position { // StmtFinally node type StmtFinally struct { + AttributeBag Position *position.Position FinallyTkn *token.Token OpenCurlyBracketTkn *token.Token @@ -624,6 +659,7 @@ func (node *StmtFinally) GetPosition() *position.Position { // StmtFor node type StmtFor struct { + AttributeBag Position *position.Position ForTkn *token.Token OpenParenthesisTkn *token.Token @@ -652,6 +688,7 @@ func (node *StmtFor) GetPosition() *position.Position { // StmtForeach node type StmtForeach struct { + AttributeBag Position *position.Position ForeachTkn *token.Token OpenParenthesisTkn *token.Token @@ -678,6 +715,7 @@ func (node *StmtForeach) GetPosition() *position.Position { // StmtFunction node type StmtFunction struct { + AttributeBag Position *position.Position AttrGroups []Vertex FunctionTkn *token.Token @@ -704,6 +742,7 @@ func (node *StmtFunction) GetPosition() *position.Position { // StmtGlobal node type StmtGlobal struct { + AttributeBag Position *position.Position GlobalTkn *token.Token Vars []Vertex @@ -721,6 +760,7 @@ func (node *StmtGlobal) GetPosition() *position.Position { // StmtGoto node type StmtGoto struct { + AttributeBag Position *position.Position GotoTkn *token.Token Label Vertex @@ -737,6 +777,7 @@ func (node *StmtGoto) GetPosition() *position.Position { // StmtHaltCompiler node type StmtHaltCompiler struct { + AttributeBag Position *position.Position HaltCompilerTkn *token.Token OpenParenthesisTkn *token.Token @@ -754,6 +795,7 @@ func (node *StmtHaltCompiler) GetPosition() *position.Position { // StmtIf node type StmtIf struct { + AttributeBag Position *position.Position IfTkn *token.Token OpenParenthesisTkn *token.Token @@ -777,6 +819,7 @@ func (node *StmtIf) GetPosition() *position.Position { // StmtInlineHtml node type StmtInlineHtml struct { + AttributeBag Position *position.Position InlineHtmlTkn *token.Token Value []byte @@ -792,6 +835,7 @@ func (node *StmtInlineHtml) GetPosition() *position.Position { // StmtEnum node holds an enum declaration, optionally backed by a scalar type. type StmtEnum struct { + AttributeBag Position *position.Position AttrGroups []Vertex EnumTkn *token.Token @@ -817,6 +861,7 @@ func (node *StmtEnum) GetPosition() *position.Position { // StmtEnumCase node holds one `case` of an enum, with a value when the enum is // backed. type StmtEnumCase struct { + AttributeBag Position *position.Position AttrGroups []Vertex CaseTkn *token.Token @@ -836,6 +881,7 @@ func (node *StmtEnumCase) GetPosition() *position.Position { // StmtInterface node type StmtInterface struct { + AttributeBag Position *position.Position AttrGroups []Vertex InterfaceTkn *token.Token @@ -858,6 +904,7 @@ func (node *StmtInterface) GetPosition() *position.Position { // StmtLabel node type StmtLabel struct { + AttributeBag Position *position.Position Name Vertex ColonTkn *token.Token @@ -873,6 +920,7 @@ func (node *StmtLabel) GetPosition() *position.Position { // StmtNamespace node type StmtNamespace struct { + AttributeBag Position *position.Position NsTkn *token.Token Name Vertex @@ -892,6 +940,7 @@ func (node *StmtNamespace) GetPosition() *position.Position { // StmtNop node type StmtNop struct { + AttributeBag Position *position.Position SemiColonTkn *token.Token } @@ -906,6 +955,7 @@ func (node *StmtNop) GetPosition() *position.Position { // StmtProperty node type StmtProperty struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -922,6 +972,7 @@ func (node *StmtProperty) GetPosition() *position.Position { // StmtPropertyList node type StmtPropertyList struct { + AttributeBag Position *position.Position AttrGroups []Vertex Modifiers []Vertex @@ -941,6 +992,7 @@ func (node *StmtPropertyList) GetPosition() *position.Position { // StmtReturn node type StmtReturn struct { + AttributeBag Position *position.Position ReturnTkn *token.Token Expr Vertex @@ -957,6 +1009,7 @@ func (node *StmtReturn) GetPosition() *position.Position { // StmtStatic node type StmtStatic struct { + AttributeBag Position *position.Position StaticTkn *token.Token Vars []Vertex @@ -974,6 +1027,7 @@ func (node *StmtStatic) GetPosition() *position.Position { // StmtStaticVar node type StmtStaticVar struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -990,6 +1044,7 @@ func (node *StmtStaticVar) GetPosition() *position.Position { // StmtStmtList node type StmtStmtList struct { + AttributeBag Position *position.Position OpenCurlyBracketTkn *token.Token Stmts []Vertex @@ -1006,6 +1061,7 @@ func (node *StmtStmtList) GetPosition() *position.Position { // StmtSwitch node type StmtSwitch struct { + AttributeBag Position *position.Position SwitchTkn *token.Token OpenParenthesisTkn *token.Token @@ -1030,6 +1086,7 @@ func (node *StmtSwitch) GetPosition() *position.Position { // StmtThrow node type StmtThrow struct { + AttributeBag Position *position.Position ThrowTkn *token.Token Expr Vertex @@ -1047,6 +1104,7 @@ func (node *StmtThrow) GetPosition() *position.Position { // ExprThrow node is a `throw` used in expression position, such as // `$value ?? throw new InvalidArgumentException()`. type ExprThrow struct { + AttributeBag Position *position.Position ThrowTkn *token.Token Expr Vertex @@ -1062,6 +1120,7 @@ func (node *ExprThrow) GetPosition() *position.Position { // StmtTrait node type StmtTrait struct { + AttributeBag Position *position.Position AttrGroups []Vertex TraitTkn *token.Token @@ -1081,6 +1140,7 @@ func (node *StmtTrait) GetPosition() *position.Position { // StmtTraitUse node type StmtTraitUse struct { + AttributeBag Position *position.Position UseTkn *token.Token Traits []Vertex @@ -1101,6 +1161,7 @@ func (node *StmtTraitUse) GetPosition() *position.Position { // StmtTraitUseAlias node type StmtTraitUseAlias struct { + AttributeBag Position *position.Position Trait Vertex DoubleColonTkn *token.Token @@ -1121,6 +1182,7 @@ func (node *StmtTraitUseAlias) GetPosition() *position.Position { // StmtTraitUsePrecedence node type StmtTraitUsePrecedence struct { + AttributeBag Position *position.Position Trait Vertex DoubleColonTkn *token.Token @@ -1141,6 +1203,7 @@ func (node *StmtTraitUsePrecedence) GetPosition() *position.Position { // StmtTry node type StmtTry struct { + AttributeBag Position *position.Position TryTkn *token.Token OpenCurlyBracketTkn *token.Token @@ -1160,6 +1223,7 @@ func (node *StmtTry) GetPosition() *position.Position { // StmtUnset node type StmtUnset struct { + AttributeBag Position *position.Position UnsetTkn *token.Token OpenParenthesisTkn *token.Token @@ -1179,6 +1243,7 @@ func (node *StmtUnset) GetPosition() *position.Position { // StmtUseList node type StmtUseList struct { + AttributeBag Position *position.Position UseTkn *token.Token Type Vertex @@ -1197,6 +1262,7 @@ func (node *StmtUseList) GetPosition() *position.Position { // StmtGroupUseList node type StmtGroupUseList struct { + AttributeBag Position *position.Position UseTkn *token.Token Type Vertex @@ -1220,6 +1286,7 @@ func (node *StmtGroupUseList) GetPosition() *position.Position { // StmtUse node type StmtUse struct { + AttributeBag Position *position.Position Type Vertex NsSeparatorTkn *token.Token @@ -1238,6 +1305,7 @@ func (node *StmtUse) GetPosition() *position.Position { // StmtWhile node type StmtWhile struct { + AttributeBag Position *position.Position WhileTkn *token.Token OpenParenthesisTkn *token.Token @@ -1259,6 +1327,7 @@ func (node *StmtWhile) GetPosition() *position.Position { // ExprArray node type ExprArray struct { + AttributeBag Position *position.Position ArrayTkn *token.Token OpenBracketTkn *token.Token @@ -1277,6 +1346,7 @@ func (node *ExprArray) GetPosition() *position.Position { // ExprArrayDimFetch node type ExprArrayDimFetch struct { + AttributeBag Position *position.Position Var Vertex OpenBracketTkn *token.Token @@ -1294,6 +1364,7 @@ func (node *ExprArrayDimFetch) GetPosition() *position.Position { // ExprArrayItem node type ExprArrayItem struct { + AttributeBag Position *position.Position EllipsisTkn *token.Token Key Vertex @@ -1312,6 +1383,7 @@ func (node *ExprArrayItem) GetPosition() *position.Position { // ExprArrowFunction node type ExprArrowFunction struct { + AttributeBag Position *position.Position StaticTkn *token.Token FnTkn *token.Token @@ -1336,6 +1408,7 @@ func (node *ExprArrowFunction) GetPosition() *position.Position { // ExprBitwiseNot node type ExprBitwiseNot struct { + AttributeBag Position *position.Position TildaTkn *token.Token Expr Vertex @@ -1351,6 +1424,7 @@ func (node *ExprBitwiseNot) GetPosition() *position.Position { // ExprBooleanNot node type ExprBooleanNot struct { + AttributeBag Position *position.Position ExclamationTkn *token.Token Expr Vertex @@ -1365,6 +1439,7 @@ func (node *ExprBooleanNot) GetPosition() *position.Position { } type ExprBrackets struct { + AttributeBag Position *position.Position OpenParenthesisTkn *token.Token Expr Vertex @@ -1381,6 +1456,7 @@ func (node *ExprBrackets) GetPosition() *position.Position { // ExprClassConstFetch node type ExprClassConstFetch struct { + AttributeBag Position *position.Position Class Vertex DoubleColonTkn *token.Token @@ -1397,6 +1473,7 @@ func (node *ExprClassConstFetch) GetPosition() *position.Position { // ExprClone node type ExprClone struct { + AttributeBag Position *position.Position CloneTkn *token.Token Expr Vertex @@ -1412,6 +1489,7 @@ func (node *ExprClone) GetPosition() *position.Position { // ExprClosure node type ExprClosure struct { + AttributeBag Position *position.Position StaticTkn *token.Token FunctionTkn *token.Token @@ -1442,6 +1520,7 @@ func (node *ExprClosure) GetPosition() *position.Position { // ExprClosureUse node type ExprClosureUse struct { + AttributeBag Position *position.Position AmpersandTkn *token.Token Var Vertex @@ -1457,6 +1536,7 @@ func (node *ExprClosureUse) GetPosition() *position.Position { // ExprConstFetch node type ExprConstFetch struct { + AttributeBag Position *position.Position Const Vertex } @@ -1471,6 +1551,7 @@ func (node *ExprConstFetch) GetPosition() *position.Position { // ExprEmpty node type ExprEmpty struct { + AttributeBag Position *position.Position EmptyTkn *token.Token OpenParenthesisTkn *token.Token @@ -1488,6 +1569,7 @@ func (node *ExprEmpty) GetPosition() *position.Position { // ExprErrorSuppress node type ExprErrorSuppress struct { + AttributeBag Position *position.Position AtTkn *token.Token Expr Vertex @@ -1503,6 +1585,7 @@ func (node *ExprErrorSuppress) GetPosition() *position.Position { // ExprEval node type ExprEval struct { + AttributeBag Position *position.Position EvalTkn *token.Token OpenParenthesisTkn *token.Token @@ -1520,6 +1603,7 @@ func (node *ExprEval) GetPosition() *position.Position { // ExprExit node type ExprExit struct { + AttributeBag Position *position.Position ExitTkn *token.Token OpenParenthesisTkn *token.Token @@ -1537,6 +1621,7 @@ func (node *ExprExit) GetPosition() *position.Position { // ExprFunctionCall node type ExprFunctionCall struct { + AttributeBag Position *position.Position Function Vertex OpenParenthesisTkn *token.Token @@ -1555,6 +1640,7 @@ func (node *ExprFunctionCall) GetPosition() *position.Position { // ExprInclude node type ExprInclude struct { + AttributeBag Position *position.Position IncludeTkn *token.Token Expr Vertex @@ -1570,6 +1656,7 @@ func (node *ExprInclude) GetPosition() *position.Position { // ExprIncludeOnce node type ExprIncludeOnce struct { + AttributeBag Position *position.Position IncludeOnceTkn *token.Token Expr Vertex @@ -1585,6 +1672,7 @@ func (node *ExprIncludeOnce) GetPosition() *position.Position { // ExprInstanceOf node type ExprInstanceOf struct { + AttributeBag Position *position.Position Expr Vertex InstanceOfTkn *token.Token @@ -1601,6 +1689,7 @@ func (node *ExprInstanceOf) GetPosition() *position.Position { // ExprIsset node type ExprIsset struct { + AttributeBag Position *position.Position IssetTkn *token.Token OpenParenthesisTkn *token.Token @@ -1619,6 +1708,7 @@ func (node *ExprIsset) GetPosition() *position.Position { // ExprList node type ExprList struct { + AttributeBag Position *position.Position ListTkn *token.Token OpenBracketTkn *token.Token @@ -1637,6 +1727,7 @@ func (node *ExprList) GetPosition() *position.Position { // ExprMethodCall node type ExprMethodCall struct { + AttributeBag Position *position.Position Var Vertex ObjectOperatorTkn *token.Token @@ -1660,6 +1751,7 @@ func (node *ExprMethodCall) GetPosition() *position.Position { // ExprNullsafeMethodCall node is a `$object?->method()` call, which short-circuits // to null when the object is null. type ExprNullsafeMethodCall struct { + AttributeBag Position *position.Position Var Vertex ObjectOperatorTkn *token.Token @@ -1683,6 +1775,7 @@ func (node *ExprNullsafeMethodCall) GetPosition() *position.Position { // ExprNullsafePropertyFetch node is a `$object?->property` access, which // short-circuits to null when the object is null. type ExprNullsafePropertyFetch struct { + AttributeBag Position *position.Position Var Vertex ObjectOperatorTkn *token.Token @@ -1701,6 +1794,7 @@ func (node *ExprNullsafePropertyFetch) GetPosition() *position.Position { // ExprNew node type ExprNew struct { + AttributeBag Position *position.Position NewTkn *token.Token Class Vertex @@ -1720,6 +1814,7 @@ func (node *ExprNew) GetPosition() *position.Position { // ExprPostDec node type ExprPostDec struct { + AttributeBag Position *position.Position Var Vertex DecTkn *token.Token @@ -1735,6 +1830,7 @@ func (node *ExprPostDec) GetPosition() *position.Position { // ExprPostInc node type ExprPostInc struct { + AttributeBag Position *position.Position Var Vertex IncTkn *token.Token @@ -1750,6 +1846,7 @@ func (node *ExprPostInc) GetPosition() *position.Position { // ExprPreDec node type ExprPreDec struct { + AttributeBag Position *position.Position DecTkn *token.Token Var Vertex @@ -1765,6 +1862,7 @@ func (node *ExprPreDec) GetPosition() *position.Position { // ExprPreInc node type ExprPreInc struct { + AttributeBag Position *position.Position IncTkn *token.Token Var Vertex @@ -1780,6 +1878,7 @@ func (node *ExprPreInc) GetPosition() *position.Position { // ExprPrint node type ExprPrint struct { + AttributeBag Position *position.Position PrintTkn *token.Token Expr Vertex @@ -1795,6 +1894,7 @@ func (node *ExprPrint) GetPosition() *position.Position { // ExprPropertyFetch node type ExprPropertyFetch struct { + AttributeBag Position *position.Position Var Vertex ObjectOperatorTkn *token.Token @@ -1813,6 +1913,7 @@ func (node *ExprPropertyFetch) GetPosition() *position.Position { // ExprRequire node type ExprRequire struct { + AttributeBag Position *position.Position RequireTkn *token.Token Expr Vertex @@ -1828,6 +1929,7 @@ func (node *ExprRequire) GetPosition() *position.Position { // ExprRequireOnce node type ExprRequireOnce struct { + AttributeBag Position *position.Position RequireOnceTkn *token.Token Expr Vertex @@ -1843,6 +1945,7 @@ func (node *ExprRequireOnce) GetPosition() *position.Position { // ExprShellExec node type ExprShellExec struct { + AttributeBag Position *position.Position OpenBacktickTkn *token.Token Parts []Vertex @@ -1859,6 +1962,7 @@ func (node *ExprShellExec) GetPosition() *position.Position { // ExprStaticCall node type ExprStaticCall struct { + AttributeBag Position *position.Position Class Vertex DoubleColonTkn *token.Token @@ -1881,6 +1985,7 @@ func (node *ExprStaticCall) GetPosition() *position.Position { // ExprStaticPropertyFetch node type ExprStaticPropertyFetch struct { + AttributeBag Position *position.Position Class Vertex DoubleColonTkn *token.Token @@ -1897,6 +2002,7 @@ func (node *ExprStaticPropertyFetch) GetPosition() *position.Position { // ExprTernary node type ExprTernary struct { + AttributeBag Position *position.Position Cond Vertex QuestionTkn *token.Token @@ -1915,6 +2021,7 @@ func (node *ExprTernary) GetPosition() *position.Position { // ExprMatch node type ExprMatch struct { + AttributeBag Position *position.Position MatchTkn *token.Token OpenParenthesisTkn *token.Token @@ -1936,6 +2043,7 @@ func (node *ExprMatch) GetPosition() *position.Position { // MatchArm node type MatchArm struct { + AttributeBag Position *position.Position DefaultTkn *token.Token Exprs []Vertex @@ -1954,6 +2062,7 @@ func (node *MatchArm) GetPosition() *position.Position { // ExprUnaryMinus node type ExprUnaryMinus struct { + AttributeBag Position *position.Position MinusTkn *token.Token Expr Vertex @@ -1969,6 +2078,7 @@ func (node *ExprUnaryMinus) GetPosition() *position.Position { // ExprUnaryPlus node type ExprUnaryPlus struct { + AttributeBag Position *position.Position PlusTkn *token.Token Expr Vertex @@ -1984,6 +2094,7 @@ func (node *ExprUnaryPlus) GetPosition() *position.Position { // ExprVariable node type ExprVariable struct { + AttributeBag Position *position.Position DollarTkn *token.Token OpenCurlyBracketTkn *token.Token @@ -2001,6 +2112,7 @@ func (node *ExprVariable) GetPosition() *position.Position { // ExprYield node type ExprYield struct { + AttributeBag Position *position.Position YieldTkn *token.Token Key Vertex @@ -2018,6 +2130,7 @@ func (node *ExprYield) GetPosition() *position.Position { // ExprYieldFrom node type ExprYieldFrom struct { + AttributeBag Position *position.Position YieldFromTkn *token.Token Expr Vertex @@ -2033,6 +2146,7 @@ func (node *ExprYieldFrom) GetPosition() *position.Position { // ExprCastArray node type ExprCastArray struct { + AttributeBag Position *position.Position CastTkn *token.Token Expr Vertex @@ -2048,6 +2162,7 @@ func (node *ExprCastArray) GetPosition() *position.Position { // ExprCastBool node type ExprCastBool struct { + AttributeBag Position *position.Position CastTkn *token.Token Expr Vertex @@ -2063,6 +2178,7 @@ func (node *ExprCastBool) GetPosition() *position.Position { // ExprCastDouble node type ExprCastDouble struct { + AttributeBag Position *position.Position CastTkn *token.Token Expr Vertex @@ -2078,6 +2194,7 @@ func (node *ExprCastDouble) GetPosition() *position.Position { // ExprCastInt node type ExprCastInt struct { + AttributeBag Position *position.Position CastTkn *token.Token Expr Vertex @@ -2093,6 +2210,7 @@ func (node *ExprCastInt) GetPosition() *position.Position { // ExprCastObject node type ExprCastObject struct { + AttributeBag Position *position.Position CastTkn *token.Token Expr Vertex @@ -2108,6 +2226,7 @@ func (node *ExprCastObject) GetPosition() *position.Position { // ExprCastString node type ExprCastString struct { + AttributeBag Position *position.Position CastTkn *token.Token Expr Vertex @@ -2123,6 +2242,7 @@ func (node *ExprCastString) GetPosition() *position.Position { // ExprCastUnset node type ExprCastUnset struct { + AttributeBag Position *position.Position CastTkn *token.Token Expr Vertex @@ -2138,6 +2258,7 @@ func (node *ExprCastUnset) GetPosition() *position.Position { // ExprAssign node type ExprAssign struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2154,6 +2275,7 @@ func (node *ExprAssign) GetPosition() *position.Position { // ExprAssignReference node type ExprAssignReference struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2171,6 +2293,7 @@ func (node *ExprAssignReference) GetPosition() *position.Position { // ExprAssignBitwiseAnd node type ExprAssignBitwiseAnd struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2187,6 +2310,7 @@ func (node *ExprAssignBitwiseAnd) GetPosition() *position.Position { // ExprAssignBitwiseOr node type ExprAssignBitwiseOr struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2203,6 +2327,7 @@ func (node *ExprAssignBitwiseOr) GetPosition() *position.Position { // ExprAssignBitwiseXor node type ExprAssignBitwiseXor struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2219,6 +2344,7 @@ func (node *ExprAssignBitwiseXor) GetPosition() *position.Position { // ExprAssignCoalesce node type ExprAssignCoalesce struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2235,6 +2361,7 @@ func (node *ExprAssignCoalesce) GetPosition() *position.Position { // ExprAssignConcat node type ExprAssignConcat struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2251,6 +2378,7 @@ func (node *ExprAssignConcat) GetPosition() *position.Position { // ExprAssignDiv node type ExprAssignDiv struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2267,6 +2395,7 @@ func (node *ExprAssignDiv) GetPosition() *position.Position { // ExprAssignMinus node type ExprAssignMinus struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2283,6 +2412,7 @@ func (node *ExprAssignMinus) GetPosition() *position.Position { // ExprAssignMod node type ExprAssignMod struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2299,6 +2429,7 @@ func (node *ExprAssignMod) GetPosition() *position.Position { // ExprAssignMul node type ExprAssignMul struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2315,6 +2446,7 @@ func (node *ExprAssignMul) GetPosition() *position.Position { // ExprAssignPlus node type ExprAssignPlus struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2331,6 +2463,7 @@ func (node *ExprAssignPlus) GetPosition() *position.Position { // ExprAssignPow node type ExprAssignPow struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2347,6 +2480,7 @@ func (node *ExprAssignPow) GetPosition() *position.Position { // ExprAssignShiftLeft node type ExprAssignShiftLeft struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2363,6 +2497,7 @@ func (node *ExprAssignShiftLeft) GetPosition() *position.Position { // ExprAssignShiftRight node type ExprAssignShiftRight struct { + AttributeBag Position *position.Position Var Vertex EqualTkn *token.Token @@ -2379,6 +2514,7 @@ func (node *ExprAssignShiftRight) GetPosition() *position.Position { // ExprBinaryBitwiseAnd node type ExprBinaryBitwiseAnd struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2395,6 +2531,7 @@ func (node *ExprBinaryBitwiseAnd) GetPosition() *position.Position { // ExprBinaryBitwiseOr node type ExprBinaryBitwiseOr struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2411,6 +2548,7 @@ func (node *ExprBinaryBitwiseOr) GetPosition() *position.Position { // ExprBinaryBitwiseXor node type ExprBinaryBitwiseXor struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2427,6 +2565,7 @@ func (node *ExprBinaryBitwiseXor) GetPosition() *position.Position { // ExprBinaryBooleanAnd node type ExprBinaryBooleanAnd struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2443,6 +2582,7 @@ func (node *ExprBinaryBooleanAnd) GetPosition() *position.Position { // ExprBinaryBooleanOr node type ExprBinaryBooleanOr struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2459,6 +2599,7 @@ func (node *ExprBinaryBooleanOr) GetPosition() *position.Position { // ExprBinaryCoalesce node type ExprBinaryCoalesce struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2475,6 +2616,7 @@ func (node *ExprBinaryCoalesce) GetPosition() *position.Position { // ExprBinaryConcat node type ExprBinaryConcat struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2491,6 +2633,7 @@ func (node *ExprBinaryConcat) GetPosition() *position.Position { // ExprBinaryDiv node type ExprBinaryDiv struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2507,6 +2650,7 @@ func (node *ExprBinaryDiv) GetPosition() *position.Position { // ExprBinaryEqual node type ExprBinaryEqual struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2523,6 +2667,7 @@ func (node *ExprBinaryEqual) GetPosition() *position.Position { // ExprBinaryGreater node type ExprBinaryGreater struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2539,6 +2684,7 @@ func (node *ExprBinaryGreater) GetPosition() *position.Position { // ExprBinaryGreaterOrEqual node type ExprBinaryGreaterOrEqual struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2555,6 +2701,7 @@ func (node *ExprBinaryGreaterOrEqual) GetPosition() *position.Position { // ExprBinaryIdentical node type ExprBinaryIdentical struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2571,6 +2718,7 @@ func (node *ExprBinaryIdentical) GetPosition() *position.Position { // ExprBinaryLogicalAnd node type ExprBinaryLogicalAnd struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2587,6 +2735,7 @@ func (node *ExprBinaryLogicalAnd) GetPosition() *position.Position { // ExprBinaryLogicalOr node type ExprBinaryLogicalOr struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2603,6 +2752,7 @@ func (node *ExprBinaryLogicalOr) GetPosition() *position.Position { // ExprBinaryLogicalXor node type ExprBinaryLogicalXor struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2619,6 +2769,7 @@ func (node *ExprBinaryLogicalXor) GetPosition() *position.Position { // ExprBinaryMinus node type ExprBinaryMinus struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2635,6 +2786,7 @@ func (node *ExprBinaryMinus) GetPosition() *position.Position { // ExprBinaryMod node type ExprBinaryMod struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2651,6 +2803,7 @@ func (node *ExprBinaryMod) GetPosition() *position.Position { // ExprBinaryMul node type ExprBinaryMul struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2667,6 +2820,7 @@ func (node *ExprBinaryMul) GetPosition() *position.Position { // ExprBinaryNotEqual node type ExprBinaryNotEqual struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2683,6 +2837,7 @@ func (node *ExprBinaryNotEqual) GetPosition() *position.Position { // ExprBinaryNotIdentical node type ExprBinaryNotIdentical struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2699,6 +2854,7 @@ func (node *ExprBinaryNotIdentical) GetPosition() *position.Position { // ExprBinaryPlus node type ExprBinaryPlus struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2715,6 +2871,7 @@ func (node *ExprBinaryPlus) GetPosition() *position.Position { // ExprBinaryPow node type ExprBinaryPow struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2731,6 +2888,7 @@ func (node *ExprBinaryPow) GetPosition() *position.Position { // ExprBinaryShiftLeft node type ExprBinaryShiftLeft struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2747,6 +2905,7 @@ func (node *ExprBinaryShiftLeft) GetPosition() *position.Position { // ExprBinaryShiftRight node type ExprBinaryShiftRight struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2763,6 +2922,7 @@ func (node *ExprBinaryShiftRight) GetPosition() *position.Position { // ExprBinarySmaller node type ExprBinarySmaller struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2779,6 +2939,7 @@ func (node *ExprBinarySmaller) GetPosition() *position.Position { // ExprBinarySmallerOrEqual node type ExprBinarySmallerOrEqual struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2795,6 +2956,7 @@ func (node *ExprBinarySmallerOrEqual) GetPosition() *position.Position { // ExprBinarySpaceship node type ExprBinarySpaceship struct { + AttributeBag Position *position.Position Left Vertex OpTkn *token.Token @@ -2810,6 +2972,7 @@ func (node *ExprBinarySpaceship) GetPosition() *position.Position { } type Name struct { + AttributeBag Position *position.Position Parts []Vertex SeparatorTkns []*token.Token @@ -2824,6 +2987,7 @@ func (node *Name) GetPosition() *position.Position { } type NameFullyQualified struct { + AttributeBag Position *position.Position NsSeparatorTkn *token.Token Parts []Vertex @@ -2839,6 +3003,7 @@ func (node *NameFullyQualified) GetPosition() *position.Position { } type NameRelative struct { + AttributeBag Position *position.Position NsTkn *token.Token NsSeparatorTkn *token.Token @@ -2855,6 +3020,7 @@ func (node *NameRelative) GetPosition() *position.Position { } type NamePart struct { + AttributeBag Position *position.Position StringTkn *token.Token Value []byte From 7d3dc55bcfb24289e9528c01c5b9de128f2a0362 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 8 Nov 2026 20:30:33 +0100 Subject: [PATCH 2/2] Add public typescope package Resolves the static type of a value expression by tracing it through the function scopes of a class subtree. Promoted from an internal package so any project on the parser can reuse it. --- pkg/typescope/typescope.go | 641 ++++++++++++++++++++++++++++++++ pkg/typescope/typescope_test.go | 333 +++++++++++++++++ 2 files changed, 974 insertions(+) create mode 100644 pkg/typescope/typescope.go create mode 100644 pkg/typescope/typescope_test.go diff --git a/pkg/typescope/typescope.go b/pkg/typescope/typescope.go new file mode 100644 index 0000000..5650292 --- /dev/null +++ b/pkg/typescope/typescope.go @@ -0,0 +1,641 @@ +// Package typescope resolves the static type of a value expression by tracing it +// through the function scopes of a class subtree. +// +// It answers one question — "what type is this expression, at this position?" — +// and is the reusable core behind the inferred-param-type rule and any future rule +// that must resolve indirection rather than read a literal: +// +// scopes := typescope.Collect(classNode) +// typeName, ok := scopes.ResolveValue(argumentExpr, callPosition) +// +// A variable resolves only when it has exactly one definition in the position's +// nearest enclosing function scope — a typed parameter or a single assignment. Two +// assignments, a reassigned parameter, an untyped parameter, or a definition in a +// different scope all leave it unresolved. Arithmetic and concat expressions +// resolve through their operands: concat is always string, modulo always int, and +// the other arithmetic operators combine their operand types (float if either is +// float, otherwise int). +// +// ResolveType additionally follows a `$this->property` fetch to the property's +// declared type, whether written as a property declaration or promoted in the +// constructor. It is kept apart from ResolveValue so the rules copying a resolved +// type onto a signature keep inferring from exactly what they did before. +package typescope + +import ( + "strings" + + "github.com/rectorphp/php-parser-in-go/pkg/ast" + "github.com/rectorphp/php-parser-in-go/pkg/position" + "github.com/rectorphp/php-parser-in-go/pkg/visitor" + "github.com/rectorphp/php-parser-in-go/pkg/visitor/traverser" +) + +// Scopes is what was gathered from a class subtree — its function scopes and the +// declared type of each of its properties — ready to resolve expression types +// against. +type Scopes struct { + functionScopes []*functionScope + propertyTypes map[string]ast.Vertex +} + +// Collect gathers every function-like scope in a class subtree, attributes each +// simple assignment to its enclosing scope, so later resolution sees a variable's +// definitions in the scope they actually belong to, and records the declared type +// of every property. +func Collect(classNode ast.Vertex) Scopes { + collector := &scopeCollector{Null: &visitor.Null{}, propertyTypes: map[string]ast.Vertex{}} + traverser.NewTraverser(collector).Traverse(classNode) + + scopes := Scopes{functionScopes: collector.scopes, propertyTypes: collector.propertyTypes} + for _, assignment := range collector.assignments { + scope := enclosingScope(assignment.position, scopes) + if scope == nil { + continue + } + scope.assignedValues[assignment.name] = append(scope.assignedValues[assignment.name], assignment.value) + } + return scopes +} + +// ResolveType resolves an expression's type like ResolveValue does, and follows a +// `$this->property` fetch to the property's declared type on top of it. +func (scopes Scopes) ResolveType(expr ast.Vertex, atPosition *position.Position) (string, bool) { + if propertyType, ok := scopes.resolvePropertyType(expr); ok { + return propertyType, true + } + return scopes.ResolveValue(expr, atPosition) +} + +// IsNullableDeclaration reports whether the type an expression is declared with +// is written nullable — a `?LeadRepository` property or parameter. It answers +// the difference between "holds a repository" and "is definitely a repository", +// which is what a caller asking for a certain type has to know: a value that may +// be null is not certainly anything. +// +// An expression with no declaration to read reports false, as does a type that +// simply is not nullable. +func (scopes Scopes) IsNullableDeclaration(expr ast.Vertex, atPosition *position.Position) bool { + declaredType, ok := scopes.declarationOf(expr, atPosition) + if !ok { + return false + } + _, nullable := declaredType.(*ast.Nullable) + return nullable +} + +// declarationOf returns the type node an expression's type is declared by: a +// property declaration for a `$this->property` fetch, a parameter declaration +// for a variable holding one. +func (scopes Scopes) declarationOf(expr ast.Vertex, atPosition *position.Position) (ast.Vertex, bool) { + if propertyFetch, ok := expr.(*ast.ExprPropertyFetch); ok { + property, named := identifierName(propertyFetch.Prop) + if !named { + return nil, false + } + declaredType, ok := scopes.propertyTypes[property] + return declaredType, ok && declaredType != nil + } + + variable, isVariable := expr.(*ast.ExprVariable) + if !isVariable { + return nil, false + } + // the parameter map is keyed by the variable name as written, `$` included + name, named := identifierName(variable.Name) + if !named { + return nil, false + } + scope := enclosingScope(atPosition, scopes) + if scope == nil { + return nil, false + } + declaredType, ok := scope.parameterTypes[name] + return declaredType, ok && declaredType != nil +} + +// resolvePropertyType reads the declared type of a `$this->someProperty` fetch. A +// property with no type, or one fetched on anything but $this, is unresolved — +// there is nothing to read the type from. +func (scopes Scopes) resolvePropertyType(expr ast.Vertex) (string, bool) { + propertyFetch, ok := expr.(*ast.ExprPropertyFetch) + if !ok { + return "", false + } + receiver, isVariable := propertyFetch.Var.(*ast.ExprVariable) + if !isVariable { + return "", false + } + receiverName, named := identifierName(receiver.Name) + if !named || receiverName != "$this" { + return "", false + } + property, ok := identifierName(propertyFetch.Prop) + if !ok { + return "", false + } + declaredType, ok := scopes.propertyTypes[property] + if !ok || declaredType == nil { + return "", false + } + return declaredTypeName(declaredType) +} + +// ResolveValue resolves the type of a value expression at the given position: a +// literal, a resolvable variable, or a type-bearing binary operator. It reports +// false when the type cannot be pinned to a single known name. +func (scopes Scopes) ResolveValue(expr ast.Vertex, atPosition *position.Position) (string, bool) { + return scopes.inferValueType(expr, atPosition, map[string]bool{}) +} + +// ResolveClass resolves an expression to a class name at the given position — a +// `new Foo()` or a variable that traces to one. It reports false when the +// expression resolves to a built-in type or cannot be resolved at all, so callers +// see only genuine class types. +func (scopes Scopes) ResolveClass(expr ast.Vertex, atPosition *position.Position) (string, bool) { + typeName, ok := scopes.ResolveValue(expr, atPosition) + if !ok || IsBuiltinType(typeName) { + return "", false + } + return typeName, true +} + +// resolveVariableType traces a variable to its single definition in the position's +// nearest enclosing function scope. A typed parameter yields its declared type; a +// single assignment yields its right-hand side's type. Anything ambiguous — no +// definition, more than one, or an untyped parameter — is unresolved. The seen set +// breaks cycles such as `$a = $a + 1`. +func (scopes Scopes) resolveVariableType(name string, atPosition *position.Position, seen map[string]bool) (string, bool) { + if seen[name] { + return "", false + } + + scope := enclosingScope(atPosition, scopes) + if scope == nil { + return "", false + } + + // a variable's type is a pure function of its scope, so the same variable + // resolved again in the same scope — e.g. one passed to several methods — + // reuses the first result instead of walking its definition anew + if cached, ok := scope.resolvedVariables[name]; ok { + return cached.name, cached.resolved + } + + typeName, resolved := scopes.resolveVariableInScope(name, scope, atPosition, seen) + scope.resolvedVariables[name] = resolvedVariable{name: typeName, resolved: resolved} + return typeName, resolved +} + +// resolveVariableInScope resolves a variable against the scope it belongs to: a +// typed parameter yields its declared type, a single assignment yields its +// right-hand side's type, and anything ambiguous is unresolved. +func (scopes Scopes) resolveVariableInScope(name string, scope *functionScope, atPosition *position.Position, seen map[string]bool) (string, bool) { + parameterType, isParameter := scope.parameterTypes[name] + assignedValues := scope.assignedValues[name] + + definitionCount := len(assignedValues) + if isParameter { + definitionCount++ + } + if definitionCount != 1 { + return "", false + } + + if isParameter { + return builtinTypeName(parameterType) + } + + nextSeen := map[string]bool{name: true} + for seenName := range seen { + nextSeen[seenName] = true + } + return scopes.inferValueType(assignedValues[0], atPosition, nextSeen) +} + +// inferValueType resolves the type of a value expression: a literal, a resolvable +// variable, or a type-bearing binary operator. Concatenation is always string, +// modulo is always int, and the other arithmetic operators combine their operand +// types. Anything else is unresolved. +func (scopes Scopes) inferValueType(expr ast.Vertex, atPosition *position.Position, seen map[string]bool) (string, bool) { + switch node := expr.(type) { + case *ast.ScalarString: + return "string", true + case *ast.ScalarLnumber: + return "int", true + case *ast.ScalarDnumber: + return "float", true + case *ast.ExprArray: + return "array", true + case *ast.ExprConstFetch: + if isBooleanConstant(node) { + return "bool", true + } + case *ast.ExprVariable: + name, ok := identifierName(node.Name) + if !ok { + return "", false + } + return scopes.resolveVariableType(name, atPosition, seen) + case *ast.ExprBinaryConcat: + return "string", true + case *ast.ExprBinaryMod: + return "int", true + case *ast.ExprBinaryPlus: + return scopes.combineNumericTypes(node.Left, node.Right, atPosition, seen) + case *ast.ExprBinaryMinus: + return scopes.combineNumericTypes(node.Left, node.Right, atPosition, seen) + case *ast.ExprBinaryMul: + return scopes.combineNumericTypes(node.Left, node.Right, atPosition, seen) + case *ast.ExprBinaryPow: + return scopes.combineNumericTypes(node.Left, node.Right, atPosition, seen) + case *ast.ExprNew: + return classNameFromNew(node) + case *ast.ExprFunctionCall: + return builtinCallReturnType(node) + } + return "", false +} + +// builtinCallReturnType resolves a call to an internal PHP function whose return +// type is a single, unconditional scalar — `string`, `int`, `float`, or `bool` — +// to that type. Only a plain single-part name is read, matched case-insensitively +// as PHP function names are; a `\sprintf` or namespaced call is left alone. +func builtinCallReturnType(node *ast.ExprFunctionCall) (string, bool) { + name, ok := node.Function.(*ast.Name) + if !ok || len(name.Parts) != 1 { + return "", false + } + part, ok := name.Parts[0].(*ast.NamePart) + if !ok { + return "", false + } + typeName, ok := builtinReturnTypes[strings.ToLower(string(part.Value))] + return typeName, ok +} + +// builtinReturnTypes maps internal PHP functions with a single, unconditional +// scalar return type to that type. A function is listed only when it returns the +// same scalar in every case across PHP 7 and 8 — never a union, nullable, or a +// `false`/`null` on failure. That excludes the large families that signal failure +// in-band: `strpos`/`preg_match` (int|false), `json_encode`/`base64_decode` +// (string|false), `substr`/`mb_strlen` (version-dependent false), and the +// numeric-or-float pair `abs`/`pow`. Adding a false-returning function here would +// let the param-type rules declare a type the value does not always hold. +var builtinReturnTypes = map[string]string{ + // string + "sprintf": "string", "vsprintf": "string", + "strtolower": "string", "strtoupper": "string", + "lcfirst": "string", "ucfirst": "string", "ucwords": "string", + "trim": "string", "ltrim": "string", "rtrim": "string", + "str_repeat": "string", "str_pad": "string", "strrev": "string", "nl2br": "string", + "addslashes": "string", "stripslashes": "string", + "addcslashes": "string", "stripcslashes": "string", "quotemeta": "string", + "htmlspecialchars": "string", "htmlentities": "string", + "html_entity_decode": "string", "htmlspecialchars_decode": "string", "strip_tags": "string", + "dechex": "string", "decoct": "string", "decbin": "string", + "chr": "string", "str_rot13": "string", "number_format": "string", + "serialize": "string", "base64_encode": "string", "bin2hex": "string", + "urlencode": "string", "urldecode": "string", + "rawurlencode": "string", "rawurldecode": "string", + "implode": "string", "join": "string", + "basename": "string", "dirname": "string", "uniqid": "string", "preg_quote": "string", + "gettype": "string", "date": "string", "gmdate": "string", "md5": "string", "sha1": "string", + "mb_strtolower": "string", "mb_strtoupper": "string", "mb_convert_case": "string", + + // int + "strlen": "int", "count": "int", "sizeof": "int", + "strcmp": "int", "strcasecmp": "int", "strncmp": "int", "strncasecmp": "int", + "strnatcmp": "int", "strnatcasecmp": "int", + "ord": "int", "crc32": "int", "time": "int", "intdiv": "int", + "levenshtein": "int", "similar_text": "int", "substr_count": "int", + "strspn": "int", "strcspn": "int", + "rand": "int", "mt_rand": "int", "random_int": "int", + "getrandmax": "int", "mt_getrandmax": "int", + "memory_get_usage": "int", "memory_get_peak_usage": "int", + + // float + "floatval": "float", "sqrt": "float", + "sin": "float", "cos": "float", "tan": "float", + "asin": "float", "acos": "float", "atan": "float", "atan2": "float", + "sinh": "float", "cosh": "float", "tanh": "float", + "exp": "float", "log": "float", "log10": "float", + "deg2rad": "float", "rad2deg": "float", "hypot": "float", "fmod": "float", + "pi": "float", "lcg_value": "float", + "ceil": "float", "floor": "float", "round": "float", + + // bool + "ctype_alpha": "bool", "ctype_alnum": "bool", "ctype_digit": "bool", "ctype_space": "bool", + "ctype_upper": "bool", "ctype_lower": "bool", "ctype_punct": "bool", "ctype_xdigit": "bool", + "ctype_cntrl": "bool", "ctype_graph": "bool", "ctype_print": "bool", + "is_array": "bool", "is_string": "bool", "is_int": "bool", "is_integer": "bool", + "is_long": "bool", "is_float": "bool", "is_double": "bool", "is_bool": "bool", + "is_null": "bool", "is_object": "bool", "is_callable": "bool", "is_iterable": "bool", + "is_scalar": "bool", "is_numeric": "bool", "is_countable": "bool", + "is_a": "bool", "is_subclass_of": "bool", + "boolval": "bool", "in_array": "bool", "array_key_exists": "bool", "key_exists": "bool", + "str_contains": "bool", "str_starts_with": "bool", "str_ends_with": "bool", + "function_exists": "bool", "method_exists": "bool", "property_exists": "bool", + "class_exists": "bool", "interface_exists": "bool", "trait_exists": "bool", "enum_exists": "bool", + "defined": "bool", "extension_loaded": "bool", "hash_equals": "bool", "fnmatch": "bool", +} + +// classNameFromNew returns the class of a `new Foo()` expression, accepting only a +// plain single-part class name so it can be copied onto another signature as +// written. A qualified, fully-qualified, or dynamic (`new $class()`) class is +// unresolved. +func classNameFromNew(node *ast.ExprNew) (string, bool) { + name, ok := node.Class.(*ast.Name) + if !ok || len(name.Parts) != 1 { + return "", false + } + part, ok := name.Parts[0].(*ast.NamePart) + if !ok { + return "", false + } + return string(part.Value), true +} + +// IsBuiltinType reports whether a resolved type name is a PHP built-in type rather +// than a class name, so the object and class rules can tell the two apart. +func IsBuiltinType(name string) bool { + switch name { + case "int", "float", "string", "bool", "array", "object", "iterable", + "callable", "void", "null", "false", "true", "mixed", "never", + "self", "static", "parent": + return true + } + return false +} + +// combineNumericTypes resolves the result type of an arithmetic operator. Both +// operands must resolve to a numeric type; the result is float when either operand +// is float, otherwise int. +func (scopes Scopes) combineNumericTypes(left, right ast.Vertex, atPosition *position.Position, seen map[string]bool) (string, bool) { + leftType, leftOk := scopes.inferValueType(left, atPosition, seen) + rightType, rightOk := scopes.inferValueType(right, atPosition, seen) + if !leftOk || !rightOk { + return "", false + } + if !isNumericType(leftType) || !isNumericType(rightType) { + return "", false + } + if leftType == "float" || rightType == "float" { + return "float", true + } + return "int", true +} + +func isNumericType(typeName string) bool { + return typeName == "int" || typeName == "float" +} + +// builtinTypeName returns a parameter's declared type as a bare name, accepting +// only a plain identifier (`int`, `array`) or a single-part name (`Foo`). A +// nullable, union, or intersection type is unresolved, since copying it verbatim +// onto another signature is not always sound. +func builtinTypeName(typeNode ast.Vertex) (string, bool) { + switch node := typeNode.(type) { + case *ast.Identifier: + return string(node.Value), true + case *ast.Name: + if len(node.Parts) != 1 { + return "", false + } + part, ok := node.Parts[0].(*ast.NamePart) + if !ok { + return "", false + } + return string(part.Value), true + } + return "", false +} + +// functionScope is one function-like body — a class method, closure, arrow +// function, or nested function — with the parameters it declares and the +// assignments made directly within it. Assignments made inside a nested scope are +// attributed to that nested scope, not this one. +type functionScope struct { + position *position.Position + parameterTypes map[string]ast.Vertex + assignedValues map[string][]ast.Vertex + + // resolvedVariables memoises each variable's resolved type within this scope, + // so repeated resolution of the same variable does no repeated work. + resolvedVariables map[string]resolvedVariable +} + +// resolvedVariable is a memoised variable resolution — the type name and whether +// it resolved at all. +type resolvedVariable struct { + name string + resolved bool +} + +// enclosingScope returns the innermost scope whose byte range contains the given +// position — the smallest one, so a position inside a closure resolves against the +// closure rather than the method around it. +func enclosingScope(target *position.Position, scopes Scopes) *functionScope { + if target == nil { + return nil + } + + var innermost *functionScope + for _, scope := range scopes.functionScopes { + if scope.position.StartPos > target.StartPos || scope.position.EndPos < target.EndPos { + continue + } + if innermost == nil || scopeWidth(scope) < scopeWidth(innermost) { + innermost = scope + } + } + return innermost +} + +func scopeWidth(scope *functionScope) int { + return scope.position.EndPos - scope.position.StartPos +} + +// scopeCollector gathers every function-like node and every simple assignment in a +// class subtree. Assignments are attributed to their enclosing scope after the +// whole subtree is walked, so nested-scope assignments land in the nested scope. +type scopeCollector struct { + *visitor.Null + scopes []*functionScope + assignments []*rawAssignment + propertyTypes map[string]ast.Vertex +} + +// StmtPropertyList records the declared type of every property it declares, so a +// `$this->someProperty` fetch can be resolved to it. +func (collector *scopeCollector) StmtPropertyList(node *ast.StmtPropertyList) { + for _, propertyVertex := range node.Props { + property, ok := propertyVertex.(*ast.StmtProperty) + if !ok { + continue + } + name, ok := variableName(property.Var) + if !ok { + continue + } + collector.propertyTypes[name] = node.Type + } +} + +// Parameter records a constructor-promoted property — a parameter carrying a +// visibility modifier declares a property of the same name and type. +func (collector *scopeCollector) Parameter(node *ast.Parameter) { + if len(node.Modifiers) == 0 { + return + } + name, ok := variableName(node.Var) + if !ok { + return + } + collector.propertyTypes[name] = node.Type +} + +type rawAssignment struct { + name string + value ast.Vertex + position *position.Position +} + +func (collector *scopeCollector) StmtClassMethod(node *ast.StmtClassMethod) { + collector.addScope(node.Position, node.Params) +} + +func (collector *scopeCollector) ExprClosure(node *ast.ExprClosure) { + collector.addScope(node.Position, node.Params) +} + +func (collector *scopeCollector) ExprArrowFunction(node *ast.ExprArrowFunction) { + collector.addScope(node.Position, node.Params) +} + +func (collector *scopeCollector) StmtFunction(node *ast.StmtFunction) { + collector.addScope(node.Position, node.Params) +} + +func (collector *scopeCollector) ExprAssign(node *ast.ExprAssign) { + if node.Position == nil { + return + } + variable, ok := node.Var.(*ast.ExprVariable) + if !ok { + return + } + name, ok := identifierName(variable.Name) + if !ok { + return + } + collector.assignments = append(collector.assignments, &rawAssignment{name: name, value: node.Expr, position: node.Position}) +} + +func (collector *scopeCollector) addScope(scopePosition *position.Position, params []ast.Vertex) { + if scopePosition == nil { + return + } + collector.scopes = append(collector.scopes, &functionScope{ + position: scopePosition, + parameterTypes: parameterTypeMap(params), + assignedValues: map[string][]ast.Vertex{}, + resolvedVariables: map[string]resolvedVariable{}, + }) +} + +// parameterTypeMap maps every parameter name to its declared type node, keeping +// untyped parameters as keys with a nil type so they still count as definitions. +func parameterTypeMap(params []ast.Vertex) map[string]ast.Vertex { + types := map[string]ast.Vertex{} + for _, parameterVertex := range params { + parameter, ok := parameterVertex.(*ast.Parameter) + if !ok { + continue + } + variable, ok := parameter.Var.(*ast.ExprVariable) + if !ok { + continue + } + name, ok := identifierName(variable.Name) + if !ok { + continue + } + types[name] = parameter.Type + } + return types +} + +// declaredTypeName reads a declared type as a bare name: a plain identifier +// (`int`), a class name (its trailing segment, so an imported and a fully +// qualified name agree), or the inner type of a nullable one — `?LeadRepository` +// still says the property holds a repository. A union or intersection type is +// unresolved, there being no single name to report. +func declaredTypeName(typeNode ast.Vertex) (string, bool) { + switch node := typeNode.(type) { + case *ast.Identifier: + return string(node.Value), true + case *ast.Nullable: + return declaredTypeName(node.Expr) + case *ast.Name: + return trailingNamePart(node.Parts) + case *ast.NameFullyQualified: + return trailingNamePart(node.Parts) + case *ast.NameRelative: + return trailingNamePart(node.Parts) + } + return "", false +} + +func trailingNamePart(parts []ast.Vertex) (string, bool) { + if len(parts) == 0 { + return "", false + } + part, ok := parts[len(parts)-1].(*ast.NamePart) + if !ok { + return "", false + } + return string(part.Value), true +} + +// variableName returns a variable node's name without the leading dollar, so a +// declared `$repository` property and a `$this->repository` fetch agree. +func variableName(node ast.Vertex) (string, bool) { + variable, ok := node.(*ast.ExprVariable) + if !ok { + return "", false + } + name, ok := identifierName(variable.Name) + if !ok { + return "", false + } + return strings.TrimPrefix(name, "$"), true +} + +// identifierName returns an identifier node's name, reporting false for any other +// node so callers can use it as a typed guard on an ast.Vertex. +func identifierName(node ast.Vertex) (string, bool) { + identifier, ok := node.(*ast.Identifier) + if !ok { + return "", false + } + return string(identifier.Value), true +} + +// isBooleanConstant reports whether a constant fetch is `true` or `false`. +func isBooleanConstant(node *ast.ExprConstFetch) bool { + name, ok := trailingName(node.Const) + if !ok { + return false + } + return strings.EqualFold(name, "true") || strings.EqualFold(name, "false") +} + +// trailingName returns the last segment of a name node. +func trailingName(node ast.Vertex) (string, bool) { + name, ok := node.(*ast.Name) + if !ok { + return "", false + } + return trailingNamePart(name.Parts) +} diff --git a/pkg/typescope/typescope_test.go b/pkg/typescope/typescope_test.go new file mode 100644 index 0000000..5b8f6bb --- /dev/null +++ b/pkg/typescope/typescope_test.go @@ -0,0 +1,333 @@ +package typescope_test + +import ( + "testing" + + "github.com/rectorphp/php-parser-in-go/pkg/ast" + "github.com/rectorphp/php-parser-in-go/pkg/conf" + "github.com/rectorphp/php-parser-in-go/pkg/parser" + "github.com/rectorphp/php-parser-in-go/pkg/typescope" + "github.com/rectorphp/php-parser-in-go/pkg/version" + "github.com/rectorphp/php-parser-in-go/pkg/visitor" + "github.com/rectorphp/php-parser-in-go/pkg/visitor/traverser" +) + +// TestResolveTypeProperty covers what ResolveType adds over ResolveValue: the +// declared type of a `$this->property` fetch, whether the property is declared +// on its own or promoted in the constructor. It is the answer behind the "is +// this an object" question a reporting rule asks. +func TestResolveTypeProperty(test *testing.T) { + tests := []struct { + name string + source string + expectedType string + resolved bool + }{ + { + name: "a declared class-typed property", + source: `repository; + } +}`, + expectedType: "LeadRepository", + resolved: true, + }, + { + name: "a promoted class-typed property", + source: `repository; + } +}`, + expectedType: "LeadRepository", + resolved: true, + }, + { + name: "a nullable property keeps the inner type", + source: `repository; + } +}`, + expectedType: "LeadRepository", + resolved: true, + }, + { + name: "a fully qualified type keeps its trailing segment", + source: `repository; + } +}`, + expectedType: "LeadRepository", + resolved: true, + }, + { + name: "a scalar property resolves to its built-in type", + source: `body; + } +}`, + expectedType: "string", + resolved: true, + }, + { + name: "an untyped property is unresolved", + source: `repository; + } +}`, + resolved: false, + }, + { + name: "a union type is unresolved", + source: `repository; + } +}`, + resolved: false, + }, + { + name: "a property of another object is unresolved", + source: `repository; + } +}`, + resolved: false, + }, + } + + for _, testCase := range tests { + test.Run(testCase.name, func(test *testing.T) { + class, assign := parseClassAndAssign(test, testCase.source) + scopes := typescope.Collect(class) + + typeName, resolved := scopes.ResolveType(assign.Expr, assign.Position) + if resolved != testCase.resolved { + test.Fatalf("resolved = %v, want %v (type %q)", resolved, testCase.resolved, typeName) + } + if resolved && typeName != testCase.expectedType { + test.Errorf("type = %q, want %q", typeName, testCase.expectedType) + } + }) + } +} + +// TestResolveValueLeavesPropertiesAlone pins the split: ResolveValue is what the +// rules copying a resolved type onto a signature use, and it keeps inferring from +// exactly what it did before the property lookup existed. +func TestResolveValueLeavesPropertiesAlone(test *testing.T) { + class, assign := parseClassAndAssign(test, `repository; + } +}`) + + if typeName, resolved := typescope.Collect(class).ResolveValue(assign.Expr, assign.Position); resolved { + test.Errorf("ResolveValue resolved a property fetch to %q, want it left alone", typeName) + } +} + +// TestResolveValueBuiltinCall covers the internal-function return types +// ResolveValue reads: a call to a builtin with a single, unconditional scalar +// return resolves to that scalar, while a function that can return false in-band +// is left unresolved so no rule declares a type the value may not hold. +func TestResolveValueBuiltinCall(test *testing.T) { + tests := []struct { + name string + call string + expectedType string + resolved bool + }{ + {name: "sprintf resolves to string", call: "sprintf('%s', $x)", expectedType: "string", resolved: true}, + {name: "trim resolves to string", call: "trim($x)", expectedType: "string", resolved: true}, + {name: "implode resolves to string", call: "implode(',', $x)", expectedType: "string", resolved: true}, + {name: "case-insensitive name resolves", call: "STRTOLOWER($x)", expectedType: "string", resolved: true}, + {name: "strlen resolves to int", call: "strlen($x)", expectedType: "int", resolved: true}, + {name: "count resolves to int", call: "count($x)", expectedType: "int", resolved: true}, + {name: "round resolves to float", call: "round($x)", expectedType: "float", resolved: true}, + {name: "sqrt resolves to float", call: "sqrt($x)", expectedType: "float", resolved: true}, + {name: "is_array resolves to bool", call: "is_array($x)", expectedType: "bool", resolved: true}, + {name: "str_contains resolves to bool", call: "str_contains($x, 'a')", expectedType: "bool", resolved: true}, + {name: "strpos is int|false, left alone", call: "strpos($x, 'a')", resolved: false}, + {name: "json_encode is string|false, left alone", call: "json_encode($x)", resolved: false}, + {name: "substr is version-dependent, left alone", call: "substr($x, 1)", resolved: false}, + {name: "abs is int|float, left alone", call: "abs($x)", resolved: false}, + {name: "an unknown function is left alone", call: "my_helper($x)", resolved: false}, + {name: "a namespaced call is left alone", call: "\\Foo\\sprintf('%s', $x)", resolved: false}, + } + + for _, testCase := range tests { + test.Run(testCase.name, func(test *testing.T) { + source := "metadata; + } +}`, + nullable: true, + }, + { + name: "a plain property", + source: `metadata; + } +}`, + nullable: false, + }, + { + name: "a nullable parameter", + source: `