diff --git a/pkg/ast/name.go b/pkg/ast/name.go new file mode 100644 index 0000000..fc9f23b --- /dev/null +++ b/pkg/ast/name.go @@ -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)] +} diff --git a/pkg/ast/name_test.go b/pkg/ast/name_test.go new file mode 100644 index 0000000..b41f642 --- /dev/null +++ b/pkg/ast/name_test.go @@ -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) + } +} diff --git a/pkg/visitor/doccomment.go b/pkg/visitor/doccomment.go new file mode 100644 index 0000000..ca07493 --- /dev/null +++ b/pkg/visitor/doccomment.go @@ -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 +} diff --git a/pkg/visitor/doccomment_test.go b/pkg/visitor/doccomment_test.go new file mode 100644 index 0000000..1ba63d2 --- /dev/null +++ b/pkg/visitor/doccomment_test.go @@ -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, `