Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions pkg/ast/name.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package ast

import "strings"

// ToString returns the source text of a name node - a Name, NameFullyQualified,
// or NameRelative joined by "\", a NamePart or Identifier value, or "" for any
// other node. The leading separator of a fully qualified name is not included.
func ToString(node Vertex) string {
switch n := node.(type) {
case *Name:
return partsToString(n.Parts)
case *NameFullyQualified:
return partsToString(n.Parts)
case *NameRelative:
return partsToString(n.Parts)
case *NamePart:
return string(n.Value)
case *Identifier:
return string(n.Value)
default:
return ""
}
}

func partsToString(parts []Vertex) string {
segments := make([]string, 0, len(parts))
for _, part := range parts {
if namePart, ok := part.(*NamePart); ok {
segments = append(segments, string(namePart.Value))
}
}
return strings.Join(segments, "\\")
}

// reservedTypes are the PHP reserved type keywords that are not class names.
var reservedTypes = map[string]bool{
"int": true, "float": true, "string": true, "bool": true, "void": true,
"array": true, "iterable": true, "callable": true, "object": true,
"mixed": true, "never": true, "null": true, "false": true, "true": true,
"self": true, "static": true, "parent": true,
}

// IsReservedType reports whether name is a PHP reserved type keyword (a scalar,
// array, callable, void, never, mixed, or a self/static/parent relative type),
// matched case-insensitively. Such a name is a builtin type, not a class
// reference, even though the parser produces a Name node for it.
func IsReservedType(name string) bool {
return reservedTypes[strings.ToLower(name)]
}
37 changes: 37 additions & 0 deletions pkg/ast/name_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package ast_test

import (
"testing"

"gotest.tools/assert"

"github.com/rectorphp/php-parser-in-go/pkg/ast"
)

func TestToString(test *testing.T) {
name := &ast.Name{Parts: []ast.Vertex{
&ast.NamePart{Value: []byte("App")},
&ast.NamePart{Value: []byte("Entity")},
&ast.NamePart{Value: []byte("User")},
}}
assert.Equal(test, "App\\Entity\\User", ast.ToString(name))

fullyQualified := &ast.NameFullyQualified{Parts: []ast.Vertex{
&ast.NamePart{Value: []byte("App")},
&ast.NamePart{Value: []byte("User")},
}}
assert.Equal(test, "App\\User", ast.ToString(fullyQualified))

assert.Equal(test, "User", ast.ToString(&ast.NamePart{Value: []byte("User")}))
assert.Equal(test, "string", ast.ToString(&ast.Identifier{Value: []byte("string")}))
assert.Equal(test, "", ast.ToString(&ast.Root{}))
}

func TestIsReservedType(test *testing.T) {
for _, reserved := range []string{"int", "STRING", "Bool", "void", "iterable", "self", "static", "mixed", "never"} {
assert.Assert(test, ast.IsReservedType(reserved), "expected %q reserved", reserved)
}
for _, className := range []string{"App\\User", "Closure", "Iterator", "Stringable"} {
assert.Assert(test, !ast.IsReservedType(className), "expected %q not reserved", className)
}
}
85 changes: 85 additions & 0 deletions pkg/visitor/doccomment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package visitor

import (
"github.com/rectorphp/php-parser-in-go/pkg/ast"
"github.com/rectorphp/php-parser-in-go/pkg/token"
)

// GetDocComment returns the /** */ doc comment immediately preceding a
// declaration node, or nil when there is none. It is supported for the nodes
// that carry a doc block: class, interface, trait and enum declarations,
// functions and methods, and property and class-constant lists. For any other
// node it returns nil.
//
// The comment is read from the free-floating tokens that precede the node's
// first significant token (an attribute group, a modifier, or the declaration
// keyword). When several doc comments precede the node, the closest one is
// returned.
func GetDocComment(node ast.Vertex) *token.Token {
return lastDocComment(leadingTokens(node))
}

// GetDocCommentText is GetDocComment returning the comment text, or "" when
// there is no doc comment.
func GetDocCommentText(node ast.Vertex) string {
if doc := GetDocComment(node); doc != nil {
return string(doc.Value)
}
return ""
}

func leadingTokens(node ast.Vertex) []*token.Token {
switch n := node.(type) {
case *ast.StmtClass:
return collectLeading(n.AttrGroups, n.Modifiers, n.ClassTkn)
case *ast.StmtInterface:
return collectLeading(n.AttrGroups, nil, n.InterfaceTkn)
case *ast.StmtTrait:
return collectLeading(n.AttrGroups, nil, n.TraitTkn)
case *ast.StmtEnum:
return collectLeading(n.AttrGroups, nil, n.EnumTkn)
case *ast.StmtFunction:
return collectLeading(n.AttrGroups, nil, n.FunctionTkn)
case *ast.StmtClassMethod:
return collectLeading(n.AttrGroups, n.Modifiers, n.FunctionTkn)
case *ast.StmtPropertyList:
return collectLeading(n.AttrGroups, n.Modifiers, nil)
case *ast.StmtClassConstList:
return collectLeading(n.AttrGroups, n.Modifiers, n.ConstTkn)
default:
return nil
}
}

func collectLeading(attrGroups, modifiers []ast.Vertex, keyword *token.Token) []*token.Token {
var tokens []*token.Token
for _, group := range attrGroups {
if attributeGroup, ok := group.(*ast.AttributeGroup); ok {
tokens = append(tokens, attributeGroup.OpenAttributeTkn)
}
}
for _, modifier := range modifiers {
if identifier, ok := modifier.(*ast.Identifier); ok {
tokens = append(tokens, identifier.IdentifierTkn)
}
}
if keyword != nil {
tokens = append(tokens, keyword)
}
return tokens
}

func lastDocComment(tokens []*token.Token) *token.Token {
var found *token.Token
for _, tkn := range tokens {
if tkn == nil {
continue
}
for _, free := range tkn.FreeFloating {
if free.ID == token.T_DOC_COMMENT {
found = free
}
}
}
return found
}
74 changes: 74 additions & 0 deletions pkg/visitor/doccomment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package visitor_test

import (
"strings"
"testing"

"gotest.tools/assert"

"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/version"
"github.com/rectorphp/php-parser-in-go/pkg/visitor"
)

func firstClass(test *testing.T, src string) *ast.StmtClass {
phpVersion, _ := version.New("8.4")
root, err := parser.Parse([]byte(src), conf.Config{Version: phpVersion})
assert.NilError(test, err)

for _, stmt := range root.(*ast.Root).Stmts {
if class, ok := stmt.(*ast.StmtClass); ok {
return class
}
if namespace, ok := stmt.(*ast.StmtNamespace); ok {
for _, inner := range namespace.Stmts {
if class, ok := inner.(*ast.StmtClass); ok {
return class
}
}
}
}
test.Fatal("no class found")
return nil
}

func TestGetDocComment(test *testing.T) {
class := firstClass(test, `<?php
/**
* @api
*/
final class Foo
{
}
`)

doc := visitor.GetDocComment(class)
assert.Assert(test, doc != nil)
assert.Assert(test, strings.Contains(string(doc.Value), "@api"))
assert.Assert(test, strings.Contains(visitor.GetDocCommentText(class), "@api"))
}

func TestGetDocCommentWithAttribute(test *testing.T) {
class := firstClass(test, `<?php
/** @deprecated */
#[SomeAttribute]
final class Bar
{
}
`)

assert.Assert(test, strings.Contains(visitor.GetDocCommentText(class), "@deprecated"))
}

func TestGetDocCommentNone(test *testing.T) {
class := firstClass(test, `<?php
final class Baz
{
}
`)

assert.Assert(test, visitor.GetDocComment(class) == nil)
assert.Equal(test, "", visitor.GetDocCommentText(class))
}
49 changes: 49 additions & 0 deletions pkg/visitor/nsresolver/attribute_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package nsresolver_test

import (
"testing"

"gotest.tools/assert"

"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/version"
"github.com/rectorphp/php-parser-in-go/pkg/visitor"
"github.com/rectorphp/php-parser-in-go/pkg/visitor/nsresolver"
"github.com/rectorphp/php-parser-in-go/pkg/visitor/traverser"
)

type attributeCollector struct {
visitor.Null
nodes []*ast.Attribute
}

func (collector *attributeCollector) Attribute(node *ast.Attribute) {
collector.nodes = append(collector.nodes, node)
}

func TestResolveAttributeName(test *testing.T) {
src := []byte(`<?php
namespace App;
use App\Attribute\AsThing;
#[AsThing]
final class Controller
{
}
`)

phpVersion, _ := version.New("8.4")
root, err := parser.Parse(src, conf.Config{Version: phpVersion})
assert.NilError(test, err)

resolver := nsresolver.NewNamespaceResolver()
traverser.NewTraverser(resolver).Traverse(root)

collector := &attributeCollector{}
traverser.NewTraverser(collector).Traverse(root)
assert.Equal(test, 1, len(collector.nodes))

resolved := resolver.ResolvedNames[collector.nodes[0].Name]
assert.Equal(test, "App\\Attribute\\AsThing", resolved)
}
4 changes: 4 additions & 0 deletions pkg/visitor/nsresolver/namespace_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ func (namespaceResolver *NamespaceResolver) ExprConstFetch(node *ast.ExprConstFe
namespaceResolver.ResolveName(node.Const, "const")
}

func (namespaceResolver *NamespaceResolver) Attribute(node *ast.Attribute) {
namespaceResolver.ResolveName(node.Name, "")
}

func (namespaceResolver *NamespaceResolver) StmtTraitUse(node *ast.StmtTraitUse) {
for _, trait := range node.Traits {
namespaceResolver.ResolveName(trait, "")
Expand Down
Loading