Repository: VKCOM/noverify
Pull request #1031 (2021-07-05 14:55:48)

linter: simplifying the root.go file
- Splitting PHPDoc parsing and validation into two separate actions.
  The functions for parsing have been moved to a separate `phpdocTypes`
  package, since otherwise circular dependencies arise;

- The function for converting a typehint node to a type has been moved
to the `types` package;

- Type normalization has been moved to the `types` package;

- The creation of a type map with normalization has been moved to
the `types` package;

- `NameNodeToString` and `NameNodeEquals` has been moved to a separate
`util` package to avoid causing circular dependencies;

- Shape information has been moved to the `types` package.

These changes are needed for solution #1006, as we need to store all closures as 
`\Closure(params):return_type` in order to be able to use existing closures and 
reduce the number of duplicates.
Since the names for the closures are created outside the walkers, we have no 
information about the types, so this information should be taken from the 
typehints and PHPDoc, and for this, we need separate functions that will allow
us to do this.


Patch:
diff --git a/src/constfold/eval.go b/src/constfold/eval.go
index 96a6ae0..7f26fd3 100644
--- a/src/constfold/eval.go
+++ b/src/constfold/eval.go
@@ -7,6 +7,7 @@ import (
 	"github.com/VKCOM/noverify/src/ir"
 	"github.com/VKCOM/noverify/src/meta"
 	"github.com/VKCOM/noverify/src/solver"
+	"github.com/VKCOM/noverify/src/utils"
 )
 
 // Eval tries to compute the e using the constant expressions folding.
@@ -97,7 +98,7 @@ func Eval(st *meta.ClassParseState, e ir.Node) meta.ConstValue {
 
 	case *ir.FunctionCallExpr:
 		// Only dirname(__FILE__) is const-folded right now.
-		if !meta.NameNodeEquals(e.Function, `dirname`) {
+		if !utils.NameNodeEquals(e.Function, `dirname`) {
 			return meta.UnknownValue
 		}
 		if len(e.Args) == 0 {
diff --git a/src/linter/autogen/autogen.go b/src/linter/autogen/autogen.go
index 9c589aa..07963f2 100644
--- a/src/linter/autogen/autogen.go
+++ b/src/linter/autogen/autogen.go
@@ -5,26 +5,26 @@ import (
 	"strings"
 
 	"github.com/VKCOM/noverify/src/ir"
-	"github.com/VKCOM/noverify/src/meta"
+	"github.com/VKCOM/noverify/src/types"
 )
 
-func GenerateShapeName(props []ShapeTypeProp, cs *meta.ClassParseState) string {
+func GenerateShapeName(props []types.ShapeProp) string {
 	var body string
 	for i, prop := range props {
 		body += prop.Key
-		types := ":"
+		typesList := ":"
 		for i, typ := range prop.Types {
-			types += typ.Elem + strings.Repeat("[]", typ.Dims)
+			typesList += typ.Elem + strings.Repeat("[]", typ.Dims)
 			if i != len(prop.Types)-1 {
-				types += ","
+				typesList += ","
 			}
 		}
 
 		if i != len(props)-1 {
-			types += ","
+			typesList += ","
 		}
 
-		body += types
+		body += typesList
 	}
 	// We'll probably generate names for anonymous classes in the
 	// same way in future. All auto-generated names should end with "$".
@@ -33,11 +33,11 @@ func GenerateShapeName(props []ShapeTypeProp, cs *meta.ClassParseState) string {
 	return fmt.Sprintf(`\shape$%s$`, body)
 }
 
-func GenerateClosureName(fun *ir.ClosureExpr, cs *meta.ClassParseState) string {
+func GenerateClosureName(fun *ir.ClosureExpr, currentFunction, currentFile string) string {
 	pos := ir.GetPosition(fun)
-	curFunction := cs.CurrentFunction
+	curFunction := currentFunction
 	if curFunction != "" {
 		curFunction = "," + curFunction
 	}
-	return fmt.Sprintf("\\Closure$(%s%s):%d$", cs.CurrentFile, curFunction, pos.StartLine)
+	return fmt.Sprintf("\\Closure$(%s%s):%d$", currentFile, curFunction, pos.StartLine)
 }
diff --git a/src/linter/autogen/shape.go b/src/linter/autogen/shape.go
deleted file mode 100644
index 8b27d58..0000000
--- a/src/linter/autogen/shape.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package autogen
-
-import (
-	"github.com/VKCOM/noverify/src/types"
-)
-
-type ShapeTypeProp struct {
-	Key   string
-	Types []types.Type
-}
-
-type ShapeTypeInfo struct {
-	Name  string
-	Props []ShapeTypeProp
-}
diff --git a/src/linter/block.go b/src/linter/block.go
index d4387c4..a000d81 100644
--- a/src/linter/block.go
+++ b/src/linter/block.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/VKCOM/noverify/src/phpdoctypes"
 	"github.com/z7zmey/php-parser/pkg/position"
 	"github.com/z7zmey/php-parser/pkg/token"
 
@@ -237,17 +238,19 @@ func (b *blockWalker) handleCommentToken(n ir.Node, t *token.Token) {
 	}
 
 	for _, p := range doc.Parsed {
-		p, ok := p.(*phpdoc.TypeVarCommentPart)
+		part, ok := p.(*phpdoc.TypeVarCommentPart)
 		if !ok || p.Name() != "var" {
 			continue
 		}
 
-		typeList, warning := typesFromPHPDoc(&b.r.ctx, p.Type)
-		if warning != "" {
-			b.r.Report(n, LevelWarning, "phpdocType", "%s on line %d", warning, p.Line())
+		converted := phpdoctypes.ToRealType(b.r.ctx.typeNormalizer.ClassFQNProvider(), part.Type)
+		moveShapesToContext(&b.r.ctx, converted.Shapes)
+		for _, warning := range converted.Warnings {
+			b.r.Report(n, LevelNotice, "phpdocType", "%s on line %d", warning, part.Line())
 		}
-		m := newTypesMap(&b.r.ctx, typeList)
-		b.ctx.sc.AddVarFromPHPDoc(strings.TrimPrefix(p.Var, "$"), m, "@var")
+
+		typesMap := types.NewMapWithNormalization(b.r.ctx.typeNormalizer, converted.Types)
+		b.ctx.sc.AddVarFromPHPDoc(strings.TrimPrefix(part.Var, "$"), typesMap, "@var")
 	}
 }
 
@@ -1153,11 +1156,15 @@ func (b *blockWalker) handleFor(s *ir.ForStmt) bool {
 func (b *blockWalker) enterArrowFunction(fun *ir.ArrowFunctionExpr) bool {
 	sc := meta.NewScope()
 
-	doc := b.r.parsePHPDoc(fun, fun.Doc, fun.Params)
-	b.r.reportPhpdocErrors(fun, doc.errs)
-	phpDocParamTypes := doc.types
+	// Indexing stage.
+	doc := phpdoctypes.Parse(fun.Doc, fun.Params, b.r.ctx.typeNormalizer)
+	moveShapesToContext(&b.r.ctx, doc.Shapes)
+
+	// Check stage.
+	errors := b.r.checkPHPDoc(fun, fun.Doc, fun.Params)
+	b.r.reportPHPDocErrors(fun, errors)
 
-	funcParams := b.r.parseFuncParams(fun.Params, phpDocParamTypes, sc, nil)
+	funcParams := b.r.parseFuncParams(fun.Params, doc.ParamTypes, sc, nil)
 	b.r.handleArrowFuncExpr(funcParams.params, fun.Expr, sc, b)
 
 	return false
@@ -1173,9 +1180,13 @@ func (b *blockWalker) enterClosure(fun *ir.ClosureExpr, haveThis bool, thisType
 		sc.AddVarName("this", types.NewMap("possibly_late_bound"), "possibly late bound $this", meta.VarAlwaysDefined)
 	}
 
-	doc := b.r.parsePHPDoc(fun, fun.Doc, fun.Params)
-	b.r.reportPhpdocErrors(fun, doc.errs)
-	phpDocParamTypes := doc.types
+	// Indexing stage.
+	doc := phpdoctypes.Parse(fun.Doc, fun.Params, b.r.ctx.typeNormalizer)
+	moveShapesToContext(&b.r.ctx, doc.Shapes)
+
+	// Check stage.
+	errors := b.r.checkPHPDoc(fun, fun.Doc, fun.Params)
+	b.r.reportPHPDocErrors(fun, errors)
 
 	var hintReturnType types.Map
 	if typ, ok := b.r.parseTypeNode(fun.ReturnType); ok {
@@ -1211,7 +1222,7 @@ func (b *blockWalker) enterClosure(fun *ir.ClosureExpr, haveThis bool, thisType
 		b.untrackVarName(v.Name)
 	}
 
-	params := b.r.parseFuncParams(fun.Params, phpDocParamTypes, sc, closureSolver)
+	params := b.r.parseFuncParams(fun.Params, doc.ParamTypes, sc, closureSolver)
 
 	funcInfo := b.r.handleFuncStmts(params.params, closureUses, fun.Stmts, sc)
 	actualReturnTypes := funcInfo.returnTypes
@@ -1223,7 +1234,7 @@ func (b *blockWalker) enterClosure(fun *ir.ClosureExpr, haveThis bool, thisType
 		b.r.checkFuncParam(param.(*ir.Parameter))
 	}
 
-	name := autogen.GenerateClosureName(fun, b.r.ctx.st)
+	name := autogen.GenerateClosureName(fun, b.r.ctx.st.CurrentFunction, b.r.ctx.st.CurrentFile)
 
 	if b.r.meta.Functions.H == nil {
 		b.r.meta.Functions = meta.NewFunctionsMap()
@@ -1237,7 +1248,7 @@ func (b *blockWalker) enterClosure(fun *ir.ClosureExpr, haveThis bool, thisType
 		MinParamsCnt: params.minParamsCount,
 		Flags:        0,
 		ExitFlags:    exitFlags,
-		Doc:          doc.info,
+		Doc:          doc.AdditionalInfo,
 	})
 
 	return false
diff --git a/src/linter/block_context.go b/src/linter/block_context.go
index 1137d08..4291b9a 100644
--- a/src/linter/block_context.go
+++ b/src/linter/block_context.go
@@ -5,6 +5,7 @@ import (
 	"github.com/VKCOM/noverify/src/ir/irutil"
 	"github.com/VKCOM/noverify/src/meta"
 	"github.com/VKCOM/noverify/src/solver"
+	"github.com/VKCOM/noverify/src/utils"
 )
 
 type customMethod struct {
@@ -43,7 +44,7 @@ func (ctx *blockContext) addCustomFunction(functionName string) {
 
 func (ctx *blockContext) customFunctionExists(nm ir.Node) bool {
 	for _, functionName := range ctx.customFunctions {
-		if meta.NameNodeEquals(nm, functionName) {
+		if utils.NameNodeEquals(nm, functionName) {
 			return true
 		}
 	}
diff --git a/src/linter/block_linter.go b/src/linter/block_linter.go
index 60adfab..f63f10c 100644
--- a/src/linter/block_linter.go
+++ b/src/linter/block_linter.go
@@ -12,6 +12,7 @@ import (
 	"github.com/VKCOM/noverify/src/quickfix"
 	"github.com/VKCOM/noverify/src/solver"
 	"github.com/VKCOM/noverify/src/types"
+	"github.com/VKCOM/noverify/src/utils"
 )
 
 type blockLinter struct {
@@ -427,10 +428,10 @@ func (b *blockLinter) checkNew(e *ir.NewExpr) {
 
 	if b.classParseState().IsTrait {
 		switch {
-		case meta.NameNodeEquals(e.Class, "self"):
+		case utils.NameNodeEquals(e.Class, "self"):
 			// Don't try to resolve "self" inside trait context.
 			return
-		case meta.NameNodeEquals(e.Class, "static"):
+		case utils.NameNodeEquals(e.Class, "static"):
 			// More or less identical to the "self" case.
 			return
 		}
@@ -452,7 +453,7 @@ func (b *blockLinter) checkNew(e *ir.NewExpr) {
 	// It's illegal to instantiate abstract class, but `static` can
 	// resolve to something else due to the late static binding,
 	// so it's the only exception to that rule.
-	if class.IsAbstract() && !meta.NameNodeEquals(e.Class, "static") {
+	if class.IsAbstract() && !utils.NameNodeEquals(e.Class, "static") {
 		b.report(e.Class, LevelError, "newAbstract", "Cannot instantiate abstract class")
 	}
 
@@ -500,7 +501,7 @@ func (b *blockLinter) checkConstFetch(e *ir.ConstFetchExpr) {
 
 	if !defined {
 		// If it's builtin constant, give a more precise report message.
-		switch nm := meta.NameNodeToString(e.Constant); strings.ToLower(nm) {
+		switch nm := utils.NameNodeToString(e.Constant); strings.ToLower(nm) {
 		case "null", "true", "false":
 			// TODO(quasilyte): should probably issue not "undefined" warning
 			// here, but something else, like "constCase" or something.
@@ -816,16 +817,16 @@ func (b *blockLinter) checkDeprecatedFunctionCall(e *ir.FunctionCallExpr, call *
 	}
 
 	if call.info.Doc.DeprecationNote != "" {
-		b.report(e.Function, LevelNotice, "deprecated", "Call to deprecated function %s (%s)", meta.NameNodeToString(e.Function), call.info.Doc.DeprecationNote)
+		b.report(e.Function, LevelNotice, "deprecated", "Call to deprecated function %s (%s)", utils.NameNodeToString(e.Function), call.info.Doc.DeprecationNote)
 		return
 	}
 
-	b.report(e.Function, LevelNotice, "deprecated", "Call to deprecated function %s", meta.NameNodeToString(e.Function))
+	b.report(e.Function, LevelNotice, "deprecated", "Call to deprecated function %s", utils.NameNodeToString(e.Function))
 }
 
 func (b *blockLinter) checkFunctionAvailability(e *ir.FunctionCallExpr, call *funcCallInfo) {
 	if !call.isFound && !b.walker.ctx.customFunctionExists(e.Function) {
-		b.report(e.Function, LevelError, "undefined", "Call to undefined function %s", meta.NameNodeToString(e.Function))
+		b.report(e.Function, LevelError, "undefined", "Call to undefined function %s", utils.NameNodeToString(e.Function))
 	}
 }
 
@@ -847,7 +848,7 @@ func (b *blockLinter) checkCallArgsCount(n ir.Node, args []ir.Node, fn meta.Func
 	}
 
 	if !enoughArgs(args, fn) {
-		b.report(n, LevelWarning, "argCount", "Too few arguments for %s", meta.NameNodeToString(n))
+		b.report(n, LevelWarning, "argCount", "Too few arguments for %s", utils.NameNodeToString(n))
 	}
 }
 
diff --git a/src/linter/phpdoc_util.go b/src/linter/phpdoc_util.go
index a7a2daa..8f344b4 100644
--- a/src/linter/phpdoc_util.go
+++ b/src/linter/phpdoc_util.go
@@ -7,7 +7,9 @@ import (
 	"github.com/VKCOM/noverify/src/ir"
 	"github.com/VKCOM/noverify/src/meta"
 	"github.com/VKCOM/noverify/src/phpdoc"
+	"github.com/VKCOM/noverify/src/phpdoctypes"
 	"github.com/VKCOM/noverify/src/solver"
+	"github.com/VKCOM/noverify/src/types"
 )
 
 type phpdocErrors struct {
@@ -47,8 +49,10 @@ func parseClassPHPDocMethod(ctx *rootContext, result *classPhpDocParseResult, pa
 		return
 	}
 
-	types, warning := typesFromPHPDoc(ctx, ctx.phpdocTypeParser.Parse(params[0]))
-	if warning != "" {
+	typ := ctx.phpdocTypeParser.Parse(params[0])
+	converted := phpdoctypes.ToRealType(ctx.typeNormalizer.ClassFQNProvider(), typ)
+	moveShapesToContext(ctx, converted.Shapes)
+	for _, warning := range converted.Warnings {
 		result.errs.pushType("%s on line %d", warning, part.Line())
 	}
 
@@ -67,7 +71,7 @@ func parseClassPHPDocMethod(ctx *rootContext, result *classPhpDocParseResult, pa
 	}
 	funcFlags |= meta.FuncFromAnnotation
 	result.methods.Set(methodName, meta.FuncInfo{
-		Typ:          newTypesMap(ctx, types),
+		Typ:          types.NewMapWithNormalization(ctx.typeNormalizer, converted.Types),
 		Name:         methodName,
 		Flags:        funcFlags,
 		MinParamsCnt: 0, // TODO: parse signature and assign a proper value
@@ -75,6 +79,12 @@ func parseClassPHPDocMethod(ctx *rootContext, result *classPhpDocParseResult, pa
 	})
 }
 
+func moveShapesToContext(ctx *rootContext, shapes types.ShapesMap) {
+	for name, shape := range shapes {
+		ctx.shapes[name] = shape
+	}
+}
+
 func parseClassPHPDocProperty(ctx *rootContext, result *classPhpDocParseResult, part *phpdoc.TypeVarCommentPart) {
 	// The syntax is:
 	//	@property [Type] [name] [<description>]
@@ -89,8 +99,9 @@ func parseClassPHPDocProperty(ctx *rootContext, result *classPhpDocParseResult,
 		result.errs.pushLint("non-canonical order of name and type on line %d", part.Line())
 	}
 
-	types, warning := typesFromPHPDoc(ctx, part.Type)
-	if warning != "" {
+	converted := phpdoctypes.ToRealType(ctx.typeNormalizer.ClassFQNProvider(), part.Type)
+	moveShapesToContext(ctx, converted.Shapes)
+	for _, warning := range converted.Warnings {
 		result.errs.pushType("%s on line %d", warning, part.Line())
 	}
 
@@ -100,7 +111,7 @@ func parseClassPHPDocProperty(ctx *rootContext, result *classPhpDocParseResult,
 	}
 
 	result.properties[part.Var[len("$"):]] = meta.PropertyInfo{
-		Typ:         newTypesMap(ctx, types),
+		Typ:         types.NewMapWithNormalization(ctx.typeNormalizer, converted.Types),
 		AccessLevel: meta.Public,
 		Flags:       meta.PropFromAnnotation,
 	}
diff --git a/src/linter/root.go b/src/linter/root.go
index c613f87..ddc0555 100644
--- a/src/linter/root.go
+++ b/src/linter/root.go
@@ -9,6 +9,8 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/VKCOM/noverify/src/phpdoctypes"
+	"github.com/VKCOM/noverify/src/utils"
 	"github.com/z7zmey/php-parser/pkg/position"
 	"github.com/z7zmey/php-parser/pkg/token"
 
@@ -72,13 +74,6 @@ type rootWalker struct {
 	checkersFilter *CheckersFilter
 }
 
-type phpDocParamEl struct {
-	optional bool
-	typ      types.Map
-}
-
-type phpDocParamsMap map[string]phpDocParamEl
-
 // InitCustom is needed to initialize walker state
 func (d *rootWalker) InitCustom() {
 	d.custom = nil
@@ -218,7 +213,7 @@ func (d *rootWalker) EnterNode(n ir.Node) (res bool) {
 		d.checkCommentMisspellings(n.ClassName, n.Doc.Raw)
 		d.checkIdentMisspellings(n.ClassName)
 		doc := d.parseClassPHPDoc(n.ClassName, n.Doc)
-		d.reportPhpdocErrors(n.ClassName, doc.errs)
+		d.reportPHPDocErrors(n.ClassName, doc.errs)
 		// If we ever need to distinguish @property-annotated and real properties,
 		// more work will be required here.
 		for name, p := range doc.properties {
@@ -501,7 +496,7 @@ func (d *rootWalker) reportUndefinedVariable(v ir.Node, maybeHave bool) {
 	sv, ok := v.(*ir.SimpleVar)
 	if !ok {
 		d.Report(v, LevelWarning, "undefined", "Unknown variable variable %s used",
-			meta.NameNodeToString(v))
+			utils.NameNodeToString(v))
 		return
 	}
 
@@ -992,13 +987,19 @@ func (d *rootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
 	d.checkCommentMisspellings(meth.MethodName, meth.Doc.Raw)
 	d.checkIdentMisspellings(meth.MethodName)
 
-	doc := d.parsePHPDoc(meth.MethodName, meth.Doc, meth.Params)
-	d.reportPhpdocErrors(meth.MethodName, doc.errs)
-	phpDocReturnType := doc.returnType
-	phpDocParamTypes := doc.types
+	// Indexing stage.
+	doc := phpdoctypes.Parse(meth.Doc, meth.Params, d.ctx.typeNormalizer)
+	moveShapesToContext(&d.ctx, doc.Shapes)
+
+	// Check stage.
+	errors := d.checkPHPDoc(meth.MethodName, meth.Doc, meth.Params)
+	d.reportPHPDocErrors(meth.MethodName, errors)
+
+	phpDocReturnType := doc.ReturnType
+	phpDocParamTypes := doc.ParamTypes
 
 	returnTypeHint, ok := d.parseTypeNode(meth.ReturnType)
-	if ok && !doc.inherit {
+	if ok && !doc.Inherit {
 		d.checkFuncReturnType(meth.MethodName, meth.MethodName.Value, returnTypeHint, phpDocReturnType)
 	}
 
@@ -1076,7 +1077,7 @@ func (d *rootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
 		AccessLevel:  modif.accessLevel,
 		Flags:        funcFlags,
 		ExitFlags:    exitFlags,
-		Doc:          doc.info,
+		Doc:          doc.AdditionalInfo,
 	})
 
 	if nm == "getIterator" && d.metaInfo().IsIndexingComplete() && solver.Implements(d.metaInfo(), d.ctx.st.CurrentClass, `\IteratorAggregate`) {
@@ -1092,7 +1093,7 @@ func (d *rootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
 	return false
 }
 
-func (d *rootWalker) reportPhpdocErrors(n ir.Node, errs phpdocErrors) {
+func (d *rootWalker) reportPHPDocErrors(n ir.Node, errs phpdocErrors) {
 	for _, err := range errs.phpdocLint {
 		d.Report(n, LevelWarning, "phpdocLint", "%s", err)
 	}
@@ -1101,7 +1102,7 @@ func (d *rootWalker) reportPhpdocErrors(n ir.Node, errs phpdocErrors) {
 	}
 }
 
-func (d *rootWalker) parsePHPDocVar(n ir.Node, doc phpdoc.Comment) (m types.Map) {
+func (d *rootWalker) parsePHPDocVar(n ir.Node, doc phpdoc.Comment) (typesMap types.Map) {
 	if phpdoc.IsSuspicious([]byte(doc.Raw)) {
 		d.Report(n, LevelWarning, "phpdocLint", "Multiline PHPDoc comment should start with /**, not /*")
 	}
@@ -1110,23 +1111,18 @@ func (d *rootWalker) parsePHPDocVar(n ir.Node, doc phpdoc.Comment) (m types.Map)
 		d.checkPHPDocRef(n, part)
 		part, ok := part.(*phpdoc.TypeVarCommentPart)
 		if ok && part.Name() == "var" {
-			typeList, warning := typesFromPHPDoc(&d.ctx, part.Type)
-			if warning != "" {
+
+			converted := phpdoctypes.ToRealType(d.ctx.typeNormalizer.ClassFQNProvider(), part.Type)
+			moveShapesToContext(&d.ctx, converted.Shapes)
+			for _, warning := range converted.Warnings {
 				d.Report(n, LevelNotice, "phpdocType", "%s on line %d", warning, part.Line())
 			}
-			m = newTypesMap(&d.ctx, typeList)
+
+			typesMap = types.NewMapWithNormalization(d.ctx.typeNormalizer, converted.Types)
 		}
 	}
 
-	return m
-}
-
-type phpDocParseResult struct {
-	returnType types.Map
-	types      phpDocParamsMap
-	info       meta.PhpDocInfo
-	errs       phpdocErrors
-	inherit    bool
+	return typesMap
 }
 
 func (d *rootWalker) isValidPHPDocRef(n ir.Node, ref string) bool {
@@ -1289,15 +1285,13 @@ func (d *rootWalker) checkPHPDocMixinRef(n ir.Node, part phpdoc.CommentPart) {
 	}
 }
 
-func (d *rootWalker) parsePHPDoc(n ir.Node, doc phpdoc.Comment, actualParams []ir.Node) phpDocParseResult {
-	var result phpDocParseResult
-
+func (d *rootWalker) checkPHPDoc(n ir.Node, doc phpdoc.Comment, actualParams []ir.Node) (errors phpdocErrors) {
 	if doc.Raw == "" {
-		return result
+		return errors
 	}
 
 	if phpdoc.IsSuspicious([]byte(doc.Raw)) {
-		result.errs.pushLint("Multiline PHPDoc comment should start with /**, not /*")
+		errors.pushLint("Multiline PHPDoc comment should start with /**, not /*")
 	}
 
 	actualParamNames := make(map[string]struct{}, len(actualParams))
@@ -1306,50 +1300,41 @@ func (d *rootWalker) parsePHPDoc(n ir.Node, doc phpdoc.Comment, actualParams []i
 		actualParamNames[p.Variable.Name] = struct{}{}
 	}
 
-	result.types = make(phpDocParamsMap, len(actualParams))
-
 	var curParam int
 
-	for _, part := range doc.Parsed {
-		d.checkPHPDocRef(n, part)
+	for _, rawPart := range doc.Parsed {
+		d.checkPHPDocRef(n, rawPart)
 
-		if part.Name() == "deprecated" {
-			part := part.(*phpdoc.RawCommentPart)
-			result.info.Deprecated = true
-			result.info.DeprecationNote = part.ParamsText
-			continue
-		}
+		if rawPart.Name() == "return" {
+			part := rawPart.(*phpdoc.TypeCommentPart)
 
-		if part.Name() == "return" {
-			part := part.(*phpdoc.TypeCommentPart)
-			typeList, warning := typesFromPHPDoc(&d.ctx, part.Type)
-			if warning != "" {
-				result.errs.pushType("%s on line %d", warning, part.Line())
+			converted := phpdoctypes.ToRealType(d.ctx.typeNormalizer.ClassFQNProvider(), part.Type)
+			for _, warning := range converted.Warnings {
+				errors.pushType("%s on line %d", warning, part.Line())
 			}
-			result.returnType = newTypesMap(&d.ctx, typeList)
 			continue
 		}
 
 		// Rest is for @param handling.
 
-		if part.Name() != "param" {
+		if rawPart.Name() != "param" {
 			continue
 		}
 
-		part := part.(*phpdoc.TypeVarCommentPart)
+		part := rawPart.(*phpdoc.TypeVarCommentPart)
 		optional := strings.Contains(part.Rest, "[optional]")
 		switch {
 		case part.Var == "":
-			result.errs.pushLint("malformed @param tag (maybe var is missing?) on line %d", part.Line())
+			errors.pushLint("malformed @param tag (maybe var is missing?) on line %d", part.Line())
 		case part.Type.IsEmpty():
-			result.errs.pushLint("malformed @param %s tag (maybe type is missing?) on line %d",
+			errors.pushLint("malformed @param %s tag (maybe type is missing?) on line %d",
 				part.Var, part.Line())
 			continue
 		}
 
 		if part.VarIsFirst {
 			// Phpstorm gives the same message.
-			result.errs.pushLint("non-canonical order of variable and type on line %d", part.Line())
+			errors.pushLint("non-canonical order of variable and type on line %d", part.Line())
 		}
 
 		variable := part.Var
@@ -1359,35 +1344,31 @@ func (d *rootWalker) parsePHPDoc(n ir.Node, doc phpdoc.Comment, actualParams []i
 			}
 		}
 		if _, ok := actualParamNames[strings.TrimPrefix(variable, "$")]; !ok {
-			result.errs.pushLint("@param for non-existing argument %s", variable)
+			errors.pushLint("@param for non-existing argument %s", variable)
 			continue
 		}
 
 		curParam++
 
-		var param phpDocParamEl
-		typeList, warning := typesFromPHPDoc(&d.ctx, part.Type)
-		if warning != "" {
-			result.errs.pushType("%s on line %d", warning, part.Line())
+		converted := phpdoctypes.ToRealType(d.ctx.typeNormalizer.ClassFQNProvider(), part.Type)
+		for _, warning := range converted.Warnings {
+			errors.pushType("%s on line %d", warning, part.Line())
 		}
-		param.typ = newTypesMap(&d.ctx, typeList)
-		param.typ.Iterate(func(t string) {
+
+		var param phpdoctypes.Param
+		param.Typ = types.NewMapWithNormalization(d.ctx.typeNormalizer, converted.Types)
+		param.Typ.Iterate(func(t string) {
 			if t == "void" {
-				result.errs.pushType("void is not a valid type for input parameter")
+				errors.pushType("void is not a valid type for input parameter")
 			}
 		})
-		param.optional = optional
-
-		variable = strings.TrimPrefix(variable, "$")
-		result.types[variable] = param
+		param.Optional = optional
 	}
 
-	result.returnType = result.returnType.Immutable()
-	result.inherit = doc.Inherit
-	return result
+	return errors
 }
 
-// parse type info, e.g. "string" in "someFunc() : string { ... }"
+// Parse type info, e.g. "string" in "someFunc() : string { ... }".
 func (d *rootWalker) parseTypeNode(n ir.Node) (typ types.Map, ok bool) {
 	if n == nil {
 		return types.Map{}, false
@@ -1395,9 +1376,9 @@ func (d *rootWalker) parseTypeNode(n ir.Node) (typ types.Map, ok bool) {
 
 	d.checkTypeNode(n)
 
-	typeList := typesFromNode(n)
-	tm := newTypesMap(&d.ctx, typeList)
-	return tm, !tm.Empty()
+	typesMap := types.NormalizedTypeHintTypes(d.ctx.typeNormalizer, n)
+
+	return typesMap, !typesMap.Empty()
 }
 
 func (d *rootWalker) checkTypeNode(n ir.Node) {
@@ -1405,7 +1386,7 @@ func (d *rootWalker) checkTypeNode(n ir.Node) {
 		return
 	}
 
-	typeList := typesFromNode(n)
+	typeList := types.TypeHintTypes(n)
 	for _, typ := range typeList {
 		if typ.Elem == "parent" && d.ctx.st.CurrentClass != "" {
 			if d.ctx.st.CurrentParentClass == "" {
@@ -1481,7 +1462,7 @@ type parseFuncParamsResult struct {
 	minParamsCount int
 }
 
-func (d *rootWalker) parseFuncParams(params []ir.Node, phpDocParamsTypes phpDocParamsMap, sc *meta.Scope, closureSolver *solver.ClosureCallerInfo) (res parseFuncParamsResult) {
+func (d *rootWalker) parseFuncParams(params []ir.Node, phpDocParamsTypes phpdoctypes.ParamsMap, sc *meta.Scope, closureSolver *solver.ClosureCallerInfo) (res parseFuncParamsResult) {
 	if len(params) == 0 {
 		return res
 	}
@@ -1499,13 +1480,13 @@ func (d *rootWalker) parseFuncParams(params []ir.Node, phpDocParamsTypes phpDocP
 		v := p.Variable
 		phpDocType := phpDocParamsTypes[v.Name]
 
-		if !phpDocType.typ.Empty() {
-			sc.AddVarName(v.Name, phpDocType.typ, "param", meta.VarAlwaysDefined)
+		if !phpDocType.Typ.Empty() {
+			sc.AddVarName(v.Name, phpDocType.Typ, "param", meta.VarAlwaysDefined)
 		}
 
-		paramTyp := phpDocType.typ
+		paramTyp := phpDocType.Typ
 
-		if p.DefaultValue == nil && !phpDocType.optional && !p.Variadic {
+		if p.DefaultValue == nil && !phpDocType.Optional && !p.Variadic {
 			minArgs++
 		}
 
@@ -1610,10 +1591,16 @@ func (d *rootWalker) enterFunction(fun *ir.FunctionStmt) bool {
 	d.checkCommentMisspellings(fun.FunctionName, fun.Doc.Raw)
 	d.checkIdentMisspellings(fun.FunctionName)
 
-	doc := d.parsePHPDoc(fun.FunctionName, fun.Doc, fun.Params)
-	d.reportPhpdocErrors(fun.FunctionName, doc.errs)
-	phpDocReturnType := doc.returnType
-	phpDocParamTypes := doc.types
+	// Indexing stage.
+	doc := phpdoctypes.Parse(fun.Doc, fun.Params, d.ctx.typeNormalizer)
+	moveShapesToContext(&d.ctx, doc.Shapes)
+
+	// Check stage.
+	errors := d.checkPHPDoc(fun, fun.Doc, fun.Params)
+	d.reportPHPDocErrors(fun, errors)
+
+	phpDocReturnType := doc.ReturnType
+	phpDocParamTypes := doc.ParamTypes
 
 	if d.meta.Functions.H == nil {
 		d.meta.Functions = meta.NewFunctionsMap()
@@ -1622,7 +1609,7 @@ func (d *rootWalker) enterFunction(fun *ir.FunctionStmt) bool {
 	sc := meta.NewScope()
 
 	returnTypeHint, ok := d.parseTypeNode(fun.ReturnType)
-	if ok && !doc.inherit {
+	if ok && !doc.Inherit {
 		d.checkFuncReturnType(fun.FunctionName, fun.FunctionName.Value, returnTypeHint, phpDocReturnType)
 	}
 
@@ -1648,13 +1635,13 @@ func (d *rootWalker) enterFunction(fun *ir.FunctionStmt) bool {
 		MinParamsCnt: funcParams.minParamsCount,
 		Flags:        funcFlags,
 		ExitFlags:    exitFlags,
-		Doc:          doc.info,
+		Doc:          doc.AdditionalInfo,
 	})
 
 	return false
 }
 
-func (d *rootWalker) checkFuncParams(funcName *ir.Identifier, params []ir.Node, funcParams parseFuncParamsResult, phpDocParamTypes phpDocParamsMap) {
+func (d *rootWalker) checkFuncParams(funcName *ir.Identifier, params []ir.Node, funcParams parseFuncParamsResult, phpDocParamTypes phpdoctypes.ParamsMap) {
 	for _, param := range params {
 		d.checkFuncParam(param.(*ir.Parameter))
 	}
@@ -1662,12 +1649,12 @@ func (d *rootWalker) checkFuncParams(funcName *ir.Identifier, params []ir.Node,
 	d.checkParamsTypeHint(funcName, funcParams, phpDocParamTypes)
 }
 
-func (d *rootWalker) checkParamsTypeHint(funcName *ir.Identifier, funcParams parseFuncParamsResult, phpDocParamTypes phpDocParamsMap) {
+func (d *rootWalker) checkParamsTypeHint(funcName *ir.Identifier, funcParams parseFuncParamsResult, phpDocParamTypes phpdoctypes.ParamsMap) {
 	for param, typeHintType := range funcParams.paramsTypeHint {
 		var phpDocType types.Map
 
 		if phpDocParamType, ok := phpDocParamTypes[param]; ok {
-			phpDocType = phpDocParamType.typ
+			phpDocType = phpDocParamType.Typ
 		}
 
 		if !d.typeHintHasMoreAccurateType(typeHintType, phpDocType) {
@@ -2191,7 +2178,7 @@ func (d *rootWalker) afterLeaveFile() {
 			props := make(meta.PropertiesMap)
 			for _, p := range shape.Props {
 				props[p.Key] = meta.PropertyInfo{
-					Typ:         newTypesMap(&d.ctx, p.Types).Immutable(),
+					Typ:         types.NewMapWithNormalization(d.ctx.typeNormalizer, p.Types).Immutable(),
 					AccessLevel: meta.Public,
 				}
 			}
diff --git a/src/linter/root_context.go b/src/linter/root_context.go
index 46fbdd4..1dd0e16 100644
--- a/src/linter/root_context.go
+++ b/src/linter/root_context.go
@@ -4,9 +4,10 @@ import (
 	"path/filepath"
 
 	"github.com/VKCOM/noverify/src/baseline"
-	"github.com/VKCOM/noverify/src/linter/autogen"
+	"github.com/VKCOM/noverify/src/ir"
 	"github.com/VKCOM/noverify/src/meta"
 	"github.com/VKCOM/noverify/src/quickfix"
+	"github.com/VKCOM/noverify/src/solver"
 	"github.com/VKCOM/noverify/src/types"
 )
 
@@ -15,10 +16,10 @@ type rootContext struct {
 
 	st *meta.ClassParseState
 
-	typeNormalizer typeNormalizer
+	typeNormalizer types.Normalizer
 
 	// shapes is a list of generated shape types for the current file.
-	shapes map[string]autogen.ShapeTypeInfo
+	shapes types.ShapesMap
 
 	baseline     baseline.FileProfile
 	hashCounters map[uint64]int // Allocated lazily
@@ -32,17 +33,17 @@ func newRootContext(config *Config, workerCtx *WorkerContext, st *meta.ClassPars
 		filename := filepath.Base(st.CurrentFile)
 		p = config.BaselineProfile.Files[filename]
 	}
+
+	classFQNProvider := func(name string) (string, bool) {
+		return solver.GetClassName(st, &ir.Name{Value: name})
+	}
+
 	return rootContext{
 		WorkerContext: workerCtx,
 
-		typeNormalizer: typeNormalizer{st: st, kphp: config.KPHP},
+		typeNormalizer: types.NewNormalizer(classFQNProvider, config.KPHP),
 		st:             st,
 		baseline:       p,
-		shapes:         map[string]autogen.ShapeTypeInfo{},
+		shapes:         types.ShapesMap{},
 	}
 }
-
-func newTypesMap(ctx *rootContext, typs []types.Type) types.Map {
-	ctx.typeNormalizer.NormalizeTypes(typs)
-	return types.NewMapFromTypes(typs)
-}
diff --git a/src/meta/metainfo.go b/src/meta/metainfo.go
index 0125df6..d9e2f73 100644
--- a/src/meta/metainfo.go
+++ b/src/meta/metainfo.go
@@ -1,7 +1,6 @@
 package meta
 
 import (
-	"github.com/VKCOM/noverify/src/ir"
 	"github.com/VKCOM/noverify/src/types"
 )
 
@@ -179,32 +178,3 @@ type ElementPosition struct {
 	Character int32
 	Length    int32 // body length
 }
-
-// NameNodeToString converts nodes of *name.Name, and *node.Identifier to string.
-// This function is a helper function to aid printing function names, not for actual code analysis.
-func NameNodeToString(n ir.Node) string {
-	switch n := n.(type) {
-	case *ir.Name:
-		return n.Value
-	case *ir.Identifier:
-		return n.Value
-	case *ir.SimpleVar:
-		return "$" + n.Name
-	case *ir.Var:
-		return "$" + NameNodeToString(n.Expr)
-	default:
-		return "<expression>"
-	}
-}
-
-// NameNodeEquals checks whether n node name value is identical to s.
-func NameNodeEquals(n ir.Node, s string) bool {
-	switch n := n.(type) {
-	case *ir.Name:
-		return n.Value == s
-	case *ir.Identifier:
-		return n.Value == s
-	default:
-		return false
-	}
-}
diff --git a/src/linter/types.go b/src/phpdoctypes/converter.go
similarity index 55%
rename from src/linter/types.go
rename to src/phpdoctypes/converter.go
index d932a57..b41363b 100644
--- a/src/linter/types.go
+++ b/src/phpdoctypes/converter.go
@@ -1,26 +1,30 @@
-package linter
+package phpdoctypes
 
 import (
 	"fmt"
 
-	"github.com/VKCOM/noverify/src/ir"
 	"github.com/VKCOM/noverify/src/linter/autogen"
-	"github.com/VKCOM/noverify/src/meta"
 	"github.com/VKCOM/noverify/src/phpdoc"
-	"github.com/VKCOM/noverify/src/solver"
 	"github.com/VKCOM/noverify/src/types"
 )
 
-// TODO: reflect source line in shape names.
-
-type warningString string
+type RealPHPDocTypes struct {
+	Types    []types.Type
+	Shapes   types.ShapesMap
+	Warnings []string
+}
 
-// typesFromPHPDoc extracts types out of the PHPDoc type string.
+// ToRealType extracts types out of the PHPDoc type string.
 //
 // No normalization is performed, but some PHPDoc-specific types
 // are simplified to be compatible with our type system.
-func typesFromPHPDoc(ctx *rootContext, typ phpdoc.Type) ([]types.Type, warningString) {
-	conv := phpdocTypeConverter{ctx: ctx}
+func ToRealType(classFQNProvider func(string) (string, bool), typ phpdoc.Type) *RealPHPDocTypes {
+	conv := TypeConverter{
+		classFQNProvider:   classFQNProvider,
+		shapeNameGenerator: autogen.GenerateShapeName,
+		shapes:             make(types.ShapesMap),
+	}
+
 	result := conv.mapType(typ.Expr)
 	if conv.nullable {
 		alreadyHasNull := false
@@ -28,7 +32,7 @@ func typesFromPHPDoc(ctx *rootContext, typ phpdoc.Type) ([]types.Type, warningSt
 		for _, tp := range result {
 			if tp.Elem == "null" {
 				alreadyHasNull = true
-				conv.warning = "repeated nullable doesn't make sense"
+				conv.warnings = append(conv.warnings, "repeated nullable doesn't make sense")
 				break
 			}
 		}
@@ -37,16 +41,23 @@ func typesFromPHPDoc(ctx *rootContext, typ phpdoc.Type) ([]types.Type, warningSt
 			result = append(result, types.Type{Elem: "null"})
 		}
 	}
-	return result, conv.warning
+
+	return &RealPHPDocTypes{
+		Types:    result,
+		Shapes:   conv.shapes,
+		Warnings: conv.warnings,
+	}
 }
 
-type phpdocTypeConverter struct {
-	ctx      *rootContext
-	warning  warningString
-	nullable bool
+type TypeConverter struct {
+	classFQNProvider   func(string) (string, bool)
+	shapeNameGenerator func([]types.ShapeProp) string
+	warnings           []string
+	nullable           bool
+	shapes             types.ShapesMap
 }
 
-func (conv *phpdocTypeConverter) mapType(e phpdoc.TypeExpr) []types.Type {
+func (conv *TypeConverter) mapType(e phpdoc.TypeExpr) []types.Type {
 	switch e.Kind {
 	case phpdoc.ExprInvalid, phpdoc.ExprUnknown:
 		if e.Value == "-" {
@@ -109,7 +120,7 @@ func (conv *phpdocTypeConverter) mapType(e phpdoc.TypeExpr) []types.Type {
 	case phpdoc.ExprOptional:
 		// Due to the fact that the optional keys for shape are processed in the mapShapeType
 		// function, while the optionality for the key is cleared, and the key itself is not
-		// processed by the mapType function, then in the mapType function the phpdoc.ExprOptional
+		// processed by the mapType function, then in the mapType function the ExprOptional
 		// type can only be in one case, if it is an incorrect syntax of the optional type.
 		conv.warn(e.Value + ": nullable syntax is ?T, not T?")
 	}
@@ -117,7 +128,7 @@ func (conv *phpdocTypeConverter) mapType(e phpdoc.TypeExpr) []types.Type {
 	return nil
 }
 
-func (conv *phpdocTypeConverter) mapArrayType(elem phpdoc.TypeExpr) []types.Type {
+func (conv *TypeConverter) mapArrayType(elem phpdoc.TypeExpr) []types.Type {
 	typeList := conv.mapType(elem)
 	if len(typeList) == 0 {
 		return []types.Type{{Elem: "mixed", Dims: 1}}
@@ -128,8 +139,8 @@ func (conv *phpdocTypeConverter) mapArrayType(elem phpdoc.TypeExpr) []types.Type
 	return typeList
 }
 
-func (conv *phpdocTypeConverter) mapShapeType(params []phpdoc.TypeExpr) []types.Type {
-	props := make([]autogen.ShapeTypeProp, 0, len(params))
+func (conv *TypeConverter) mapShapeType(params []phpdoc.TypeExpr) []types.Type {
+	props := make([]types.ShapeProp, 0, len(params))
 	for i, p := range params {
 		if p.Value == "*" || p.Value == "..." {
 			continue
@@ -167,7 +178,11 @@ func (conv *phpdocTypeConverter) mapShapeType(params []phpdoc.TypeExpr) []types.
 				continue
 			}
 
-			className, ok := solver.GetClassName(conv.ctx.st, &ir.Name{Value: typ.Elem})
+			if conv.classFQNProvider == nil {
+				continue
+			}
+
+			className, ok := conv.classFQNProvider(typ.Elem)
 			if !ok {
 				continue
 			}
@@ -182,22 +197,22 @@ func (conv *phpdocTypeConverter) mapShapeType(params []phpdoc.TypeExpr) []types.
 			conv.nullable = false
 		}
 
-		props = append(props, autogen.ShapeTypeProp{
+		props = append(props, types.ShapeProp{
 			Key:   key.Value,
 			Types: typeList,
 		})
 	}
 
-	shape := autogen.ShapeTypeInfo{
-		Name:  autogen.GenerateShapeName(props, conv.ctx.st),
+	shape := types.ShapeInfo{
+		Name:  conv.shapeNameGenerator(props),
 		Props: props,
 	}
-	conv.ctx.shapes[shape.Name] = shape
+	conv.shapes[shape.Name] = shape
 
 	return []types.Type{{Elem: shape.Name}}
 }
 
-func (conv *phpdocTypeConverter) mapTupleType(params []phpdoc.TypeExpr) []types.Type {
+func (conv *TypeConverter) mapTupleType(params []phpdoc.TypeExpr) []types.Type {
 	typeList := make([]phpdoc.TypeExpr, 0, len(params))
 	for i, p := range params {
 		if p.Value == "*" || p.Value == "..." {
@@ -221,91 +236,6 @@ func (conv *phpdocTypeConverter) mapTupleType(params []phpdoc.TypeExpr) []types.
 	return conv.mapShapeType(typeList)
 }
 
-func (conv *phpdocTypeConverter) warn(msg string) {
-	if conv.warning == "" {
-		conv.warning = warningString(msg)
-	}
-}
-
-// typesFromNode converts type hint node to meta types.
-//
-// No normalization is performed.
-func typesFromNode(typeNode ir.Node) []types.Type {
-	n := typeNode
-
-	var results []types.Type
-	if nullable, ok := typeNode.(*ir.Nullable); ok {
-		n = nullable.Expr
-		results = make([]types.Type, 0, 2)
-		results = append(results, types.Type{Elem: "null"})
-	} else {
-		results = make([]types.Type, 0, 1)
-	}
-
-	// There is a trick here.
-	// Unlike with phpdoc types, having `integer` here
-	// means that we need to force it to be interpreted as
-	// `\integer`, not as `int`. This is why we prepend `\`.
-	typ := types.Type{Elem: meta.NameNodeToString(n)}
-	if types.IsAlias(typ.Elem) {
-		typ.Elem = `\` + typ.Elem
-	}
-
-	results = append(results, typ)
-
-	return results
-}
-
-type typeNormalizer struct {
-	st   *meta.ClassParseState
-	kphp bool
-}
-
-func (n typeNormalizer) NormalizeTypes(typeList []types.Type) {
-	for i := range typeList {
-		n.normalizeType(&typeList[i])
-	}
-}
-
-func (n typeNormalizer) normalizeType(typ *types.Type) {
-	if types.IsTrivial(typ.Elem) {
-		return
-	}
-
-	if typename, has := types.Alias(typ.Elem); has {
-		typ.Elem = typename
-		return
-	}
-
-	if typ.Elem == "any" && n.kphp {
-		// `any` is a special KPHP type that is more-or-less
-		// identical to `mixed|object`. In PHP, `mixed` already covers
-		// objects, so there is no need to add `object`.
-		// See https://php.watch/versions/8.0/mixed-type
-		typ.Elem = "mixed"
-		return
-	}
-
-	switch typ.Elem {
-	case "array":
-		// Rewrite `array` to `mixed[]`.
-		// If it's `array[]`, it'll become `mixed[][]`.
-		typ.Dims++
-		typ.Elem = "mixed"
-	case "$this":
-		// Handle `$this` as `static` alias in phpdoc context.
-		typ.Elem = "static"
-	case "static":
-		// Don't replace `static` phpdoc type annotation too early
-		// to make it possible to handle late static binding.
-	default:
-		if typ.Elem[0] == '\\' {
-			return // Already FQN?
-		}
-		fullClassName, ok := solver.GetClassName(n.st, &ir.Name{Value: typ.Elem})
-		if !ok {
-			panic(fmt.Sprintf("can't expand type name: '%s'", typ.Elem))
-		}
-		typ.Elem = fullClassName
-	}
+func (conv *TypeConverter) warn(msg string) {
+	conv.warnings = append(conv.warnings, msg)
 }
diff --git a/src/phpdoctypes/parser.go b/src/phpdoctypes/parser.go
new file mode 100644
index 0000000..2617e68
--- /dev/null
+++ b/src/phpdoctypes/parser.go
@@ -0,0 +1,93 @@
+package phpdoctypes
+
+import (
+	"strings"
+
+	"github.com/VKCOM/noverify/src/ir"
+	"github.com/VKCOM/noverify/src/meta"
+	"github.com/VKCOM/noverify/src/phpdoc"
+	"github.com/VKCOM/noverify/src/types"
+)
+
+type Param struct {
+	Optional bool
+	Typ      types.Map
+}
+
+type ParamsMap map[string]Param
+
+type ParseResult struct {
+	ReturnType     types.Map
+	ParamTypes     ParamsMap
+	AdditionalInfo meta.PhpDocInfo
+	Inherit        bool
+
+	Shapes types.ShapesMap
+}
+
+func Parse(doc phpdoc.Comment, actualParams []ir.Node, normalizer types.Normalizer) (result ParseResult) {
+	if doc.Raw == "" {
+		return result
+	}
+
+	result.Shapes = make(types.ShapesMap)
+	result.ParamTypes = make(ParamsMap)
+
+	var curParam int
+
+	for _, rawPart := range doc.Parsed {
+
+		if rawPart.Name() == "deprecated" {
+			part := rawPart.(*phpdoc.RawCommentPart)
+			result.AdditionalInfo.Deprecated = true
+			result.AdditionalInfo.DeprecationNote = part.ParamsText
+			continue
+		}
+
+		if rawPart.Name() == "return" {
+			part := rawPart.(*phpdoc.TypeCommentPart)
+
+			converted := ToRealType(normalizer.ClassFQNProvider(), part.Type)
+			for name, shape := range converted.Shapes {
+				result.Shapes[name] = shape
+			}
+
+			result.ReturnType = types.NewMapWithNormalization(normalizer, converted.Types)
+			continue
+		}
+
+		// Rest is for @param handling.
+
+		if rawPart.Name() != "param" {
+			continue
+		}
+
+		part := rawPart.(*phpdoc.TypeVarCommentPart)
+		optional := strings.Contains(part.Rest, "[optional]")
+
+		variable := part.Var
+		if !strings.HasPrefix(variable, "$") {
+			if len(actualParams) > curParam {
+				variable = actualParams[curParam].(*ir.Parameter).Variable.Name
+			}
+		}
+
+		curParam++
+
+		converted := ToRealType(normalizer.ClassFQNProvider(), part.Type)
+		for name, shape := range converted.Shapes {
+			result.Shapes[name] = shape
+		}
+
+		var param Param
+		param.Typ = types.NewMapWithNormalization(normalizer, converted.Types)
+		param.Optional = optional
+
+		variable = strings.TrimPrefix(variable, "$")
+		result.ParamTypes[variable] = param
+	}
+
+	result.ReturnType = result.ReturnType.Immutable()
+	result.Inherit = doc.Inherit
+	return result
+}
diff --git a/src/rules/parser.go b/src/rules/parser.go
index 8a5fda4..8604601 100644
--- a/src/rules/parser.go
+++ b/src/rules/parser.go
@@ -12,10 +12,10 @@ import (
 	"github.com/VKCOM/noverify/src/ir/irconv"
 	"github.com/VKCOM/noverify/src/ir/irutil"
 	"github.com/VKCOM/noverify/src/linter/lintapi"
-	"github.com/VKCOM/noverify/src/meta"
 	"github.com/VKCOM/noverify/src/php/parseutil"
 	"github.com/VKCOM/noverify/src/phpdoc"
 	"github.com/VKCOM/noverify/src/phpgrep"
+	"github.com/VKCOM/noverify/src/utils"
 )
 
 var magicComment = regexp.MustCompile(`\* @(?:warning|error|info|maybe) `)
@@ -325,7 +325,7 @@ func (p *parser) parseRule(st ir.Node, proto *Rule) error {
 		if len(st.Stmts) != 0 {
 			return p.errorf(st, "namespace with body is not supported")
 		}
-		p.namespace = meta.NameNodeToString(st.NamespaceName)
+		p.namespace = utils.NameNodeToString(st.NamespaceName)
 		if strings.Contains(p.namespace, `\`) {
 			return p.errorf(st, "multi-part namespace names are not supported")
 		}
diff --git a/src/solver/exprtype.go b/src/solver/exprtype.go
index cb39585..728dba8 100644
--- a/src/solver/exprtype.go
+++ b/src/solver/exprtype.go
@@ -9,6 +9,7 @@ import (
 	"github.com/VKCOM/noverify/src/linter/autogen"
 	"github.com/VKCOM/noverify/src/meta"
 	"github.com/VKCOM/noverify/src/types"
+	"github.com/VKCOM/noverify/src/utils"
 )
 
 // CustomType specifies a mapping between some AST structure
@@ -153,7 +154,7 @@ func exprTypeLocalCustom(sc *meta.Scope, cs *meta.ClassParseState, n ir.Node, cu
 	case *ir.CloneExpr:
 		return ExprTypeLocalCustom(sc, cs, n.Expr, custom)
 	case *ir.ClosureExpr:
-		name := autogen.GenerateClosureName(n, cs)
+		name := autogen.GenerateClosureName(n, cs.CurrentFunction, cs.CurrentFile)
 		return types.NewMap(name)
 	case *ir.MagicConstant:
 		return magicConstantType(n)
@@ -328,7 +329,7 @@ func arrayType(sc *meta.Scope, cs *meta.ClassParseState, items []*ir.ArrayItemEx
 }
 
 func newExprType(n *ir.NewExpr, cs *meta.ClassParseState) types.Map {
-	if meta.NameNodeToString(n.Class) == "static" {
+	if utils.NameNodeToString(n.Class) == "static" {
 		return types.NewMap("static")
 	}
 	nm, ok := GetClassName(cs, n.Class)
diff --git a/src/types/map.go b/src/types/map.go
index b36dbd0..8643e23 100644
--- a/src/types/map.go
+++ b/src/types/map.go
@@ -128,6 +128,11 @@ func NewPreciseMap(str string) Map {
 	return m
 }
 
+func NewMapWithNormalization(norm Normalizer, types []Type) Map {
+	norm.NormalizeTypes(types)
+	return NewMapFromTypes(types)
+}
+
 // MergeMaps creates a new types map from union of specified type maps
 func MergeMaps(maps ...Map) Map {
 	totalLen := 0
diff --git a/src/types/normalizer.go b/src/types/normalizer.go
new file mode 100644
index 0000000..ff3ec68
--- /dev/null
+++ b/src/types/normalizer.go
@@ -0,0 +1,72 @@
+package types
+
+import (
+	"fmt"
+)
+
+type Normalizer struct {
+	classFQNProvider func(string) (string, bool)
+	kphp             bool
+}
+
+func NewNormalizer(classFQNProvider func(string) (string, bool), kphp bool) Normalizer {
+	return Normalizer{classFQNProvider: classFQNProvider, kphp: kphp}
+}
+
+func (n Normalizer) ClassFQNProvider() func(string) (string, bool) {
+	return n.classFQNProvider
+}
+
+func (n Normalizer) NormalizeTypes(typeList []Type) {
+	for i := range typeList {
+		n.normalizeType(&typeList[i])
+	}
+}
+
+func (n Normalizer) normalizeType(typ *Type) {
+	if IsTrivial(typ.Elem) {
+		return
+	}
+
+	if typename, has := Alias(typ.Elem); has {
+		typ.Elem = typename
+		return
+	}
+
+	if typ.Elem == "any" && n.kphp {
+		// `any` is a special KPHP type that is more-or-less
+		// identical to `mixed|object`. In PHP, `mixed` already covers
+		// objects, so there is no need to add `object`.
+		// See https://php.watch/versions/8.0/mixed-type
+		typ.Elem = "mixed"
+		return
+	}
+
+	switch typ.Elem {
+	case "array":
+		// Rewrite `array` to `mixed[]`.
+		// If it's `array[]`, it'll become `mixed[][]`.
+		typ.Dims++
+		typ.Elem = "mixed"
+	case "$this":
+		// Handle `$this` as `static` alias in phpdoc context.
+		typ.Elem = "static"
+	case "static":
+		// Don't replace `static` phpdoc type annotation too early
+		// to make it possible to handle late static binding.
+	default:
+		if typ.Elem[0] == '\\' {
+			return // Already FQN?
+		}
+
+		if n.classFQNProvider == nil {
+			return
+		}
+
+		fullClassName, ok := n.classFQNProvider(typ.Elem)
+		if !ok {
+			panic(fmt.Sprintf("can't expand type name: '%s'", typ.Elem))
+		}
+		typ.Elem = fullClassName
+	}
+}
diff --git a/src/types/shape.go b/src/types/shape.go
new file mode 100644
index 0000000..334efc7
--- /dev/null
+++ b/src/types/shape.go
@@ -0,0 +1,13 @@
+package types
+
+type ShapesMap map[string]ShapeInfo
+
+type ShapeProp struct {
+	Key   string
+	Types []Type
+}
+
+type ShapeInfo struct {
+	Name  string
+	Props []ShapeProp
+}
diff --git a/src/types/typehint.go b/src/types/typehint.go
new file mode 100644
index 0000000..ccbc515
--- /dev/null
+++ b/src/types/typehint.go
@@ -0,0 +1,40 @@
+package types
+
+import (
+	"github.com/VKCOM/noverify/src/ir"
+	"github.com/VKCOM/noverify/src/utils"
+)
+
+func NormalizedTypeHintTypes(normalizer Normalizer, typeNode ir.Node) Map {
+	typeList := TypeHintTypes(typeNode)
+	return NewMapWithNormalization(normalizer, typeList)
+}
+
+// TypeHintTypes converts type hint node to meta types.
+//
+// No normalization is performed.
+func TypeHintTypes(typeNode ir.Node) []Type {
+	n := typeNode
+
+	var results []Type
+	if nullable, ok := typeNode.(*ir.Nullable); ok {
+		n = nullable.Expr
+		results = make([]Type, 0, 2)
+		results = append(results, Type{Elem: "null"})
+	} else {
+		results = make([]Type, 0, 1)
+	}
+
+	// There is a trick here.
+	// Unlike with phpdoc types, having `integer` here
+	// means that we need to force it to be interpreted as
+	// `\integer`, not as `int`. This is why we prepend `\`.
+	typ := Type{Elem: utils.NameNodeToString(n)}
+	if IsAlias(typ.Elem) {
+		typ.Elem = `\` + typ.Elem
+	}
+
+	results = append(results, typ)
+
+	return results
+}
diff --git a/src/utils/utils.go b/src/utils/utils.go
new file mode 100644
index 0000000..eb1b9fd
--- /dev/null
+++ b/src/utils/utils.go
@@ -0,0 +1,34 @@
+package utils
+
+import (
+	"github.com/VKCOM/noverify/src/ir"
+)
+
+// NameNodeToString converts nodes of *name.Name, and *node.Identifier to string.
+// This function is a helper function to aid printing function names, not for actual code analysis.
+func NameNodeToString(n ir.Node) string {
+	switch n := n.(type) {
+	case *ir.Name:
+		return n.Value
+	case *ir.Identifier:
+		return n.Value
+	case *ir.SimpleVar:
+		return "$" + n.Name
+	case *ir.Var:
+		return "$" + NameNodeToString(n.Expr)
+	default:
+		return "<expression>"
+	}
+}
+
+// NameNodeEquals checks whether n node name value is identical to s.
+func NameNodeEquals(n ir.Node, s string) bool {
+	switch n := n.(type) {
+	case *ir.Name:
+		return n.Value == s
+	case *ir.Identifier:
+		return n.Value == s
+	default:
+		return false
+	}
+}


Test patch:
diff --git a/src/linter/phpdoc_test.go b/src/linter/phpdoc_test.go
index 199a096..9dad67e 100644
--- a/src/linter/phpdoc_test.go
+++ b/src/linter/phpdoc_test.go
@@ -6,6 +6,8 @@ import (
 
 	"github.com/VKCOM/noverify/src/meta"
 	"github.com/VKCOM/noverify/src/phpdoc"
+	"github.com/VKCOM/noverify/src/phpdoctypes"
+	"github.com/VKCOM/noverify/src/types"
 )
 
 func TestParseClassPHPDoc(t *testing.T) {
@@ -118,7 +120,10 @@ func BenchmarkParseTypes(b *testing.B) {
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
 		parsedType := ctx.phpdocTypeParser.Parse(typeString)
-		types, _ := typesFromPHPDoc(&ctx, parsedType)
-		_ = newTypesMap(&ctx, types)
+
+		converted := phpdoctypes.ToRealType(ctx.typeNormalizer.ClassFQNProvider(), parsedType)
+		moveShapesToContext(&ctx, converted.Shapes)
+
+		_ = types.NewMapWithNormalization(ctx.typeNormalizer, converted.Types)
 	}
 }
diff --git a/src/tests/checkers/phpdoc_test.go b/src/tests/checkers/phpdoc_test.go
index 6b16912..5d137ca 100644
--- a/src/tests/checkers/phpdoc_test.go
+++ b/src/tests/checkers/phpdoc_test.go
@@ -435,6 +435,7 @@ func TestPHPDocType(t *testing.T) {
 `)
 	test.Expect = []string{
 		`[][]string: array syntax is T[], not []T on line 2`,
+		`[]string: array syntax is T[], not []T on line 2`,
 		`use float type instead of double`,
 		`use float type instead of real`,
 		`use int type instead of integer`,
diff --git a/src/tests/exprtype/exprtype_test.go b/src/tests/exprtype/exprtype_test.go
index e4f3a91..afdaaed 100644
--- a/src/tests/exprtype/exprtype_test.go
+++ b/src/tests/exprtype/exprtype_test.go
@@ -5,13 +5,13 @@ import (
 	"sync"
 	"testing"
 
+	"github.com/VKCOM/noverify/src/utils"
 	"github.com/google/go-cmp/cmp"
 
 	"github.com/VKCOM/noverify/src/ir"
 	"github.com/VKCOM/noverify/src/ir/irutil"
 	"github.com/VKCOM/noverify/src/linter"
 	"github.com/VKCOM/noverify/src/linttest"
-	"github.com/VKCOM/noverify/src/meta"
 	"github.com/VKCOM/noverify/src/types"
 	"github.com/VKCOM/noverify/src/workspace"
 )
@@ -2983,7 +2983,7 @@ func (w *exprTypeWalker) LeaveNode(n ir.Node) {}
 
 func (w *exprTypeWalker) EnterNode(n ir.Node) bool {
 	call, ok := n.(*ir.FunctionCallExpr)
-	if ok && meta.NameNodeEquals(call.Function, `exprtype`) {
+	if ok && utils.NameNodeEquals(call.Function, `exprtype`) {
 		checkedExpr := call.Arg(0).Expr
 		expectedType := call.Arg(1).Expr.(*ir.String).Value
 		actualType, ok := exprTypeResult[checkedExpr]
@@ -3017,7 +3017,7 @@ func (c *exprTypeCollector) AfterEnterNode(n ir.Node) {
 	}
 
 	call, ok := n.(*ir.FunctionCallExpr)
-	if !ok || !meta.NameNodeEquals(call.Function, `exprtype`) {
+	if !ok || !utils.NameNodeEquals(call.Function, `exprtype`) {
 		return
 	}
 	checkedExpr := call.Arg(0).Expr